@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,27 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { QueryClientProvider } from '@tanstack/react-query'
5
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
6
+ import { ThemeProvider } from 'next-themes'
7
+ import { Toaster } from 'sonner'
8
+ import { getQueryClient } from '@/lib/query-client'
9
+
10
+ export function Providers({ children }: { children: React.ReactNode }) {
11
+ const [queryClient] = useState(() => getQueryClient())
12
+
13
+ return (
14
+ <QueryClientProvider client={queryClient}>
15
+ <ThemeProvider
16
+ attribute="class"
17
+ defaultTheme="system"
18
+ enableSystem
19
+ disableTransitionOnChange
20
+ >
21
+ {children}
22
+ <Toaster richColors position="top-right" />
23
+ </ThemeProvider>
24
+ <ReactQueryDevtools initialIsOpen={false} />
25
+ </QueryClientProvider>
26
+ )
27
+ }
@@ -20,15 +20,10 @@ const navItems: NavItem[] = [
20
20
  icon: 'Dashboard',
21
21
  },
22
22
  {
23
- path: '/clientes',
24
- name: 'Clientes',
23
+ path: '/users',
24
+ name: 'Usuarios',
25
25
  icon: 'Users',
26
26
  },
27
- {
28
- path: '/documents',
29
- name: 'Documentos',
30
- icon: 'Documents',
31
- },
32
27
  {
33
28
  path: '/settings',
34
29
  name: 'Configuración',
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ import { Column } from '@tanstack/react-table'
4
+ import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { Button } from '@/components/ui/button'
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from '@/components/ui/dropdown-menu'
15
+
16
+ interface DataTableColumnHeaderProps<TData, TValue>
17
+ extends React.HTMLAttributes<HTMLDivElement> {
18
+ column: Column<TData, TValue>
19
+ title: string
20
+ }
21
+
22
+ export function DataTableColumnHeader<TData, TValue>({
23
+ column,
24
+ title,
25
+ className,
26
+ }: DataTableColumnHeaderProps<TData, TValue>) {
27
+ if (!column.getCanSort()) {
28
+ return <div className={cn(className)}>{title}</div>
29
+ }
30
+
31
+ return (
32
+ <div className={cn('flex items-center space-x-2', className)}>
33
+ <DropdownMenu>
34
+ <DropdownMenuTrigger asChild>
35
+ <Button
36
+ variant="ghost"
37
+ size="sm"
38
+ className="-ml-3 h-8 data-[state=open]:bg-accent"
39
+ >
40
+ <span>{title}</span>
41
+ {column.getIsSorted() === 'desc' ? (
42
+ <ArrowDown className="ml-2 h-4 w-4" />
43
+ ) : column.getIsSorted() === 'asc' ? (
44
+ <ArrowUp className="ml-2 h-4 w-4" />
45
+ ) : (
46
+ <ChevronsUpDown className="ml-2 h-4 w-4" />
47
+ )}
48
+ </Button>
49
+ </DropdownMenuTrigger>
50
+ <DropdownMenuContent align="start">
51
+ <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
52
+ <ArrowUp className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
53
+ Ascendente
54
+ </DropdownMenuItem>
55
+ <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
56
+ <ArrowDown className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
57
+ Descendente
58
+ </DropdownMenuItem>
59
+ <DropdownMenuSeparator />
60
+ <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
61
+ <EyeOff className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
62
+ Ocultar
63
+ </DropdownMenuItem>
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ </div>
67
+ )
68
+ }
@@ -0,0 +1,99 @@
1
+ 'use client'
2
+
3
+ import { Table } from '@tanstack/react-table'
4
+ import {
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ ChevronsLeft,
8
+ ChevronsRight,
9
+ } from 'lucide-react'
10
+
11
+ import { Button } from '@/components/ui/button'
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '@/components/ui/select'
19
+
20
+ interface DataTablePaginationProps<TData> {
21
+ table: Table<TData>
22
+ }
23
+
24
+ export function DataTablePagination<TData>({
25
+ table,
26
+ }: DataTablePaginationProps<TData>) {
27
+ return (
28
+ <div className="flex items-center justify-between px-2">
29
+ <div className="flex-1 text-sm text-muted-foreground">
30
+ {table.getFilteredSelectedRowModel().rows.length} de{' '}
31
+ {table.getFilteredRowModel().rows.length} fila(s) seleccionada(s).
32
+ </div>
33
+ <div className="flex items-center space-x-6 lg:space-x-8">
34
+ <div className="flex items-center space-x-2">
35
+ <p className="text-sm font-medium">Filas por página</p>
36
+ <Select
37
+ value={`${table.getState().pagination.pageSize}`}
38
+ onValueChange={(value) => {
39
+ table.setPageSize(Number(value))
40
+ }}
41
+ >
42
+ <SelectTrigger className="h-8 w-[70px]">
43
+ <SelectValue placeholder={table.getState().pagination.pageSize} />
44
+ </SelectTrigger>
45
+ <SelectContent side="top">
46
+ {[10, 20, 30, 40, 50].map((pageSize) => (
47
+ <SelectItem key={pageSize} value={`${pageSize}`}>
48
+ {pageSize}
49
+ </SelectItem>
50
+ ))}
51
+ </SelectContent>
52
+ </Select>
53
+ </div>
54
+ <div className="flex w-[100px] items-center justify-center text-sm font-medium">
55
+ Página {table.getState().pagination.pageIndex + 1} de{' '}
56
+ {table.getPageCount()}
57
+ </div>
58
+ <div className="flex items-center space-x-2">
59
+ <Button
60
+ variant="outline"
61
+ className="hidden h-8 w-8 p-0 lg:flex"
62
+ onClick={() => table.setPageIndex(0)}
63
+ disabled={!table.getCanPreviousPage()}
64
+ >
65
+ <span className="sr-only">Ir a primera página</span>
66
+ <ChevronsLeft className="h-4 w-4" />
67
+ </Button>
68
+ <Button
69
+ variant="outline"
70
+ className="h-8 w-8 p-0"
71
+ onClick={() => table.previousPage()}
72
+ disabled={!table.getCanPreviousPage()}
73
+ >
74
+ <span className="sr-only">Ir a página anterior</span>
75
+ <ChevronLeft className="h-4 w-4" />
76
+ </Button>
77
+ <Button
78
+ variant="outline"
79
+ className="h-8 w-8 p-0"
80
+ onClick={() => table.nextPage()}
81
+ disabled={!table.getCanNextPage()}
82
+ >
83
+ <span className="sr-only">Ir a página siguiente</span>
84
+ <ChevronRight className="h-4 w-4" />
85
+ </Button>
86
+ <Button
87
+ variant="outline"
88
+ className="hidden h-8 w-8 p-0 lg:flex"
89
+ onClick={() => table.setPageIndex(table.getPageCount() - 1)}
90
+ disabled={!table.getCanNextPage()}
91
+ >
92
+ <span className="sr-only">Ir a última página</span>
93
+ <ChevronsRight className="h-4 w-4" />
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ )
99
+ }
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+
3
+ import { Table } from '@tanstack/react-table'
4
+ import { X } from 'lucide-react'
5
+
6
+ import { Button } from '@/components/ui/button'
7
+ import { Input } from '@/components/ui/input'
8
+ import { DataTableViewOptions } from './data-table-view-options'
9
+
10
+ interface DataTableToolbarProps<TData> {
11
+ table: Table<TData>
12
+ searchKey?: string
13
+ searchPlaceholder?: string
14
+ }
15
+
16
+ export function DataTableToolbar<TData>({
17
+ table,
18
+ searchKey,
19
+ searchPlaceholder = 'Buscar...',
20
+ }: DataTableToolbarProps<TData>) {
21
+ const isFiltered = table.getState().columnFilters.length > 0
22
+
23
+ return (
24
+ <div className="flex items-center justify-between">
25
+ <div className="flex flex-1 items-center space-x-2">
26
+ {searchKey && (
27
+ <Input
28
+ placeholder={searchPlaceholder}
29
+ value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ''}
30
+ onChange={(event) =>
31
+ table.getColumn(searchKey)?.setFilterValue(event.target.value)
32
+ }
33
+ className="h-8 w-[150px] lg:w-[250px]"
34
+ />
35
+ )}
36
+ {isFiltered && (
37
+ <Button
38
+ variant="ghost"
39
+ onClick={() => table.resetColumnFilters()}
40
+ className="h-8 px-2 lg:px-3"
41
+ >
42
+ Limpiar
43
+ <X className="ml-2 h-4 w-4" />
44
+ </Button>
45
+ )}
46
+ </div>
47
+ <DataTableViewOptions table={table} />
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import { Table } from '@tanstack/react-table'
4
+ import { Settings2 } from 'lucide-react'
5
+
6
+ import { Button } from '@/components/ui/button'
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuCheckboxItem,
10
+ DropdownMenuContent,
11
+ DropdownMenuLabel,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from '@/components/ui/dropdown-menu'
15
+
16
+ interface DataTableViewOptionsProps<TData> {
17
+ table: Table<TData>
18
+ }
19
+
20
+ export function DataTableViewOptions<TData>({
21
+ table,
22
+ }: DataTableViewOptionsProps<TData>) {
23
+ return (
24
+ <DropdownMenu>
25
+ <DropdownMenuTrigger asChild>
26
+ <Button
27
+ variant="outline"
28
+ size="sm"
29
+ className="ml-auto hidden h-8 lg:flex"
30
+ >
31
+ <Settings2 className="mr-2 h-4 w-4" />
32
+ Columnas
33
+ </Button>
34
+ </DropdownMenuTrigger>
35
+ <DropdownMenuContent align="end" className="w-[150px]">
36
+ <DropdownMenuLabel>Mostrar columnas</DropdownMenuLabel>
37
+ <DropdownMenuSeparator />
38
+ {table
39
+ .getAllColumns()
40
+ .filter(
41
+ (column) =>
42
+ typeof column.accessorFn !== 'undefined' && column.getCanHide()
43
+ )
44
+ .map((column) => {
45
+ return (
46
+ <DropdownMenuCheckboxItem
47
+ key={column.id}
48
+ className="capitalize"
49
+ checked={column.getIsVisible()}
50
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
51
+ >
52
+ {column.id}
53
+ </DropdownMenuCheckboxItem>
54
+ )
55
+ })}
56
+ </DropdownMenuContent>
57
+ </DropdownMenu>
58
+ )
59
+ }
@@ -0,0 +1,128 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ ColumnDef,
6
+ ColumnFiltersState,
7
+ SortingState,
8
+ VisibilityState,
9
+ flexRender,
10
+ getCoreRowModel,
11
+ getFilteredRowModel,
12
+ getPaginationRowModel,
13
+ getSortedRowModel,
14
+ useReactTable,
15
+ } from '@tanstack/react-table'
16
+
17
+ import {
18
+ Table,
19
+ TableBody,
20
+ TableCell,
21
+ TableHead,
22
+ TableHeader,
23
+ TableRow,
24
+ } from '@/components/ui/table'
25
+ import { Input } from '@/components/ui/input'
26
+ import { DataTablePagination } from './data-table-pagination'
27
+ import { DataTableViewOptions } from './data-table-view-options'
28
+
29
+ interface DataTableProps<TData, TValue> {
30
+ columns: ColumnDef<TData, TValue>[]
31
+ data: TData[]
32
+ searchKey?: string
33
+ searchPlaceholder?: string
34
+ }
35
+
36
+ export function DataTable<TData, TValue>({
37
+ columns,
38
+ data,
39
+ searchKey,
40
+ searchPlaceholder = 'Buscar...',
41
+ }: DataTableProps<TData, TValue>) {
42
+ const [sorting, setSorting] = React.useState<SortingState>([])
43
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
44
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
45
+ const [rowSelection, setRowSelection] = React.useState({})
46
+
47
+ const table = useReactTable({
48
+ data,
49
+ columns,
50
+ getCoreRowModel: getCoreRowModel(),
51
+ getPaginationRowModel: getPaginationRowModel(),
52
+ onSortingChange: setSorting,
53
+ getSortedRowModel: getSortedRowModel(),
54
+ onColumnFiltersChange: setColumnFilters,
55
+ getFilteredRowModel: getFilteredRowModel(),
56
+ onColumnVisibilityChange: setColumnVisibility,
57
+ onRowSelectionChange: setRowSelection,
58
+ state: {
59
+ sorting,
60
+ columnFilters,
61
+ columnVisibility,
62
+ rowSelection,
63
+ },
64
+ })
65
+
66
+ return (
67
+ <div className="space-y-4">
68
+ <div className="flex items-center justify-between">
69
+ {searchKey && (
70
+ <Input
71
+ placeholder={searchPlaceholder}
72
+ value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ''}
73
+ onChange={(event) =>
74
+ table.getColumn(searchKey)?.setFilterValue(event.target.value)
75
+ }
76
+ className="max-w-sm"
77
+ />
78
+ )}
79
+ <DataTableViewOptions table={table} />
80
+ </div>
81
+ <div className="rounded-md border">
82
+ <Table>
83
+ <TableHeader>
84
+ {table.getHeaderGroups().map((headerGroup) => (
85
+ <TableRow key={headerGroup.id}>
86
+ {headerGroup.headers.map((header) => {
87
+ return (
88
+ <TableHead key={header.id}>
89
+ {header.isPlaceholder
90
+ ? null
91
+ : flexRender(
92
+ header.column.columnDef.header,
93
+ header.getContext()
94
+ )}
95
+ </TableHead>
96
+ )
97
+ })}
98
+ </TableRow>
99
+ ))}
100
+ </TableHeader>
101
+ <TableBody>
102
+ {table.getRowModel().rows?.length ? (
103
+ table.getRowModel().rows.map((row) => (
104
+ <TableRow
105
+ key={row.id}
106
+ data-state={row.getIsSelected() && 'selected'}
107
+ >
108
+ {row.getVisibleCells().map((cell) => (
109
+ <TableCell key={cell.id}>
110
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
111
+ </TableCell>
112
+ ))}
113
+ </TableRow>
114
+ ))
115
+ ) : (
116
+ <TableRow>
117
+ <TableCell colSpan={columns.length} className="h-24 text-center">
118
+ No hay resultados.
119
+ </TableCell>
120
+ </TableRow>
121
+ )}
122
+ </TableBody>
123
+ </Table>
124
+ </div>
125
+ <DataTablePagination table={table} />
126
+ </div>
127
+ )
128
+ }
@@ -0,0 +1,5 @@
1
+ export { DataTable } from './data-table'
2
+ export { DataTablePagination } from './data-table-pagination'
3
+ export { DataTableColumnHeader } from './data-table-column-header'
4
+ export { DataTableToolbar } from './data-table-toolbar'
5
+ export { DataTableViewOptions } from './data-table-view-options'
@@ -0,0 +1,44 @@
1
+ // Motion primitives y variantes
2
+ export {
3
+ MotionDiv,
4
+ MotionSpan,
5
+ MotionButton,
6
+ MotionUl,
7
+ MotionLi,
8
+ MotionNav,
9
+ MotionSection,
10
+ MotionArticle,
11
+ MotionHeader,
12
+ MotionFooter,
13
+ MotionMain,
14
+ MotionAside,
15
+ FadeIn,
16
+ ScaleIn,
17
+ StaggerList,
18
+ StaggerItem,
19
+ AnimatePresence,
20
+ // Variantes
21
+ fadeIn,
22
+ fadeInUp,
23
+ fadeInDown,
24
+ fadeInLeft,
25
+ fadeInRight,
26
+ scaleIn,
27
+ slideInFromBottom,
28
+ slideInFromTop,
29
+ staggerContainer,
30
+ staggerItem,
31
+ // Transiciones
32
+ springTransition,
33
+ easeTransition,
34
+ smoothTransition,
35
+ } from '../motion'
36
+
37
+ // Page transitions
38
+ export {
39
+ PageTransition,
40
+ PageSection,
41
+ PageHeader,
42
+ PageTitle,
43
+ PageDescription,
44
+ } from '../page-transition'
@@ -1,7 +1,9 @@
1
+ 'use client'
2
+
1
3
  import * as React from "react"
2
4
  import { Slot } from "@radix-ui/react-slot"
5
+ import { motion } from "framer-motion"
3
6
  import { cva, type VariantProps } from "class-variance-authority"
4
-
5
7
  import { cn } from "@/lib/utils"
6
8
 
7
9
  const buttonVariants = cva(
@@ -19,7 +21,6 @@ const buttonVariants = cva(
19
21
  default: [
20
22
  "bg-primary text-primary-foreground",
21
23
  "hover:bg-primary/90",
22
- "active:scale-[0.98]",
23
24
  ].join(" "),
24
25
  destructive: [
25
26
  "bg-destructive text-destructive-foreground",
@@ -63,27 +64,55 @@ const buttonVariants = cva(
63
64
  }
64
65
  )
65
66
 
66
- function Button({
67
- className,
68
- variant = "default",
69
- size = "default",
70
- asChild = false,
71
- ...props
72
- }: React.ComponentProps<"button"> &
67
+ type ButtonProps = React.ComponentProps<"button"> &
73
68
  VariantProps<typeof buttonVariants> & {
74
69
  asChild?: boolean
75
- }) {
76
- const Comp = asChild ? Slot : "button"
70
+ animate?: boolean
71
+ }
72
+
73
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
74
+ ({ className, variant = "default", size = "default", asChild = false, animate = true, ...props }, ref) => {
75
+ if (asChild) {
76
+ return (
77
+ <Slot
78
+ ref={ref as React.Ref<HTMLElement>}
79
+ data-slot="button"
80
+ data-variant={variant}
81
+ data-size={size}
82
+ className={cn(buttonVariants({ variant, size, className }))}
83
+ {...props}
84
+ />
85
+ )
86
+ }
77
87
 
78
- return (
79
- <Comp
80
- data-slot="button"
81
- data-variant={variant}
82
- data-size={size}
83
- className={cn(buttonVariants({ variant, size, className }))}
84
- {...props}
85
- />
86
- )
87
- }
88
+ if (!animate) {
89
+ return (
90
+ <button
91
+ ref={ref}
92
+ data-slot="button"
93
+ data-variant={variant}
94
+ data-size={size}
95
+ className={cn(buttonVariants({ variant, size, className }))}
96
+ {...props}
97
+ />
98
+ )
99
+ }
100
+
101
+ return (
102
+ <motion.button
103
+ ref={ref}
104
+ data-slot="button"
105
+ data-variant={variant}
106
+ data-size={size}
107
+ whileHover={{ scale: 1.02 }}
108
+ whileTap={{ scale: 0.98 }}
109
+ transition={{ duration: 0.1 }}
110
+ className={cn(buttonVariants({ variant, size, className }))}
111
+ {...(props as React.ComponentPropsWithoutRef<typeof motion.button>)}
112
+ />
113
+ )
114
+ }
115
+ )
116
+ Button.displayName = "Button"
88
117
 
89
118
  export { Button, buttonVariants }
@@ -1,13 +1,37 @@
1
- import * as React from "react"
1
+ 'use client'
2
2
 
3
+ import * as React from "react"
4
+ import { motion, type HTMLMotionProps } from "framer-motion"
3
5
  import { cn } from "@/lib/utils"
4
6
 
5
- function Card({ className, ...props }: React.ComponentProps<"div">) {
7
+ interface CardProps extends HTMLMotionProps<"div"> {
8
+ animate?: boolean
9
+ }
10
+
11
+ function Card({ className, animate = true, ...props }: CardProps) {
12
+ if (!animate) {
13
+ return (
14
+ <div
15
+ data-slot="card"
16
+ className={cn(
17
+ "rounded-lg border border-border bg-card text-card-foreground",
18
+ className
19
+ )}
20
+ {...(props as React.ComponentProps<"div">)}
21
+ />
22
+ )
23
+ }
24
+
6
25
  return (
7
- <div
26
+ <motion.div
8
27
  data-slot="card"
28
+ initial={{ opacity: 0, y: 10 }}
29
+ animate={{ opacity: 1, y: 0 }}
30
+ transition={{ duration: 0.3, ease: "easeOut" }}
31
+ whileHover={{ y: -2, transition: { duration: 0.2 } }}
9
32
  className={cn(
10
33
  "rounded-lg border border-border bg-card text-card-foreground",
34
+ "transition-shadow hover:shadow-md",
11
35
  className
12
36
  )}
13
37
  {...props}