ar-saas 0.3.1 → 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.
Files changed (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +338 -314
  3. package/dist/cli.js +19 -0
  4. package/dist/generator.js +166 -55
  5. package/package.json +52 -50
  6. package/templates/backend/.env.example +67 -67
  7. package/templates/backend/.prettierrc +4 -4
  8. package/templates/backend/README.md +249 -168
  9. package/templates/backend/eslint.config.mjs +35 -35
  10. package/templates/backend/nest-cli.json +8 -8
  11. package/templates/backend/package-lock.json +10979 -10979
  12. package/templates/backend/package.json +88 -88
  13. package/templates/backend/src/app.controller.spec.ts +24 -24
  14. package/templates/backend/src/app.controller.ts +15 -15
  15. package/templates/backend/src/app.module.ts +40 -40
  16. package/templates/backend/src/app.service.ts +11 -11
  17. package/templates/backend/src/common/base/base.repository.ts +221 -221
  18. package/templates/backend/src/common/base/base.schema.ts +24 -24
  19. package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
  20. package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
  21. package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
  22. package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
  23. package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
  24. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
  25. package/templates/backend/src/main.ts +51 -51
  26. package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
  27. package/templates/backend/src/modules/auth/auth.module.ts +20 -20
  28. package/templates/backend/src/modules/auth/auth.service.ts +257 -257
  29. package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
  30. package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
  31. package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
  32. package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
  33. package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
  34. package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
  35. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
  36. package/templates/backend/src/modules/mail/mail.module.ts +9 -9
  37. package/templates/backend/src/modules/mail/mail.service.ts +141 -141
  38. package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
  39. package/templates/backend/src/modules/users/users.module.ts +14 -14
  40. package/templates/backend/src/modules/users/users.repository.ts +51 -51
  41. package/templates/backend/src/modules/users/users.service.ts +104 -104
  42. package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
  43. package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
  44. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
  45. package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
  46. package/templates/backend/test/app.e2e-spec.ts +25 -25
  47. package/templates/backend/test/jest-e2e.json +9 -9
  48. package/templates/backend/tsconfig.build.json +4 -4
  49. package/templates/backend/tsconfig.json +26 -26
  50. package/templates/frontend/.env.local.example +1 -1
  51. package/templates/frontend/README.md +152 -0
  52. package/templates/frontend/components.json +20 -20
  53. package/templates/frontend/eslint.config.mjs +14 -14
  54. package/templates/frontend/next.config.ts +5 -5
  55. package/templates/frontend/package-lock.json +6722 -6722
  56. package/templates/frontend/package.json +48 -48
  57. package/templates/frontend/pnpm-lock.yaml +5012 -5012
  58. package/templates/frontend/pnpm-workspace.yaml +3 -3
  59. package/templates/frontend/postcss.config.mjs +7 -7
  60. package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
  61. package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
  62. package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
  63. package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
  64. package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
  65. package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
  66. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
  67. package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
  68. package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
  69. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
  70. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
  71. package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
  72. package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
  73. package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
  74. package/templates/frontend/src/app/globals.css +81 -81
  75. package/templates/frontend/src/app/layout.tsx +26 -26
  76. package/templates/frontend/src/app/page.tsx +5 -45
  77. package/templates/frontend/src/app/setup/page.tsx +371 -275
  78. package/templates/frontend/src/components/dashboard/header.tsx +89 -89
  79. package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
  80. package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
  81. package/templates/frontend/src/components/landing/faq.tsx +39 -39
  82. package/templates/frontend/src/components/landing/features.tsx +54 -54
  83. package/templates/frontend/src/components/landing/footer.tsx +76 -76
  84. package/templates/frontend/src/components/landing/hero.tsx +72 -72
  85. package/templates/frontend/src/components/landing/navbar.tsx +78 -78
  86. package/templates/frontend/src/components/landing/pricing.tsx +90 -90
  87. package/templates/frontend/src/components/ui/accordion.tsx +52 -52
  88. package/templates/frontend/src/components/ui/avatar.tsx +46 -46
  89. package/templates/frontend/src/components/ui/badge.tsx +30 -30
  90. package/templates/frontend/src/components/ui/button.tsx +52 -52
  91. package/templates/frontend/src/components/ui/card.tsx +50 -50
  92. package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
  93. package/templates/frontend/src/components/ui/dialog.tsx +100 -100
  94. package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
  95. package/templates/frontend/src/components/ui/form.tsx +158 -158
  96. package/templates/frontend/src/components/ui/input.tsx +21 -21
  97. package/templates/frontend/src/components/ui/label.tsx +22 -22
  98. package/templates/frontend/src/components/ui/separator.tsx +25 -25
  99. package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
  100. package/templates/frontend/src/components/ui/switch.tsx +28 -28
  101. package/templates/frontend/src/components/ui/tabs.tsx +54 -54
  102. package/templates/frontend/src/components/ui/textarea.tsx +20 -20
  103. package/templates/frontend/src/components/ui/toast.tsx +109 -109
  104. package/templates/frontend/src/components/ui/toaster.tsx +30 -30
  105. package/templates/frontend/src/config/site.ts +197 -197
  106. package/templates/frontend/src/hooks/use-toast.ts +116 -116
  107. package/templates/frontend/src/lib/api/auth.ts +39 -39
  108. package/templates/frontend/src/lib/api/client.ts +66 -66
  109. package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
  110. package/templates/frontend/src/lib/utils.ts +6 -6
  111. package/templates/frontend/src/providers/auth-provider.tsx +60 -60
  112. package/templates/frontend/src/types/api.ts +12 -12
  113. package/templates/frontend/src/types/auth.ts +27 -27
  114. package/templates/frontend/tsconfig.json +23 -23
@@ -1,3 +1,3 @@
1
- allowBuilds:
2
- sharp: set this to true or false
3
- unrs-resolver: set this to true or false
1
+ allowBuilds:
2
+ sharp: set this to true or false
3
+ unrs-resolver: set this to true or false
@@ -1,7 +1,7 @@
1
- const config = {
2
- plugins: {
3
- '@tailwindcss/postcss': {},
4
- },
5
- }
6
-
7
- export default config
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
6
+
7
+ export default config
@@ -1,84 +1,84 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import Link from 'next/link'
5
- import { useForm } from 'react-hook-form'
6
- import { authApi } from '@/lib/api/auth'
7
- import { Button } from '@/components/ui/button'
8
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
9
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
10
- import { Input } from '@/components/ui/input'
11
-
12
- interface ForgotPasswordForm {
13
- email: string
14
- }
15
-
16
- export default function ForgotPasswordPage() {
17
- const [sent, setSent] = useState(false)
18
-
19
- const form = useForm<ForgotPasswordForm>({
20
- defaultValues: { email: '' },
21
- })
22
-
23
- async function onSubmit(data: ForgotPasswordForm) {
24
- await authApi.forgotPassword(data.email).catch(() => null)
25
- setSent(true)
26
- }
27
-
28
- if (sent) {
29
- return (
30
- <Card>
31
- <CardHeader>
32
- <CardTitle>Revisá tu email</CardTitle>
33
- <CardDescription>
34
- Si el email está registrado, te enviamos un link para restablecer tu contraseña.
35
- </CardDescription>
36
- </CardHeader>
37
- <CardContent>
38
- <Button asChild variant="outline" className="w-full">
39
- <Link href="/login">Volver al inicio de sesión</Link>
40
- </Button>
41
- </CardContent>
42
- </Card>
43
- )
44
- }
45
-
46
- return (
47
- <Card>
48
- <CardHeader>
49
- <CardTitle>Olvidé mi contraseña</CardTitle>
50
- <CardDescription>
51
- Ingresá tu email y te enviamos el link de restablecimiento.
52
- </CardDescription>
53
- </CardHeader>
54
- <CardContent>
55
- <Form {...form}>
56
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
57
- <FormField
58
- control={form.control}
59
- name="email"
60
- rules={{ required: 'El email es requerido', pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' } }}
61
- render={({ field }) => (
62
- <FormItem>
63
- <FormLabel>Email</FormLabel>
64
- <FormControl>
65
- <Input type="email" placeholder="juan@empresa.com" {...field} />
66
- </FormControl>
67
- <FormMessage />
68
- </FormItem>
69
- )}
70
- />
71
- <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
72
- {form.formState.isSubmitting ? 'Enviando...' : 'Enviar link de restablecimiento'}
73
- </Button>
74
- </form>
75
- </Form>
76
- <p className="mt-4 text-center text-sm text-muted-foreground">
77
- <Link href="/login" className="underline underline-offset-4 hover:text-foreground">
78
- Volver al inicio de sesión
79
- </Link>
80
- </p>
81
- </CardContent>
82
- </Card>
83
- )
84
- }
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { useForm } from 'react-hook-form'
6
+ import { authApi } from '@/lib/api/auth'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
9
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
10
+ import { Input } from '@/components/ui/input'
11
+
12
+ interface ForgotPasswordForm {
13
+ email: string
14
+ }
15
+
16
+ export default function ForgotPasswordPage() {
17
+ const [sent, setSent] = useState(false)
18
+
19
+ const form = useForm<ForgotPasswordForm>({
20
+ defaultValues: { email: '' },
21
+ })
22
+
23
+ async function onSubmit(data: ForgotPasswordForm) {
24
+ await authApi.forgotPassword(data.email).catch(() => null)
25
+ setSent(true)
26
+ }
27
+
28
+ if (sent) {
29
+ return (
30
+ <Card>
31
+ <CardHeader>
32
+ <CardTitle>Revisá tu email</CardTitle>
33
+ <CardDescription>
34
+ Si el email está registrado, te enviamos un link para restablecer tu contraseña.
35
+ </CardDescription>
36
+ </CardHeader>
37
+ <CardContent>
38
+ <Button asChild variant="outline" className="w-full">
39
+ <Link href="/login">Volver al inicio de sesión</Link>
40
+ </Button>
41
+ </CardContent>
42
+ </Card>
43
+ )
44
+ }
45
+
46
+ return (
47
+ <Card>
48
+ <CardHeader>
49
+ <CardTitle>Olvidé mi contraseña</CardTitle>
50
+ <CardDescription>
51
+ Ingresá tu email y te enviamos el link de restablecimiento.
52
+ </CardDescription>
53
+ </CardHeader>
54
+ <CardContent>
55
+ <Form {...form}>
56
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
57
+ <FormField
58
+ control={form.control}
59
+ name="email"
60
+ rules={{ required: 'El email es requerido', pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' } }}
61
+ render={({ field }) => (
62
+ <FormItem>
63
+ <FormLabel>Email</FormLabel>
64
+ <FormControl>
65
+ <Input type="email" placeholder="juan@empresa.com" {...field} />
66
+ </FormControl>
67
+ <FormMessage />
68
+ </FormItem>
69
+ )}
70
+ />
71
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
72
+ {form.formState.isSubmitting ? 'Enviando...' : 'Enviar link de restablecimiento'}
73
+ </Button>
74
+ </form>
75
+ </Form>
76
+ <p className="mt-4 text-center text-sm text-muted-foreground">
77
+ <Link href="/login" className="underline underline-offset-4 hover:text-foreground">
78
+ Volver al inicio de sesión
79
+ </Link>
80
+ </p>
81
+ </CardContent>
82
+ </Card>
83
+ )
84
+ }
@@ -1,28 +1,28 @@
1
- 'use client'
2
-
3
- import { useEffect } from 'react'
4
- import { useRouter } from 'next/navigation'
5
- import { useAuth } from '@/lib/hooks/use-auth'
6
-
7
- export default function AuthLayout({ children }: { children: React.ReactNode }) {
8
- const { isAuthenticated, isLoading } = useAuth()
9
- const router = useRouter()
10
-
11
- useEffect(() => {
12
- if (!isLoading && isAuthenticated) {
13
- router.replace('/dashboard')
14
- }
15
- }, [isAuthenticated, isLoading, router])
16
-
17
- if (isLoading) return null
18
-
19
- return (
20
- <div className="min-h-screen flex flex-col items-center justify-center bg-muted/40 px-4">
21
- <div className="mb-8 text-center">
22
- <h1 className="text-2xl font-bold tracking-tight">create-saas-ar</h1>
23
- <p className="text-sm text-muted-foreground mt-1">Tu SaaS listo para Argentina</p>
24
- </div>
25
- <div className="w-full max-w-md">{children}</div>
26
- </div>
27
- )
28
- }
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useAuth } from '@/lib/hooks/use-auth'
6
+
7
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
8
+ const { isAuthenticated, isLoading } = useAuth()
9
+ const router = useRouter()
10
+
11
+ useEffect(() => {
12
+ if (!isLoading && isAuthenticated) {
13
+ router.replace('/dashboard')
14
+ }
15
+ }, [isAuthenticated, isLoading, router])
16
+
17
+ if (isLoading) return null
18
+
19
+ return (
20
+ <div className="min-h-screen flex flex-col items-center justify-center bg-muted/40 px-4">
21
+ <div className="mb-8 text-center">
22
+ <h1 className="text-2xl font-bold tracking-tight">create-saas-ar</h1>
23
+ <p className="text-sm text-muted-foreground mt-1">Tu SaaS listo para Argentina</p>
24
+ </div>
25
+ <div className="w-full max-w-md">{children}</div>
26
+ </div>
27
+ )
28
+ }
@@ -1,111 +1,111 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import Link from 'next/link'
5
- import { Eye, EyeOff } from 'lucide-react'
6
- import { useForm } from 'react-hook-form'
7
- import { useAuth } from '@/lib/hooks/use-auth'
8
- import { Button } from '@/components/ui/button'
9
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
10
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
11
- import { Input } from '@/components/ui/input'
12
-
13
- interface LoginForm {
14
- email: string
15
- password: string
16
- }
17
-
18
- export default function LoginPage() {
19
- const { login } = useAuth()
20
- const [showPassword, setShowPassword] = useState(false)
21
- const [apiError, setApiError] = useState('')
22
-
23
- const form = useForm<LoginForm>({
24
- defaultValues: { email: '', password: '' },
25
- })
26
-
27
- async function onSubmit(data: LoginForm) {
28
- setApiError('')
29
- try {
30
- await login(data.email, data.password)
31
- } catch (err: unknown) {
32
- const msg = err instanceof Error
33
- ? err.message
34
- : (err as { message?: string })?.message ?? 'Error al iniciar sesión'
35
- setApiError(msg)
36
- }
37
- }
38
-
39
- return (
40
- <Card>
41
- <CardHeader>
42
- <CardTitle>Iniciar sesión</CardTitle>
43
- <CardDescription>Ingresá tu email y contraseña</CardDescription>
44
- </CardHeader>
45
- <CardContent>
46
- <Form {...form}>
47
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
48
- <FormField
49
- control={form.control}
50
- name="email"
51
- rules={{ required: 'El email es requerido', pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' } }}
52
- render={({ field }) => (
53
- <FormItem>
54
- <FormLabel>Email</FormLabel>
55
- <FormControl>
56
- <Input type="email" placeholder="juan@empresa.com" {...field} />
57
- </FormControl>
58
- <FormMessage />
59
- </FormItem>
60
- )}
61
- />
62
- <FormField
63
- control={form.control}
64
- name="password"
65
- rules={{ required: 'La contraseña es requerida' }}
66
- render={({ field }) => (
67
- <FormItem>
68
- <div className="flex items-center justify-between">
69
- <FormLabel>Contraseña</FormLabel>
70
- <Link href="/forgot-password" className="text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground">
71
- ¿Olvidaste tu contraseña?
72
- </Link>
73
- </div>
74
- <FormControl>
75
- <div className="relative">
76
- <Input
77
- type={showPassword ? 'text' : 'password'}
78
- placeholder="Tu contraseña"
79
- className="pr-10"
80
- {...field}
81
- />
82
- <button
83
- type="button"
84
- onClick={() => setShowPassword((v) => !v)}
85
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
86
- tabIndex={-1}
87
- >
88
- {showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
89
- </button>
90
- </div>
91
- </FormControl>
92
- <FormMessage />
93
- </FormItem>
94
- )}
95
- />
96
- {apiError && <p className="text-sm text-destructive">{apiError}</p>}
97
- <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
98
- {form.formState.isSubmitting ? 'Ingresando...' : 'Iniciar sesión'}
99
- </Button>
100
- </form>
101
- </Form>
102
- <p className="mt-4 text-center text-sm text-muted-foreground">
103
- ¿No tenés cuenta?{' '}
104
- <Link href="/register" className="underline underline-offset-4 hover:text-foreground">
105
- Registrate
106
- </Link>
107
- </p>
108
- </CardContent>
109
- </Card>
110
- )
111
- }
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { Eye, EyeOff } from 'lucide-react'
6
+ import { useForm } from 'react-hook-form'
7
+ import { useAuth } from '@/lib/hooks/use-auth'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
10
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
11
+ import { Input } from '@/components/ui/input'
12
+
13
+ interface LoginForm {
14
+ email: string
15
+ password: string
16
+ }
17
+
18
+ export default function LoginPage() {
19
+ const { login } = useAuth()
20
+ const [showPassword, setShowPassword] = useState(false)
21
+ const [apiError, setApiError] = useState('')
22
+
23
+ const form = useForm<LoginForm>({
24
+ defaultValues: { email: '', password: '' },
25
+ })
26
+
27
+ async function onSubmit(data: LoginForm) {
28
+ setApiError('')
29
+ try {
30
+ await login(data.email, data.password)
31
+ } catch (err: unknown) {
32
+ const msg = err instanceof Error
33
+ ? err.message
34
+ : (err as { message?: string })?.message ?? 'Error al iniciar sesión'
35
+ setApiError(msg)
36
+ }
37
+ }
38
+
39
+ return (
40
+ <Card>
41
+ <CardHeader>
42
+ <CardTitle>Iniciar sesión</CardTitle>
43
+ <CardDescription>Ingresá tu email y contraseña</CardDescription>
44
+ </CardHeader>
45
+ <CardContent>
46
+ <Form {...form}>
47
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
48
+ <FormField
49
+ control={form.control}
50
+ name="email"
51
+ rules={{ required: 'El email es requerido', pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' } }}
52
+ render={({ field }) => (
53
+ <FormItem>
54
+ <FormLabel>Email</FormLabel>
55
+ <FormControl>
56
+ <Input type="email" placeholder="juan@empresa.com" {...field} />
57
+ </FormControl>
58
+ <FormMessage />
59
+ </FormItem>
60
+ )}
61
+ />
62
+ <FormField
63
+ control={form.control}
64
+ name="password"
65
+ rules={{ required: 'La contraseña es requerida' }}
66
+ render={({ field }) => (
67
+ <FormItem>
68
+ <div className="flex items-center justify-between">
69
+ <FormLabel>Contraseña</FormLabel>
70
+ <Link href="/forgot-password" className="text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground">
71
+ ¿Olvidaste tu contraseña?
72
+ </Link>
73
+ </div>
74
+ <FormControl>
75
+ <div className="relative">
76
+ <Input
77
+ type={showPassword ? 'text' : 'password'}
78
+ placeholder="Tu contraseña"
79
+ className="pr-10"
80
+ {...field}
81
+ />
82
+ <button
83
+ type="button"
84
+ onClick={() => setShowPassword((v) => !v)}
85
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
86
+ tabIndex={-1}
87
+ >
88
+ {showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
89
+ </button>
90
+ </div>
91
+ </FormControl>
92
+ <FormMessage />
93
+ </FormItem>
94
+ )}
95
+ />
96
+ {apiError && <p className="text-sm text-destructive">{apiError}</p>}
97
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
98
+ {form.formState.isSubmitting ? 'Ingresando...' : 'Iniciar sesión'}
99
+ </Button>
100
+ </form>
101
+ </Form>
102
+ <p className="mt-4 text-center text-sm text-muted-foreground">
103
+ ¿No tenés cuenta?{' '}
104
+ <Link href="/register" className="underline underline-offset-4 hover:text-foreground">
105
+ Registrate
106
+ </Link>
107
+ </p>
108
+ </CardContent>
109
+ </Card>
110
+ )
111
+ }