'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 info@open-frame.net 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 (

{children}

); } function MockToolbar({ left, right }: { left: React.ReactNode; right?: React.ReactNode }) { return (
{left} {right}
); } function Avatar({ initial }: { initial: string }) { return (
{initial}
); } function Timecode({ children }: { children: React.ReactNode }) { return ( {children} ); } function Toggle({ on }: { on: boolean }) { return ( ); }