ar-saas 0.3.0 → 0.3.2
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.
- package/LICENSE +21 -21
- package/README.md +338 -314
- package/dist/cli.js +19 -0
- package/dist/generator.js +166 -55
- package/package.json +52 -50
- package/templates/backend/.env.example +67 -67
- package/templates/backend/.prettierrc +4 -4
- package/templates/backend/README.md +249 -168
- package/templates/backend/eslint.config.mjs +35 -35
- package/templates/backend/nest-cli.json +8 -8
- package/templates/backend/package-lock.json +10979 -10979
- package/templates/backend/package.json +88 -88
- package/templates/backend/src/app.controller.spec.ts +24 -24
- package/templates/backend/src/app.controller.ts +15 -15
- package/templates/backend/src/app.module.ts +40 -40
- package/templates/backend/src/app.service.ts +11 -11
- package/templates/backend/src/common/base/base.repository.ts +221 -221
- package/templates/backend/src/common/base/base.schema.ts +24 -24
- package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
- package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
- package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
- package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
- package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
- package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
- package/templates/backend/src/main.ts +51 -51
- package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
- package/templates/backend/src/modules/auth/auth.module.ts +20 -20
- package/templates/backend/src/modules/auth/auth.service.ts +257 -257
- package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
- package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
- package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
- package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
- package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
- package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
- package/templates/backend/src/modules/mail/mail.module.ts +9 -9
- package/templates/backend/src/modules/mail/mail.service.ts +141 -141
- package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
- package/templates/backend/src/modules/users/users.module.ts +14 -14
- package/templates/backend/src/modules/users/users.repository.ts +51 -51
- package/templates/backend/src/modules/users/users.service.ts +104 -104
- package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
- package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
- package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
- package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
- package/templates/backend/test/app.e2e-spec.ts +25 -25
- package/templates/backend/test/jest-e2e.json +9 -9
- package/templates/backend/tsconfig.build.json +4 -4
- package/templates/backend/tsconfig.json +26 -26
- package/templates/frontend/.env.local.example +1 -1
- package/templates/frontend/README.md +152 -0
- package/templates/frontend/components.json +20 -20
- package/templates/frontend/eslint.config.mjs +14 -14
- package/templates/frontend/next.config.ts +5 -5
- package/templates/frontend/package-lock.json +6722 -6722
- package/templates/frontend/package.json +48 -48
- package/templates/frontend/pnpm-lock.yaml +5012 -5012
- package/templates/frontend/pnpm-workspace.yaml +3 -3
- package/templates/frontend/postcss.config.mjs +7 -7
- package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
- package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
- package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
- package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
- package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
- package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
- package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
- package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
- package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
- package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
- package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
- package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
- package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
- package/templates/frontend/src/app/globals.css +81 -81
- package/templates/frontend/src/app/layout.tsx +26 -26
- package/templates/frontend/src/app/page.tsx +5 -45
- package/templates/frontend/src/app/setup/page.tsx +371 -275
- package/templates/frontend/src/components/dashboard/header.tsx +89 -89
- package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
- package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
- package/templates/frontend/src/components/landing/faq.tsx +39 -39
- package/templates/frontend/src/components/landing/features.tsx +54 -54
- package/templates/frontend/src/components/landing/footer.tsx +76 -76
- package/templates/frontend/src/components/landing/hero.tsx +72 -72
- package/templates/frontend/src/components/landing/navbar.tsx +78 -78
- package/templates/frontend/src/components/landing/pricing.tsx +90 -90
- package/templates/frontend/src/components/ui/accordion.tsx +52 -52
- package/templates/frontend/src/components/ui/avatar.tsx +46 -46
- package/templates/frontend/src/components/ui/badge.tsx +30 -30
- package/templates/frontend/src/components/ui/button.tsx +52 -52
- package/templates/frontend/src/components/ui/card.tsx +50 -50
- package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
- package/templates/frontend/src/components/ui/dialog.tsx +100 -100
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
- package/templates/frontend/src/components/ui/form.tsx +158 -158
- package/templates/frontend/src/components/ui/input.tsx +21 -21
- package/templates/frontend/src/components/ui/label.tsx +22 -22
- package/templates/frontend/src/components/ui/separator.tsx +25 -25
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
- package/templates/frontend/src/components/ui/switch.tsx +28 -28
- package/templates/frontend/src/components/ui/tabs.tsx +54 -54
- package/templates/frontend/src/components/ui/textarea.tsx +20 -20
- package/templates/frontend/src/components/ui/toast.tsx +109 -109
- package/templates/frontend/src/components/ui/toaster.tsx +30 -30
- package/templates/frontend/src/config/site.ts +197 -197
- package/templates/frontend/src/hooks/use-toast.ts +116 -116
- package/templates/frontend/src/lib/api/auth.ts +39 -39
- package/templates/frontend/src/lib/api/client.ts +66 -66
- package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
- package/templates/frontend/src/lib/utils.ts +6 -6
- package/templates/frontend/src/providers/auth-provider.tsx +60 -60
- package/templates/frontend/src/types/api.ts +12 -12
- package/templates/frontend/src/types/auth.ts +27 -27
- package/templates/frontend/tsconfig.json +23 -23
|
@@ -1,76 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,72 +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 · Cancelá cuando quieras · 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
|
-
}
|
|
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 · Cancelá cuando quieras · 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
|
+
}
|
|
@@ -1,78 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,90 +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
|
-
}
|
|
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
|
+
}
|