ar-saas 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +107 -25
  2. package/dist/cli.js +33 -1
  3. package/dist/generator.js +12 -3
  4. package/package.json +7 -3
  5. package/templates/frontend/package.json +21 -13
  6. package/templates/frontend/pnpm-lock.yaml +5012 -0
  7. package/templates/frontend/pnpm-workspace.yaml +3 -0
  8. package/templates/frontend/src/app/(auth)/register/page.tsx +49 -7
  9. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -0
  10. package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +81 -12
  11. package/templates/frontend/src/app/(dashboard)/layout.tsx +11 -32
  12. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -0
  13. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -0
  14. package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -0
  15. package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -0
  16. package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -0
  17. package/templates/frontend/src/app/page.tsx +43 -3
  18. package/templates/frontend/src/app/setup/page.tsx +1 -4
  19. package/templates/frontend/src/components/dashboard/header.tsx +89 -0
  20. package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -0
  21. package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -0
  22. package/templates/frontend/src/components/landing/faq.tsx +39 -0
  23. package/templates/frontend/src/components/landing/features.tsx +54 -0
  24. package/templates/frontend/src/components/landing/footer.tsx +76 -0
  25. package/templates/frontend/src/components/landing/hero.tsx +72 -0
  26. package/templates/frontend/src/components/landing/navbar.tsx +78 -0
  27. package/templates/frontend/src/components/landing/pricing.tsx +90 -0
  28. package/templates/frontend/src/components/ui/accordion.tsx +52 -0
  29. package/templates/frontend/src/components/ui/avatar.tsx +46 -0
  30. package/templates/frontend/src/components/ui/badge.tsx +30 -0
  31. package/templates/frontend/src/components/ui/checkbox.tsx +27 -0
  32. package/templates/frontend/src/components/ui/dialog.tsx +100 -0
  33. package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -0
  34. package/templates/frontend/src/components/ui/separator.tsx +25 -0
  35. package/templates/frontend/src/components/ui/skeleton.tsx +7 -0
  36. package/templates/frontend/src/components/ui/switch.tsx +28 -0
  37. package/templates/frontend/src/components/ui/tabs.tsx +54 -0
  38. package/templates/frontend/src/components/ui/textarea.tsx +20 -0
  39. package/templates/frontend/src/config/site.ts +197 -0
@@ -0,0 +1,89 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import { User, Settings, CreditCard, LogOut } from 'lucide-react'
6
+ import { useAuth } from '@/lib/hooks/use-auth'
7
+ import { Avatar, AvatarFallback } from '@/components/ui/avatar'
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuLabel,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuTrigger,
15
+ } from '@/components/ui/dropdown-menu'
16
+
17
+ const breadcrumbMap: Record<string, string> = {
18
+ '/dashboard': 'Dashboard',
19
+ '/profile': 'Perfil',
20
+ '/settings': 'Ajustes',
21
+ '/billing': 'Facturación',
22
+ '/team': 'Equipo',
23
+ }
24
+
25
+ function getInitials(name: string) {
26
+ return name
27
+ .split(' ')
28
+ .map((n) => n[0])
29
+ .slice(0, 2)
30
+ .join('')
31
+ .toUpperCase()
32
+ }
33
+
34
+ export function DashboardHeader() {
35
+ const { user, logout } = useAuth()
36
+ const pathname = usePathname()
37
+ const pageTitle = breadcrumbMap[pathname] ?? 'Dashboard'
38
+
39
+ return (
40
+ <header className="flex h-14 items-center justify-between border-b bg-background px-6">
41
+ <h1 className="text-sm font-semibold">{pageTitle}</h1>
42
+
43
+ <DropdownMenu>
44
+ <DropdownMenuTrigger asChild>
45
+ <button className="flex items-center gap-2 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring">
46
+ <Avatar className="size-8">
47
+ <AvatarFallback className="text-xs">
48
+ {user?.name ? getInitials(user.name) : 'U'}
49
+ </AvatarFallback>
50
+ </Avatar>
51
+ </button>
52
+ </DropdownMenuTrigger>
53
+ <DropdownMenuContent align="end" className="w-52">
54
+ <DropdownMenuLabel className="font-normal">
55
+ <p className="font-medium leading-none">{user?.name}</p>
56
+ <p className="mt-1 text-xs text-muted-foreground">{user?.email}</p>
57
+ </DropdownMenuLabel>
58
+ <DropdownMenuSeparator />
59
+ <DropdownMenuItem asChild>
60
+ <Link href="/profile">
61
+ <User className="mr-2 size-4" />
62
+ Perfil
63
+ </Link>
64
+ </DropdownMenuItem>
65
+ <DropdownMenuItem asChild>
66
+ <Link href="/settings">
67
+ <Settings className="mr-2 size-4" />
68
+ Ajustes
69
+ </Link>
70
+ </DropdownMenuItem>
71
+ <DropdownMenuItem asChild>
72
+ <Link href="/billing">
73
+ <CreditCard className="mr-2 size-4" />
74
+ Facturación
75
+ </Link>
76
+ </DropdownMenuItem>
77
+ <DropdownMenuSeparator />
78
+ <DropdownMenuItem
79
+ className="text-destructive focus:text-destructive"
80
+ onClick={logout}
81
+ >
82
+ <LogOut className="mr-2 size-4" />
83
+ Cerrar sesión
84
+ </DropdownMenuItem>
85
+ </DropdownMenuContent>
86
+ </DropdownMenu>
87
+ </header>
88
+ )
89
+ }
@@ -0,0 +1,71 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import {
6
+ LayoutDashboard,
7
+ User,
8
+ Settings,
9
+ CreditCard,
10
+ Users,
11
+ LogOut,
12
+ } from 'lucide-react'
13
+ import { siteConfig } from '@/config/site'
14
+ import { useAuth } from '@/lib/hooks/use-auth'
15
+ import { Button } from '@/components/ui/button'
16
+ import { Separator } from '@/components/ui/separator'
17
+ import { cn } from '@/lib/utils'
18
+
19
+ const navItems = [
20
+ { label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
21
+ { label: 'Perfil', href: '/profile', icon: User },
22
+ { label: 'Ajustes', href: '/settings', icon: Settings },
23
+ { label: 'Facturación', href: '/billing', icon: CreditCard },
24
+ { label: 'Equipo', href: '/team', icon: Users },
25
+ ]
26
+
27
+ export function DashboardSidebar() {
28
+ const pathname = usePathname()
29
+ const { logout } = useAuth()
30
+
31
+ return (
32
+ <aside className="flex w-60 shrink-0 flex-col border-r bg-card">
33
+ <div className="flex h-14 items-center border-b px-4">
34
+ <Link href="/" className="text-sm font-bold tracking-tight">
35
+ {siteConfig.name}
36
+ </Link>
37
+ </div>
38
+
39
+ <nav className="flex-1 space-y-1 p-3">
40
+ {navItems.map(({ label, href, icon: Icon }) => (
41
+ <Button
42
+ key={href}
43
+ asChild
44
+ variant={pathname === href ? 'secondary' : 'ghost'}
45
+ className={cn(
46
+ 'w-full justify-start',
47
+ pathname === href && 'font-medium',
48
+ )}
49
+ >
50
+ <Link href={href}>
51
+ <Icon className="mr-2 size-4" />
52
+ {label}
53
+ </Link>
54
+ </Button>
55
+ ))}
56
+ </nav>
57
+
58
+ <div className="p-3">
59
+ <Separator className="mb-3" />
60
+ <Button
61
+ variant="ghost"
62
+ className="w-full justify-start text-muted-foreground hover:text-foreground"
63
+ onClick={logout}
64
+ >
65
+ <LogOut className="mr-2 size-4" />
66
+ Cerrar sesión
67
+ </Button>
68
+ </div>
69
+ </aside>
70
+ )
71
+ }
@@ -0,0 +1,34 @@
1
+ import { type LucideIcon } from 'lucide-react'
2
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ interface StatCardProps {
6
+ title: string
7
+ value: string
8
+ description?: string
9
+ icon: LucideIcon
10
+ trend?: { value: string; positive: boolean }
11
+ className?: string
12
+ }
13
+
14
+ export function StatCard({ title, value, description, icon: Icon, trend, className }: StatCardProps) {
15
+ return (
16
+ <Card className={cn('', className)}>
17
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
18
+ <CardTitle className="text-sm font-medium">{title}</CardTitle>
19
+ <Icon className="size-4 text-muted-foreground" />
20
+ </CardHeader>
21
+ <CardContent>
22
+ <div className="text-2xl font-bold">{value}</div>
23
+ {trend && (
24
+ <p className={cn('mt-1 text-xs', trend.positive ? 'text-green-600' : 'text-red-500')}>
25
+ {trend.positive ? '+' : ''}{trend.value} vs. mes anterior
26
+ </p>
27
+ )}
28
+ {description && !trend && (
29
+ <p className="mt-1 text-xs text-muted-foreground">{description}</p>
30
+ )}
31
+ </CardContent>
32
+ </Card>
33
+ )
34
+ }
@@ -0,0 +1,39 @@
1
+ import { siteConfig } from '@/config/site'
2
+ import {
3
+ Accordion,
4
+ AccordionContent,
5
+ AccordionItem,
6
+ AccordionTrigger,
7
+ } from '@/components/ui/accordion'
8
+
9
+ export function LandingFaq() {
10
+ return (
11
+ <section id="faq" className="py-20 md:py-28">
12
+ <div className="mx-auto max-w-3xl px-4">
13
+ <div className="text-center">
14
+ <h2 className="text-3xl font-bold tracking-tight md:text-4xl">
15
+ Preguntas frecuentes
16
+ </h2>
17
+ <p className="mt-4 text-muted-foreground">
18
+ Si no encontrás lo que buscás, escribinos a{' '}
19
+ <a
20
+ href={`mailto:${siteConfig.supportEmail}`}
21
+ className="underline underline-offset-4 hover:text-foreground"
22
+ >
23
+ {siteConfig.supportEmail}
24
+ </a>
25
+ </p>
26
+ </div>
27
+
28
+ <Accordion type="single" collapsible className="mt-12">
29
+ {siteConfig.faq.map((item, index) => (
30
+ <AccordionItem key={index} value={`item-${index}`}>
31
+ <AccordionTrigger className="text-left">{item.question}</AccordionTrigger>
32
+ <AccordionContent className="text-muted-foreground">{item.answer}</AccordionContent>
33
+ </AccordionItem>
34
+ ))}
35
+ </Accordion>
36
+ </div>
37
+ </section>
38
+ )
39
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ Zap,
3
+ Shield,
4
+ BarChart3,
5
+ Users,
6
+ Globe,
7
+ Headphones,
8
+ type LucideIcon,
9
+ } from 'lucide-react'
10
+ import { siteConfig } from '@/config/site'
11
+
12
+ const iconMap: Record<string, LucideIcon> = {
13
+ Zap,
14
+ Shield,
15
+ BarChart3,
16
+ Users,
17
+ Globe,
18
+ Headphones,
19
+ }
20
+
21
+ export function LandingFeatures() {
22
+ return (
23
+ <section id="features" className="py-20 md:py-28">
24
+ <div className="mx-auto max-w-6xl px-4">
25
+ <div className="mx-auto max-w-2xl text-center">
26
+ <h2 className="text-3xl font-bold tracking-tight md:text-4xl">
27
+ Todo lo que necesitás para lanzar
28
+ </h2>
29
+ <p className="mt-4 text-muted-foreground">
30
+ Funcionalidades pensadas para que te enfoques en el negocio, no en la infraestructura.
31
+ </p>
32
+ </div>
33
+
34
+ <div className="mt-16 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
35
+ {siteConfig.features.map((feature) => {
36
+ const Icon = iconMap[feature.icon] ?? Zap
37
+ return (
38
+ <div
39
+ key={feature.title}
40
+ className="group rounded-xl border bg-card p-6 transition-shadow hover:shadow-md"
41
+ >
42
+ <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
43
+ <Icon className="size-5" />
44
+ </div>
45
+ <h3 className="font-semibold">{feature.title}</h3>
46
+ <p className="mt-2 text-sm text-muted-foreground">{feature.description}</p>
47
+ </div>
48
+ )
49
+ })}
50
+ </div>
51
+ </div>
52
+ </section>
53
+ )
54
+ }
@@ -0,0 +1,76 @@
1
+ import Link from 'next/link'
2
+ import { Twitter, Github, Linkedin } from 'lucide-react'
3
+ import { siteConfig } from '@/config/site'
4
+
5
+ export function LandingFooter() {
6
+ const { footer } = siteConfig
7
+
8
+ return (
9
+ <footer className="border-t bg-card">
10
+ <div className="mx-auto max-w-6xl px-4 py-12">
11
+ <div className="grid gap-8 sm:grid-cols-2 md:grid-cols-4">
12
+ {/* Brand */}
13
+ <div className="sm:col-span-2 md:col-span-1">
14
+ <p className="text-lg font-bold">{siteConfig.name}</p>
15
+ <p className="mt-2 text-sm text-muted-foreground">{siteConfig.tagline}</p>
16
+ <div className="mt-4 flex gap-3">
17
+ {footer.social.twitter && (
18
+ <a
19
+ href={footer.social.twitter}
20
+ target="_blank"
21
+ rel="noopener noreferrer"
22
+ className="text-muted-foreground transition-colors hover:text-foreground"
23
+ >
24
+ <Twitter className="size-4" />
25
+ </a>
26
+ )}
27
+ {footer.social.github && (
28
+ <a
29
+ href={footer.social.github}
30
+ target="_blank"
31
+ rel="noopener noreferrer"
32
+ className="text-muted-foreground transition-colors hover:text-foreground"
33
+ >
34
+ <Github className="size-4" />
35
+ </a>
36
+ )}
37
+ {footer.social.linkedin && (
38
+ <a
39
+ href={footer.social.linkedin}
40
+ target="_blank"
41
+ rel="noopener noreferrer"
42
+ className="text-muted-foreground transition-colors hover:text-foreground"
43
+ >
44
+ <Linkedin className="size-4" />
45
+ </a>
46
+ )}
47
+ </div>
48
+ </div>
49
+
50
+ {/* Link columns */}
51
+ {footer.columns.map((col) => (
52
+ <div key={col.title}>
53
+ <p className="text-sm font-semibold">{col.title}</p>
54
+ <ul className="mt-4 space-y-3">
55
+ {col.links.map((link) => (
56
+ <li key={link.label}>
57
+ <Link
58
+ href={link.href}
59
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
60
+ >
61
+ {link.label}
62
+ </Link>
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ </div>
67
+ ))}
68
+ </div>
69
+
70
+ <div className="mt-12 border-t pt-6 text-center text-sm text-muted-foreground">
71
+ {footer.copyright}
72
+ </div>
73
+ </div>
74
+ </footer>
75
+ )
76
+ }
@@ -0,0 +1,72 @@
1
+ import Link from 'next/link'
2
+ import { ArrowRight } from 'lucide-react'
3
+ import { siteConfig } from '@/config/site'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Badge } from '@/components/ui/badge'
6
+
7
+ export function LandingHero() {
8
+ return (
9
+ <section className="relative overflow-hidden py-20 md:py-32">
10
+ {/* Background gradient */}
11
+ <div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_80%_60%_at_50%_-10%,hsl(var(--primary)/0.12),transparent)]" />
12
+
13
+ <div className="mx-auto max-w-6xl px-4 text-center">
14
+ <Badge variant="outline" className="mb-6 px-4 py-1.5 text-sm">
15
+ Listo para producción en minutos
16
+ </Badge>
17
+
18
+ <h1 className="mx-auto max-w-4xl text-4xl font-bold tracking-tight md:text-6xl lg:text-7xl">
19
+ {siteConfig.hero.headline}
20
+ </h1>
21
+
22
+ <p className="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground md:text-xl">
23
+ {siteConfig.hero.description}
24
+ </p>
25
+
26
+ <div className="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
27
+ <Button size="lg" asChild className="gap-2">
28
+ <Link href={siteConfig.hero.cta.href}>
29
+ {siteConfig.hero.cta.label}
30
+ <ArrowRight className="size-4" />
31
+ </Link>
32
+ </Button>
33
+ <Button size="lg" variant="outline" asChild>
34
+ <Link href={siteConfig.hero.ctaSecondary.href}>
35
+ {siteConfig.hero.ctaSecondary.label}
36
+ </Link>
37
+ </Button>
38
+ </div>
39
+
40
+ {/* Social proof */}
41
+ <p className="mt-8 text-sm text-muted-foreground">
42
+ Sin tarjeta de crédito &middot; Cancelá cuando quieras &middot; Soporte en español
43
+ </p>
44
+
45
+ {/* App mockup placeholder */}
46
+ <div className="mx-auto mt-16 max-w-4xl overflow-hidden rounded-xl border bg-muted/50 shadow-2xl">
47
+ <div className="flex h-8 items-center gap-2 border-b bg-muted px-4">
48
+ <div className="size-3 rounded-full bg-red-400" />
49
+ <div className="size-3 rounded-full bg-yellow-400" />
50
+ <div className="size-3 rounded-full bg-green-400" />
51
+ <div className="mx-2 h-4 flex-1 rounded bg-muted-foreground/20" />
52
+ </div>
53
+ <div className="grid grid-cols-[200px_1fr] divide-x" style={{ minHeight: 320 }}>
54
+ <div className="space-y-2 bg-card p-4">
55
+ {[...Array(6)].map((_, i) => (
56
+ <div key={i} className="h-7 rounded-md bg-muted" />
57
+ ))}
58
+ </div>
59
+ <div className="space-y-4 p-6">
60
+ <div className="grid grid-cols-3 gap-3">
61
+ {[...Array(3)].map((_, i) => (
62
+ <div key={i} className="h-20 rounded-lg border bg-card" />
63
+ ))}
64
+ </div>
65
+ <div className="h-40 rounded-lg border bg-card" />
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </section>
71
+ )
72
+ }
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { useState } from 'react'
5
+ import { Menu, X } from 'lucide-react'
6
+ import { siteConfig } from '@/config/site'
7
+ import { Button } from '@/components/ui/button'
8
+
9
+ export function LandingNavbar() {
10
+ const [open, setOpen] = useState(false)
11
+
12
+ return (
13
+ <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
14
+ <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4">
15
+ <Link href="/" className="text-lg font-bold tracking-tight">
16
+ {siteConfig.name}
17
+ </Link>
18
+
19
+ {/* Desktop nav */}
20
+ <nav className="hidden items-center gap-6 md:flex">
21
+ {siteConfig.nav.links.map((link) => (
22
+ <Link
23
+ key={link.href}
24
+ href={link.href}
25
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
26
+ >
27
+ {link.label}
28
+ </Link>
29
+ ))}
30
+ </nav>
31
+
32
+ <div className="hidden items-center gap-2 md:flex">
33
+ <Button variant="ghost" asChild size="sm">
34
+ <Link href="/login">Iniciar sesión</Link>
35
+ </Button>
36
+ <Button asChild size="sm">
37
+ <Link href="/register">Empezar gratis</Link>
38
+ </Button>
39
+ </div>
40
+
41
+ {/* Mobile toggle */}
42
+ <button
43
+ className="md:hidden"
44
+ onClick={() => setOpen((v) => !v)}
45
+ aria-label="Menú"
46
+ >
47
+ {open ? <X className="size-5" /> : <Menu className="size-5" />}
48
+ </button>
49
+ </div>
50
+
51
+ {/* Mobile menu */}
52
+ {open && (
53
+ <div className="border-t bg-background px-4 pb-4 md:hidden">
54
+ <nav className="flex flex-col gap-3 pt-4">
55
+ {siteConfig.nav.links.map((link) => (
56
+ <Link
57
+ key={link.href}
58
+ href={link.href}
59
+ className="text-sm text-muted-foreground"
60
+ onClick={() => setOpen(false)}
61
+ >
62
+ {link.label}
63
+ </Link>
64
+ ))}
65
+ <div className="mt-2 flex flex-col gap-2">
66
+ <Button variant="outline" asChild>
67
+ <Link href="/login">Iniciar sesión</Link>
68
+ </Button>
69
+ <Button asChild>
70
+ <Link href="/register">Empezar gratis</Link>
71
+ </Button>
72
+ </div>
73
+ </nav>
74
+ </div>
75
+ )}
76
+ </header>
77
+ )
78
+ }
@@ -0,0 +1,90 @@
1
+ import Link from 'next/link'
2
+ import { Check } from 'lucide-react'
3
+ import { siteConfig } from '@/config/site'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Badge } from '@/components/ui/badge'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ export function LandingPricing() {
9
+ return (
10
+ <section id="pricing" className="py-20 md:py-28">
11
+ <div className="mx-auto max-w-6xl px-4">
12
+ <div className="mx-auto max-w-2xl text-center">
13
+ <h2 className="text-3xl font-bold tracking-tight md:text-4xl">
14
+ Precios claros y sin sorpresas
15
+ </h2>
16
+ <p className="mt-4 text-muted-foreground">
17
+ Elegí el plan que mejor se adapte a tu equipo. Cambiá cuando quieras.
18
+ </p>
19
+ </div>
20
+
21
+ <div className="mt-16 grid gap-8 md:grid-cols-3">
22
+ {siteConfig.pricing.map((plan) => (
23
+ <div
24
+ key={plan.name}
25
+ className={cn(
26
+ 'relative flex flex-col rounded-xl border p-8',
27
+ plan.highlight
28
+ ? 'border-primary bg-primary text-primary-foreground shadow-lg'
29
+ : 'bg-card',
30
+ )}
31
+ >
32
+ {plan.highlight && (
33
+ <Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-background text-foreground shadow">
34
+ Más popular
35
+ </Badge>
36
+ )}
37
+
38
+ <div>
39
+ <h3 className="text-lg font-semibold">{plan.name}</h3>
40
+ <p
41
+ className={cn(
42
+ 'mt-1 text-sm',
43
+ plan.highlight ? 'text-primary-foreground/80' : 'text-muted-foreground',
44
+ )}
45
+ >
46
+ {plan.description}
47
+ </p>
48
+ <div className="mt-4 flex items-end gap-1">
49
+ <span className="text-4xl font-bold">{plan.price}</span>
50
+ {plan.period && (
51
+ <span
52
+ className={cn(
53
+ 'mb-1 text-sm',
54
+ plan.highlight ? 'text-primary-foreground/70' : 'text-muted-foreground',
55
+ )}
56
+ >
57
+ {plan.period}
58
+ </span>
59
+ )}
60
+ </div>
61
+ </div>
62
+
63
+ <ul className="my-8 flex-1 space-y-3">
64
+ {plan.features.map((f) => (
65
+ <li key={f} className="flex items-center gap-3 text-sm">
66
+ <Check
67
+ className={cn(
68
+ 'size-4 shrink-0',
69
+ plan.highlight ? 'text-primary-foreground' : 'text-primary',
70
+ )}
71
+ />
72
+ {f}
73
+ </li>
74
+ ))}
75
+ </ul>
76
+
77
+ <Button
78
+ asChild
79
+ variant={plan.highlight ? 'secondary' : 'outline'}
80
+ className="w-full"
81
+ >
82
+ <Link href={plan.href}>{plan.cta}</Link>
83
+ </Button>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ </section>
89
+ )
90
+ }
@@ -0,0 +1,52 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as AccordionPrimitive from '@radix-ui/react-accordion'
5
+ import { ChevronDown } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const Accordion = AccordionPrimitive.Root
9
+
10
+ const AccordionItem = React.forwardRef<
11
+ React.ElementRef<typeof AccordionPrimitive.Item>,
12
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
13
+ >(({ className, ...props }, ref) => (
14
+ <AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
15
+ ))
16
+ AccordionItem.displayName = 'AccordionItem'
17
+
18
+ const AccordionTrigger = React.forwardRef<
19
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
20
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
21
+ >(({ className, children, ...props }, ref) => (
22
+ <AccordionPrimitive.Header className="flex">
23
+ <AccordionPrimitive.Trigger
24
+ ref={ref}
25
+ className={cn(
26
+ 'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
33
+ </AccordionPrimitive.Trigger>
34
+ </AccordionPrimitive.Header>
35
+ ))
36
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
37
+
38
+ const AccordionContent = React.forwardRef<
39
+ React.ElementRef<typeof AccordionPrimitive.Content>,
40
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
41
+ >(({ className, children, ...props }, ref) => (
42
+ <AccordionPrimitive.Content
43
+ ref={ref}
44
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
45
+ {...props}
46
+ >
47
+ <div className={cn('pb-4 pt-0', className)}>{children}</div>
48
+ </AccordionPrimitive.Content>
49
+ ))
50
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
51
+
52
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }