dad798a904
Deploy to TrueNAS / deploy (push) Failing after 33s
- Landing page with hero, feature grid, trust signals - Split-layout login redesign (admin + portal) - Pricing page with storage tiers (5GB/50GB/unlimited) - StorageQuotaService backend (V36 migration, 402 on exceeded) - Frontend storage integration + 402 error handling - StorageController uses TenantContext for tenant isolation - onTierChange() hook for subscription tier updates
256 lines
8.5 KiB
TypeScript
256 lines
8.5 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import { useTranslations } from "next-intl"
|
|
import { Building2, Cannabis, Check, Leaf } from "lucide-react"
|
|
|
|
const plans = [
|
|
{
|
|
id: "starter",
|
|
icon: Leaf,
|
|
price: "19",
|
|
memberLimit: "30",
|
|
storage: "5",
|
|
features: [
|
|
"memberManagement",
|
|
"distributionTracking",
|
|
"complianceReports",
|
|
"quotaMonitoring",
|
|
"memberPortal",
|
|
"emailSupport",
|
|
],
|
|
},
|
|
{
|
|
id: "pro",
|
|
icon: Cannabis,
|
|
price: "49",
|
|
memberLimit: "100",
|
|
storage: "50",
|
|
popular: true,
|
|
features: [
|
|
"allStarter",
|
|
"growCalendar",
|
|
"staffManagement",
|
|
"advancedReports",
|
|
"pdfExport",
|
|
"apiAccess",
|
|
"prioritySupport",
|
|
],
|
|
},
|
|
{
|
|
id: "enterprise",
|
|
icon: Building2,
|
|
price: null,
|
|
memberLimit: "unlimited",
|
|
storage: "custom",
|
|
features: [
|
|
"allPro",
|
|
"unlimitedMembers",
|
|
"multiClub",
|
|
"customIntegrations",
|
|
"sla",
|
|
"dedicatedSupport",
|
|
"onboarding",
|
|
],
|
|
},
|
|
]
|
|
|
|
const faqs = [
|
|
{ id: "trial" },
|
|
{ id: "payment" },
|
|
{ id: "cancel" },
|
|
{ id: "data" },
|
|
{ id: "migration" },
|
|
{ id: "storage" },
|
|
]
|
|
|
|
export default function PricingPage() {
|
|
const t = useTranslations("marketing.pricing")
|
|
|
|
return (
|
|
<div className="py-16">
|
|
{/* Hero */}
|
|
<div className="container mx-auto px-4 text-center mb-16">
|
|
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl mb-4">
|
|
{t("title")}
|
|
</h1>
|
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
{t("subtitle")}
|
|
</p>
|
|
<div className="mt-6 inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2 text-sm font-medium text-primary">
|
|
<Cannabis className="h-4 w-4" />
|
|
{t("trialBadge")}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Plan Cards */}
|
|
<div className="container mx-auto px-4 mb-20">
|
|
<div className="grid grid-cols-1 gap-8 md:grid-cols-3 max-w-5xl mx-auto">
|
|
{plans.map((plan) => {
|
|
const Icon = plan.icon
|
|
return (
|
|
<div
|
|
key={plan.id}
|
|
className={`relative rounded-2xl border p-8 flex flex-col ${
|
|
plan.popular
|
|
? "border-primary shadow-lg ring-1 ring-primary"
|
|
: "border-border"
|
|
}`}
|
|
>
|
|
{plan.popular && (
|
|
<div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
|
|
{t("popular")}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
|
<Icon className="h-5 w-5 text-primary" />
|
|
</div>
|
|
<h3 className="text-xl font-bold">
|
|
{t(`plans.${plan.id}.name`)}
|
|
</h3>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t(`plans.${plan.id}.description`)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
{plan.price ? (
|
|
<div className="flex items-baseline gap-1">
|
|
<span className="text-4xl font-bold">€{plan.price}</span>
|
|
<span className="text-muted-foreground">
|
|
/ {t("perMonth")}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<div className="text-4xl font-bold">{t("contactUs")}</div>
|
|
)}
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{t(`plans.${plan.id}.memberNote`, {
|
|
limit: plan.memberLimit,
|
|
})}
|
|
</p>
|
|
<div className="mt-2 inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium">
|
|
{t(`storage.${plan.id}`)}
|
|
{plan.id === "pro" && (
|
|
<span className="text-muted-foreground ml-1">
|
|
{t("storage.proOverage")}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<ul className="space-y-3 mb-8 flex-1">
|
|
{plan.features.map((feature) => (
|
|
<li key={feature} className="flex items-start gap-2">
|
|
<Check className="h-4 w-4 text-primary mt-0.5 shrink-0" />
|
|
<span className="text-sm">
|
|
{t(`features.${feature}`)}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<Link
|
|
href={plan.price ? "/login" : "mailto:info@plate-software.de"}
|
|
className={`inline-flex h-11 items-center justify-center rounded-md px-6 text-sm font-medium transition-colors ${
|
|
plan.popular
|
|
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
: "border border-input bg-background hover:bg-accent hover:text-accent-foreground"
|
|
}`}
|
|
>
|
|
{plan.price ? t("startTrial") : t("contactSales")}
|
|
</Link>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Feature Comparison Table */}
|
|
<div className="container mx-auto px-4 mb-20 overflow-hidden">
|
|
<h2 className="text-2xl font-bold text-center mb-8">
|
|
{t("comparisonTitle")}
|
|
</h2>
|
|
<div className="max-w-4xl mx-auto overflow-x-auto -mx-4 px-4 sm:mx-auto sm:px-0">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="text-left py-3 px-4 font-medium">
|
|
{t("feature")}
|
|
</th>
|
|
<th className="text-center py-3 px-4 font-medium">Starter</th>
|
|
<th className="text-center py-3 px-4 font-medium">Pro</th>
|
|
<th className="text-center py-3 px-4 font-medium">
|
|
Enterprise
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y">
|
|
{[
|
|
"compMembers",
|
|
"compStorage",
|
|
"compOverage",
|
|
"compDistributions",
|
|
"compReports",
|
|
"compGrow",
|
|
"compStaff",
|
|
"compApi",
|
|
"compMultiClub",
|
|
"compSupport",
|
|
].map((row) => (
|
|
<tr key={row}>
|
|
<td className="py-3 px-4">{t(`comparison.${row}.label`)}</td>
|
|
<td className="py-3 px-4 text-center">
|
|
{t(`comparison.${row}.starter`)}
|
|
</td>
|
|
<td className="py-3 px-4 text-center">
|
|
{t(`comparison.${row}.pro`)}
|
|
</td>
|
|
<td className="py-3 px-4 text-center">
|
|
{t(`comparison.${row}.enterprise`)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* FAQ */}
|
|
<div className="container mx-auto px-4 mb-16">
|
|
<h2 className="text-2xl font-bold text-center mb-8">{t("faqTitle")}</h2>
|
|
<div className="max-w-3xl mx-auto space-y-6">
|
|
{faqs.map((faq) => (
|
|
<div key={faq.id} className="rounded-lg border p-6">
|
|
<h3 className="font-semibold mb-2">
|
|
{t(`faq.${faq.id}.question`)}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t(`faq.${faq.id}.answer`)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* CTA */}
|
|
<div className="container mx-auto px-4 text-center">
|
|
<div className="rounded-2xl bg-primary/5 border border-primary/20 p-12 max-w-3xl mx-auto">
|
|
<h2 className="text-2xl font-bold mb-3">{t("ctaTitle")}</h2>
|
|
<p className="text-muted-foreground mb-6">{t("ctaSubtitle")}</p>
|
|
<Link
|
|
href="/login"
|
|
className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
>
|
|
{t("ctaButton")}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|