mirror of
https://github.com/documenso/documenso.git
synced 2025-11-22 12:41:36 +10:00
Merge branch 'feat/refresh' into feat/open-early-adopters
This commit is contained in:
@ -39,7 +39,7 @@ export default function ContentPage({ params }: { params: { content: string } })
|
||||
const MDXContent = useMDXComponent(post.body.code);
|
||||
|
||||
return (
|
||||
<article className="prose prose-slate mx-auto">
|
||||
<article className="prose dark:prose-invert mx-auto">
|
||||
<MDXContent components={mdxComponents} />
|
||||
</article>
|
||||
);
|
||||
|
||||
@ -39,21 +39,21 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
|
||||
const MDXContent = useMDXComponent(post.body.code);
|
||||
|
||||
return (
|
||||
<article className="prose prose-slate mx-auto py-8">
|
||||
<article className="prose dark:prose-invert mx-auto py-8">
|
||||
<div className="mb-6 text-center">
|
||||
<time dateTime={post.date} className="mb-1 text-xs text-gray-600">
|
||||
<time dateTime={post.date} className="text-muted-foreground mb-1 text-xs">
|
||||
{new Date(post.date).toLocaleDateString()}
|
||||
</time>
|
||||
|
||||
<h1 className="text-3xl font-bold">{post.title}</h1>
|
||||
|
||||
<div className="not-prose relative -mt-2 flex items-center gap-x-4 border-b border-t py-4">
|
||||
<div className="h-10 w-10 rounded-full bg-gray-50">
|
||||
<div className="bg-foreground h-10 w-10 rounded-full">
|
||||
{post.authorImage && (
|
||||
<img
|
||||
src={post.authorImage}
|
||||
alt={`Image of ${post.authorName}`}
|
||||
className="h-10 w-10 rounded-full bg-gray-50"
|
||||
className="bg-foreground/10 h-10 w-10 rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -13,13 +13,13 @@ export default function BlogPage() {
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">From the blog</h1>
|
||||
|
||||
<p className="mx-auto mt-4 max-w-xl text-center text-lg leading-normal text-[#31373D]">
|
||||
<p className="text-muted-foreground mx-auto mt-4 max-w-xl text-center text-lg leading-normal">
|
||||
Get the latest news from Documenso, including product updates, team announcements and
|
||||
more!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 divide-y divide-slate-100 border-t border-slate-200 ">
|
||||
<div className="divide-muted-foreground/20 border-muted-foreground/20 mt-10 divide-y border-t">
|
||||
{blogPosts.map((post, i) => (
|
||||
<article
|
||||
key={`blog-${i}`}
|
||||
@ -57,12 +57,12 @@ export default function BlogPage() {
|
||||
</div>
|
||||
|
||||
<div className="relative mt-4 flex items-center gap-x-4">
|
||||
<div className="h-10 w-10 rounded-full bg-slate-50">
|
||||
<div className="bg-foreground/5 h-10 w-10 rounded-full">
|
||||
{post.authorImage && (
|
||||
<img
|
||||
src={post.authorImage}
|
||||
alt={`Image of ${post.authorName}`}
|
||||
className="h-10 w-10 rounded-full bg-slate-50"
|
||||
className="bg-foreground/5 h-10 w-10 rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -58,40 +58,40 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
|
||||
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<h1 className="text-3xl font-bold text-slate-900 md:text-4xl">
|
||||
<h1 className="text-foreground text-3xl font-bold md:text-4xl">
|
||||
Welcome to the <span className="text-primary">open signing</span> revolution{' '}
|
||||
<u>{user.name}</u>
|
||||
</h1>
|
||||
|
||||
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg">
|
||||
<p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
|
||||
It's not every day you get to be part of a revolution.
|
||||
</p>
|
||||
|
||||
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg">
|
||||
<p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
|
||||
But today is that day, by signing up to Documenso, you're joining a movement of people who
|
||||
want to make the world a better place.
|
||||
</p>
|
||||
|
||||
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg">
|
||||
<p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
|
||||
We're going to change the way people sign documents. We're going to make it easier, faster,
|
||||
and more secure. And we're going to do it together.
|
||||
</p>
|
||||
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold text-slate-900">Let's do it together</h2>
|
||||
<h2 className="text-foreground text-2xl font-bold">Let's do it together</h2>
|
||||
|
||||
<div className="-mx-4 mt-8 flex md:-mx-8">
|
||||
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
|
||||
<p
|
||||
className={cn(
|
||||
'text-4xl font-semibold text-slate-900 md:text-5xl',
|
||||
'text-foreground text-4xl font-semibold md:text-5xl',
|
||||
fontCaveat.className,
|
||||
)}
|
||||
>
|
||||
Timur
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-slate-500 md:text-lg">
|
||||
<p className="text-muted-foreground text-sm md:text-lg">
|
||||
Timur Ercan
|
||||
<span className="block lg:hidden" />
|
||||
<span className="hidden lg:inline"> - </span>
|
||||
@ -102,14 +102,14 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
|
||||
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
|
||||
<p
|
||||
className={cn(
|
||||
'text-4xl font-semibold text-slate-900 md:text-5xl',
|
||||
'text-foreground text-4xl font-semibold md:text-5xl',
|
||||
fontCaveat.className,
|
||||
)}
|
||||
>
|
||||
Lucas
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-slate-500 md:text-lg">
|
||||
<p className="text-muted-foreground text-sm md:text-lg">
|
||||
Lucas Smith
|
||||
<span className="block lg:hidden" />
|
||||
<span className="hidden lg:inline"> - </span>
|
||||
@ -119,12 +119,16 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
|
||||
|
||||
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
|
||||
{signatureDataUrl && (
|
||||
<img src={signatureDataUrl} alt="your-signature" className="max-w-[172px]" />
|
||||
<img
|
||||
src={signatureDataUrl}
|
||||
alt="your-signature"
|
||||
className="max-w-[172px] dark:invert"
|
||||
/>
|
||||
)}
|
||||
{!signatureDataUrl && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-4xl font-semibold text-slate-900 md:text-5xl',
|
||||
'text-foreground text-4xl font-semibold md:text-5xl',
|
||||
fontCaveat.className,
|
||||
)}
|
||||
>
|
||||
@ -132,7 +136,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-slate-500 md:text-lg">
|
||||
<p className="text-muted-foreground text-sm md:text-lg">
|
||||
{user.name}
|
||||
<span className="block lg:hidden" />
|
||||
<span className="hidden lg:inline"> - </span>
|
||||
@ -143,20 +147,20 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold text-slate-900">Your sign in details</h2>
|
||||
<h2 className="text-foreground text-2xl font-bold">Your sign in details</h2>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-lg text-slate-500">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
<span className="font-bold">Email:</span> {user.email}
|
||||
</p>
|
||||
|
||||
<p className="mt-2 text-lg text-slate-500">
|
||||
<p className="text-muted-foreground mt-2 text-lg">
|
||||
<span className="font-bold">Password:</span>{' '}
|
||||
<PasswordReveal password={password ?? 'password'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm italic text-slate-500">
|
||||
<p className="text-muted-foreground mt-4 text-sm italic">
|
||||
This is a temporary password. Please change it as soon as possible.
|
||||
</p>
|
||||
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { Footer } from '~/components/(marketing)/footer';
|
||||
import { Header } from '~/components/(marketing)/header';
|
||||
@ -8,15 +12,31 @@ export type MarketingLayoutProps = {
|
||||
};
|
||||
|
||||
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
setScrollY(window.scrollY);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
|
||||
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md">
|
||||
<div
|
||||
className={cn('fixed left-0 top-0 z-50 w-full bg-transparent', {
|
||||
'bg-background/50 backdrop-blur-md': scrollY > 5,
|
||||
})}
|
||||
>
|
||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div>
|
||||
|
||||
<Footer className="mt-24 bg-transparent backdrop-blur-[2px]" />
|
||||
<Footer className="bg-background border-muted mt-24 border-t" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export default async function OpenPage() {
|
||||
.then((res) => ZEarlyAdoptersResponse.parse(res));
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-12 max-w-screen-lg">
|
||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
|
||||
|
||||
|
||||
84
apps/marketing/src/app/(marketing)/oss-friends/container.tsx
Normal file
84
apps/marketing/src/app/(marketing)/oss-friends/container.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Variants, motion } from 'framer-motion';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { TOSSFriendsSchema } from './schema';
|
||||
|
||||
const ContainerVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.075,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const CardVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const randomDegrees = () => {
|
||||
const degrees = [45, 120, -140, -45];
|
||||
|
||||
return degrees[Math.floor(Math.random() * degrees.length)];
|
||||
};
|
||||
|
||||
export type OSSFriendsContainerProps = {
|
||||
className?: string;
|
||||
ossFriends: TOSSFriendsSchema;
|
||||
};
|
||||
|
||||
export const OSSFriendsContainer = ({ className, ossFriends }: OSSFriendsContainerProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
className={cn('grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3', className)}
|
||||
variants={ContainerVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
>
|
||||
{ossFriends.map((friend, index) => (
|
||||
<motion.div key={index} className="h-full w-full" variants={CardVariants}>
|
||||
<Card
|
||||
className="h-full"
|
||||
degrees={randomDegrees()}
|
||||
gradient={index % 2 === 0}
|
||||
spotlight={index % 2 !== 0}
|
||||
>
|
||||
<CardContent className="flex h-full flex-col p-6">
|
||||
<CardTitle>
|
||||
<Link href={friend.href}>{friend.name}</Link>
|
||||
</CardTitle>
|
||||
|
||||
<p className="text-foreground mt-4 flex-1 text-sm">{friend.description}</p>
|
||||
|
||||
<div className="mt-8">
|
||||
<Link target="_blank" href={friend.href}>
|
||||
<Button>Learn more</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@ -1,152 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Variants, motion } from 'framer-motion';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
||||
import { z } from 'zod';
|
||||
|
||||
import backgroundPattern from '~/assets/background-pattern.png';
|
||||
|
||||
const OSSFriends = [
|
||||
{
|
||||
name: 'BoxyHQ',
|
||||
description:
|
||||
'BoxyHQ’s suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.',
|
||||
href: 'https://boxyhq.com',
|
||||
},
|
||||
{
|
||||
name: 'Cal.com',
|
||||
description:
|
||||
'Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.',
|
||||
href: 'https://cal.com',
|
||||
},
|
||||
{
|
||||
name: 'Crowd.dev',
|
||||
description:
|
||||
'Centralize community, product, and customer data to understand which companies are engaging with your open source project.',
|
||||
href: 'https://www.crowd.dev',
|
||||
},
|
||||
{
|
||||
name: 'Documenso',
|
||||
description:
|
||||
'The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.',
|
||||
href: 'https://documenso.com',
|
||||
},
|
||||
{
|
||||
name: 'Erxes',
|
||||
description:
|
||||
'The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.',
|
||||
href: 'https://erxes.io',
|
||||
},
|
||||
{
|
||||
name: 'Formbricks',
|
||||
description:
|
||||
'Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.',
|
||||
href: 'https://formbricks.com',
|
||||
},
|
||||
{
|
||||
name: 'Forward Email',
|
||||
description:
|
||||
'Free email forwarding for custom domains. For 6 years and counting, we are the go-to email service for thousands of creators, developers, and businesses.',
|
||||
href: 'https://forwardemail.net',
|
||||
},
|
||||
{
|
||||
name: 'GitWonk',
|
||||
description:
|
||||
'GitWonk is an open-source technical documentation tool, designed and built focusing on the developer experience.',
|
||||
href: 'https://gitwonk.com',
|
||||
},
|
||||
{
|
||||
name: 'Hanko',
|
||||
description:
|
||||
'Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.',
|
||||
href: 'https://www.hanko.io',
|
||||
},
|
||||
{
|
||||
name: 'HTMX',
|
||||
description:
|
||||
'HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.',
|
||||
href: 'https://htmx.org',
|
||||
},
|
||||
{
|
||||
name: 'Infisical',
|
||||
description:
|
||||
'Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.',
|
||||
href: 'https://infisical.com',
|
||||
},
|
||||
{
|
||||
name: 'Novu',
|
||||
description:
|
||||
'The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.',
|
||||
href: 'https://novu.co',
|
||||
},
|
||||
{
|
||||
name: 'OpenBB',
|
||||
description:
|
||||
'Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.',
|
||||
href: 'https://openbb.co',
|
||||
},
|
||||
{
|
||||
name: 'Sniffnet',
|
||||
description:
|
||||
'Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.',
|
||||
href: 'https://www.sniffnet.net',
|
||||
},
|
||||
{
|
||||
name: 'Typebot',
|
||||
description:
|
||||
'Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.',
|
||||
href: 'https://typebot.io',
|
||||
},
|
||||
{
|
||||
name: 'Webiny',
|
||||
description:
|
||||
'Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.',
|
||||
href: 'https://www.webiny.com',
|
||||
},
|
||||
{
|
||||
name: 'Webstudio',
|
||||
description: 'Webstudio is an open source alternative to Webflow',
|
||||
href: 'https://webstudio.is',
|
||||
},
|
||||
];
|
||||
import { OSSFriendsContainer } from './container';
|
||||
import { TOSSFriendsSchema, ZOSSFriendsSchema } from './schema';
|
||||
|
||||
const ContainerVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.075,
|
||||
export default async function OSSFriendsPage() {
|
||||
const ossFriends: TOSSFriendsSchema = await fetch('https://formbricks.com/api/oss-friends', {
|
||||
next: {
|
||||
revalidate: 3600,
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
.then(async (res) => res.json())
|
||||
.then(async (data) => z.object({ data: ZOSSFriendsSchema }).parseAsync(data))
|
||||
.then(({ data }) => data)
|
||||
.catch(() => []);
|
||||
|
||||
const CardVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const randomDegrees = () => {
|
||||
const degrees = [45, 120, -140, -45];
|
||||
|
||||
return degrees[Math.floor(Math.random() * degrees.length)];
|
||||
};
|
||||
|
||||
export default function OSSFriendsPage() {
|
||||
return (
|
||||
<div className="relative mt-12">
|
||||
<div className="text-center">
|
||||
@ -154,49 +25,19 @@ export default function OSSFriendsPage() {
|
||||
Our <span title="Open Source Software">OSS</span> Friends
|
||||
</h1>
|
||||
|
||||
<p className="mx-auto mt-4 max-w-[55ch] text-lg leading-normal text-[#31373D]">
|
||||
<p className="text-foreground mx-auto mt-4 max-w-[55ch] text-lg leading-normal">
|
||||
We love open source and so should you, below you can find a list of our friends who are
|
||||
just as passionate about open source as we are.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="mt-12 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"
|
||||
variants={ContainerVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
>
|
||||
{OSSFriends.map((friend, index) => (
|
||||
<motion.div key={index} className="h-full w-full" variants={CardVariants}>
|
||||
<Card
|
||||
className="h-full"
|
||||
degrees={randomDegrees()}
|
||||
gradient={index % 2 === 0}
|
||||
spotlight={index % 2 !== 0}
|
||||
>
|
||||
<CardContent className="flex h-full flex-col p-6">
|
||||
<CardTitle>
|
||||
<Link href={friend.href}>{friend.name}</Link>
|
||||
</CardTitle>
|
||||
|
||||
<p className="mt-4 flex-1 text-sm text-slate-700">{friend.description}</p>
|
||||
|
||||
<div className="mt-8">
|
||||
<Link target="_blank" href={friend.href}>
|
||||
<Button>Learn more</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
<OSSFriendsContainer className="mt-12" ossFriends={ossFriends} />
|
||||
|
||||
<div className="absolute inset-0 -z-10 flex items-start justify-center">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover md:-mr-[50vw] md:scale-150 lg:scale-[175%]"
|
||||
className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:-mr-[50vw] md:scale-150 lg:scale-[175%]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
11
apps/marketing/src/app/(marketing)/oss-friends/schema.ts
Normal file
11
apps/marketing/src/app/(marketing)/oss-friends/schema.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZOSSFriendsSchema = z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
href: z.string().url(),
|
||||
description: z.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type TOSSFriendsSchema = z.infer<typeof ZOSSFriendsSchema>;
|
||||
@ -20,14 +20,14 @@ export type PricingPageProps = {
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<div className="mt-6 sm:mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
|
||||
|
||||
<p className="mt-4 text-lg leading-normal text-[#31373D]">
|
||||
<p className="text-foreground mt-4 text-lg leading-normal">
|
||||
Designed for every stage of your journey.
|
||||
</p>
|
||||
<p className="text-lg leading-normal text-[#31373D]">Get started today.</p>
|
||||
<p className="text-foreground text-lg leading-normal">Get started today.</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
@ -45,7 +45,7 @@ export default function PricingPage() {
|
||||
What is the difference between the plans?
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
You can self-host Documenso for free or use our ready-to-use hosted version. The
|
||||
hosted version comes with additional support, painless scalability and more. Early
|
||||
adopters of the community plan will get access to all features we build this year, for
|
||||
@ -59,7 +59,7 @@ export default function PricingPage() {
|
||||
How do you handle my data?
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Securely. Our data centers are located in Frankfurt (Germany), giving us the best
|
||||
local privacy laws. We are very aware of the sensitive nature of our data and follow
|
||||
best practices to ensure the security and integrity of the data entrusted to us.
|
||||
@ -71,7 +71,7 @@ export default function PricingPage() {
|
||||
Why should I use your hosting service?
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Using our hosted version is the easiest way to get started, you can simply subscribe
|
||||
and start signing your documents. We take care of the infrastructure, so you can focus
|
||||
on your business. Additionally, when using our hosted version you benefit from our
|
||||
@ -84,7 +84,7 @@ export default function PricingPage() {
|
||||
How can I contribute?
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
That's awesome. You can take a look at the current{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
@ -111,7 +111,7 @@ export default function PricingPage() {
|
||||
Can I use Documenso commercially?
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you
|
||||
can use it for free and even modify it to fit your needs, as long as you publish your
|
||||
changes under the same license.
|
||||
@ -123,7 +123,7 @@ export default function PricingPage() {
|
||||
Why should I prefer Documenso over DocuSign or some other signing tool?
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
Documenso is a community effort to create an open and vibrant ecosystem around a tool,
|
||||
everybody is free to use and adapt. By being truly open we want to create trusted
|
||||
infrastructure for the future of the internet.
|
||||
@ -135,7 +135,7 @@ export default function PricingPage() {
|
||||
Where can I get support?
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||
We are happy to assist you at{' '}
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success';
|
||||
|
||||
export type SinglePlayerModeSuccessPageProps = {
|
||||
params: {
|
||||
token?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function SinglePlayerModeSuccessPage({
|
||||
params: { token },
|
||||
}: SinglePlayerModeSuccessPageProps) {
|
||||
if (!token) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const document = await getDocumentAndRecipientByToken({
|
||||
token,
|
||||
}).catch(() => null);
|
||||
|
||||
if (!document || document.status !== DocumentStatus.COMPLETED) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <SinglePlayerModeSuccess document={document} />;
|
||||
}
|
||||
244
apps/marketing/src/app/(marketing)/single-player-mode/page.tsx
Normal file
244
apps/marketing/src/app/(marketing)/single-player-mode/page.tsx
Normal file
@ -0,0 +1,244 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { base64 } from '@documenso/lib/universal/base64';
|
||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { Field, Prisma, Recipient } from '@documenso/prisma/client';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
|
||||
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
|
||||
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
||||
import {
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerHeader,
|
||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { createSinglePlayerDocument } from '~/components/(marketing)/single-player-mode/create-single-player-document.action';
|
||||
|
||||
type SinglePlayerModeStep = 'fields' | 'sign';
|
||||
|
||||
export default function SinglePlayerModePage() {
|
||||
const analytics = useAnalytics();
|
||||
const router = useRouter();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
|
||||
|
||||
const [step, setStep] = useState<SinglePlayerModeStep>('fields');
|
||||
const [fields, setFields] = useState<Field[]>([]);
|
||||
|
||||
const documentFlow: Record<SinglePlayerModeStep, DocumentFlowStep> = {
|
||||
fields: {
|
||||
title: 'Add document',
|
||||
description: 'Upload a document and add fields.',
|
||||
stepIndex: 1,
|
||||
onBackStep: uploadedFile
|
||||
? () => {
|
||||
setUploadedFile(null);
|
||||
setFields([]);
|
||||
}
|
||||
: undefined,
|
||||
onNextStep: () => setStep('sign'),
|
||||
},
|
||||
sign: {
|
||||
title: 'Sign',
|
||||
description: 'Enter your details.',
|
||||
stepIndex: 2,
|
||||
onBackStep: () => setStep('fields'),
|
||||
},
|
||||
};
|
||||
|
||||
const currentDocumentFlow = documentFlow[step];
|
||||
|
||||
useEffect(() => {
|
||||
analytics.startSessionRecording('marketing_session_recording_spm');
|
||||
|
||||
return () => {
|
||||
analytics.stopSessionRecording();
|
||||
};
|
||||
}, [analytics]);
|
||||
|
||||
/**
|
||||
* Insert the selected fields into the local state.
|
||||
*/
|
||||
const onFieldsSubmit = (data: TAddFieldsFormSchema) => {
|
||||
if (!uploadedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFields(
|
||||
data.fields.map((field, i) => ({
|
||||
id: i,
|
||||
documentId: -1,
|
||||
recipientId: -1,
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: new Prisma.Decimal(field.pageX),
|
||||
positionY: new Prisma.Decimal(field.pageY),
|
||||
width: new Prisma.Decimal(field.pageWidth),
|
||||
height: new Prisma.Decimal(field.pageHeight),
|
||||
customText: '',
|
||||
inserted: false,
|
||||
})),
|
||||
);
|
||||
|
||||
analytics.capture('Marketing: SPM - Fields added');
|
||||
|
||||
documentFlow.fields.onNextStep?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload, create, sign and send the document.
|
||||
*/
|
||||
const onSignSubmit = async (data: TAddSignatureFormSchema) => {
|
||||
if (!uploadedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const putFileData = await putFile(uploadedFile.file);
|
||||
|
||||
const documentToken = await createSinglePlayerDocument({
|
||||
documentData: {
|
||||
type: putFileData.type,
|
||||
data: putFileData.data,
|
||||
},
|
||||
documentName: uploadedFile.file.name,
|
||||
signer: data,
|
||||
fields: fields.map((field) => ({
|
||||
page: field.page,
|
||||
type: field.type,
|
||||
positionX: field.positionX.toNumber(),
|
||||
positionY: field.positionY.toNumber(),
|
||||
width: field.width.toNumber(),
|
||||
height: field.height.toNumber(),
|
||||
})),
|
||||
});
|
||||
|
||||
analytics.capture('Marketing: SPM - Document signed', {
|
||||
signer: data.email,
|
||||
});
|
||||
|
||||
router.push(`/single-player-mode/${documentToken}/success`);
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description: 'Please try again later.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const placeholderRecipient: Recipient = {
|
||||
id: -1,
|
||||
documentId: -1,
|
||||
email: '',
|
||||
name: '',
|
||||
token: '',
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
readStatus: 'OPENED',
|
||||
signingStatus: 'NOT_SIGNED',
|
||||
sendStatus: 'NOT_SENT',
|
||||
};
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const base64String = base64.encode(new Uint8Array(arrayBuffer));
|
||||
|
||||
setUploadedFile({
|
||||
file,
|
||||
fileBase64: `data:application/pdf;base64,${base64String}`,
|
||||
});
|
||||
|
||||
analytics.capture('Marketing: SPM - Document uploaded');
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description: 'Please try again later.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6 sm:mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold lg:text-5xl">Single Player Mode</h1>
|
||||
|
||||
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
|
||||
View our{' '}
|
||||
<Link
|
||||
href={'/pricing'}
|
||||
target="_blank"
|
||||
className="hover:text-foreground/80 font-semibold transition-colors"
|
||||
>
|
||||
community plan
|
||||
</Link>{' '}
|
||||
for exclusive features, including the ability to collaborate with multiple signers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid w-full grid-cols-12 gap-8">
|
||||
<div className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7">
|
||||
{uploadedFile ? (
|
||||
<Card gradient>
|
||||
<CardContent className="p-2">
|
||||
<LazyPDFViewer document={uploadedFile.fileBase64} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<DocumentDropzone className="h-[80vh] max-h-[60rem]" onDrop={onFileDrop} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
|
||||
<DocumentFlowFormContainerHeader
|
||||
title={currentDocumentFlow.title}
|
||||
description={currentDocumentFlow.description}
|
||||
/>
|
||||
|
||||
{/* Add fields to PDF page. */}
|
||||
{step === 'fields' && (
|
||||
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
|
||||
<AddFieldsFormPartial
|
||||
documentFlow={documentFlow.fields}
|
||||
hideRecipients={true}
|
||||
recipients={uploadedFile ? [placeholderRecipient] : []}
|
||||
numberOfSteps={Object.keys(documentFlow).length}
|
||||
fields={fields}
|
||||
onSubmit={onFieldsSubmit}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{/* Enter user details and signature. */}
|
||||
{step === 'sign' && (
|
||||
<AddSignatureFormPartial
|
||||
documentFlow={documentFlow.sign}
|
||||
numberOfSteps={Object.keys(documentFlow).length}
|
||||
fields={fields}
|
||||
onSubmit={onSignSubmit}
|
||||
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
|
||||
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
|
||||
/>
|
||||
)}
|
||||
</DocumentFlowFormContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,20 @@
|
||||
import { Inter } from 'next/font/google';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { Caveat, Inter } from 'next/font/google';
|
||||
|
||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Toaster } from '@documenso/ui/primitives/toaster';
|
||||
|
||||
import { ThemeProvider } from '~/providers/next-theme';
|
||||
import { PlausibleProvider } from '~/providers/plausible';
|
||||
import { PostHogPageview } from '~/providers/posthog';
|
||||
|
||||
import './globals.css';
|
||||
|
||||
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
||||
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
|
||||
|
||||
export const metadata = {
|
||||
title: 'Documenso - The Open Source DocuSign Alternative',
|
||||
@ -32,9 +40,15 @@ export const metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getAllAnonymousFlags();
|
||||
|
||||
return (
|
||||
<html lang="en" className={fontInter.variable} suppressHydrationWarning>
|
||||
<html
|
||||
lang="en"
|
||||
className={cn(fontInter.variable, fontCaveat.variable)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
@ -42,8 +56,17 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</head>
|
||||
|
||||
<Suspense>
|
||||
<PostHogPageview />
|
||||
</Suspense>
|
||||
|
||||
<body>
|
||||
<PlausibleProvider>{children}</PlausibleProvider>
|
||||
<FeatureFlagProvider initialFlags={flags}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<PlausibleProvider>{children}</PlausibleProvider>
|
||||
</ThemeProvider>
|
||||
</FeatureFlagProvider>
|
||||
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
65
apps/marketing/src/app/not-found.tsx
Normal file
65
apps/marketing/src/app/not-found.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import backgroundPattern from '~/assets/background-pattern.png';
|
||||
|
||||
export default function NotFound() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
|
||||
<div className="absolute -inset-24 -z-10">
|
||||
<motion.div
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
|
||||
>
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
|
||||
<div>
|
||||
<p className="text-muted-foreground font-semibold">404 Page not found</p>
|
||||
|
||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-sm">
|
||||
The page you are looking for was moved, removed, renamed or might never have existed.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
|
||||
<Button className="w-32" asChild>
|
||||
<Link href="/">Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -4,11 +4,11 @@ import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/*',
|
||||
disallow: ['/_next/*'],
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
},
|
||||
],
|
||||
sitemap: `${getBaseUrl()}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user