Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions apps/remix/app/components/marketing/Features.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<motion.div
variants={fadeUp}
className="relative overflow-hidden rounded-2xl border bg-card p-6 shadow-sm dark:border-white/10 dark:bg-card/80"
>
<div className="flex size-11 items-center justify-center rounded-xl border bg-secondary/40 text-primary shadow-sm">
<feature.icon className="size-5" />
</div>
<div className="mt-5 font-semibold text-lg tracking-[-0.01em]">{feature.title}</div>
<p className="mt-2 text-muted-foreground text-sm leading-relaxed">{feature.body}</p>
</motion.div>
);
}

export function Features() {
return (
<section
id="features"
className="relative scroll-mt-20 overflow-hidden border-y bg-gradient-to-b from-background via-muted/40 to-background"
>
<div className="relative mx-auto max-w-6xl px-6 py-24">
<motion.div
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.2 }}
variants={fadeUp}
className="mx-auto max-w-2xl"
>
<SectionHeading
centered
eyebrow="Features"
title="Everything you need to get documents signed"
description="From the first upload to the final signature — Keep Contracts covers the whole workflow without getting in your way."
/>
</motion.div>
<motion.div
className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3"
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.1 }}
variants={staggerFast}
>
{FEATURES.map((feature) => (
<FeatureCard key={feature.title} feature={feature} />
))}
</motion.div>
</div>
</section>
);
}
51 changes: 51 additions & 0 deletions apps/remix/app/components/marketing/FinalCta.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="border-t">
<div className="mx-auto max-w-6xl px-6 py-20">
<motion.div
initial={{ opacity: 0, y: 32, scale: 0.98 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
viewport={{ once: true, amount: 0.25 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="relative isolate overflow-hidden rounded-3xl bg-primary px-8 py-16 text-center text-primary-foreground"
>
<div
aria-hidden
className="absolute inset-0 -z-10 opacity-[0.08] [background-image:linear-gradient(to_right,#fff_1px,transparent_1px),linear-gradient(to_bottom,#fff_1px,transparent_1px)] [background-size:40px_40px] [mask-image:radial-gradient(ellipse_80%_80%_at_50%_50%,#000_40%,transparent_100%)]"
/>
<h2 className="mx-auto max-w-xl text-balance font-bold text-[clamp(28px,3.6vw,42px)] leading-[1.1] tracking-[-0.025em]">
Your next contract could be signed by lunch
</h2>
<p className="mx-auto mt-4 max-w-lg text-base leading-relaxed opacity-85">
No back-and-forth, no printing and scanning. Send your first document and see how fast getting it signed can
be.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-3">
<Link
to="/signup"
className="group inline-flex h-12 items-center gap-2 rounded-md bg-background px-7 font-medium text-[15px] text-primary transition-all hover:-translate-y-px hover:shadow-lg"
>
Sign up for free
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
</Link>
{/* 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) */}
<a
href="#features"
onClick={(event) => 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
</a>
</div>
<p className="mt-6 text-[13px] opacity-75">Free to start · No credit card required</p>
</motion.div>
</div>
</section>
);
}
67 changes: 67 additions & 0 deletions apps/remix/app/components/marketing/Hero.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
<div className="absolute inset-0 opacity-60 [background-image:linear-gradient(to_right,hsl(var(--border))_1px,transparent_1px),linear-gradient(to_bottom,hsl(var(--border))_1px,transparent_1px)] [background-size:56px_56px] [mask-image:radial-gradient(ellipse_70%_55%_at_50%_0%,#000_60%,transparent_100%)]" />
<div className="absolute top-[-12%] left-1/2 h-[480px] w-[800px] max-w-full -translate-x-1/2 rounded-full bg-primary/10 blur-[130px]" />
</div>
);
}

export function Hero() {
return (
<section className="relative isolate overflow-hidden px-6 pt-32 pb-20 text-center">
<HeroBackground />
<motion.div className="relative z-10 mx-auto max-w-4xl" variants={stagger} initial="hidden" animate="show">
<motion.h1
variants={fadeUp}
className="mx-auto max-w-3xl text-balance font-bold text-[clamp(36px,5.6vw,60px)] leading-[1.05] tracking-[-0.03em]"
>
Get contracts <Highlight>signed</Highlight>, not stuck in someone&apos;s inbox
</motion.h1>

<motion.p
variants={fadeUp}
className="mx-auto mt-6 max-w-xl text-pretty text-lg text-muted-foreground leading-relaxed"
>
Fast, secure e-signing for teams and individuals.
</motion.p>

<motion.div variants={fadeUp} className="mt-9 flex flex-wrap justify-center gap-3">
<Link
to="/signup"
className="group inline-flex h-12 items-center gap-2 rounded-md bg-primary px-7 font-semibold text-[15px] text-primary-foreground shadow-lg ring-1 ring-primary/20 transition-all hover:-translate-y-px hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
>
Sign up for free
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
</Link>
{/* 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) */}
<a
href="#how-it-works"
onClick={(event) => 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
</a>
</motion.div>
</motion.div>

<motion.div
className="relative z-10 mt-16"
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: 0.3 }}
>
<SigningPreviewCard />
</motion.div>
</section>
);
}
5 changes: 5 additions & 0 deletions apps/remix/app/components/marketing/Highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ReactNode } from 'react';

export function Highlight({ children }: { children: ReactNode }) {
return <span className="bg-[linear-gradient(transparent_78%,hsl(var(--primary))_78%)] px-0.5">{children}</span>;
}
66 changes: 66 additions & 0 deletions apps/remix/app/components/marketing/HowItWorks.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section id="how-it-works" className="mx-auto max-w-6xl scroll-mt-20 px-6 py-24">
<motion.div initial="hidden" whileInView="show" viewport={{ once: true, amount: 0.2 }} variants={fadeUp}>
<SectionHeading
centered
eyebrow="How it works"
title="From upload to signed in three steps"
description="Keep Contracts keeps the workflow frictionless, so getting a document signed takes minutes, not days."
/>
</motion.div>

<motion.div
className="mt-14 grid gap-6 md:grid-cols-3"
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.2 }}
variants={staggerFast}
>
{STEPS.map((step, index) => (
<motion.div key={step.title} variants={fadeUp} className="relative rounded-2xl border p-6 shadow-sm">
<div className="flex size-11 items-center justify-center rounded-xl bg-primary/10 text-primary">
<step.icon className="size-5" />
</div>
<div className="mt-5 font-semibold text-muted-foreground text-xs uppercase tracking-[0.08em]">
Step {index + 1}
</div>
<div className="mt-1.5 font-semibold text-lg tracking-[-0.01em]">{step.title}</div>
<p className="mt-2 text-muted-foreground text-sm leading-relaxed">{step.description}</p>
</motion.div>
))}
</motion.div>
</section>
);
}
21 changes: 21 additions & 0 deletions apps/remix/app/components/marketing/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex min-h-screen flex-col bg-background text-foreground">
<MarketingHeader />
<main id="top" className="flex-1">
<Hero />
<HowItWorks />
<Features />
<FinalCta />
</main>
<MarketingFooter />
</div>
);
}
42 changes: 42 additions & 0 deletions apps/remix/app/components/marketing/MarketingFooter.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<footer id="about" className="scroll-mt-20 border-t bg-muted">
<div className="mx-auto max-w-6xl px-6 pt-14 pb-8">
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-10">
<div className="max-w-[280px]">
<BrandingLogo className="h-6 w-auto" />
<p className="mt-3 text-[13px] text-muted-foreground leading-relaxed">
Send, sign, and manage contracts with ease, all in one place.
</p>
</div>
<nav aria-label="Product" className="flex flex-col gap-2.5">
<div className="font-semibold text-[13px] text-foreground">Product</div>
{PRODUCT_LINKS.map((link) => (
<a
key={link.label}
href={link.href}
onClick={(event) => scrollToSection(event, link.href.slice(1))}
className="text-[13px] text-muted-foreground transition-colors hover:text-foreground"
>
{link.label}
</a>
))}
</nav>
</div>
<div className="mt-10 flex flex-wrap justify-between gap-3 border-t pt-5 text-muted-foreground text-xs">
<span>© {new Date().getFullYear()} DataThink. All rights reserved.</span>
<span>Keep Contracts — secure document signing</span>
</div>
</div>
</footer>
);
}
Loading
Loading