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,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
|
+
}
|