@create-lft-app/nextjs 3.2.0 → 3.3.0
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/README.md +549 -549
- package/package.json +48 -48
- package/template/.claude/skills/anti-patterns.md +150 -0
- package/template/.claude/skills/drizzle-schema.md +178 -0
- package/template/.claude/skills/formatting.md +56 -0
- package/template/.claude/skills/module-architecture.md +143 -0
- package/template/.claude/skills/supabase-server-actions.md +199 -0
- package/template/.claude/skills/ui-patterns.md +161 -0
- package/template/CLAUDE.md +114 -1239
- package/template/drizzle.config.ts +12 -12
- package/template/eslint.config.mjs +16 -16
- package/template/gitignore +36 -36
- package/template/next.config.ts +7 -7
- package/template/package.json +86 -86
- package/template/postcss.config.mjs +7 -7
- package/template/proxy.ts +12 -12
- package/template/public/logolft.svg +11 -11
- package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
- package/template/src/app/(auth)/dashboard/page.tsx +9 -9
- package/template/src/app/(auth)/layout.tsx +7 -7
- package/template/src/app/(auth)/users/page.tsx +9 -9
- package/template/src/app/(auth)/users/users-content.tsx +26 -26
- package/template/src/app/(public)/layout.tsx +7 -7
- package/template/src/app/(public)/login/page.tsx +17 -17
- package/template/src/app/api/webhooks/route.ts +20 -20
- package/template/src/app/globals.css +249 -249
- package/template/src/app/layout.tsx +37 -37
- package/template/src/app/page.tsx +5 -5
- package/template/src/app/providers.tsx +27 -27
- package/template/src/components/layout/main-content.tsx +28 -28
- package/template/src/components/layout/sidebar-context.tsx +33 -33
- package/template/src/components/layout/sidebar.tsx +141 -141
- package/template/src/components/tables/data-table-column-header.tsx +68 -68
- package/template/src/components/tables/data-table-date-filter.tsx +203 -203
- package/template/src/components/tables/data-table-faceted-filter.tsx +185 -185
- package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -130
- package/template/src/components/tables/data-table-number-filter.tsx +295 -295
- package/template/src/components/tables/data-table-pagination.tsx +99 -99
- package/template/src/components/tables/data-table-toolbar.tsx +140 -140
- package/template/src/components/tables/data-table-view-options.tsx +63 -63
- package/template/src/components/tables/data-table.tsx +148 -148
- package/template/src/components/tables/index.ts +9 -9
- package/template/src/components/ui/accordion.tsx +58 -58
- package/template/src/components/ui/alert-dialog.tsx +165 -165
- package/template/src/components/ui/alert.tsx +66 -66
- package/template/src/components/ui/animations/index.ts +44 -44
- package/template/src/components/ui/avatar.tsx +55 -55
- package/template/src/components/ui/badge.tsx +50 -50
- package/template/src/components/ui/button.tsx +118 -118
- package/template/src/components/ui/calendar.tsx +220 -220
- package/template/src/components/ui/card.tsx +113 -113
- package/template/src/components/ui/checkbox.tsx +38 -38
- package/template/src/components/ui/collapsible.tsx +33 -33
- package/template/src/components/ui/command.tsx +196 -196
- package/template/src/components/ui/dialog.tsx +156 -156
- package/template/src/components/ui/dropdown-menu.tsx +280 -280
- package/template/src/components/ui/form.tsx +171 -171
- package/template/src/components/ui/icons.tsx +167 -167
- package/template/src/components/ui/input.tsx +28 -28
- package/template/src/components/ui/label.tsx +25 -25
- package/template/src/components/ui/motion.tsx +197 -197
- package/template/src/components/ui/page-transition.tsx +166 -166
- package/template/src/components/ui/popover.tsx +59 -59
- package/template/src/components/ui/progress.tsx +32 -32
- package/template/src/components/ui/radio-group.tsx +45 -45
- package/template/src/components/ui/scroll-area.tsx +63 -63
- package/template/src/components/ui/select.tsx +208 -208
- package/template/src/components/ui/separator.tsx +28 -28
- package/template/src/components/ui/sheet.tsx +170 -170
- package/template/src/components/ui/sidebar.tsx +726 -726
- package/template/src/components/ui/skeleton.tsx +15 -15
- package/template/src/components/ui/slider.tsx +58 -58
- package/template/src/components/ui/sonner.tsx +47 -47
- package/template/src/components/ui/spinner.tsx +27 -27
- package/template/src/components/ui/submit-button.tsx +47 -47
- package/template/src/components/ui/switch.tsx +31 -31
- package/template/src/components/ui/table.tsx +120 -120
- package/template/src/components/ui/tabs.tsx +75 -75
- package/template/src/components/ui/textarea.tsx +26 -26
- package/template/src/components/ui/tooltip.tsx +70 -70
- package/template/src/config/navigation.ts +59 -59
- package/template/src/config/roles.ts +27 -27
- package/template/src/config/site.ts +12 -12
- package/template/src/db/index.ts +12 -12
- package/template/src/db/schema/index.ts +1 -1
- package/template/src/db/schema/users.ts +16 -16
- package/template/src/db/seed.ts +39 -39
- package/template/src/hooks/index.ts +3 -3
- package/template/src/hooks/use-mobile.ts +21 -21
- package/template/src/hooks/useDataTable.ts +82 -82
- package/template/src/hooks/useDebounce.ts +49 -49
- package/template/src/hooks/useMediaQuery.ts +36 -36
- package/template/src/lib/date/config.ts +36 -36
- package/template/src/lib/date/formatters.ts +127 -127
- package/template/src/lib/date/index.ts +26 -26
- package/template/src/lib/excel/exporter.ts +89 -89
- package/template/src/lib/excel/index.ts +14 -14
- package/template/src/lib/excel/parser.ts +96 -96
- package/template/src/lib/query-client.ts +35 -35
- package/template/src/lib/supabase/admin.ts +23 -23
- package/template/src/lib/supabase/client.ts +11 -11
- package/template/src/lib/supabase/proxy.ts +67 -67
- package/template/src/lib/supabase/server.ts +38 -38
- package/template/src/lib/supabase/types.ts +53 -53
- package/template/src/lib/utils.ts +6 -6
- package/template/src/lib/validations/common.ts +75 -75
- package/template/src/lib/validations/index.ts +20 -20
- package/template/src/modules/auth/actions/auth-actions.ts +59 -59
- package/template/src/modules/auth/components/login-form.tsx +68 -68
- package/template/src/modules/auth/hooks/useAuth.ts +38 -38
- package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
- package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
- package/template/src/modules/auth/index.ts +12 -12
- package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
- package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
- package/template/src/modules/users/actions/users-actions.ts +166 -166
- package/template/src/modules/users/columns.tsx +106 -106
- package/template/src/modules/users/components/users-list.tsx +48 -48
- package/template/src/modules/users/hooks/useUsers.ts +39 -39
- package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
- package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
- package/template/src/modules/users/index.ts +30 -30
- package/template/src/modules/users/schemas/users.schema.ts +51 -51
- package/template/src/modules/users/stores/useUsersStore.ts +60 -60
- package/template/src/modules/users/types/auth-user.types.ts +42 -42
- package/template/src/modules/users/utils/user-mapper.ts +32 -32
- package/template/src/stores/index.ts +1 -1
- package/template/src/stores/useUiStore.ts +55 -55
- package/template/src/types/api.ts +28 -28
- package/template/src/types/index.ts +2 -2
- package/template/src/types/table.ts +34 -34
- package/template/supabase/config.toml +94 -94
- package/template/tsconfig.json +42 -42
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
// Common reusable schemas
|
|
4
|
-
|
|
5
|
-
export const emailSchema = z
|
|
6
|
-
.string()
|
|
7
|
-
.min(1, 'El email es requerido')
|
|
8
|
-
.email('Email inválido')
|
|
9
|
-
|
|
10
|
-
export const passwordSchema = z
|
|
11
|
-
.string()
|
|
12
|
-
.min(1, 'La contraseña es requerida')
|
|
13
|
-
.min(6, 'La contraseña debe tener al menos 6 caracteres')
|
|
14
|
-
.max(100, 'La contraseña no puede tener más de 100 caracteres')
|
|
15
|
-
|
|
16
|
-
export const phoneSchema = z
|
|
17
|
-
.string()
|
|
18
|
-
.min(1, 'El teléfono es requerido')
|
|
19
|
-
.regex(/^[+]?[\d\s-()]+$/, 'Teléfono inválido')
|
|
20
|
-
.min(10, 'El teléfono debe tener al menos 10 dígitos')
|
|
21
|
-
|
|
22
|
-
export const urlSchema = z
|
|
23
|
-
.string()
|
|
24
|
-
.url('URL inválida')
|
|
25
|
-
.or(z.literal(''))
|
|
26
|
-
|
|
27
|
-
export const uuidSchema = z
|
|
28
|
-
.string()
|
|
29
|
-
.uuid('ID inválido')
|
|
30
|
-
|
|
31
|
-
export const dateSchema = z
|
|
32
|
-
.string()
|
|
33
|
-
.datetime({ message: 'Fecha inválida' })
|
|
34
|
-
|
|
35
|
-
export const positiveNumberSchema = z
|
|
36
|
-
.number()
|
|
37
|
-
.positive('El número debe ser positivo')
|
|
38
|
-
|
|
39
|
-
export const nonNegativeNumberSchema = z
|
|
40
|
-
.number()
|
|
41
|
-
.nonnegative('El número no puede ser negativo')
|
|
42
|
-
|
|
43
|
-
export const percentageSchema = z
|
|
44
|
-
.number()
|
|
45
|
-
.min(0, 'El porcentaje no puede ser menor a 0')
|
|
46
|
-
.max(100, 'El porcentaje no puede ser mayor a 100')
|
|
47
|
-
|
|
48
|
-
export const currencySchema = z
|
|
49
|
-
.number()
|
|
50
|
-
.nonnegative('El monto no puede ser negativo')
|
|
51
|
-
.multipleOf(0.01, 'El monto debe tener máximo 2 decimales')
|
|
52
|
-
|
|
53
|
-
// Pagination schemas
|
|
54
|
-
export const paginationSchema = z.object({
|
|
55
|
-
page: z.number().int().positive().default(1),
|
|
56
|
-
pageSize: z.number().int().positive().max(100).default(10),
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
export const sortSchema = z.object({
|
|
60
|
-
sortBy: z.string().optional(),
|
|
61
|
-
sortOrder: z.enum(['asc', 'desc']).default('asc'),
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
// Search schema
|
|
65
|
-
export const searchSchema = z.object({
|
|
66
|
-
query: z.string().optional(),
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
// Combined query params schema
|
|
70
|
-
export const queryParamsSchema = paginationSchema.merge(sortSchema).merge(searchSchema)
|
|
71
|
-
|
|
72
|
-
export type PaginationInput = z.infer<typeof paginationSchema>
|
|
73
|
-
export type SortInput = z.infer<typeof sortSchema>
|
|
74
|
-
export type SearchInput = z.infer<typeof searchSchema>
|
|
75
|
-
export type QueryParamsInput = z.infer<typeof queryParamsSchema>
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// Common reusable schemas
|
|
4
|
+
|
|
5
|
+
export const emailSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1, 'El email es requerido')
|
|
8
|
+
.email('Email inválido')
|
|
9
|
+
|
|
10
|
+
export const passwordSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1, 'La contraseña es requerida')
|
|
13
|
+
.min(6, 'La contraseña debe tener al menos 6 caracteres')
|
|
14
|
+
.max(100, 'La contraseña no puede tener más de 100 caracteres')
|
|
15
|
+
|
|
16
|
+
export const phoneSchema = z
|
|
17
|
+
.string()
|
|
18
|
+
.min(1, 'El teléfono es requerido')
|
|
19
|
+
.regex(/^[+]?[\d\s-()]+$/, 'Teléfono inválido')
|
|
20
|
+
.min(10, 'El teléfono debe tener al menos 10 dígitos')
|
|
21
|
+
|
|
22
|
+
export const urlSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.url('URL inválida')
|
|
25
|
+
.or(z.literal(''))
|
|
26
|
+
|
|
27
|
+
export const uuidSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.uuid('ID inválido')
|
|
30
|
+
|
|
31
|
+
export const dateSchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.datetime({ message: 'Fecha inválida' })
|
|
34
|
+
|
|
35
|
+
export const positiveNumberSchema = z
|
|
36
|
+
.number()
|
|
37
|
+
.positive('El número debe ser positivo')
|
|
38
|
+
|
|
39
|
+
export const nonNegativeNumberSchema = z
|
|
40
|
+
.number()
|
|
41
|
+
.nonnegative('El número no puede ser negativo')
|
|
42
|
+
|
|
43
|
+
export const percentageSchema = z
|
|
44
|
+
.number()
|
|
45
|
+
.min(0, 'El porcentaje no puede ser menor a 0')
|
|
46
|
+
.max(100, 'El porcentaje no puede ser mayor a 100')
|
|
47
|
+
|
|
48
|
+
export const currencySchema = z
|
|
49
|
+
.number()
|
|
50
|
+
.nonnegative('El monto no puede ser negativo')
|
|
51
|
+
.multipleOf(0.01, 'El monto debe tener máximo 2 decimales')
|
|
52
|
+
|
|
53
|
+
// Pagination schemas
|
|
54
|
+
export const paginationSchema = z.object({
|
|
55
|
+
page: z.number().int().positive().default(1),
|
|
56
|
+
pageSize: z.number().int().positive().max(100).default(10),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export const sortSchema = z.object({
|
|
60
|
+
sortBy: z.string().optional(),
|
|
61
|
+
sortOrder: z.enum(['asc', 'desc']).default('asc'),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Search schema
|
|
65
|
+
export const searchSchema = z.object({
|
|
66
|
+
query: z.string().optional(),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Combined query params schema
|
|
70
|
+
export const queryParamsSchema = paginationSchema.merge(sortSchema).merge(searchSchema)
|
|
71
|
+
|
|
72
|
+
export type PaginationInput = z.infer<typeof paginationSchema>
|
|
73
|
+
export type SortInput = z.infer<typeof sortSchema>
|
|
74
|
+
export type SearchInput = z.infer<typeof searchSchema>
|
|
75
|
+
export type QueryParamsInput = z.infer<typeof queryParamsSchema>
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
export {
|
|
2
|
-
emailSchema,
|
|
3
|
-
passwordSchema,
|
|
4
|
-
phoneSchema,
|
|
5
|
-
urlSchema,
|
|
6
|
-
uuidSchema,
|
|
7
|
-
dateSchema,
|
|
8
|
-
positiveNumberSchema,
|
|
9
|
-
nonNegativeNumberSchema,
|
|
10
|
-
percentageSchema,
|
|
11
|
-
currencySchema,
|
|
12
|
-
paginationSchema,
|
|
13
|
-
sortSchema,
|
|
14
|
-
searchSchema,
|
|
15
|
-
queryParamsSchema,
|
|
16
|
-
type PaginationInput,
|
|
17
|
-
type SortInput,
|
|
18
|
-
type SearchInput,
|
|
19
|
-
type QueryParamsInput,
|
|
20
|
-
} from './common'
|
|
1
|
+
export {
|
|
2
|
+
emailSchema,
|
|
3
|
+
passwordSchema,
|
|
4
|
+
phoneSchema,
|
|
5
|
+
urlSchema,
|
|
6
|
+
uuidSchema,
|
|
7
|
+
dateSchema,
|
|
8
|
+
positiveNumberSchema,
|
|
9
|
+
nonNegativeNumberSchema,
|
|
10
|
+
percentageSchema,
|
|
11
|
+
currencySchema,
|
|
12
|
+
paginationSchema,
|
|
13
|
+
sortSchema,
|
|
14
|
+
searchSchema,
|
|
15
|
+
queryParamsSchema,
|
|
16
|
+
type PaginationInput,
|
|
17
|
+
type SortInput,
|
|
18
|
+
type SearchInput,
|
|
19
|
+
type QueryParamsInput,
|
|
20
|
+
} from './common'
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
'use server'
|
|
2
|
-
|
|
3
|
-
import { createClient } from '@/lib/supabase/server'
|
|
4
|
-
import { redirect } from 'next/navigation'
|
|
5
|
-
import { loginSchema, registerSchema, type LoginInput, type RegisterInput } from '../schemas/auth.schema'
|
|
6
|
-
|
|
7
|
-
export async function login(input: LoginInput) {
|
|
8
|
-
const parsed = loginSchema.safeParse(input)
|
|
9
|
-
|
|
10
|
-
if (!parsed.success) {
|
|
11
|
-
return { error: parsed.error.errors[0].message }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const supabase = await createClient()
|
|
15
|
-
|
|
16
|
-
const { error } = await supabase.auth.signInWithPassword({
|
|
17
|
-
email: parsed.data.email,
|
|
18
|
-
password: parsed.data.password,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
if (error) {
|
|
22
|
-
return { error: error.message }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
redirect('/dashboard')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function register(input: RegisterInput) {
|
|
29
|
-
const parsed = registerSchema.safeParse(input)
|
|
30
|
-
|
|
31
|
-
if (!parsed.success) {
|
|
32
|
-
return { error: parsed.error.errors[0].message }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const supabase = await createClient()
|
|
36
|
-
|
|
37
|
-
const { error } = await supabase.auth.signUp({
|
|
38
|
-
email: parsed.data.email,
|
|
39
|
-
password: parsed.data.password,
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
if (error) {
|
|
43
|
-
return { error: error.message }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
redirect('/dashboard')
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function logout() {
|
|
50
|
-
const supabase = await createClient()
|
|
51
|
-
await supabase.auth.signOut()
|
|
52
|
-
redirect('/login')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function getUser() {
|
|
56
|
-
const supabase = await createClient()
|
|
57
|
-
const { data: { user } } = await supabase.auth.getUser()
|
|
58
|
-
return user
|
|
59
|
-
}
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@/lib/supabase/server'
|
|
4
|
+
import { redirect } from 'next/navigation'
|
|
5
|
+
import { loginSchema, registerSchema, type LoginInput, type RegisterInput } from '../schemas/auth.schema'
|
|
6
|
+
|
|
7
|
+
export async function login(input: LoginInput) {
|
|
8
|
+
const parsed = loginSchema.safeParse(input)
|
|
9
|
+
|
|
10
|
+
if (!parsed.success) {
|
|
11
|
+
return { error: parsed.error.errors[0].message }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const supabase = await createClient()
|
|
15
|
+
|
|
16
|
+
const { error } = await supabase.auth.signInWithPassword({
|
|
17
|
+
email: parsed.data.email,
|
|
18
|
+
password: parsed.data.password,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
return { error: error.message }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
redirect('/dashboard')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function register(input: RegisterInput) {
|
|
29
|
+
const parsed = registerSchema.safeParse(input)
|
|
30
|
+
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return { error: parsed.error.errors[0].message }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const supabase = await createClient()
|
|
36
|
+
|
|
37
|
+
const { error } = await supabase.auth.signUp({
|
|
38
|
+
email: parsed.data.email,
|
|
39
|
+
password: parsed.data.password,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
return { error: error.message }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
redirect('/dashboard')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function logout() {
|
|
50
|
+
const supabase = await createClient()
|
|
51
|
+
await supabase.auth.signOut()
|
|
52
|
+
redirect('/login')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getUser() {
|
|
56
|
+
const supabase = await createClient()
|
|
57
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
58
|
+
return user
|
|
59
|
+
}
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useForm } from 'react-hook-form'
|
|
4
|
-
import { zodResolver } from '@hookform/resolvers/zod'
|
|
5
|
-
import { toast } from 'sonner'
|
|
6
|
-
import { Button } from '@/components/ui/button'
|
|
7
|
-
import { Input } from '@/components/ui/input'
|
|
8
|
-
import { Label } from '@/components/ui/label'
|
|
9
|
-
import { useAuth } from '../hooks/useAuth'
|
|
10
|
-
import { loginSchema, type LoginInput } from '../schemas/auth.schema'
|
|
11
|
-
|
|
12
|
-
export function LoginForm() {
|
|
13
|
-
const { login, isLoggingIn } = useAuth()
|
|
14
|
-
|
|
15
|
-
const form = useForm<LoginInput>({
|
|
16
|
-
resolver: zodResolver(loginSchema),
|
|
17
|
-
defaultValues: {
|
|
18
|
-
email: '',
|
|
19
|
-
password: '',
|
|
20
|
-
},
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
async function onSubmit(data: LoginInput) {
|
|
24
|
-
const result = await login(data)
|
|
25
|
-
|
|
26
|
-
if (result?.error) {
|
|
27
|
-
toast.error(result.error)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
33
|
-
<div className="space-y-2">
|
|
34
|
-
<Label htmlFor="email">Email</Label>
|
|
35
|
-
<Input
|
|
36
|
-
id="email"
|
|
37
|
-
type="email"
|
|
38
|
-
placeholder="tu@email.com"
|
|
39
|
-
{...form.register('email')}
|
|
40
|
-
/>
|
|
41
|
-
{form.formState.errors.email && (
|
|
42
|
-
<p className="text-sm text-destructive">
|
|
43
|
-
{form.formState.errors.email.message}
|
|
44
|
-
</p>
|
|
45
|
-
)}
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div className="space-y-2">
|
|
49
|
-
<Label htmlFor="password">Contraseña</Label>
|
|
50
|
-
<Input
|
|
51
|
-
id="password"
|
|
52
|
-
type="password"
|
|
53
|
-
placeholder="••••••••"
|
|
54
|
-
{...form.register('password')}
|
|
55
|
-
/>
|
|
56
|
-
{form.formState.errors.password && (
|
|
57
|
-
<p className="text-sm text-destructive">
|
|
58
|
-
{form.formState.errors.password.message}
|
|
59
|
-
</p>
|
|
60
|
-
)}
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<Button type="submit" className="w-full" disabled={isLoggingIn}>
|
|
64
|
-
{isLoggingIn ? 'Iniciando sesión...' : 'Iniciar sesión'}
|
|
65
|
-
</Button>
|
|
66
|
-
</form>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useForm } from 'react-hook-form'
|
|
4
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Input } from '@/components/ui/input'
|
|
8
|
+
import { Label } from '@/components/ui/label'
|
|
9
|
+
import { useAuth } from '../hooks/useAuth'
|
|
10
|
+
import { loginSchema, type LoginInput } from '../schemas/auth.schema'
|
|
11
|
+
|
|
12
|
+
export function LoginForm() {
|
|
13
|
+
const { login, isLoggingIn } = useAuth()
|
|
14
|
+
|
|
15
|
+
const form = useForm<LoginInput>({
|
|
16
|
+
resolver: zodResolver(loginSchema),
|
|
17
|
+
defaultValues: {
|
|
18
|
+
email: '',
|
|
19
|
+
password: '',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
async function onSubmit(data: LoginInput) {
|
|
24
|
+
const result = await login(data)
|
|
25
|
+
|
|
26
|
+
if (result?.error) {
|
|
27
|
+
toast.error(result.error)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
33
|
+
<div className="space-y-2">
|
|
34
|
+
<Label htmlFor="email">Email</Label>
|
|
35
|
+
<Input
|
|
36
|
+
id="email"
|
|
37
|
+
type="email"
|
|
38
|
+
placeholder="tu@email.com"
|
|
39
|
+
{...form.register('email')}
|
|
40
|
+
/>
|
|
41
|
+
{form.formState.errors.email && (
|
|
42
|
+
<p className="text-sm text-destructive">
|
|
43
|
+
{form.formState.errors.email.message}
|
|
44
|
+
</p>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="space-y-2">
|
|
49
|
+
<Label htmlFor="password">Contraseña</Label>
|
|
50
|
+
<Input
|
|
51
|
+
id="password"
|
|
52
|
+
type="password"
|
|
53
|
+
placeholder="••••••••"
|
|
54
|
+
{...form.register('password')}
|
|
55
|
+
/>
|
|
56
|
+
{form.formState.errors.password && (
|
|
57
|
+
<p className="text-sm text-destructive">
|
|
58
|
+
{form.formState.errors.password.message}
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<Button type="submit" className="w-full" disabled={isLoggingIn}>
|
|
64
|
+
{isLoggingIn ? 'Iniciando sesión...' : 'Iniciar sesión'}
|
|
65
|
+
</Button>
|
|
66
|
+
</form>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useShallow } from 'zustand/react/shallow'
|
|
4
|
-
import { useAuthStore } from '../stores/useAuthStore'
|
|
5
|
-
import { useAuthQueries } from './useAuthQueries'
|
|
6
|
-
import { useAuthMutations } from './useAuthMutations'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Hook unificado para autenticación.
|
|
10
|
-
* Los componentes SOLO deben importar este hook, nunca useAuthQueries o useAuthMutations directamente.
|
|
11
|
-
*/
|
|
12
|
-
export function useAuth() {
|
|
13
|
-
const state = useAuthStore(useShallow((s) => s.state))
|
|
14
|
-
const actions = useAuthStore(useShallow((s) => s.actions))
|
|
15
|
-
|
|
16
|
-
const queries = useAuthQueries()
|
|
17
|
-
const mutations = useAuthMutations()
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
// State from store (UI state)
|
|
21
|
-
...state,
|
|
22
|
-
...actions,
|
|
23
|
-
|
|
24
|
-
// Queries
|
|
25
|
-
user: queries.user,
|
|
26
|
-
session: queries.session,
|
|
27
|
-
isAuthenticated: queries.isAuthenticated,
|
|
28
|
-
isLoadingUser: queries.isLoading,
|
|
29
|
-
|
|
30
|
-
// Mutations
|
|
31
|
-
login: mutations.login,
|
|
32
|
-
register: mutations.register,
|
|
33
|
-
logout: mutations.logout,
|
|
34
|
-
isLoggingIn: mutations.isLoggingIn,
|
|
35
|
-
isRegistering: mutations.isRegistering,
|
|
36
|
-
isLoggingOut: mutations.isLoggingOut,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
4
|
+
import { useAuthStore } from '../stores/useAuthStore'
|
|
5
|
+
import { useAuthQueries } from './useAuthQueries'
|
|
6
|
+
import { useAuthMutations } from './useAuthMutations'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook unificado para autenticación.
|
|
10
|
+
* Los componentes SOLO deben importar este hook, nunca useAuthQueries o useAuthMutations directamente.
|
|
11
|
+
*/
|
|
12
|
+
export function useAuth() {
|
|
13
|
+
const state = useAuthStore(useShallow((s) => s.state))
|
|
14
|
+
const actions = useAuthStore(useShallow((s) => s.actions))
|
|
15
|
+
|
|
16
|
+
const queries = useAuthQueries()
|
|
17
|
+
const mutations = useAuthMutations()
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
// State from store (UI state)
|
|
21
|
+
...state,
|
|
22
|
+
...actions,
|
|
23
|
+
|
|
24
|
+
// Queries
|
|
25
|
+
user: queries.user,
|
|
26
|
+
session: queries.session,
|
|
27
|
+
isAuthenticated: queries.isAuthenticated,
|
|
28
|
+
isLoadingUser: queries.isLoading,
|
|
29
|
+
|
|
30
|
+
// Mutations
|
|
31
|
+
login: mutations.login,
|
|
32
|
+
register: mutations.register,
|
|
33
|
+
logout: mutations.logout,
|
|
34
|
+
isLoggingIn: mutations.isLoggingIn,
|
|
35
|
+
isRegistering: mutations.isRegistering,
|
|
36
|
+
isLoggingOut: mutations.isLoggingOut,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
4
|
-
import { login, register, logout } from '../actions/auth-actions'
|
|
5
|
-
import { authKeys } from './useAuthQueries'
|
|
6
|
-
import type { LoginInput, RegisterInput } from '../schemas/auth.schema'
|
|
7
|
-
|
|
8
|
-
export function useAuthMutations() {
|
|
9
|
-
const queryClient = useQueryClient()
|
|
10
|
-
|
|
11
|
-
const loginMutation = useMutation({
|
|
12
|
-
mutationFn: (input: LoginInput) => login(input),
|
|
13
|
-
onSuccess: () => {
|
|
14
|
-
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
15
|
-
},
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const registerMutation = useMutation({
|
|
19
|
-
mutationFn: (input: RegisterInput) => register(input),
|
|
20
|
-
onSuccess: () => {
|
|
21
|
-
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
22
|
-
},
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
const logoutMutation = useMutation({
|
|
26
|
-
mutationFn: () => logout(),
|
|
27
|
-
onSuccess: () => {
|
|
28
|
-
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
29
|
-
queryClient.clear()
|
|
30
|
-
},
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
login: loginMutation.mutateAsync,
|
|
35
|
-
register: registerMutation.mutateAsync,
|
|
36
|
-
logout: logoutMutation.mutateAsync,
|
|
37
|
-
isLoggingIn: loginMutation.isPending,
|
|
38
|
-
isRegistering: registerMutation.isPending,
|
|
39
|
-
isLoggingOut: logoutMutation.isPending,
|
|
40
|
-
loginError: loginMutation.error,
|
|
41
|
-
registerError: registerMutation.error,
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { login, register, logout } from '../actions/auth-actions'
|
|
5
|
+
import { authKeys } from './useAuthQueries'
|
|
6
|
+
import type { LoginInput, RegisterInput } from '../schemas/auth.schema'
|
|
7
|
+
|
|
8
|
+
export function useAuthMutations() {
|
|
9
|
+
const queryClient = useQueryClient()
|
|
10
|
+
|
|
11
|
+
const loginMutation = useMutation({
|
|
12
|
+
mutationFn: (input: LoginInput) => login(input),
|
|
13
|
+
onSuccess: () => {
|
|
14
|
+
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const registerMutation = useMutation({
|
|
19
|
+
mutationFn: (input: RegisterInput) => register(input),
|
|
20
|
+
onSuccess: () => {
|
|
21
|
+
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const logoutMutation = useMutation({
|
|
26
|
+
mutationFn: () => logout(),
|
|
27
|
+
onSuccess: () => {
|
|
28
|
+
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
29
|
+
queryClient.clear()
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
login: loginMutation.mutateAsync,
|
|
35
|
+
register: registerMutation.mutateAsync,
|
|
36
|
+
logout: logoutMutation.mutateAsync,
|
|
37
|
+
isLoggingIn: loginMutation.isPending,
|
|
38
|
+
isRegistering: registerMutation.isPending,
|
|
39
|
+
isLoggingOut: logoutMutation.isPending,
|
|
40
|
+
loginError: loginMutation.error,
|
|
41
|
+
registerError: registerMutation.error,
|
|
42
|
+
}
|
|
43
|
+
}
|