@create-lft-app/nextjs 1.0.2 → 3.0.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.
Files changed (81) hide show
  1. package/package.json +9 -3
  2. package/template/CLAUDE.md +279 -0
  3. package/template/drizzle.config.ts +12 -0
  4. package/template/package.json +31 -6
  5. package/template/proxy.ts +12 -0
  6. package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -0
  7. package/template/src/app/(auth)/dashboard/page.tsx +9 -0
  8. package/template/src/app/(auth)/layout.tsx +7 -0
  9. package/template/src/app/(auth)/users/page.tsx +9 -0
  10. package/template/src/app/(auth)/users/users-content.tsx +26 -0
  11. package/template/src/app/(public)/layout.tsx +7 -0
  12. package/template/src/app/(public)/login/page.tsx +17 -0
  13. package/template/src/app/api/webhooks/route.ts +20 -0
  14. package/template/src/app/layout.tsx +13 -12
  15. package/template/src/app/providers.tsx +27 -0
  16. package/template/src/components/layout/{midday-sidebar.tsx → sidebar.tsx} +2 -7
  17. package/template/src/components/tables/data-table-column-header.tsx +68 -0
  18. package/template/src/components/tables/data-table-pagination.tsx +99 -0
  19. package/template/src/components/tables/data-table-toolbar.tsx +50 -0
  20. package/template/src/components/tables/data-table-view-options.tsx +59 -0
  21. package/template/src/components/tables/data-table.tsx +128 -0
  22. package/template/src/components/tables/index.ts +5 -0
  23. package/template/src/components/ui/animations/index.ts +44 -0
  24. package/template/src/components/ui/button.tsx +50 -21
  25. package/template/src/components/ui/card.tsx +27 -3
  26. package/template/src/components/ui/dialog.tsx +38 -35
  27. package/template/src/components/ui/motion.tsx +197 -0
  28. package/template/src/components/ui/page-transition.tsx +166 -0
  29. package/template/src/components/ui/sheet.tsx +65 -41
  30. package/template/src/config/navigation.ts +69 -0
  31. package/template/src/config/site.ts +12 -0
  32. package/template/src/db/index.ts +12 -0
  33. package/template/src/db/schema/index.ts +1 -0
  34. package/template/src/db/schema/users.ts +16 -0
  35. package/template/src/db/seed.ts +39 -0
  36. package/template/src/hooks/index.ts +3 -0
  37. package/template/src/hooks/useDataTable.ts +82 -0
  38. package/template/src/hooks/useDebounce.ts +49 -0
  39. package/template/src/hooks/useMediaQuery.ts +36 -0
  40. package/template/src/lib/date/config.ts +34 -0
  41. package/template/src/lib/date/formatters.ts +120 -0
  42. package/template/src/lib/date/index.ts +19 -0
  43. package/template/src/lib/excel/exporter.ts +89 -0
  44. package/template/src/lib/excel/index.ts +14 -0
  45. package/template/src/lib/excel/parser.ts +96 -0
  46. package/template/src/lib/query-client.ts +35 -0
  47. package/template/src/lib/supabase/client.ts +5 -2
  48. package/template/src/lib/supabase/proxy.ts +67 -0
  49. package/template/src/lib/supabase/server.ts +6 -4
  50. package/template/src/lib/supabase/types.ts +53 -0
  51. package/template/src/lib/validations/common.ts +75 -0
  52. package/template/src/lib/validations/index.ts +20 -0
  53. package/template/src/modules/auth/actions/auth-actions.ts +51 -4
  54. package/template/src/modules/auth/components/login-form.tsx +68 -0
  55. package/template/src/modules/auth/hooks/useAuth.ts +38 -0
  56. package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -0
  57. package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -0
  58. package/template/src/modules/auth/index.ts +12 -0
  59. package/template/src/modules/auth/schemas/auth.schema.ts +32 -0
  60. package/template/src/modules/auth/stores/useAuthStore.ts +37 -0
  61. package/template/src/modules/users/actions/users-actions.ts +94 -0
  62. package/template/src/modules/users/columns.tsx +86 -0
  63. package/template/src/modules/users/components/users-list.tsx +22 -0
  64. package/template/src/modules/users/hooks/useUsers.ts +39 -0
  65. package/template/src/modules/users/hooks/useUsersMutations.ts +55 -0
  66. package/template/src/modules/users/hooks/useUsersQueries.ts +35 -0
  67. package/template/src/modules/users/index.ts +12 -0
  68. package/template/src/modules/users/schemas/users.schema.ts +23 -0
  69. package/template/src/modules/users/stores/useUsersStore.ts +60 -0
  70. package/template/src/stores/index.ts +1 -0
  71. package/template/src/stores/useUiStore.ts +55 -0
  72. package/template/src/types/api.ts +28 -0
  73. package/template/src/types/index.ts +2 -0
  74. package/template/src/types/table.ts +34 -0
  75. package/template/supabase/config.toml +94 -0
  76. package/template/tsconfig.json +2 -1
  77. package/template/tsconfig.tsbuildinfo +1 -0
  78. package/template/next-env.d.ts +0 -6
  79. package/template/package-lock.json +0 -8454
  80. package/template/src/app/dashboard/page.tsx +0 -111
  81. package/template/src/components/dashboard/widget.tsx +0 -113
@@ -0,0 +1,67 @@
1
+ import { createServerClient, type CookieOptions } from '@supabase/ssr'
2
+ import { NextResponse, type NextRequest } from 'next/server'
3
+
4
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
5
+ const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY
6
+
7
+ type CookieToSet = {
8
+ name: string
9
+ value: string
10
+ options: CookieOptions
11
+ }
12
+
13
+ export async function updateSession(request: NextRequest) {
14
+ let supabaseResponse = NextResponse.next({
15
+ request: {
16
+ headers: request.headers,
17
+ },
18
+ })
19
+
20
+ const supabase = createServerClient(
21
+ supabaseUrl!,
22
+ supabaseKey!,
23
+ {
24
+ cookies: {
25
+ getAll() {
26
+ return request.cookies.getAll()
27
+ },
28
+ setAll(cookiesToSet: CookieToSet[]) {
29
+ cookiesToSet.forEach(({ name, value }) =>
30
+ request.cookies.set(name, value)
31
+ )
32
+ supabaseResponse = NextResponse.next({
33
+ request,
34
+ })
35
+ cookiesToSet.forEach(({ name, value, options }) =>
36
+ supabaseResponse.cookies.set(name, value, options)
37
+ )
38
+ },
39
+ },
40
+ }
41
+ )
42
+
43
+ // Refresh session if expired
44
+ const { data: { user } } = await supabase.auth.getUser()
45
+
46
+ // Define route protection
47
+ const pathname = request.nextUrl.pathname
48
+
49
+ const protectedPaths = ['/dashboard', '/users', '/settings', '/reports']
50
+ const publicOnlyPaths = ['/login', '/register']
51
+
52
+ const isProtectedPath = protectedPaths.some((path) => pathname.startsWith(path))
53
+ const isPublicOnlyPath = publicOnlyPaths.some((path) => pathname.startsWith(path))
54
+
55
+ // Redirect logic
56
+ if (!user && isProtectedPath) {
57
+ const redirectUrl = new URL('/login', request.url)
58
+ redirectUrl.searchParams.set('redirectTo', pathname)
59
+ return NextResponse.redirect(redirectUrl)
60
+ }
61
+
62
+ if (user && isPublicOnlyPath) {
63
+ return NextResponse.redirect(new URL('/dashboard', request.url))
64
+ }
65
+
66
+ return supabaseResponse
67
+ }
@@ -1,6 +1,8 @@
1
- import { createServerClient } from '@supabase/ssr'
1
+ import { createServerClient, type CookieOptions } from '@supabase/ssr'
2
2
  import { cookies } from 'next/headers'
3
- import type { CookieOptions } from '@supabase/ssr'
3
+
4
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
5
+ const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY
4
6
 
5
7
  type CookieToSet = {
6
8
  name: string
@@ -12,8 +14,8 @@ export async function createClient() {
12
14
  const cookieStore = await cookies()
13
15
 
14
16
  return createServerClient(
15
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
16
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
17
+ supabaseUrl!,
18
+ supabaseKey!,
17
19
  {
18
20
  cookies: {
19
21
  getAll() {
@@ -0,0 +1,53 @@
1
+ export type Json =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+ | { [key: string]: Json | undefined }
7
+ | Json[]
8
+
9
+ export interface Database {
10
+ public: {
11
+ Tables: {
12
+ users: {
13
+ Row: {
14
+ id: string
15
+ email: string
16
+ name: string
17
+ role: 'admin' | 'user' | 'viewer'
18
+ avatar_url: string | null
19
+ created_at: string
20
+ updated_at: string
21
+ }
22
+ Insert: {
23
+ id?: string
24
+ email: string
25
+ name: string
26
+ role?: 'admin' | 'user' | 'viewer'
27
+ avatar_url?: string | null
28
+ created_at?: string
29
+ updated_at?: string
30
+ }
31
+ Update: {
32
+ id?: string
33
+ email?: string
34
+ name?: string
35
+ role?: 'admin' | 'user' | 'viewer'
36
+ avatar_url?: string | null
37
+ created_at?: string
38
+ updated_at?: string
39
+ }
40
+ Relationships: []
41
+ }
42
+ }
43
+ Views: {
44
+ [_ in never]: never
45
+ }
46
+ Functions: {
47
+ [_ in never]: never
48
+ }
49
+ Enums: {
50
+ user_role: 'admin' | 'user' | 'viewer'
51
+ }
52
+ }
53
+ }
@@ -0,0 +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>
@@ -0,0 +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,12 +1,59 @@
1
1
  'use server'
2
2
 
3
+ import { createClient } from '@/lib/supabase/server'
3
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
+ }
4
48
 
5
49
  export async function logout() {
6
- // TODO: Implementar con Supabase
7
- // const supabase = await createClient()
8
- // await supabase.auth.signOut()
50
+ const supabase = await createClient()
51
+ await supabase.auth.signOut()
9
52
  redirect('/login')
10
53
  }
11
54
 
12
- export const logoutAction = logout
55
+ export async function getUser() {
56
+ const supabase = await createClient()
57
+ const { data: { user } } = await supabase.auth.getUser()
58
+ return user
59
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { useQuery } from '@tanstack/react-query'
4
+ import { createClient } from '@/lib/supabase/client'
5
+
6
+ export const authKeys = {
7
+ all: ['auth'] as const,
8
+ user: () => [...authKeys.all, 'user'] as const,
9
+ session: () => [...authKeys.all, 'session'] as const,
10
+ }
11
+
12
+ export function useAuthQueries() {
13
+ const supabase = createClient()
14
+
15
+ const userQuery = useQuery({
16
+ queryKey: authKeys.user(),
17
+ queryFn: async () => {
18
+ const { data: { user }, error } = await supabase.auth.getUser()
19
+ if (error) throw error
20
+ return user
21
+ },
22
+ staleTime: 1000 * 60 * 5, // 5 minutos
23
+ })
24
+
25
+ const sessionQuery = useQuery({
26
+ queryKey: authKeys.session(),
27
+ queryFn: async () => {
28
+ const { data: { session }, error } = await supabase.auth.getSession()
29
+ if (error) throw error
30
+ return session
31
+ },
32
+ staleTime: 1000 * 60 * 5,
33
+ })
34
+
35
+ return {
36
+ user: userQuery.data,
37
+ session: sessionQuery.data,
38
+ isLoading: userQuery.isLoading || sessionQuery.isLoading,
39
+ isAuthenticated: !!userQuery.data,
40
+ userQuery,
41
+ sessionQuery,
42
+ }
43
+ }
@@ -0,0 +1,12 @@
1
+ // Components
2
+ export { LoginForm } from './components/login-form'
3
+
4
+ // Hooks - Solo exportar el hook unificado
5
+ export { useAuth } from './hooks/useAuth'
6
+
7
+ // Schemas
8
+ export { loginSchema, registerSchema } from './schemas/auth.schema'
9
+ export type { LoginInput, RegisterInput } from './schemas/auth.schema'
10
+
11
+ // Actions
12
+ export { login, register, logout, getUser } from './actions/auth-actions'
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod'
2
+
3
+ export const loginSchema = z.object({
4
+ email: z
5
+ .string()
6
+ .min(1, 'El email es requerido')
7
+ .email('Email inválido'),
8
+ password: z
9
+ .string()
10
+ .min(1, 'La contraseña es requerida')
11
+ .min(6, 'La contraseña debe tener al menos 6 caracteres'),
12
+ })
13
+
14
+ export const registerSchema = z.object({
15
+ email: z
16
+ .string()
17
+ .min(1, 'El email es requerido')
18
+ .email('Email inválido'),
19
+ password: z
20
+ .string()
21
+ .min(1, 'La contraseña es requerida')
22
+ .min(6, 'La contraseña debe tener al menos 6 caracteres'),
23
+ confirmPassword: z
24
+ .string()
25
+ .min(1, 'Confirma tu contraseña'),
26
+ }).refine((data) => data.password === data.confirmPassword, {
27
+ message: 'Las contraseñas no coinciden',
28
+ path: ['confirmPassword'],
29
+ })
30
+
31
+ export type LoginInput = z.infer<typeof loginSchema>
32
+ export type RegisterInput = z.infer<typeof registerSchema>
@@ -0,0 +1,37 @@
1
+ import { create } from 'zustand'
2
+ import type { User } from '@supabase/supabase-js'
3
+
4
+ interface AuthState {
5
+ state: {
6
+ user: User | null
7
+ isLoading: boolean
8
+ }
9
+ actions: {
10
+ setUser: (user: User | null) => void
11
+ setLoading: (isLoading: boolean) => void
12
+ reset: () => void
13
+ }
14
+ }
15
+
16
+ const initialState = {
17
+ user: null,
18
+ isLoading: true,
19
+ }
20
+
21
+ export const useAuthStore = create<AuthState>()((set) => ({
22
+ state: initialState,
23
+ actions: {
24
+ setUser: (user) =>
25
+ set((state) => ({
26
+ state: { ...state.state, user },
27
+ })),
28
+ setLoading: (isLoading) =>
29
+ set((state) => ({
30
+ state: { ...state.state, isLoading },
31
+ })),
32
+ reset: () =>
33
+ set(() => ({
34
+ state: initialState,
35
+ })),
36
+ },
37
+ }))
@@ -0,0 +1,94 @@
1
+ 'use server'
2
+
3
+ import { createClient } from '@/lib/supabase/server'
4
+ import { createUserSchema, updateUserSchema, type CreateUserInput, type UpdateUserInput } from '../schemas/users.schema'
5
+
6
+ export async function getUsers() {
7
+ const supabase = await createClient()
8
+ const { data: { user } } = await supabase.auth.getUser()
9
+
10
+ if (!user) throw new Error('Unauthorized')
11
+
12
+ const { data, error } = await supabase
13
+ .from('users')
14
+ .select('*')
15
+ .order('created_at', { ascending: false })
16
+
17
+ if (error) throw error
18
+ return data
19
+ }
20
+
21
+ export async function getUserById(id: string) {
22
+ const supabase = await createClient()
23
+ const { data: { user } } = await supabase.auth.getUser()
24
+
25
+ if (!user) throw new Error('Unauthorized')
26
+
27
+ const { data, error } = await supabase
28
+ .from('users')
29
+ .select('*')
30
+ .eq('id', id)
31
+ .single()
32
+
33
+ if (error) throw error
34
+ return data
35
+ }
36
+
37
+ export async function createUser(input: CreateUserInput) {
38
+ const parsed = createUserSchema.safeParse(input)
39
+
40
+ if (!parsed.success) {
41
+ throw new Error(parsed.error.errors[0].message)
42
+ }
43
+
44
+ const supabase = await createClient()
45
+ const { data: { user } } = await supabase.auth.getUser()
46
+
47
+ if (!user) throw new Error('Unauthorized')
48
+
49
+ const { data, error } = await supabase
50
+ .from('users')
51
+ .insert(parsed.data)
52
+ .select()
53
+ .single()
54
+
55
+ if (error) throw error
56
+ return data
57
+ }
58
+
59
+ export async function updateUser(id: string, input: UpdateUserInput) {
60
+ const parsed = updateUserSchema.safeParse(input)
61
+
62
+ if (!parsed.success) {
63
+ throw new Error(parsed.error.errors[0].message)
64
+ }
65
+
66
+ const supabase = await createClient()
67
+ const { data: { user } } = await supabase.auth.getUser()
68
+
69
+ if (!user) throw new Error('Unauthorized')
70
+
71
+ const { data, error } = await supabase
72
+ .from('users')
73
+ .update(parsed.data)
74
+ .eq('id', id)
75
+ .select()
76
+ .single()
77
+
78
+ if (error) throw error
79
+ return data
80
+ }
81
+
82
+ export async function deleteUser(id: string) {
83
+ const supabase = await createClient()
84
+ const { data: { user } } = await supabase.auth.getUser()
85
+
86
+ if (!user) throw new Error('Unauthorized')
87
+
88
+ const { error } = await supabase
89
+ .from('users')
90
+ .delete()
91
+ .eq('id', id)
92
+
93
+ if (error) throw error
94
+ }