Separate YouTube video titles from on-video song/artist fields with Pro gating, serve curated fonts and watermark assets for 1:1 preview parity, and include billing/API/docs/deploy stack for self-hosted release. Co-authored-by: Cursor <cursoragent@cursor.com>
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useRef } from "react";
|
||
import { ScrollReveal } from "@/components/ScrollReveal";
|
||
import { SectionScrollTitle } from "@/components/SectionScrollTitle";
|
||
import { SignInButton } from "@/components/SignInButton";
|
||
import { getVideoAttributionText } from "@/lib/branding";
|
||
import { SALES_EMAIL } from "@/lib/plans";
|
||
|
||
const FEATURE_LABELS = [
|
||
"Infrastructure",
|
||
"Limit",
|
||
"Batch mode",
|
||
"Bulk image matching",
|
||
"File type",
|
||
"Playlists",
|
||
"API support",
|
||
"ID3 tags",
|
||
"Support",
|
||
"Watermark",
|
||
] as const;
|
||
|
||
type PlanFeatures = Record<(typeof FEATURE_LABELS)[number], string>;
|
||
|
||
type InfrastructureChip = "cloud" | "self-hosted";
|
||
|
||
type PlanConfig = {
|
||
name: string;
|
||
badge: string;
|
||
price: string;
|
||
priceNote: string;
|
||
features: PlanFeatures;
|
||
infrastructureChips: InfrastructureChip[];
|
||
watermarkNote?: string;
|
||
cta:
|
||
| { type: "signin" }
|
||
| { type: "link"; label: string; href: string }
|
||
| { type: "disabled"; label: string }
|
||
| { type: "mailto"; label: string; email: string; subject: string };
|
||
highlighted: boolean;
|
||
hover: string;
|
||
accent: string;
|
||
badgeClass: string;
|
||
ctaClass?: string;
|
||
};
|
||
|
||
const PLANS: PlanConfig[] = [
|
||
{
|
||
name: "Bedroom Producer",
|
||
badge: "Free",
|
||
price: "€0",
|
||
priceNote: "/ forever",
|
||
features: {
|
||
Infrastructure: "Cloud",
|
||
Limit: "10 videos* / month · 720p · buy 1–15 extras (€0.25 each)",
|
||
"Batch mode": "Limited batch · up to 3 files",
|
||
"Bulk image matching": "1 static image only",
|
||
"File type": "MP3",
|
||
Playlists: "Not included",
|
||
"API support": "Not included",
|
||
"ID3 tags": "Auto-fill title & metadata from MP3 tags",
|
||
Support: "Community",
|
||
Watermark: "Optional Songs2VID badge · bottom-right",
|
||
},
|
||
infrastructureChips: ["cloud"],
|
||
watermarkNote:
|
||
`Show "${getVideoAttributionText()}" to support open-source development - opt out anytime for a clean video.`,
|
||
cta: { type: "signin" },
|
||
highlighted: false,
|
||
hover:
|
||
"hover:border-red-500/40 hover:bg-red-500/[0.04] hover:shadow-lg hover:shadow-red-500/15",
|
||
accent: "text-red-400",
|
||
badgeClass: "bg-gray-800 text-gray-400",
|
||
},
|
||
{
|
||
name: "Independent Artist",
|
||
badge: "Pro",
|
||
price: "€5",
|
||
priceNote: "/ mo",
|
||
features: {
|
||
Infrastructure: "Cloud",
|
||
Limit: "50 videos / month · 1080p",
|
||
"Batch mode": "Full batch · up to 5 files",
|
||
"Bulk image matching": "Unique image per track (PRO)",
|
||
"File type": "MP3 / WAV / FLAC",
|
||
Playlists: "Create & add uploads to YouTube playlists",
|
||
"API support": "REST API · upload, batch & playlists",
|
||
"ID3 tags": "Extended metadata support",
|
||
Support: "E-Mail",
|
||
Watermark: "Custom text/logo/fonts · art-track layouts + blur (PRO)",
|
||
},
|
||
infrastructureChips: ["cloud"],
|
||
cta: { type: "link", label: "Upgrade to Pro", href: "/dashboard/settings" },
|
||
highlighted: true,
|
||
hover:
|
||
"hover:border-accent/50 hover:bg-accent/[0.06] hover:shadow-lg hover:shadow-accent/20",
|
||
accent: "text-accent",
|
||
badgeClass: "bg-accent/15 text-accent",
|
||
},
|
||
{
|
||
name: "Record Label / Studio",
|
||
badge: "Enterprise",
|
||
price: "Custom",
|
||
priceNote: "/ contact sales",
|
||
features: {
|
||
Infrastructure: "Cloud or self-hosted",
|
||
Limit: "Unlimited 4K · zero limit",
|
||
"Batch mode": "Unlimited synchronized batch processing",
|
||
"Bulk image matching": "Unique image per track",
|
||
"File type": "WAV / FLAC / lossless",
|
||
Playlists: "Org-wide playlist workflows",
|
||
"API support": "Full API access · custom integrations & SLAs",
|
||
"ID3 tags": "Full metadata · custom mapping",
|
||
Support: "Top-priority**",
|
||
Watermark: "Custom branding · position studio",
|
||
},
|
||
infrastructureChips: ["cloud", "self-hosted"],
|
||
cta: {
|
||
type: "mailto",
|
||
label: "Contact Sales",
|
||
email: SALES_EMAIL,
|
||
subject: "Songs2VID Enterprise Inquiry",
|
||
},
|
||
highlighted: false,
|
||
hover:
|
||
"hover:border-[#609926]/45 hover:bg-[#609926]/[0.06] hover:shadow-lg hover:shadow-[#609926]/20",
|
||
accent: "text-[#609926]",
|
||
badgeClass: "bg-[#609926]/15 text-[#609926]",
|
||
ctaClass:
|
||
"border-[#609926]/40 bg-[#609926]/10 text-[#609926] hover:border-[#609926]/60 hover:bg-[#609926]/20",
|
||
},
|
||
];
|
||
|
||
function FeatureValue({ value }: { value: string }) {
|
||
return <span className="text-sm text-gray-200">{value}</span>;
|
||
}
|
||
|
||
function CloudIcon({ className }: { className?: string }) {
|
||
return (
|
||
<svg
|
||
className={className}
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
aria-hidden="true"
|
||
>
|
||
<path
|
||
d="M7.5 18.5h9.75a4.25 4.25 0 0 0 .55-8.46A5.75 5.75 0 0 0 6.9 8.8 4.5 4.5 0 0 0 7.5 18.5Z"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
function ServerIcon({ className }: { className?: string }) {
|
||
return (
|
||
<svg
|
||
className={className}
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
aria-hidden="true"
|
||
>
|
||
<rect x="4" y="5" width="16" height="6" rx="2" />
|
||
<rect x="4" y="13" width="16" height="6" rx="2" />
|
||
<path d="M7.5 8h.01M7.5 16h.01" strokeLinecap="round" />
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
function InfrastructureChips({
|
||
chips,
|
||
}: {
|
||
chips: InfrastructureChip[];
|
||
}) {
|
||
return (
|
||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||
{chips.includes("cloud") && (
|
||
<span className="inline-flex items-center gap-2 rounded-lg border border-gray-700/60 bg-surface-dark/40 px-3 py-2 text-sm text-gray-200">
|
||
<CloudIcon className="h-4 w-4 text-gray-300" />
|
||
Cloud
|
||
</span>
|
||
)}
|
||
{chips.includes("self-hosted") && (
|
||
<span className="inline-flex items-center gap-2 rounded-lg border border-gray-700/60 bg-surface-dark/40 px-3 py-2 text-sm text-gray-200">
|
||
<ServerIcon className="h-4 w-4 text-gray-300" />
|
||
Self-hosted
|
||
</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PricingCard({ plan }: { plan: PlanConfig }) {
|
||
const {
|
||
name,
|
||
badge,
|
||
price,
|
||
priceNote,
|
||
features,
|
||
watermarkNote,
|
||
infrastructureChips,
|
||
cta,
|
||
highlighted,
|
||
hover,
|
||
accent,
|
||
badgeClass,
|
||
ctaClass,
|
||
} = plan;
|
||
|
||
return (
|
||
<div
|
||
className={`group relative flex h-full flex-col rounded-2xl border bg-surface p-6 transition-all duration-300 sm:p-7 ${
|
||
highlighted
|
||
? "border-accent/40 shadow-[0_0_32px_rgba(74,158,255,0.12)]"
|
||
: "border-gray-700/50"
|
||
} ${hover}`}
|
||
>
|
||
{highlighted && (
|
||
<span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-accent px-3 py-1 text-[10px] font-semibold uppercase tracking-wider text-white">
|
||
Most popular
|
||
</span>
|
||
)}
|
||
|
||
<div className="mb-6">
|
||
<div className="mb-3 flex items-start justify-between gap-3">
|
||
<h3 className="text-lg font-semibold text-white">{name}</h3>
|
||
<span
|
||
className={`shrink-0 rounded px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${badgeClass}`}
|
||
>
|
||
{badge}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex items-end gap-1">
|
||
<span className={`text-4xl font-bold ${accent}`}>{price}</span>
|
||
<span className="mb-1 text-sm text-gray-500">{priceNote}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<ul className="mb-6 flex-1 space-y-4 border-t border-gray-700/50 pt-6">
|
||
{FEATURE_LABELS.map((label) => (
|
||
<li key={label}>
|
||
<p className="text-xs font-medium uppercase tracking-wider text-gray-500">{label}</p>
|
||
{label === "Infrastructure" ? (
|
||
<InfrastructureChips chips={infrastructureChips} />
|
||
) : (
|
||
<FeatureValue value={features[label]} />
|
||
)}
|
||
{label === "Watermark" && watermarkNote && (
|
||
<p className="mt-1.5 rounded-lg border border-gray-700/60 bg-surface-dark/80 px-3 py-2 text-xs leading-relaxed text-gray-400">
|
||
{watermarkNote}
|
||
</p>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
|
||
<div className="mt-auto">
|
||
{cta.type === "signin" && <SignInButton fullWidth />}
|
||
{cta.type === "link" && (
|
||
<Link
|
||
href={cta.href}
|
||
className={`inline-flex w-full items-center justify-center rounded border px-4 py-3 text-sm font-medium transition-all duration-300 ${ctaClass ?? "border-gray-600 text-white hover:border-gray-400"}`}
|
||
>
|
||
{cta.label}
|
||
</Link>
|
||
)}
|
||
{cta.type === "disabled" && (
|
||
<button
|
||
type="button"
|
||
disabled
|
||
className="w-full cursor-not-allowed rounded border border-gray-700 bg-surface-light px-4 py-3 text-sm font-medium text-gray-500"
|
||
>
|
||
{cta.label}
|
||
</button>
|
||
)}
|
||
{cta.type === "mailto" && (
|
||
<a
|
||
href={`mailto:${cta.email}?subject=${encodeURIComponent(cta.subject)}`}
|
||
className={`inline-flex w-full items-center justify-center rounded border px-4 py-3 text-sm font-medium transition-all duration-300 ${ctaClass ?? ""}`}
|
||
>
|
||
{cta.label}
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function PricingSection() {
|
||
const sectionRef = useRef<HTMLElement>(null);
|
||
|
||
return (
|
||
<section
|
||
ref={sectionRef}
|
||
id="pricing"
|
||
className="relative scroll-mt-24 overflow-x-visible overflow-y-hidden px-6 py-24"
|
||
>
|
||
<SectionScrollTitle sectionRef={sectionRef} title="Pricing" />
|
||
|
||
<div className="relative z-10 mx-auto max-w-7xl">
|
||
<ScrollReveal>
|
||
<h2 className="mb-4 text-center text-3xl font-bold text-white">Pricing</h2>
|
||
<p className="mx-auto mb-10 max-w-2xl text-center text-gray-400">
|
||
Free with optional extras, Pro at €5/month, or Enterprise for studios.
|
||
</p>
|
||
</ScrollReveal>
|
||
|
||
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3 xl:gap-6">
|
||
{PLANS.map((plan, index) => (
|
||
<ScrollReveal key={plan.name} delay={index * 100}>
|
||
<PricingCard plan={plan} />
|
||
</ScrollReveal>
|
||
))}
|
||
</div>
|
||
|
||
<ScrollReveal delay={360}>
|
||
<p className="mx-auto mt-10 max-w-2xl text-center text-xs text-gray-500">
|
||
*Free includes 10 videos/month; buy 1–15 extras at €0.25 each (max 15 extras). After both
|
||
are used, Pro (€5/mo · 50 videos) is required. Total balance capped at 30. Deduction
|
||
order: monthly first, then extras.
|
||
</p>
|
||
<p className="mx-auto mt-4 max-w-2xl text-center text-xs text-gray-500">
|
||
**Technical support is strictly reserved for Managed Cloud and paid Professional Setup
|
||
agreements; independent self-hosted deployments are community-supported.
|
||
</p>
|
||
</ScrollReveal>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|