feat: add subscription service — user auth, Stripe billing, API keys, dashboard
- NextAuth v5 with email+password credentials, JWT sessions - Registration, login, email verification, password reset flows - Stripe integration: Free (15/day), Starter ($5/1k/mo), Pro ($20/100k/mo) - API key management (cb_ prefix) with hash-based validation - Dashboard with generations history, settings, billing management - Rate limiting: Redis daily counter (free), DB monthly (paid) - Generate route auth: Bearer API key + session, anonymous allowed - Worker userId propagation for generation history - Pricing section on landing page, auth-aware navbar - Middleware with route protection, CORS for codeboard.vectry.tech - Docker env vars for auth, Stripe, email (smtp.migadu.com)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import { RepoInput } from "@/components/repo-input";
|
||||
import { ExampleRepoCard } from "@/components/example-repo-card";
|
||||
import { ScrollSection } from "@/components/scroll-section";
|
||||
@@ -17,6 +18,9 @@ import {
|
||||
Terminal,
|
||||
FileCode,
|
||||
CheckCircle2,
|
||||
Check,
|
||||
Crown,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
|
||||
export default function HomePage() {
|
||||
@@ -74,6 +78,42 @@ export default function HomePage() {
|
||||
},
|
||||
];
|
||||
|
||||
const pricingTiers = [
|
||||
{
|
||||
name: "Free",
|
||||
price: 0,
|
||||
period: "forever",
|
||||
description: "Get started with CodeBoard",
|
||||
generations: "15 / day",
|
||||
features: ["15 generations per day", "Public repository support", "Interactive documentation", "Architecture diagrams"],
|
||||
cta: "Get Started",
|
||||
href: "/register",
|
||||
highlighted: false,
|
||||
},
|
||||
{
|
||||
name: "Starter",
|
||||
price: 5,
|
||||
period: "month",
|
||||
description: "For regular use",
|
||||
generations: "1,000 / month",
|
||||
features: ["1,000 generations per month", "Generation history", "API key access", "Priority support"],
|
||||
cta: "Start Free Trial",
|
||||
href: "/register",
|
||||
highlighted: true,
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
price: 20,
|
||||
period: "month",
|
||||
description: "For teams & power users",
|
||||
generations: "100,000 / month",
|
||||
features: ["100,000 generations per month", "Full generation history", "Multiple API keys", "Dedicated support", "Custom integrations"],
|
||||
cta: "Start Free Trial",
|
||||
href: "/register",
|
||||
highlighted: false,
|
||||
},
|
||||
];
|
||||
|
||||
const exampleRepos = [
|
||||
{
|
||||
name: "sindresorhus/p-limit",
|
||||
@@ -271,8 +311,8 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div className="hidden sm:block w-px bg-zinc-800" />
|
||||
<div className="text-center">
|
||||
<div className="text-2xl sm:text-3xl font-bold text-white">100%</div>
|
||||
<div className="text-sm text-zinc-500">Free for public repos</div>
|
||||
<div className="text-2xl sm:text-3xl font-bold text-white">Free</div>
|
||||
<div className="text-sm text-zinc-500">tier to start</div>
|
||||
</div>
|
||||
<div className="hidden sm:block w-px bg-zinc-800" />
|
||||
<div className="text-center">
|
||||
@@ -402,6 +442,98 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pricing" className="py-20 lg:py-32">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ScrollSection>
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass mb-6">
|
||||
<Zap className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm text-zinc-300">Pricing</span>
|
||||
</div>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||
Simple, Transparent <span className="gradient-text">Pricing</span>
|
||||
</h2>
|
||||
<p className="text-zinc-400 max-w-xl mx-auto">
|
||||
Start free, scale when you need to
|
||||
</p>
|
||||
</div>
|
||||
</ScrollSection>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
||||
{pricingTiers.map((tier, i) => (
|
||||
<ScrollSection key={tier.name} delay={i + 1}>
|
||||
<div
|
||||
className={`relative group h-full rounded-2xl p-8 transition-all duration-300 hover:-translate-y-1 ${
|
||||
tier.highlighted
|
||||
? "glass-strong border-blue-500/30 shadow-lg shadow-blue-500/10"
|
||||
: "glass hover:bg-white/[0.05]"
|
||||
}`}
|
||||
>
|
||||
{tier.highlighted && (
|
||||
<>
|
||||
<div className="absolute -inset-px rounded-2xl bg-gradient-to-b from-blue-500/20 via-transparent to-blue-500/10 -z-10" />
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-500/20 border border-blue-500/30 text-xs font-medium text-blue-300">
|
||||
<Crown className="w-3 h-3" />
|
||||
Most Popular
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">{tier.name}</h3>
|
||||
<p className="text-sm text-zinc-500">{tier.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-baseline gap-1">
|
||||
{tier.price === 0 ? (
|
||||
<span className="text-4xl font-bold text-white">Free</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-4xl font-bold text-white">${tier.price}</span>
|
||||
<span className="text-zinc-500">/ {tier.period}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-zinc-400">
|
||||
<span className="text-blue-400 font-medium">{tier.generations}</span> generations
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 space-y-3">
|
||||
{tier.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className={`flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center mt-0.5 ${
|
||||
tier.highlighted
|
||||
? "bg-blue-500/20 text-blue-400"
|
||||
: "bg-white/10 text-zinc-400"
|
||||
}`}>
|
||||
<Check className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm text-zinc-300">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto">
|
||||
<Link
|
||||
href={tier.href}
|
||||
className={`block w-full text-center py-3 px-6 rounded-xl text-sm font-medium transition-all duration-200 ${
|
||||
tier.highlighted
|
||||
? "btn-primary"
|
||||
: "glass border border-white/10 text-white hover:bg-white/10 hover:border-white/20"
|
||||
}`}
|
||||
>
|
||||
{tier.cta}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-20 lg:py-32">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ScrollSection>
|
||||
@@ -452,7 +584,7 @@ export default function HomePage() {
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<span>Free for public repositories</span>
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<span>No signup required</span>
|
||||
<span>Free tier available</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user