diff --git a/apps/remix/app/components/marketing/Features.tsx b/apps/remix/app/components/marketing/Features.tsx
new file mode 100644
index 0000000000..0277da8b0b
--- /dev/null
+++ b/apps/remix/app/components/marketing/Features.tsx
@@ -0,0 +1,98 @@
+import { motion } from 'framer-motion';
+import type { LucideIcon } from 'lucide-react';
+import { Building2, Code2, Copy, PenTool, ShieldCheck, Workflow } from 'lucide-react';
+import type { ReactNode } from 'react';
+
+import { fadeUp, staggerFast } from './motion-variants';
+import { SectionHeading } from './SectionHeading';
+
+interface Feature {
+ icon: LucideIcon;
+ title: string;
+ body: ReactNode;
+}
+
+const FEATURES: Feature[] = [
+ {
+ icon: PenTool,
+ title: 'Legally binding e-signatures',
+ body: 'Send a document, get it signed. Every signature is timestamped and certified with a full audit trail attached to the final PDF.',
+ },
+ {
+ icon: Copy,
+ title: 'Reusable templates',
+ body: 'Build a document once (contract, NDA, agreement) then send it to as many recipients as you need without starting over.',
+ },
+ {
+ icon: Workflow,
+ title: 'Multi-recipient signing order',
+ body: 'Route a document to multiple signers and approvers in order, and track exactly where it is in the process at any moment.',
+ },
+ {
+ icon: Building2,
+ title: 'Teams, organizations & SSO',
+ body: 'Invite your team with role-based permissions, shared templates, and an organization workspace that keeps everything in one place.',
+ },
+ {
+ icon: Code2,
+ title: 'Embed, API & webhooks',
+ body: 'Embed signing directly inside your own product, automate document creation through the API, or trigger workflows off webhook events.',
+ },
+ {
+ icon: ShieldCheck,
+ title: 'Audit trail & certificates',
+ body: 'Every open, view, and signature is logged, with a certificate of completion attached to the final PDF so you always have a record.',
+ },
+];
+
+function FeatureCard({ feature }: { feature: Feature }) {
+ return (
+
+
+
+
+ {feature.title}
+ {feature.body}
+
+ );
+}
+
+export function Features() {
+ return (
+
+
+
+
+
+
+ {FEATURES.map((feature) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/FinalCta.tsx b/apps/remix/app/components/marketing/FinalCta.tsx
new file mode 100644
index 0000000000..3594806551
--- /dev/null
+++ b/apps/remix/app/components/marketing/FinalCta.tsx
@@ -0,0 +1,51 @@
+import { motion } from 'framer-motion';
+import { ArrowRight } from 'lucide-react';
+import { Link } from 'react-router';
+
+import { scrollToSection } from './scroll-to-section';
+
+export function FinalCta() {
+ return (
+
+
+
+
+
+ Your next contract could be signed by lunch
+
+
+ No back-and-forth, no printing and scanning. Send your first document and see how fast getting it signed can
+ be.
+
+
+ Free to start · No credit card required
+
+
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/Hero.tsx b/apps/remix/app/components/marketing/Hero.tsx
new file mode 100644
index 0000000000..ea52f05148
--- /dev/null
+++ b/apps/remix/app/components/marketing/Hero.tsx
@@ -0,0 +1,67 @@
+import { motion } from 'framer-motion';
+import { ArrowRight } from 'lucide-react';
+import { Link } from 'react-router';
+
+import { Highlight } from './Highlight';
+import { fadeUp, stagger } from './motion-variants';
+import { SigningPreviewCard } from './SigningPreviewCard';
+import { scrollToSection } from './scroll-to-section';
+
+function HeroBackground() {
+ return (
+
+ );
+}
+
+export function Hero() {
+ return (
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/Highlight.tsx b/apps/remix/app/components/marketing/Highlight.tsx
new file mode 100644
index 0000000000..cc6f10b983
--- /dev/null
+++ b/apps/remix/app/components/marketing/Highlight.tsx
@@ -0,0 +1,5 @@
+import type { ReactNode } from 'react';
+
+export function Highlight({ children }: { children: ReactNode }) {
+ return {children} ;
+}
diff --git a/apps/remix/app/components/marketing/HowItWorks.tsx b/apps/remix/app/components/marketing/HowItWorks.tsx
new file mode 100644
index 0000000000..0abfc3581f
--- /dev/null
+++ b/apps/remix/app/components/marketing/HowItWorks.tsx
@@ -0,0 +1,66 @@
+import { motion } from 'framer-motion';
+import type { LucideIcon } from 'lucide-react';
+import { FileUp, Send, Signature } from 'lucide-react';
+
+import { fadeUp, staggerFast } from './motion-variants';
+import { SectionHeading } from './SectionHeading';
+
+interface Step {
+ icon: LucideIcon;
+ title: string;
+ description: string;
+}
+
+const STEPS: Step[] = [
+ {
+ icon: FileUp,
+ title: 'Upload & prepare',
+ description: 'Drop in a PDF and add signature, date, and text fields exactly where you need them.',
+ },
+ {
+ icon: Send,
+ title: 'Send to recipients',
+ description: 'Add signers, set a signing order if it matters, and send. Each person gets a secure link.',
+ },
+ {
+ icon: Signature,
+ title: 'Track & get notified',
+ description: 'Watch status updates in real time. The signed, certified document lands in your inbox automatically.',
+ },
+];
+
+export function HowItWorks() {
+ return (
+
+
+
+
+
+
+ {STEPS.map((step, index) => (
+
+
+
+
+
+ Step {index + 1}
+
+ {step.title}
+ {step.description}
+
+ ))}
+
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/LandingPage.tsx b/apps/remix/app/components/marketing/LandingPage.tsx
new file mode 100644
index 0000000000..614881604d
--- /dev/null
+++ b/apps/remix/app/components/marketing/LandingPage.tsx
@@ -0,0 +1,21 @@
+import { Features } from './Features';
+import { FinalCta } from './FinalCta';
+import { Hero } from './Hero';
+import { HowItWorks } from './HowItWorks';
+import { MarketingFooter } from './MarketingFooter';
+import { MarketingHeader } from './MarketingHeader';
+
+export function LandingPage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/MarketingFooter.tsx b/apps/remix/app/components/marketing/MarketingFooter.tsx
new file mode 100644
index 0000000000..16e8745016
--- /dev/null
+++ b/apps/remix/app/components/marketing/MarketingFooter.tsx
@@ -0,0 +1,42 @@
+import { BrandingLogo } from '~/components/general/branding-logo';
+
+import { scrollToSection } from './scroll-to-section';
+
+const PRODUCT_LINKS = [
+ { label: 'Features', href: '#features' },
+ { label: 'How it works', href: '#how-it-works' },
+];
+
+export function MarketingFooter() {
+ return (
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/MarketingHeader.tsx b/apps/remix/app/components/marketing/MarketingHeader.tsx
new file mode 100644
index 0000000000..abc8f4a16b
--- /dev/null
+++ b/apps/remix/app/components/marketing/MarketingHeader.tsx
@@ -0,0 +1,112 @@
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@documenso/ui/primitives/sheet';
+import { Menu } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import { Link } from 'react-router';
+
+import { BrandingLogo } from '~/components/general/branding-logo';
+
+import { scrollToSection } from './scroll-to-section';
+import { ThemeToggle } from './ThemeToggle';
+
+const NAV_LINKS = [
+ { label: 'Features', href: '#features' },
+ { label: 'How it works', href: '#how-it-works' },
+];
+
+export function MarketingHeader() {
+ const [isScrolled, setIsScrolled] = useState(false);
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+
+ useEffect(() => {
+ const updateScrolled = () => setIsScrolled(window.scrollY > 8);
+
+ updateScrolled();
+ window.addEventListener('scroll', updateScrolled, { passive: true });
+
+ return () => window.removeEventListener('scroll', updateScrolled);
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/SectionHeading.tsx b/apps/remix/app/components/marketing/SectionHeading.tsx
new file mode 100644
index 0000000000..ff01b5373d
--- /dev/null
+++ b/apps/remix/app/components/marketing/SectionHeading.tsx
@@ -0,0 +1,24 @@
+import { cn } from '@documenso/ui/lib/utils';
+import type { ReactNode } from 'react';
+
+interface SectionHeadingProps {
+ eyebrow: string;
+ title: ReactNode;
+ description?: ReactNode;
+ centered?: boolean;
+ className?: string;
+}
+
+export function SectionHeading({ eyebrow, title, description, centered = false, className }: SectionHeadingProps) {
+ return (
+
+
{eyebrow}
+
+ {title}
+
+ {description && (
+
{description}
+ )}
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/SigningPreviewCard.tsx b/apps/remix/app/components/marketing/SigningPreviewCard.tsx
new file mode 100644
index 0000000000..e9712841f9
--- /dev/null
+++ b/apps/remix/app/components/marketing/SigningPreviewCard.tsx
@@ -0,0 +1,51 @@
+import { CheckCircle2, Clock, FileText } from 'lucide-react';
+
+const RECIPIENTS = [
+ { name: 'Jordan Ellis', role: 'Signer', status: 'signed' as const },
+ { name: 'Melissa Barron', role: 'Signer', status: 'signed' as const },
+ { name: 'Marcus Webb', role: 'Approver', status: 'pending' as const },
+];
+
+export function SigningPreviewCard() {
+ return (
+
+
+
+
+
+
+
+
Master Services Agreement
+
Sent 2 minutes ago
+
+
+
In progress
+
+
+
+ {RECIPIENTS.map((recipient) => (
+
+
+
{recipient.name}
+
{recipient.role}
+
+ {recipient.status === 'signed' ? (
+
+
+ Signed
+
+ ) : (
+
+
+ Awaiting
+
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/ThemeToggle.tsx b/apps/remix/app/components/marketing/ThemeToggle.tsx
new file mode 100644
index 0000000000..97e517d6a7
--- /dev/null
+++ b/apps/remix/app/components/marketing/ThemeToggle.tsx
@@ -0,0 +1,20 @@
+import { Button } from '@documenso/ui/primitives/button';
+import { Moon, Sun } from 'lucide-react';
+import { Theme, useTheme } from 'remix-themes';
+
+export function ThemeToggle() {
+ const [theme, setTheme] = useTheme();
+ const isDark = theme === Theme.DARK;
+
+ return (
+ setTheme(isDark ? Theme.LIGHT : Theme.DARK)}
+ >
+ {isDark ? : }
+
+ );
+}
diff --git a/apps/remix/app/components/marketing/motion-variants.ts b/apps/remix/app/components/marketing/motion-variants.ts
new file mode 100644
index 0000000000..1048a5a642
--- /dev/null
+++ b/apps/remix/app/components/marketing/motion-variants.ts
@@ -0,0 +1,22 @@
+import type { Variants } from 'framer-motion';
+
+const ease = [0.22, 1, 0.36, 1] as const;
+
+export const fadeUp: Variants = {
+ hidden: { opacity: 0, y: 24 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.55, ease },
+ },
+};
+
+export const stagger: Variants = {
+ hidden: {},
+ show: { transition: { staggerChildren: 0.09 } },
+};
+
+export const staggerFast: Variants = {
+ hidden: {},
+ show: { transition: { staggerChildren: 0.07 } },
+};
diff --git a/apps/remix/app/components/marketing/scroll-to-section.ts b/apps/remix/app/components/marketing/scroll-to-section.ts
new file mode 100644
index 0000000000..31457d6a2c
--- /dev/null
+++ b/apps/remix/app/components/marketing/scroll-to-section.ts
@@ -0,0 +1,19 @@
+import type { MouseEvent } from 'react';
+
+/**
+ * Scrolls via JS instead of letting the browser handle the `href="#id"` navigation natively.
+ * Real bug this works around: React Router's `` fights with the native
+ * hash-triggered smooth scroll when a second in-page link is clicked before the first scroll
+ * finishes, freezing the page partway between the two sections. The `href` is kept on the
+ * anchor so it still works (as an instant jump) before hydration or with JS disabled.
+ */
+export function scrollToSection(event: MouseEvent, id: string) {
+ const target = document.getElementById(id);
+
+ if (!target) {
+ return;
+ }
+
+ event.preventDefault();
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+}
diff --git a/apps/remix/app/routes/_index.tsx b/apps/remix/app/routes/_index.tsx
index b684169375..d476f2881c 100644
--- a/apps/remix/app/routes/_index.tsx
+++ b/apps/remix/app/routes/_index.tsx
@@ -5,47 +5,52 @@ import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { ZTeamUrlSchema } from '@documenso/trpc/server/team-router/schema';
import { redirect } from 'react-router';
+import { LandingPage } from '~/components/marketing/LandingPage';
+
import type { Route } from './+types/_index';
export async function loader({ request }: Route.LoaderArgs) {
const session = await getOptionalSession(request);
- if (session.isAuthenticated) {
- const teamUrlCookie = extractCookieFromHeaders('preferred-team-url', request.headers);
+ if (!session.isAuthenticated) {
+ return null;
+ }
- // const referrer = request.headers.get('referer');
- // let isReferrerFromTeamUrl = false;
+ const teamUrlCookie = extractCookieFromHeaders('preferred-team-url', request.headers);
- // if (referrer) {
- // const referrerUrl = new URL(referrer);
+ // const referrer = request.headers.get('referer');
+ // let isReferrerFromTeamUrl = false;
- // if (referrerUrl.pathname.startsWith('/t/')) {
- // isReferrerFromTeamUrl = true;
- // }
- // }
+ // if (referrer) {
+ // const referrerUrl = new URL(referrer);
- const preferredTeamUrl =
- teamUrlCookie && ZTeamUrlSchema.safeParse(teamUrlCookie).success ? teamUrlCookie : undefined;
+ // if (referrerUrl.pathname.startsWith('/t/')) {
+ // isReferrerFromTeamUrl = true;
+ // }
+ // }
- // // Early return for no preferred team.
- // if (!preferredTeamUrl || isReferrerFromTeamUrl) {
- // throw redirect('/inbox');
- // }
+ const preferredTeamUrl = teamUrlCookie && ZTeamUrlSchema.safeParse(teamUrlCookie).success ? teamUrlCookie : undefined;
- const teams = await getTeams({ userId: session.user.id });
+ // // Early return for no preferred team.
+ // if (!preferredTeamUrl || isReferrerFromTeamUrl) {
+ // throw redirect('/inbox');
+ // }
- let currentTeam = teams.find((team) => team.url === preferredTeamUrl);
+ const teams = await getTeams({ userId: session.user.id });
- if (!currentTeam && teams.length === 1) {
- currentTeam = teams[0];
- }
+ let currentTeam = teams.find((team) => team.url === preferredTeamUrl);
- if (!currentTeam) {
- throw redirect('/inbox');
- }
+ if (!currentTeam && teams.length === 1) {
+ currentTeam = teams[0];
+ }
- throw redirect(formatDocumentsPath(currentTeam.url));
+ if (!currentTeam) {
+ throw redirect('/inbox');
}
- throw redirect('/signin');
+ throw redirect(formatDocumentsPath(currentTeam.url));
+}
+
+export default function IndexPage() {
+ return ;
}
diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css
index a13c21f984..61ab809f62 100644
--- a/packages/ui/styles/theme.css
+++ b/packages/ui/styles/theme.css
@@ -189,6 +189,10 @@
}
@layer base {
+ html {
+ scroll-behavior: smooth;
+ }
+
* {
@apply border-border;
}