331 lines
10 KiB
TypeScript
331 lines
10 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 { SALES_EMAIL } from "@/lib/plans";
|
|
|
|
const FEATURE_LABELS = [
|
|
"Infrastructure",
|
|
"Limit",
|
|
"Batch mode",
|
|
"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: "14 videos / 12 hours · 720p",
|
|
"Batch mode": "Limited batch · up to 3 files",
|
|
"File type": "MP3",
|
|
Playlists: "Not included",
|
|
"API support": "Not included",
|
|
"ID3 tags": "Auto-fill title & metadata from MP3 tags",
|
|
Support: "Community",
|
|
Watermark: "Optional · support badge",
|
|
},
|
|
infrastructureChips: ["cloud"],
|
|
watermarkNote:
|
|
"Show \"Uploaded through Songs2YT.com\" 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",
|
|
"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: "Fully customizable · remove completely",
|
|
},
|
|
infrastructureChips: ["cloud"],
|
|
cta: { type: "disabled", label: "Coming soon" },
|
|
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",
|
|
"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: "Fully customizable · remove completely",
|
|
},
|
|
infrastructureChips: ["cloud", "self-hosted"],
|
|
cta: {
|
|
type: "mailto",
|
|
label: "Contact Sales",
|
|
email: SALES_EMAIL,
|
|
subject: "Songs2YT 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 for getting started, Pro for creators, or Enterprise for teams.
|
|
</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">
|
|
*Pro is a monthly subscription with included quota (extensions via support). Self-hosted
|
|
open-source deployments have no quotas.
|
|
</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>
|
|
);
|
|
}
|