@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,12 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js'
2
+ import postgres from 'postgres'
3
+ import * as schema from './schema'
4
+
5
+ const connectionString = process.env.DATABASE_URL!
6
+
7
+ // Disable prefetch as it is not supported for "Transaction" pool mode
8
+ const client = postgres(connectionString, { prepare: false })
9
+
10
+ export const db = drizzle(client, { schema })
11
+
12
+ export type Database = typeof db
@@ -0,0 +1 @@
1
+ export * from './users'
@@ -0,0 +1,16 @@
1
+ import { pgTable, uuid, varchar, text, timestamp, pgEnum } from 'drizzle-orm/pg-core'
2
+
3
+ export const userRoleEnum = pgEnum('user_role', ['admin', 'user', 'viewer'])
4
+
5
+ export const users = pgTable('users', {
6
+ id: uuid('id').primaryKey().defaultRandom(),
7
+ email: varchar('email', { length: 255 }).notNull().unique(),
8
+ name: varchar('name', { length: 255 }).notNull(),
9
+ role: userRoleEnum('role').default('user').notNull(),
10
+ avatarUrl: text('avatar_url'),
11
+ createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
12
+ updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
13
+ })
14
+
15
+ export type User = typeof users.$inferSelect
16
+ export type NewUser = typeof users.$inferInsert
@@ -0,0 +1,39 @@
1
+ import { db } from './index'
2
+ import { users } from './schema'
3
+
4
+ async function seed() {
5
+ console.log('🌱 Seeding database...')
6
+
7
+ // Clear existing data
8
+ await db.delete(users)
9
+
10
+ // Insert seed data
11
+ await db.insert(users).values([
12
+ {
13
+ email: 'admin@example.com',
14
+ name: 'Admin User',
15
+ role: 'admin',
16
+ },
17
+ {
18
+ email: 'user@example.com',
19
+ name: 'Regular User',
20
+ role: 'user',
21
+ },
22
+ {
23
+ email: 'viewer@example.com',
24
+ name: 'Viewer User',
25
+ role: 'viewer',
26
+ },
27
+ ])
28
+
29
+ console.log('✅ Database seeded successfully!')
30
+ }
31
+
32
+ seed()
33
+ .catch((error) => {
34
+ console.error('❌ Seed failed:', error)
35
+ process.exit(1)
36
+ })
37
+ .finally(() => {
38
+ process.exit(0)
39
+ })
@@ -0,0 +1,3 @@
1
+ export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop } from './useMediaQuery'
2
+ export { useDebounce, useDebouncedCallback } from './useDebounce'
3
+ export { useDataTable } from './useDataTable'
@@ -0,0 +1,82 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import {
5
+ ColumnDef,
6
+ ColumnFiltersState,
7
+ SortingState,
8
+ VisibilityState,
9
+ getCoreRowModel,
10
+ getFilteredRowModel,
11
+ getPaginationRowModel,
12
+ getSortedRowModel,
13
+ useReactTable,
14
+ TableOptions,
15
+ } from '@tanstack/react-table'
16
+
17
+ interface UseDataTableOptions<TData> {
18
+ data: TData[]
19
+ columns: ColumnDef<TData, unknown>[]
20
+ initialPageSize?: number
21
+ enableRowSelection?: boolean
22
+ }
23
+
24
+ export function useDataTable<TData>({
25
+ data,
26
+ columns,
27
+ initialPageSize = 10,
28
+ enableRowSelection = false,
29
+ }: UseDataTableOptions<TData>) {
30
+ const [sorting, setSorting] = useState<SortingState>([])
31
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
32
+ const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
33
+ const [rowSelection, setRowSelection] = useState({})
34
+
35
+ const tableOptions: TableOptions<TData> = useMemo(
36
+ () => ({
37
+ data,
38
+ columns,
39
+ getCoreRowModel: getCoreRowModel(),
40
+ getPaginationRowModel: getPaginationRowModel(),
41
+ onSortingChange: setSorting,
42
+ getSortedRowModel: getSortedRowModel(),
43
+ onColumnFiltersChange: setColumnFilters,
44
+ getFilteredRowModel: getFilteredRowModel(),
45
+ onColumnVisibilityChange: setColumnVisibility,
46
+ onRowSelectionChange: setRowSelection,
47
+ enableRowSelection,
48
+ state: {
49
+ sorting,
50
+ columnFilters,
51
+ columnVisibility,
52
+ rowSelection,
53
+ },
54
+ initialState: {
55
+ pagination: {
56
+ pageSize: initialPageSize,
57
+ },
58
+ },
59
+ }),
60
+ [data, columns, sorting, columnFilters, columnVisibility, rowSelection, enableRowSelection, initialPageSize]
61
+ )
62
+
63
+ const table = useReactTable(tableOptions)
64
+
65
+ const selectedRows = useMemo(
66
+ () => table.getFilteredSelectedRowModel().rows.map((row) => row.original),
67
+ [table]
68
+ )
69
+
70
+ return {
71
+ table,
72
+ sorting,
73
+ columnFilters,
74
+ columnVisibility,
75
+ rowSelection,
76
+ selectedRows,
77
+ setSorting,
78
+ setColumnFilters,
79
+ setColumnVisibility,
80
+ setRowSelection,
81
+ }
82
+ }
@@ -0,0 +1,49 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback, useRef } from 'react'
4
+
5
+ export function useDebounce<T>(value: T, delay: number): T {
6
+ const [debouncedValue, setDebouncedValue] = useState<T>(value)
7
+
8
+ useEffect(() => {
9
+ const timer = setTimeout(() => {
10
+ setDebouncedValue(value)
11
+ }, delay)
12
+
13
+ return () => {
14
+ clearTimeout(timer)
15
+ }
16
+ }, [value, delay])
17
+
18
+ return debouncedValue
19
+ }
20
+
21
+ export function useDebouncedCallback<T extends (...args: Parameters<T>) => ReturnType<T>>(
22
+ callback: T,
23
+ delay: number
24
+ ): T {
25
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null)
26
+
27
+ const debouncedCallback = useCallback(
28
+ (...args: Parameters<T>) => {
29
+ if (timeoutRef.current) {
30
+ clearTimeout(timeoutRef.current)
31
+ }
32
+
33
+ timeoutRef.current = setTimeout(() => {
34
+ callback(...args)
35
+ }, delay)
36
+ },
37
+ [callback, delay]
38
+ ) as T
39
+
40
+ useEffect(() => {
41
+ return () => {
42
+ if (timeoutRef.current) {
43
+ clearTimeout(timeoutRef.current)
44
+ }
45
+ }
46
+ }, [])
47
+
48
+ return debouncedCallback
49
+ }
@@ -0,0 +1,36 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ export function useMediaQuery(query: string): boolean {
6
+ const [matches, setMatches] = useState(false)
7
+
8
+ useEffect(() => {
9
+ const media = window.matchMedia(query)
10
+
11
+ if (media.matches !== matches) {
12
+ setMatches(media.matches)
13
+ }
14
+
15
+ const listener = (event: MediaQueryListEvent) => {
16
+ setMatches(event.matches)
17
+ }
18
+
19
+ media.addEventListener('change', listener)
20
+ return () => media.removeEventListener('change', listener)
21
+ }, [matches, query])
22
+
23
+ return matches
24
+ }
25
+
26
+ export function useIsMobile(): boolean {
27
+ return useMediaQuery('(max-width: 768px)')
28
+ }
29
+
30
+ export function useIsTablet(): boolean {
31
+ return useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
32
+ }
33
+
34
+ export function useIsDesktop(): boolean {
35
+ return useMediaQuery('(min-width: 1025px)')
36
+ }
@@ -0,0 +1,34 @@
1
+ import dayjs from 'dayjs'
2
+ import utc from 'dayjs/plugin/utc'
3
+ import timezone from 'dayjs/plugin/timezone'
4
+ import relativeTime from 'dayjs/plugin/relativeTime'
5
+ import localizedFormat from 'dayjs/plugin/localizedFormat'
6
+ import isBetween from 'dayjs/plugin/isBetween'
7
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
8
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
9
+ import 'dayjs/locale/es'
10
+
11
+ // Extend dayjs with plugins
12
+ dayjs.extend(utc)
13
+ dayjs.extend(timezone)
14
+ dayjs.extend(relativeTime)
15
+ dayjs.extend(localizedFormat)
16
+ dayjs.extend(isBetween)
17
+ dayjs.extend(isSameOrAfter)
18
+ dayjs.extend(isSameOrBefore)
19
+
20
+ // Set default locale to Spanish
21
+ dayjs.locale('es')
22
+
23
+ // Set default timezone (can be overridden)
24
+ const DEFAULT_TIMEZONE = 'America/Mexico_City'
25
+
26
+ export function setDefaultTimezone(tz: string) {
27
+ dayjs.tz.setDefault(tz)
28
+ }
29
+
30
+ // Initialize with default timezone
31
+ setDefaultTimezone(DEFAULT_TIMEZONE)
32
+
33
+ export { dayjs }
34
+ export default dayjs
@@ -0,0 +1,120 @@
1
+ import { dayjs } from './config'
2
+ import type { Dayjs, ConfigType } from 'dayjs'
3
+
4
+ type DateInput = ConfigType
5
+
6
+ /**
7
+ * Format date to localized short format (e.g., "15/01/2024")
8
+ */
9
+ export function formatDate(date: DateInput): string {
10
+ return dayjs(date).format('DD/MM/YYYY')
11
+ }
12
+
13
+ /**
14
+ * Format date to localized long format (e.g., "15 de enero de 2024")
15
+ */
16
+ export function formatDateLong(date: DateInput): string {
17
+ return dayjs(date).format('D [de] MMMM [de] YYYY')
18
+ }
19
+
20
+ /**
21
+ * Format time (e.g., "14:30")
22
+ */
23
+ export function formatTime(date: DateInput): string {
24
+ return dayjs(date).format('HH:mm')
25
+ }
26
+
27
+ /**
28
+ * Format time with seconds (e.g., "14:30:45")
29
+ */
30
+ export function formatTimeWithSeconds(date: DateInput): string {
31
+ return dayjs(date).format('HH:mm:ss')
32
+ }
33
+
34
+ /**
35
+ * Format date and time (e.g., "15/01/2024 14:30")
36
+ */
37
+ export function formatDateTime(date: DateInput): string {
38
+ return dayjs(date).format('DD/MM/YYYY HH:mm')
39
+ }
40
+
41
+ /**
42
+ * Format date and time long (e.g., "15 de enero de 2024 a las 14:30")
43
+ */
44
+ export function formatDateTimeLong(date: DateInput): string {
45
+ return dayjs(date).format('D [de] MMMM [de] YYYY [a las] HH:mm')
46
+ }
47
+
48
+ /**
49
+ * Format relative time (e.g., "hace 2 horas")
50
+ */
51
+ export function formatRelative(date: DateInput): string {
52
+ return dayjs(date).fromNow()
53
+ }
54
+
55
+ /**
56
+ * Format ISO string
57
+ */
58
+ export function formatISO(date: DateInput): string {
59
+ return dayjs(date).toISOString()
60
+ }
61
+
62
+ /**
63
+ * Format for API requests (ISO format)
64
+ */
65
+ export function formatForApi(date: DateInput): string {
66
+ return dayjs(date).utc().format()
67
+ }
68
+
69
+ /**
70
+ * Parse a date string and return a Dayjs object
71
+ */
72
+ export function parseDate(date: DateInput): Dayjs {
73
+ return dayjs(date)
74
+ }
75
+
76
+ /**
77
+ * Get start of day
78
+ */
79
+ export function startOfDay(date: DateInput): Dayjs {
80
+ return dayjs(date).startOf('day')
81
+ }
82
+
83
+ /**
84
+ * Get end of day
85
+ */
86
+ export function endOfDay(date: DateInput): Dayjs {
87
+ return dayjs(date).endOf('day')
88
+ }
89
+
90
+ /**
91
+ * Check if a date is today
92
+ */
93
+ export function isToday(date: DateInput): boolean {
94
+ return dayjs(date).isSame(dayjs(), 'day')
95
+ }
96
+
97
+ /**
98
+ * Check if a date is in the past
99
+ */
100
+ export function isPast(date: DateInput): boolean {
101
+ return dayjs(date).isBefore(dayjs())
102
+ }
103
+
104
+ /**
105
+ * Check if a date is in the future
106
+ */
107
+ export function isFuture(date: DateInput): boolean {
108
+ return dayjs(date).isAfter(dayjs())
109
+ }
110
+
111
+ /**
112
+ * Get difference between two dates in specified unit
113
+ */
114
+ export function diff(
115
+ date1: DateInput,
116
+ date2: DateInput,
117
+ unit: 'day' | 'week' | 'month' | 'year' | 'hour' | 'minute' = 'day'
118
+ ): number {
119
+ return dayjs(date1).diff(dayjs(date2), unit)
120
+ }
@@ -0,0 +1,19 @@
1
+ export { dayjs, setDefaultTimezone } from './config'
2
+ export {
3
+ formatDate,
4
+ formatDateLong,
5
+ formatTime,
6
+ formatTimeWithSeconds,
7
+ formatDateTime,
8
+ formatDateTimeLong,
9
+ formatRelative,
10
+ formatISO,
11
+ formatForApi,
12
+ parseDate,
13
+ startOfDay,
14
+ endOfDay,
15
+ isToday,
16
+ isPast,
17
+ isFuture,
18
+ diff,
19
+ } from './formatters'
@@ -0,0 +1,89 @@
1
+ import * as XLSX from 'xlsx'
2
+
3
+ export interface ExportOptions {
4
+ filename?: string
5
+ sheetName?: string
6
+ headers?: string[]
7
+ columnWidths?: number[]
8
+ }
9
+
10
+ /**
11
+ * Export data to Excel file and trigger download
12
+ */
13
+ export function exportToExcel<T extends Record<string, unknown>>(
14
+ data: T[],
15
+ options: ExportOptions = {}
16
+ ): void {
17
+ const {
18
+ filename = 'export',
19
+ sheetName = 'Sheet1',
20
+ headers,
21
+ columnWidths,
22
+ } = options
23
+
24
+ // Create worksheet from data
25
+ const worksheet = XLSX.utils.json_to_sheet(data, {
26
+ header: headers,
27
+ })
28
+
29
+ // Set column widths if provided
30
+ if (columnWidths) {
31
+ worksheet['!cols'] = columnWidths.map((width) => ({ wch: width }))
32
+ }
33
+
34
+ // Create workbook
35
+ const workbook = XLSX.utils.book_new()
36
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
37
+
38
+ // Generate filename with extension
39
+ const fullFilename = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`
40
+
41
+ // Trigger download
42
+ XLSX.writeFile(workbook, fullFilename)
43
+ }
44
+
45
+ /**
46
+ * Export data to Excel buffer (for server-side usage)
47
+ */
48
+ export function exportToExcelBuffer<T extends Record<string, unknown>>(
49
+ data: T[],
50
+ options: Omit<ExportOptions, 'filename'> = {}
51
+ ): Buffer {
52
+ const { sheetName = 'Sheet1', headers, columnWidths } = options
53
+
54
+ const worksheet = XLSX.utils.json_to_sheet(data, {
55
+ header: headers,
56
+ })
57
+
58
+ if (columnWidths) {
59
+ worksheet['!cols'] = columnWidths.map((width) => ({ wch: width }))
60
+ }
61
+
62
+ const workbook = XLSX.utils.book_new()
63
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
64
+
65
+ return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })
66
+ }
67
+
68
+ /**
69
+ * Create an Excel template with headers only
70
+ */
71
+ export function createTemplate(
72
+ headers: string[],
73
+ options: Omit<ExportOptions, 'headers'> = {}
74
+ ): void {
75
+ const { filename = 'template', sheetName = 'Sheet1' } = options
76
+
77
+ const worksheet = XLSX.utils.aoa_to_sheet([headers])
78
+
79
+ // Auto-fit column widths based on header length
80
+ worksheet['!cols'] = headers.map((header) => ({
81
+ wch: Math.max(header.length + 2, 12),
82
+ }))
83
+
84
+ const workbook = XLSX.utils.book_new()
85
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
86
+
87
+ const fullFilename = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`
88
+ XLSX.writeFile(workbook, fullFilename)
89
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ parseExcelBuffer,
3
+ parseExcelFile,
4
+ validateExcelData,
5
+ type ParseOptions,
6
+ type ParseResult,
7
+ } from './parser'
8
+
9
+ export {
10
+ exportToExcel,
11
+ exportToExcelBuffer,
12
+ createTemplate,
13
+ type ExportOptions,
14
+ } from './exporter'
@@ -0,0 +1,96 @@
1
+ import * as XLSX from 'xlsx'
2
+
3
+ export interface ParseOptions {
4
+ sheet?: number | string
5
+ header?: number
6
+ range?: string
7
+ raw?: boolean
8
+ }
9
+
10
+ export interface ParseResult<T = Record<string, unknown>> {
11
+ data: T[]
12
+ headers: string[]
13
+ sheetNames: string[]
14
+ }
15
+
16
+ /**
17
+ * Parse an Excel file buffer to JSON
18
+ */
19
+ export function parseExcelBuffer<T = Record<string, unknown>>(
20
+ buffer: ArrayBuffer,
21
+ options: ParseOptions = {}
22
+ ): ParseResult<T> {
23
+ const workbook = XLSX.read(buffer, { type: 'array' })
24
+ const sheetNames = workbook.SheetNames
25
+
26
+ const sheetName = typeof options.sheet === 'number'
27
+ ? sheetNames[options.sheet]
28
+ : options.sheet ?? sheetNames[0]
29
+
30
+ const worksheet = workbook.Sheets[sheetName]
31
+
32
+ if (!worksheet) {
33
+ throw new Error(`Sheet "${sheetName}" not found`)
34
+ }
35
+
36
+ const jsonOptions: XLSX.Sheet2JSONOpts = {
37
+ header: options.header,
38
+ range: options.range,
39
+ raw: options.raw ?? false,
40
+ }
41
+
42
+ const data = XLSX.utils.sheet_to_json<T>(worksheet, jsonOptions)
43
+
44
+ // Get headers from the first row
45
+ const range = XLSX.utils.decode_range(worksheet['!ref'] ?? 'A1')
46
+ const headers: string[] = []
47
+
48
+ for (let col = range.s.c; col <= range.e.c; col++) {
49
+ const cell = worksheet[XLSX.utils.encode_cell({ r: 0, c: col })]
50
+ headers.push(cell?.v?.toString() ?? `Column${col + 1}`)
51
+ }
52
+
53
+ return {
54
+ data,
55
+ headers,
56
+ sheetNames,
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Parse an Excel file from a File object
62
+ */
63
+ export async function parseExcelFile<T = Record<string, unknown>>(
64
+ file: File,
65
+ options: ParseOptions = {}
66
+ ): Promise<ParseResult<T>> {
67
+ const buffer = await file.arrayBuffer()
68
+ return parseExcelBuffer<T>(buffer, options)
69
+ }
70
+
71
+ /**
72
+ * Validate Excel data against a schema
73
+ */
74
+ export function validateExcelData<T>(
75
+ data: unknown[],
76
+ validator: (row: unknown, index: number) => T | null
77
+ ): { valid: T[]; errors: { row: number; error: string }[] } {
78
+ const valid: T[] = []
79
+ const errors: { row: number; error: string }[] = []
80
+
81
+ data.forEach((row, index) => {
82
+ try {
83
+ const result = validator(row, index)
84
+ if (result !== null) {
85
+ valid.push(result)
86
+ }
87
+ } catch (error) {
88
+ errors.push({
89
+ row: index + 2, // +2 because Excel rows are 1-indexed and header is row 1
90
+ error: error instanceof Error ? error.message : 'Validation error',
91
+ })
92
+ }
93
+ })
94
+
95
+ return { valid, errors }
96
+ }
@@ -0,0 +1,35 @@
1
+ import { QueryClient, defaultShouldDehydrateQuery, isServer } from '@tanstack/react-query'
2
+
3
+ function makeQueryClient() {
4
+ return new QueryClient({
5
+ defaultOptions: {
6
+ queries: {
7
+ staleTime: 60 * 1000, // 1 minute
8
+ gcTime: 5 * 60 * 1000, // 5 minutes
9
+ retry: 1,
10
+ refetchOnWindowFocus: false,
11
+ },
12
+ dehydrate: {
13
+ shouldDehydrateQuery: (query) =>
14
+ defaultShouldDehydrateQuery(query) ||
15
+ query.state.status === 'pending',
16
+ },
17
+ },
18
+ })
19
+ }
20
+
21
+ let browserQueryClient: QueryClient | undefined = undefined
22
+
23
+ export function getQueryClient() {
24
+ if (isServer) {
25
+ // Server: always make a new query client
26
+ return makeQueryClient()
27
+ } else {
28
+ // Browser: make a new query client if we don't already have one
29
+ // This is very important, so we don't re-make a new client if React
30
+ // suspends during the initial render. This may not be needed if we
31
+ // have a suspense boundary BELOW the creation of the query client
32
+ if (!browserQueryClient) browserQueryClient = makeQueryClient()
33
+ return browserQueryClient
34
+ }
35
+ }
@@ -1,8 +1,11 @@
1
1
  import { createBrowserClient } from '@supabase/ssr'
2
2
 
3
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
4
+ const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY
5
+
3
6
  export function createClient() {
4
7
  return createBrowserClient(
5
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
6
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
8
+ supabaseUrl!,
9
+ supabaseKey!
7
10
  )
8
11
  }