From dd4fd0b55d3d2c525787ba747b455d954f34a029 Mon Sep 17 00:00:00 2001 From: Austin Mangelson Date: Tue, 21 Jul 2026 16:41:42 -0600 Subject: [PATCH 1/2] feat: add marketing components and landing page structure --- .../app/components/marketing/Features.tsx | 98 +++++++++++++++ .../app/components/marketing/FinalCta.tsx | 51 ++++++++ apps/remix/app/components/marketing/Hero.tsx | 67 +++++++++++ .../app/components/marketing/Highlight.tsx | 5 + .../app/components/marketing/HowItWorks.tsx | 66 +++++++++++ .../app/components/marketing/LandingPage.tsx | 21 ++++ .../components/marketing/MarketingFooter.tsx | 42 +++++++ .../components/marketing/MarketingHeader.tsx | 112 ++++++++++++++++++ .../components/marketing/SectionHeading.tsx | 24 ++++ .../marketing/SigningPreviewCard.tsx | 51 ++++++++ .../app/components/marketing/ThemeToggle.tsx | 20 ++++ .../components/marketing/motion-variants.ts | 22 ++++ .../components/marketing/scroll-to-section.ts | 19 +++ apps/remix/app/routes/_index.tsx | 57 +++++---- packages/ui/styles/theme.css | 4 + 15 files changed, 633 insertions(+), 26 deletions(-) create mode 100644 apps/remix/app/components/marketing/Features.tsx create mode 100644 apps/remix/app/components/marketing/FinalCta.tsx create mode 100644 apps/remix/app/components/marketing/Hero.tsx create mode 100644 apps/remix/app/components/marketing/Highlight.tsx create mode 100644 apps/remix/app/components/marketing/HowItWorks.tsx create mode 100644 apps/remix/app/components/marketing/LandingPage.tsx create mode 100644 apps/remix/app/components/marketing/MarketingFooter.tsx create mode 100644 apps/remix/app/components/marketing/MarketingHeader.tsx create mode 100644 apps/remix/app/components/marketing/SectionHeading.tsx create mode 100644 apps/remix/app/components/marketing/SigningPreviewCard.tsx create mode 100644 apps/remix/app/components/marketing/ThemeToggle.tsx create mode 100644 apps/remix/app/components/marketing/motion-variants.ts create mode 100644 apps/remix/app/components/marketing/scroll-to-section.ts 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..d63d14ec10 --- /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 or scanning. Send your first document and see how fast getting it signed can + be. +

+
+ + Sign up for free + + + {/* biome-ignore lint/a11y/useValidAnchor: real href for pre-hydration/no-JS fallback; onClick upgrades to a JS scroll to avoid a ScrollRestoration/native-hash-nav race (see scroll-to-section.ts) */} + scrollToSection(event, 'features')} + className="inline-flex h-12 items-center rounded-md border border-primary-foreground/40 px-7 font-medium text-[15px] transition-all hover:bg-primary-foreground/10" + > + Explore features + +
+

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 ( +
+ + + + Get contracts signed, not stuck in someone's inbox + + + + Fast, secure e-signing for teams and individuals. + + + + + Sign up for free + + + {/* biome-ignore lint/a11y/useValidAnchor: real href for pre-hydration/no-JS fallback; onClick upgrades to a JS scroll to avoid a ScrollRestoration/native-hash-nav race (see scroll-to-section.ts) */} + scrollToSection(event, 'how-it-works')} + className="inline-flex h-12 items-center rounded-md border-2 border-foreground/15 bg-background/70 px-7 font-semibold text-[15px] text-foreground shadow-sm backdrop-blur transition-all hover:-translate-y-px hover:border-primary/40 hover:bg-background" + > + See how it works + + + + + + + +
+ ); +} 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 ( +
+
+ {/* biome-ignore lint/a11y/useValidAnchor: real href for pre-hydration/no-JS fallback; onClick upgrades to a JS scroll to avoid a ScrollRestoration/native-hash-nav race (see scroll-to-section.ts) */} + scrollToSection(event, 'top')} aria-label="Keep Contracts home"> + + + + + +
+ + + +
+ + +
+
+ ); +} 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 ( + + ); +} 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; } From eadea65964903462996e539af11da6425ffc00d1 Mon Sep 17 00:00:00 2001 From: Austin Mangelson Date: Tue, 21 Jul 2026 16:43:42 -0600 Subject: [PATCH 2/2] chore: small language change --- apps/remix/app/components/marketing/FinalCta.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/remix/app/components/marketing/FinalCta.tsx b/apps/remix/app/components/marketing/FinalCta.tsx index d63d14ec10..3594806551 100644 --- a/apps/remix/app/components/marketing/FinalCta.tsx +++ b/apps/remix/app/components/marketing/FinalCta.tsx @@ -23,7 +23,7 @@ export function FinalCta() { Your next contract could be signed by lunch

- No back-and-forth, no printing or scanning. Send your first document and see how fast getting it signed can + No back-and-forth, no printing and scanning. Send your first document and see how fast getting it signed can be.