mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
Add SEO comparison landing pages and footer compare links.
Introduces dynamic marketing comparison routes, competitor data, and Compare sections on the homepage and marketing footer.
This commit is contained in:
+32
-25
@@ -2,6 +2,7 @@
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { MarketingCompareLinks } from '@/components/marketing/marketing-compare-links';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import {
|
||||
@@ -880,35 +881,41 @@ export function LandingPage({ isLoggedIn }: LandingPageProps) {
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-border bg-background px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto flex max-w-[1200px] flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<Video className="h-4 w-4 text-primary" />
|
||||
<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" />
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
© 2026 IPEK TECH LLC. All rights reserved.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight, Github, MoveRight } from 'lucide-react';
|
||||
import { FeatureComparisonTable } from '@/components/marketing/feature-comparison-table';
|
||||
import { MarketingFooter } from '@/components/marketing/marketing-footer';
|
||||
import { MarketingHeader } from '@/components/marketing/marketing-header';
|
||||
import { PricingComparison } from '@/components/marketing/pricing-comparison';
|
||||
import { ProductVisual } from '@/components/marketing/product-visual';
|
||||
import type { ComparisonPageDefinition } from '@/lib/marketing/comparison-types';
|
||||
import { getCompetitorName, comparisonPageMap } from '@/lib/marketing/comparison-pages';
|
||||
import { seoConfig } from '@/lib/seo';
|
||||
|
||||
interface ComparisonPageProps {
|
||||
page: ComparisonPageDefinition;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export function ComparisonPage({ page, isLoggedIn }: ComparisonPageProps) {
|
||||
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
|
||||
const competitorName = getCompetitorName(page) ?? 'Alternatives';
|
||||
const relatedSlugs = page.relatedSlugs.filter((slug) => slug in comparisonPageMap);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-sans">
|
||||
<MarketingHeader isLoggedIn={isLoggedIn} />
|
||||
|
||||
<main>
|
||||
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-24">
|
||||
<div className="mx-auto grid w-full max-w-[1200px] gap-12 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-primary">
|
||||
{page.eyebrow}
|
||||
</p>
|
||||
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.03em] md:text-6xl">
|
||||
{page.headline}
|
||||
</h1>
|
||||
<p className="mt-6 max-w-2xl text-base text-muted-foreground md:text-lg">
|
||||
{page.subheadline}
|
||||
</p>
|
||||
<p className="mt-4 max-w-2xl border border-primary/30 bg-primary/5 px-4 py-3 text-sm text-foreground/90 md:text-base">
|
||||
<span className="font-medium text-primary">No per-member or guest fees.</span> One
|
||||
$10/month hosted plan covers your whole team and every client reviewer link.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href={hostedCtaHref}
|
||||
className="group relative isolate inline-flex h-12 items-center justify-center overflow-hidden border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground transition-transform duration-300 hover:scale-[1.02]"
|
||||
>
|
||||
Start free trial
|
||||
<MoveRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</Link>
|
||||
<a
|
||||
href={seoConfig.githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex h-12 items-center justify-center border border-border bg-background px-8 text-sm font-medium text-foreground transition-colors hover:bg-card"
|
||||
>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
View GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<ProductVisual variant={page.visualVariant} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
||||
<div className="mx-auto w-full max-w-[1200px]">
|
||||
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
|
||||
{page.solutionTitle}
|
||||
</h2>
|
||||
<p className="mt-4 max-w-3xl text-base text-muted-foreground md:text-lg">
|
||||
{page.solutionNarrative}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-border bg-background px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
||||
<div className="mx-auto grid w-full max-w-[1200px] gap-8 lg:grid-cols-2">
|
||||
<div className="border border-primary/30 bg-primary/5 p-6">
|
||||
<h3 className="text-xl font-semibold">Where OpenFrame fits best</h3>
|
||||
<ul className="mt-4 space-y-3 text-sm text-foreground/90 md:text-base">
|
||||
{page.bestForOpenFrame.map((item) => (
|
||||
<li key={item}>- {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="border border-border bg-card p-6">
|
||||
<h3 className="text-xl font-semibold">Where {competitorName} may still fit</h3>
|
||||
<ul className="mt-4 space-y-3 text-sm text-muted-foreground md:text-base">
|
||||
{page.bestForCompetitor.map((item) => (
|
||||
<li key={item}>- {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-border bg-card/10 px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
||||
<div className="mx-auto grid w-full max-w-[1200px] gap-8 lg:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">OpenFrame advantages</h3>
|
||||
<ul className="mt-4 space-y-3 text-sm md:text-base">
|
||||
{page.openframeWins.map((item) => (
|
||||
<li key={item} className="border border-border bg-background p-4">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">{competitorName} advantages</h3>
|
||||
<ul className="mt-4 space-y-3 text-sm text-muted-foreground md:text-base">
|
||||
{page.competitorWins.map((item) => (
|
||||
<li key={item} className="border border-border bg-background p-4">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
||||
<div className="mx-auto w-full max-w-[1200px] space-y-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
|
||||
Feature comparison
|
||||
</h2>
|
||||
<p className="mt-3 max-w-3xl text-sm text-muted-foreground md:text-base">
|
||||
Honest tradeoffs based on official product, pricing, and help documentation. Verify
|
||||
current plans before buying.
|
||||
</p>
|
||||
</div>
|
||||
<FeatureComparisonTable rows={page.featureRows} competitorName={competitorName} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-border bg-[#0a0a0a] px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
||||
<div className="mx-auto w-full max-w-[1200px] space-y-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
|
||||
Pricing comparison
|
||||
</h2>
|
||||
<p className="mt-3 max-w-3xl text-sm text-muted-foreground md:text-base">
|
||||
OpenFrame is $10/month flat with a 7-day free trial. You do not pay per team member,
|
||||
collaborator, or guest reviewer. Self-hosting is free with Docker.
|
||||
</p>
|
||||
</div>
|
||||
<PricingComparison
|
||||
rows={page.pricingRows}
|
||||
footnote={page.pricingFootnote}
|
||||
competitorName={competitorName}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
|
||||
<div className="mx-auto w-full max-w-[1200px]">
|
||||
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">FAQ</h2>
|
||||
<div className="mt-8 space-y-4">
|
||||
{page.faq.map((item) => (
|
||||
<div key={item.question} className="border border-border bg-card p-6">
|
||||
<h3 className="text-lg font-semibold">{item.question}</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground md:text-base">{item.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{relatedSlugs.length > 0 ? (
|
||||
<section className="border-b border-border bg-card/10 px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto w-full max-w-[1200px]">
|
||||
<h2 className="text-xl font-semibold">Related comparisons</h2>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{relatedSlugs.map((slug) => (
|
||||
<Link
|
||||
key={slug}
|
||||
href={`/${slug}`}
|
||||
className="inline-flex items-center gap-2 border border-border bg-background px-4 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
{slug.replaceAll('-', ' ')}
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto flex w-full max-w-[1200px] flex-col items-center justify-between gap-4 border border-border bg-card p-8 text-center md:flex-row md:text-left">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
|
||||
Start your free trial
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Your first client review link takes minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href={hostedCtaHref}
|
||||
className="inline-flex h-12 items-center justify-center border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground"
|
||||
>
|
||||
Start free trial
|
||||
</Link>
|
||||
<a
|
||||
href={seoConfig.githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex h-12 items-center justify-center border border-border bg-background px-8 text-sm font-medium text-foreground"
|
||||
>
|
||||
View GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<MarketingFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Check, Minus, X } from 'lucide-react';
|
||||
import type { FeatureRow } from '@/lib/marketing/comparison-types';
|
||||
|
||||
function renderStatus(value: FeatureRow['openframe']) {
|
||||
if (value === 'yes') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-emerald-400">
|
||||
<Check className="h-4 w-4" />
|
||||
Yes
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (value === 'no') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-red-400/90">
|
||||
<X className="h-4 w-4" />
|
||||
No
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (value === 'partial') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-amber-300">
|
||||
<Minus className="h-4 w-4" />
|
||||
Partial
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="text-foreground/90">{value}</span>;
|
||||
}
|
||||
|
||||
interface FeatureComparisonTableProps {
|
||||
rows: FeatureRow[];
|
||||
competitorName: string;
|
||||
}
|
||||
|
||||
export function FeatureComparisonTable({ rows, competitorName }: FeatureComparisonTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto border border-border bg-card">
|
||||
<table className="min-w-full text-left text-sm">
|
||||
<thead className="border-b border-border bg-background/60">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium text-muted-foreground">Feature</th>
|
||||
<th className="px-4 py-3 font-medium text-primary">OpenFrame</th>
|
||||
<th className="px-4 py-3 font-medium text-muted-foreground">{competitorName}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.label} className="border-b border-border/60 last:border-b-0">
|
||||
<td className="px-4 py-3 align-top">
|
||||
<div className="font-medium text-foreground">{row.label}</div>
|
||||
{row.note ? (
|
||||
<div className="mt-1 text-xs text-muted-foreground">{row.note}</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">{renderStatus(row.openframe)}</td>
|
||||
<td className="px-4 py-3 align-top">{renderStatus(row.competitor)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import Link from 'next/link';
|
||||
import { compareFooterLinks } from '@/lib/marketing/comparison-pages';
|
||||
|
||||
export function MarketingCompareLinks() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
|
||||
Compare
|
||||
</span>
|
||||
<nav className="flex flex-col gap-1.5">
|
||||
{compareFooterLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Link from 'next/link';
|
||||
import { Video } from 'lucide-react';
|
||||
import { seoConfig } from '@/lib/seo';
|
||||
import { MarketingCompareLinks } from '@/components/marketing/marketing-compare-links';
|
||||
|
||||
export function MarketingFooter() {
|
||||
return (
|
||||
<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" />
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
© 2026 IPEK TECH LLC. All rights reserved.
|
||||
</span>
|
||||
</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={seoConfig.githubUrl}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import Link from 'next/link';
|
||||
import { MoveRight, Video } from 'lucide-react';
|
||||
import { seoConfig } from '@/lib/seo';
|
||||
|
||||
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';
|
||||
|
||||
interface MarketingHeaderProps {
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export function MarketingHeader({ isLoggedIn }: MarketingHeaderProps) {
|
||||
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
|
||||
|
||||
return (
|
||||
<header className="border-b border-border bg-background/90 backdrop-blur-md">
|
||||
<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={seoConfig.githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</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="mr-4 hidden text-xs font-medium text-muted-foreground hover:text-foreground sm:block"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link href={hostedCtaHref} 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { PricingRow } from '@/lib/marketing/comparison-types';
|
||||
|
||||
interface PricingComparisonProps {
|
||||
rows: PricingRow[];
|
||||
footnote?: string;
|
||||
competitorName: string;
|
||||
}
|
||||
|
||||
export function PricingComparison({ rows, footnote, competitorName }: PricingComparisonProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="border border-border bg-card p-5">
|
||||
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">{row.label}</p>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-[0.12em] text-primary">OpenFrame</p>
|
||||
<p className="mt-1 text-sm text-foreground">{row.openframe}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{competitorName}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-foreground/90">{row.competitor}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{footnote ? <p className="text-xs text-muted-foreground">{footnote}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import Image from 'next/image';
|
||||
import { CheckCircle, Link as LinkIcon, MessageSquare, Mic, PenTool } from 'lucide-react';
|
||||
import type { VisualVariant } from '@/lib/marketing/comparison-types';
|
||||
|
||||
interface ProductVisualProps {
|
||||
variant: VisualVariant;
|
||||
}
|
||||
|
||||
export function ProductVisual({ variant }: ProductVisualProps) {
|
||||
if (variant === 'landing-compare') {
|
||||
return (
|
||||
<div className="relative aspect-[16/10] overflow-hidden border border-border bg-card">
|
||||
<Image
|
||||
src="/landing/compare-v2.webp"
|
||||
alt="OpenFrame side-by-side version compare"
|
||||
fill
|
||||
className="object-cover object-top"
|
||||
sizes="(max-width: 768px) 100vw, 540px"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'landing-dashboard') {
|
||||
return (
|
||||
<div className="relative aspect-[16/10] overflow-hidden border border-border bg-card">
|
||||
<Image
|
||||
src="/landing/deep-dive-dashboard-2.webp"
|
||||
alt="OpenFrame video review dashboard"
|
||||
fill
|
||||
className="object-cover object-top"
|
||||
sizes="(max-width: 768px) 100vw, 540px"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'version-compare') {
|
||||
return (
|
||||
<div className="border border-border bg-card p-4">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{['V2', 'V3'].map((version) => (
|
||||
<div key={version} className="border border-border/60 bg-background p-3">
|
||||
<div className="mb-3 flex items-center justify-between text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
<span>{version}</span>
|
||||
<span className="text-primary">Compare mode</span>
|
||||
</div>
|
||||
<div className="aspect-video bg-gradient-to-br from-zinc-900 via-zinc-800 to-zinc-900" />
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="h-1.5 w-full rounded-full bg-border">
|
||||
<div
|
||||
className={`h-1.5 rounded-full bg-primary ${version === 'V2' ? 'w-1/3' : 'w-2/3'}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="font-mono text-[10px] text-muted-foreground">
|
||||
{version === 'V2' ? '00:01:12 color too cool' : '00:01:12 warmed up +2'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'approval-workflow') {
|
||||
return (
|
||||
<div className="border border-border bg-card p-5">
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Client Cut V3
|
||||
</p>
|
||||
<p className="mt-1 text-lg font-semibold">Approval requests</p>
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-2 border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs text-emerald-300">
|
||||
<CheckCircle className="h-3.5 w-3.5" />2 of 3 approved
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{[
|
||||
{ name: 'Client Producer', status: 'Approved', tone: 'text-emerald-300' },
|
||||
{ name: 'Brand Manager', status: 'Approved', tone: 'text-emerald-300' },
|
||||
{ name: 'Legal Review', status: 'Pending', tone: 'text-amber-300' },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="flex items-center justify-between border border-border/60 bg-background px-4 py-3"
|
||||
>
|
||||
<span className="text-sm">{item.name}</span>
|
||||
<span className={`text-xs uppercase tracking-[0.12em] ${item.tone}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'guest-review') {
|
||||
return (
|
||||
<div className="border border-border bg-card p-5">
|
||||
<div className="flex items-start gap-3 border border-primary/30 bg-primary/5 p-4">
|
||||
<LinkIcon className="mt-0.5 h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">open-frame.net/watch/share-7f3a</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Client opens the link, enters a name, and reviews in the browser. No account required.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 aspect-video border border-border/60 bg-gradient-to-br from-zinc-900 via-zinc-800 to-zinc-950" />
|
||||
<div className="mt-4 flex items-center gap-3 border border-border/60 bg-background p-3">
|
||||
<MessageSquare className="h-4 w-4 text-primary" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Client note at 00:02:14</p>
|
||||
<p className="text-xs text-muted-foreground">Lower the music under the VO here.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'voice-notes') {
|
||||
return (
|
||||
<div className="border border-border bg-card p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center bg-secondary font-mono text-xs">
|
||||
C
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono text-[11px] font-medium">Client</p>
|
||||
<p className="font-mono text-[10px] text-muted-foreground">00:03:45</p>
|
||||
</div>
|
||||
</div>
|
||||
<Mic className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="mt-6 flex h-16 items-center gap-1 overflow-hidden">
|
||||
{Array.from({ length: 36 }).map((_, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="w-full flex-1 bg-primary/60"
|
||||
style={{
|
||||
height: `${[30, 80, 50, 90, 40, 70, 60, 45, 85, 55][index % 10]}%`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Voice note pinned to the exact frame—no more typing long explanations.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-border bg-card p-5">
|
||||
<div className="aspect-video border border-border/60 bg-gradient-to-br from-zinc-900 via-zinc-800 to-zinc-950" />
|
||||
<div className="relative mt-4 border border-border/60 bg-background p-4">
|
||||
<div className="absolute right-4 top-4 rounded-full border border-primary/40 p-2 text-primary">
|
||||
<PenTool className="h-4 w-4" />
|
||||
</div>
|
||||
<p className="font-mono text-[10px] text-muted-foreground">00:01:48</p>
|
||||
<p className="mt-2 text-sm">Move the lower third up one line so it clears the subject.</p>
|
||||
<div className="mt-4 h-1.5 w-full rounded-full bg-border">
|
||||
<div className="h-1.5 w-[42%] rounded-full bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user