mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
841 lines
36 KiB
TypeScript
841 lines
36 KiB
TypeScript
'use client';
|
|
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import { CtaLink } from '@/components/marketing/cta-link';
|
|
import { MarketingCompareLinks } from '@/components/marketing/marketing-compare-links';
|
|
import { useEffect, useRef } from 'react';
|
|
import { gsap } from 'gsap';
|
|
import {
|
|
Video,
|
|
MoveRight,
|
|
Play,
|
|
Mic,
|
|
Users,
|
|
Tag,
|
|
Code,
|
|
Lock,
|
|
Upload,
|
|
Share2,
|
|
MessageSquare,
|
|
Check,
|
|
CheckCircle2,
|
|
Copy,
|
|
Link as LinkIcon,
|
|
Github,
|
|
ArrowRight,
|
|
} from 'lucide-react';
|
|
|
|
interface LandingPageProps {
|
|
isLoggedIn: boolean;
|
|
}
|
|
|
|
const controlButtonClass =
|
|
'group relative isolate inline-flex h-8 items-center justify-center overflow-hidden border border-border bg-background px-2.5 text-[11px] font-medium text-foreground transition-colors duration-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:h-9 sm:px-4 sm:text-xs';
|
|
|
|
const primaryCtaClass =
|
|
'group relative isolate inline-flex min-h-12 items-center justify-center gap-2 overflow-hidden border border-primary bg-primary px-6 py-3 text-center text-[13px] font-semibold text-primary-foreground transition-colors duration-300 hover:bg-primary/90 sm:px-8 sm:text-sm';
|
|
|
|
const labelClass = 'text-[11px] uppercase tracking-[0.14em] text-muted-foreground';
|
|
|
|
const trustSignals = [
|
|
{ label: 'No client accounts', icon: Users },
|
|
{ label: 'Flat $10 per month', icon: Tag },
|
|
{ label: 'Fair Source, self-hostable', icon: Code },
|
|
{ label: 'Private by default', icon: Lock },
|
|
];
|
|
|
|
const steps = [
|
|
{
|
|
label: 'Upload a cut',
|
|
description: 'Drop a file, or import an unlisted YouTube video.',
|
|
icon: Upload,
|
|
},
|
|
{
|
|
label: 'Share the link',
|
|
description: 'Set permissions once, the client needs no account.',
|
|
icon: Share2,
|
|
},
|
|
{
|
|
label: 'Timestamped feedback',
|
|
description: 'Comments, voice notes and drawings land on the frame.',
|
|
icon: MessageSquare,
|
|
},
|
|
{
|
|
label: 'Approve and move on',
|
|
description: 'The cut gets a signed off Approved status, export the notes.',
|
|
icon: Check,
|
|
},
|
|
];
|
|
|
|
const hostedFeatures = [
|
|
'Unlimited collaborators and clients',
|
|
'Comments, voice notes, annotations',
|
|
'Version compare, history, approvals',
|
|
'Permissioned share links, PDF and CSV export',
|
|
'Unlimited unlisted YouTube imports',
|
|
'200 GB storage, add 100 GB for $5/mo',
|
|
];
|
|
|
|
const selfHostedFeatures = [
|
|
'Full source code, read it and audit it',
|
|
'Docker setup for self-hosting',
|
|
'Every release becomes Apache 2.0 two years after publication',
|
|
];
|
|
|
|
const faq = [
|
|
{
|
|
q: 'Do clients need an account?',
|
|
a: 'No. They can review in the browser with a share link.',
|
|
},
|
|
{
|
|
q: 'Is OpenFrame open source?',
|
|
a: 'OpenFrame is Fair Source, licensed under the Functional Source License (FSL). You can read and audit the full source code and self-host it, and every release automatically becomes Apache 2.0 open source two years after publication.',
|
|
},
|
|
{
|
|
q: 'Is there a free trial?',
|
|
a: 'Yes. Hosted Cloud starts with a 7-day free trial and never asks for a card to begin it. After that it is a flat $10/mo, with no per-seat or per-client fees.',
|
|
},
|
|
{
|
|
q: 'How is this different from a Google Drive link?',
|
|
a: 'Drive does not give timestamped discussion, voice notes, annotations, or version compare, which is where approval time is actually saved.',
|
|
},
|
|
{
|
|
q: 'What happens if I exceed my storage?',
|
|
a: 'You can add 100 GB for $5/mo. If you need much more, contact us at [email protected] and we will help you choose the best setup.',
|
|
},
|
|
{
|
|
q: 'Can I self-host?',
|
|
a: 'Yes. The full source code is public and ships with a Docker setup for self-hosting. Hosted Cloud is for teams who want zero setup.',
|
|
},
|
|
];
|
|
|
|
// Bar heights for the voice note waveform, in percent. The first twenty read
|
|
// as "played", the rest as "remaining".
|
|
const waveformBars = [
|
|
28, 52, 74, 40, 96, 62, 34, 80, 46, 90, 38, 66, 88, 30, 72, 50, 84, 42, 94, 56, 32, 68, 44, 86,
|
|
36, 60, 92, 48, 26, 70, 54, 82, 38, 64,
|
|
];
|
|
|
|
const shareOptions = [
|
|
{ label: 'Can comment', on: true },
|
|
{ label: 'Ask for a name before commenting', on: true },
|
|
{ label: 'Allow download of the original file', on: false },
|
|
{ label: 'Show earlier versions', on: false },
|
|
];
|
|
|
|
const heroGridStyle = {
|
|
backgroundImage:
|
|
'linear-gradient(to right, color-mix(in oklab, var(--foreground) 5%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 5%, transparent) 1px, transparent 1px)',
|
|
backgroundSize: '48px 48px',
|
|
maskImage:
|
|
'radial-gradient(ellipse 820px 640px at 50% 30%, #000 0%, rgba(0,0,0,0.6) 52%, transparent 80%)',
|
|
WebkitMaskImage:
|
|
'radial-gradient(ellipse 820px 640px at 50% 30%, #000 0%, rgba(0,0,0,0.6) 52%, transparent 80%)',
|
|
} as const;
|
|
|
|
const heroGlowStyle = {
|
|
background:
|
|
'radial-gradient(closest-side, color-mix(in oklab, var(--primary) 18%, transparent), color-mix(in oklab, var(--primary) 6%, transparent) 55%, transparent 100%)',
|
|
} as const;
|
|
|
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<h2 className="text-3xl font-semibold leading-[1.05] tracking-[-0.02em] md:text-[42px]">
|
|
{children}
|
|
</h2>
|
|
);
|
|
}
|
|
|
|
function MockToolbar({ left, right }: { left: React.ReactNode; right?: React.ReactNode }) {
|
|
return (
|
|
<div className="flex h-9 items-center justify-between gap-3 border-b border-border px-3">
|
|
<span className="truncate text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
|
|
{left}
|
|
</span>
|
|
{right}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Avatar({ initial }: { initial: string }) {
|
|
return (
|
|
<div className="flex h-[30px] w-[30px] shrink-0 items-center justify-center bg-secondary text-xs">
|
|
{initial}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Timecode({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<span className="bg-primary/10 px-2 py-[3px] text-[11px] tracking-[0.04em] text-primary">
|
|
{children}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function Toggle({ on }: { on: boolean }) {
|
|
return (
|
|
<div
|
|
aria-hidden="true"
|
|
className={`flex h-5 w-[38px] shrink-0 items-center px-[3px] ${on ? 'justify-end bg-primary' : 'justify-start bg-muted'}`}
|
|
>
|
|
<div
|
|
className={`h-[14px] w-[14px] ${on ? 'bg-primary-foreground' : 'bg-muted-foreground/60'}`}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function LandingPage({ isLoggedIn }: LandingPageProps) {
|
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
|
|
// Same button, two audiences. Once you are signed in it goes to the dashboard,
|
|
// and offering a trial to someone who is already using the product reads as a
|
|
// mistake rather than as an offer.
|
|
const hostedCtaLabel = isLoggedIn ? 'Open dashboard' : 'Start 7-day free trial, no card required';
|
|
|
|
useEffect(() => {
|
|
const ctx = gsap.context(() => {
|
|
gsap.from('[data-hero-copy]', {
|
|
y: 40,
|
|
opacity: 0,
|
|
duration: 1,
|
|
stagger: 0.15,
|
|
ease: 'power4.out',
|
|
});
|
|
|
|
const bars = gsap.utils.toArray<HTMLElement>('.voice-bar');
|
|
bars.forEach((bar, index) => {
|
|
gsap.set(bar, { transformOrigin: 'center center' });
|
|
gsap.to(bar, {
|
|
scaleY: gsap.utils.random(0.3, 1.5),
|
|
duration: gsap.utils.random(0.4, 0.8),
|
|
repeat: -1,
|
|
yoyo: true,
|
|
delay: index * 0.05,
|
|
ease: 'power2.inOut',
|
|
});
|
|
});
|
|
}, rootRef);
|
|
|
|
return () => {
|
|
ctx.revert();
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
ref={rootRef}
|
|
className="min-h-screen overflow-x-hidden bg-background text-foreground font-sans selection:bg-primary/20"
|
|
>
|
|
<header className="fixed inset-x-0 top-0 z-50 border-b border-border bg-background">
|
|
<div className="mx-auto flex h-14 w-full max-w-[1200px] items-center justify-between px-4 sm:h-16 sm:px-6 lg:px-10">
|
|
<Link
|
|
href="/"
|
|
className="group relative isolate inline-flex items-center gap-2 overflow-hidden border border-border bg-background px-3 py-2"
|
|
>
|
|
<span className="pointer-events-none absolute inset-0 -translate-x-[101%] bg-primary/10 transition-transform duration-300 group-hover:translate-x-0" />
|
|
<Video className="relative z-10 h-4 w-4 text-primary" />
|
|
<span className="relative z-10 text-xs font-semibold tracking-[0.12em]">OPENFRAME</span>
|
|
</Link>
|
|
|
|
<nav className="hidden items-center gap-6 text-[11px] font-medium uppercase tracking-[0.14em] md:flex">
|
|
<Link
|
|
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
href="#features"
|
|
>
|
|
Features
|
|
</Link>
|
|
<Link
|
|
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
href="#pricing"
|
|
>
|
|
Pricing
|
|
</Link>
|
|
<a
|
|
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
href="https://github.com/yusufipk/OpenFrame"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
GitHub
|
|
</a>
|
|
<a
|
|
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
href="mailto:[email protected]"
|
|
>
|
|
Contact
|
|
</a>
|
|
</nav>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{isLoggedIn ? (
|
|
<Link href="/dashboard" className={controlButtonClass}>
|
|
<span className="pointer-events-none absolute inset-0 -translate-x-[101%] bg-primary/10 transition-transform duration-300 group-hover:translate-x-0" />
|
|
<span className="relative z-10 inline-flex items-center gap-2">
|
|
Dashboard
|
|
<MoveRight className="h-3.5 w-3.5" />
|
|
</span>
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<Link
|
|
href="/login"
|
|
className="text-xs font-medium text-muted-foreground hover:text-foreground hidden sm:block mr-4"
|
|
>
|
|
Log in
|
|
</Link>
|
|
<Link href="/register" className={controlButtonClass}>
|
|
<span className="pointer-events-none absolute inset-0 -translate-x-[101%] bg-primary/10 transition-transform duration-300 group-hover:translate-x-0" />
|
|
<span className="relative z-10">Start free trial</span>
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="relative">
|
|
{/* 1) HERO */}
|
|
<section className="relative overflow-hidden px-4 pb-16 pt-20 sm:px-6 sm:pt-24 lg:px-8 lg:pb-24">
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-0"
|
|
style={heroGridStyle}
|
|
/>
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute left-1/2 top-[55%] h-[560px] w-[1000px] max-w-[140vw] -translate-x-1/2"
|
|
style={heroGlowStyle}
|
|
/>
|
|
|
|
<div className="relative mx-auto flex w-full max-w-[1200px] flex-col items-center gap-12 lg:gap-16">
|
|
<div className="flex w-full max-w-[900px] flex-col items-center gap-6 text-center">
|
|
<h1
|
|
data-hero-copy
|
|
className="text-4xl font-semibold leading-[0.95] tracking-[-0.03em] sm:text-5xl md:text-6xl lg:text-7xl"
|
|
>
|
|
Get client sign-off from one link.
|
|
</h1>
|
|
|
|
<p
|
|
data-hero-copy
|
|
className="max-w-[660px] text-base leading-relaxed text-muted-foreground md:text-lg"
|
|
>
|
|
Timestamped comments, voice notes and annotations on one timeline. Clients review in
|
|
the browser, no account.
|
|
</p>
|
|
|
|
<div data-hero-copy className="flex flex-col items-center gap-3.5">
|
|
<CtaLink href={hostedCtaHref} className={primaryCtaClass}>
|
|
{hostedCtaLabel}
|
|
<MoveRight className="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-1" />
|
|
</CtaLink>
|
|
<a
|
|
href="https://github.com/yusufipk/OpenFrame"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
>
|
|
Prefer self-hosting? View on GitHub <ArrowRight className="ml-1 inline h-3 w-3" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-hero-copy className="w-full max-w-[1100px]">
|
|
<div className="relative aspect-video w-full overflow-hidden border border-border bg-card shadow-2xl">
|
|
<video
|
|
src="/landing/hero-flow.mp4"
|
|
poster="/landing/deep-dive-dashboard-2.webp"
|
|
autoPlay
|
|
loop
|
|
muted
|
|
playsInline
|
|
className="absolute inset-0 h-full w-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 2) TRUST STRIP */}
|
|
<section className="border-y border-border bg-card/30 px-4 py-6 sm:px-6 lg:px-8">
|
|
<div className="mx-auto grid w-full max-w-[1200px] grid-cols-2 gap-px bg-border md:grid-cols-4">
|
|
{trustSignals.map((signal) => (
|
|
<div
|
|
key={signal.label}
|
|
className="flex items-center gap-2.5 bg-background px-4 py-3.5 sm:px-5"
|
|
>
|
|
<signal.icon className="h-4 w-4 shrink-0 text-primary" />
|
|
<span className="text-[11px] uppercase tracking-[0.14em]">{signal.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* 3) FEATURES */}
|
|
<section id="features" className="scroll-mt-20">
|
|
{/* Version compare */}
|
|
<div className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
|
<div className="mx-auto grid w-full max-w-[1200px] items-center gap-10 lg:grid-cols-[340px_minmax(0,1fr)] lg:gap-14">
|
|
<div className="flex flex-col gap-4">
|
|
<SectionTitle>See exactly what changed.</SectionTitle>
|
|
<p className="text-base leading-relaxed text-muted-foreground">
|
|
Put two cuts on one timeline and scrub them together, then approve without asking
|
|
which version this is.
|
|
</p>
|
|
</div>
|
|
<div className="min-w-0 border border-border bg-card">
|
|
<MockToolbar
|
|
left="Compare versions"
|
|
right={
|
|
<div className="flex items-center gap-2 text-[10px] tracking-[0.04em]">
|
|
<span className="bg-secondary px-2 py-[3px]">v2</span>
|
|
<span className="text-muted-foreground">against</span>
|
|
<span className="bg-primary/10 px-2 py-[3px] text-primary">v1</span>
|
|
</div>
|
|
}
|
|
/>
|
|
<div className="relative aspect-video w-full overflow-hidden bg-black">
|
|
<video
|
|
src="/landing/compare-cuts.mp4"
|
|
poster="/landing/compare-cuts-poster.webp"
|
|
aria-label="OpenFrame side by side version comparison demo"
|
|
autoPlay
|
|
loop
|
|
muted
|
|
playsInline
|
|
preload="none"
|
|
className="absolute inset-0 h-full w-full object-contain"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Voice notes and annotations */}
|
|
<div className="border-b border-border bg-card/30 px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
|
<div className="mx-auto grid w-full max-w-[1200px] items-center gap-10 lg:grid-cols-[minmax(0,1fr)_340px] lg:gap-14">
|
|
<div className="flex flex-col gap-4 lg:order-2">
|
|
<SectionTitle>Say it, or draw it.</SectionTitle>
|
|
<p className="text-base leading-relaxed text-muted-foreground">
|
|
Record a note or circle the frame, and both land on the exact second instead of in
|
|
a follow-up email.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="min-w-0 border border-border bg-card lg:order-1">
|
|
<MockToolbar
|
|
left="Comment thread, Version 3"
|
|
right={
|
|
<span className="hidden text-[10px] uppercase tracking-[0.14em] text-muted-foreground sm:block">
|
|
Sorted by timecode
|
|
</span>
|
|
}
|
|
/>
|
|
<div className="flex flex-col gap-3.5 p-4 sm:p-5">
|
|
<div className="flex flex-col gap-3.5 border border-border bg-background p-4 sm:p-[18px]">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="flex items-center gap-2.5">
|
|
<Avatar initial="M" />
|
|
<span className="text-[13px] font-medium">Michael A.</span>
|
|
<Timecode>00:04:44</Timecode>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-[10px] uppercase tracking-[0.14em] text-primary">
|
|
<Mic className="h-3.5 w-3.5" />
|
|
<span>Voice note</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3.5">
|
|
<div
|
|
aria-hidden="true"
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center bg-primary text-primary-foreground"
|
|
>
|
|
<Play className="h-3.5 w-3.5 fill-current" />
|
|
</div>
|
|
<div className="flex h-11 min-w-0 flex-1 items-center gap-[3px]">
|
|
{waveformBars.map((height, index) => (
|
|
<span
|
|
key={index}
|
|
className={`voice-bar flex-1 ${index < 20 ? 'bg-primary/70' : 'bg-foreground/15'}`}
|
|
style={{ height: `${height}%` }}
|
|
/>
|
|
))}
|
|
</div>
|
|
<span className="shrink-0 text-[11px] tracking-[0.04em] text-muted-foreground">
|
|
0:11 / 0:19
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="border border-border px-2 py-[3px] text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
|
|
Feedback
|
|
</span>
|
|
<span className="text-[11px] text-muted-foreground">Reply</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4 border border-border bg-background p-4 sm:flex-row sm:p-[18px]">
|
|
<div className="relative h-[112px] w-full shrink-0 overflow-hidden border border-border bg-black sm:w-[200px]">
|
|
<Image
|
|
src="/landing/deep-dive-dashboard-2.webp"
|
|
alt="Annotated frame"
|
|
fill
|
|
className="origin-[30%_40%] scale-[2.4] object-cover brightness-75"
|
|
sizes="(min-width: 640px) 480px, 240vw"
|
|
/>
|
|
<svg
|
|
aria-hidden="true"
|
|
className="absolute inset-0 h-full w-full text-primary"
|
|
viewBox="0 0 200 112"
|
|
preserveAspectRatio="none"
|
|
fill="none"
|
|
>
|
|
<ellipse
|
|
cx="98"
|
|
cy="52"
|
|
rx="52"
|
|
ry="28"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
/>
|
|
<path
|
|
d="M144 38 Q164 22 182 32"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="flex min-w-0 flex-col gap-2.5">
|
|
<div className="flex flex-wrap items-center gap-2.5">
|
|
<Avatar initial="D" />
|
|
<span className="text-[13px] font-medium">David K.</span>
|
|
<Timecode>00:01:49</Timecode>
|
|
<span className="border border-purple-500/40 px-2 py-[3px] text-[10px] uppercase tracking-[0.14em] text-purple-700 dark:text-purple-300">
|
|
Annotated
|
|
</span>
|
|
</div>
|
|
<p className="text-sm leading-relaxed">
|
|
This block needs to clear the subject, I drew where it should sit.
|
|
</p>
|
|
<div className="flex items-center gap-2.5 text-[11px] text-muted-foreground">
|
|
<span>Reply</span>
|
|
<span>Resolve</span>
|
|
<span>Export to PDF</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Share and approve */}
|
|
<div className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
|
<div className="mx-auto grid w-full max-w-[1200px] items-center gap-10 lg:grid-cols-[340px_minmax(0,1fr)] lg:gap-14">
|
|
<div className="flex flex-col gap-4">
|
|
<SectionTitle>One link, one approval.</SectionTitle>
|
|
<p className="text-base leading-relaxed text-muted-foreground">
|
|
Decide what the link allows, send it, and get an Approved status you can point at
|
|
later.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex min-w-0 items-center justify-center border border-border bg-gradient-to-br from-card to-background p-4 sm:p-8">
|
|
<div className="flex w-full max-w-[520px] flex-col border border-border bg-card">
|
|
<div className="flex h-11 items-center justify-between border-b border-border px-4">
|
|
<div className="flex items-center gap-2">
|
|
<Share2 className="h-[15px] w-[15px] text-primary" />
|
|
<span className="text-xs font-medium">Share review link</span>
|
|
</div>
|
|
<span className="text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
|
|
Version 3
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-col gap-4 p-4 sm:p-[18px]">
|
|
<div className="flex items-stretch border border-border bg-background">
|
|
<div className="flex h-10 min-w-0 flex-1 items-center gap-2 px-3">
|
|
<LinkIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
<span className="truncate text-xs">open-frame.net/watch/share-7f3a</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 bg-primary px-3.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-primary-foreground">
|
|
<Copy className="h-[13px] w-[13px]" />
|
|
<span>Copy</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-px border border-border bg-border">
|
|
{shareOptions.map((option) => (
|
|
<div
|
|
key={option.label}
|
|
className="flex items-center justify-between gap-3 bg-background px-3.5 py-3"
|
|
>
|
|
<span className={`text-xs ${option.on ? '' : 'text-muted-foreground'}`}>
|
|
{option.label}
|
|
</span>
|
|
<Toggle on={option.on} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-2 border border-primary/35 bg-primary/10 px-3.5 py-3">
|
|
<div className="flex items-center gap-2.5">
|
|
<CheckCircle2 className="h-4 w-4 text-primary" />
|
|
<span className="text-[11px] uppercase tracking-[0.14em] text-primary">
|
|
Approved by
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2.5">
|
|
<span className="text-xs">David K.</span>
|
|
<span className="text-[11px] tracking-[0.04em] text-muted-foreground">
|
|
22.02.2026 16:04
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 4) HOW IT WORKS */}
|
|
<section className="border-b border-border bg-card/30 px-4 py-14 sm:px-6 lg:px-8 lg:py-16">
|
|
<div className="mx-auto flex w-full max-w-[1200px] flex-col gap-7">
|
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
<h2 className="text-2xl font-semibold tracking-[-0.02em] md:text-[28px]">
|
|
From upload to approval, in one flow.
|
|
</h2>
|
|
<span className={labelClass}>Four steps, one link</span>
|
|
</div>
|
|
<div className="grid gap-px border border-border bg-border sm:grid-cols-2 lg:grid-cols-4">
|
|
{steps.map((step, index) => (
|
|
<div key={step.label} className="flex flex-col gap-3 bg-background p-5">
|
|
<div className="flex items-center justify-between">
|
|
<step.icon className="h-5 w-5 text-primary" />
|
|
<span className="text-[11px] tracking-[0.14em] text-muted-foreground/60">
|
|
0{index + 1}
|
|
</span>
|
|
</div>
|
|
<span className="text-sm font-medium">{step.label}</span>
|
|
<span className="text-xs leading-relaxed text-muted-foreground">
|
|
{step.description}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 5) PRICING */}
|
|
<section
|
|
id="pricing"
|
|
className="scroll-mt-20 border-b border-border bg-card/30 px-4 py-16 sm:px-6 lg:px-8 lg:py-20"
|
|
>
|
|
<div className="mx-auto flex w-full max-w-[1200px] flex-col gap-8">
|
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
<SectionTitle>Let us run it, or run it yourself.</SectionTitle>
|
|
<span className={labelClass}>No per-seat fees</span>
|
|
</div>
|
|
|
|
<div className="grid gap-5 md:grid-cols-3">
|
|
<div className="relative flex flex-col gap-[18px] border border-primary/40 bg-card p-6 sm:p-7">
|
|
<span className="absolute -top-[11px] left-6 bg-primary px-2 py-[3px] text-[10px] font-semibold uppercase tracking-[0.14em] text-primary-foreground sm:left-7">
|
|
Recommended
|
|
</span>
|
|
<div className="flex flex-col gap-2">
|
|
<span className={labelClass}>Hosted cloud</span>
|
|
<div className="flex items-baseline gap-2 text-primary">
|
|
<span className="text-[40px] font-semibold tracking-[-0.02em]">$10</span>
|
|
<span className="text-sm">/ month</span>
|
|
</div>
|
|
<span className="text-xs text-muted-foreground">
|
|
7-day free trial, no credit card. Cancel anytime.
|
|
</span>
|
|
</div>
|
|
<ul className="flex flex-col gap-2.5 text-[13px]">
|
|
{hostedFeatures.map((feature) => (
|
|
<li key={feature} className="flex items-start gap-2">
|
|
<Check className="mt-0.5 h-[15px] w-[15px] shrink-0 text-primary" />
|
|
<span>{feature}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div className="mt-auto pt-2">
|
|
<CtaLink href={hostedCtaHref} className={`${primaryCtaClass} w-full`}>
|
|
{isLoggedIn ? 'Open dashboard' : 'Start 7-day free trial'}
|
|
</CtaLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-[18px] border border-border bg-card p-6 sm:p-7">
|
|
<div className="flex flex-col gap-2">
|
|
<span className={labelClass}>Fair Source, self-hosted</span>
|
|
<span className="text-[40px] font-semibold tracking-[-0.02em] text-primary">
|
|
Free
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
<a
|
|
href="https://fsl.software/"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="underline underline-offset-2 hover:text-foreground"
|
|
>
|
|
Functional Source License
|
|
</a>
|
|
. Your infrastructure, your data.
|
|
</span>
|
|
</div>
|
|
<ul className="flex flex-col gap-2.5 text-[13px]">
|
|
{selfHostedFeatures.map((feature) => (
|
|
<li key={feature} className="flex items-start gap-2">
|
|
<Check className="mt-0.5 h-[15px] w-[15px] shrink-0 text-primary" />
|
|
<span>{feature}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div className="mt-auto pt-2">
|
|
<a
|
|
href="https://github.com/yusufipk/OpenFrame"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex h-12 w-full items-center justify-center gap-2 border border-border bg-background text-[13px] font-medium transition-colors hover:border-foreground/30"
|
|
>
|
|
<Github className="h-[15px] w-[15px]" />
|
|
View on GitHub
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-[18px] border border-border bg-card p-6 sm:p-7">
|
|
<div className="flex flex-col gap-2">
|
|
<span className={labelClass}>Need more?</span>
|
|
<span className="text-[40px] font-semibold tracking-[-0.02em] text-primary">
|
|
Let's talk
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
Custom capacity and setup help for high volume teams.
|
|
</span>
|
|
</div>
|
|
<p className="text-[13px] leading-relaxed">
|
|
Tell us your storage, usage and workflow, we will recommend the right approach.
|
|
Hosted, self-hosted, or a mix of both.
|
|
</p>
|
|
<div className="mt-auto flex flex-col gap-3 pt-2">
|
|
<span className="text-xs text-muted-foreground">info@open-frame.net</span>
|
|
<a
|
|
href="mailto:[email protected]"
|
|
className="inline-flex h-12 w-full items-center justify-center border border-border bg-background text-[13px] font-medium transition-colors hover:border-foreground/30"
|
|
>
|
|
Contact us
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 6) FAQ */}
|
|
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
|
<div className="mx-auto flex w-full max-w-[1200px] flex-col gap-7">
|
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
<SectionTitle>Questions people ask.</SectionTitle>
|
|
<a
|
|
href="mailto:[email protected]"
|
|
className={`${labelClass} hover:text-foreground`}
|
|
>
|
|
info@open-frame.net
|
|
</a>
|
|
</div>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{faq.map((item) => (
|
|
<div
|
|
key={item.q}
|
|
className="flex flex-col gap-2.5 border border-border bg-card p-5 transition-colors hover:border-primary/45"
|
|
>
|
|
<h3 className="text-[15px] font-semibold">{item.q}</h3>
|
|
<p className="text-[13px] leading-relaxed text-muted-foreground">{item.a}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 7) FINAL CTA */}
|
|
<section className="border-b border-border bg-card/30 px-4 py-14 sm:px-6 lg:px-8">
|
|
<div className="mx-auto flex w-full max-w-[1200px] flex-col items-center justify-between gap-6 text-center md:flex-row md:text-left">
|
|
<div className="flex flex-col gap-2">
|
|
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
|
|
Stop chasing feedback. Start getting approvals.
|
|
</h2>
|
|
<span className="text-sm text-muted-foreground">
|
|
Your first review link takes minutes.
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-col items-center gap-2.5 md:shrink-0 md:items-end">
|
|
<CtaLink href={hostedCtaHref} className={`${primaryCtaClass} md:whitespace-nowrap`}>
|
|
{hostedCtaLabel}
|
|
<MoveRight className="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-1" />
|
|
</CtaLink>
|
|
<a
|
|
href="https://github.com/yusufipk/OpenFrame"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-[11px] text-muted-foreground transition-colors hover:text-foreground"
|
|
>
|
|
Prefer self-hosting? View on GitHub
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer className="border-t border-border bg-background px-4 py-8 sm:px-6 lg:px-8">
|
|
<div className="mx-auto grid max-w-[1200px] gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
<div className="flex items-start gap-2">
|
|
<Video className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
|
<div className="flex flex-col gap-1">
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
© 2026 IPEK TECH LLC. All rights reserved.
|
|
</span>
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
30 North Gould Street, Suite N, Sheridan, WY 82801, United States
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<MarketingCompareLinks />
|
|
<div className="flex flex-col gap-2">
|
|
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
|
|
Legal
|
|
</span>
|
|
<div className="flex flex-col gap-1.5">
|
|
<a
|
|
href="mailto:[email protected]"
|
|
className="text-xs text-muted-foreground hover:text-foreground"
|
|
>
|
|
info@open-frame.net
|
|
</a>
|
|
<a
|
|
href="https://github.com/yusufipk/OpenFrame"
|
|
className="text-xs text-muted-foreground hover:text-foreground"
|
|
>
|
|
GitHub
|
|
</a>
|
|
<Link href="/terms" className="text-xs text-muted-foreground hover:text-foreground">
|
|
Terms
|
|
</Link>
|
|
<Link href="/privacy" className="text-xs text-muted-foreground hover:text-foreground">
|
|
Privacy
|
|
</Link>
|
|
<Link href="/refund" className="text-xs text-muted-foreground hover:text-foreground">
|
|
Refund Policy
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|