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,89 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,71 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,34 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,39 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,54 +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
|
-
}
|
|
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
|
+
}
|