forge-admin 0.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 (131) hide show
  1. package/README.md +73 -0
  2. package/app.db +0 -0
  3. package/components.json +20 -0
  4. package/dist/assets/index-BPVmexx_.css +1 -0
  5. package/dist/assets/index-BtNewH3n.js +258 -0
  6. package/dist/favicon.ico +0 -0
  7. package/dist/index.html +27 -0
  8. package/dist/placeholder.svg +1 -0
  9. package/dist/robots.txt +14 -0
  10. package/eslint.config.js +26 -0
  11. package/index.html +26 -0
  12. package/package.json +107 -0
  13. package/postcss.config.js +6 -0
  14. package/public/favicon.ico +0 -0
  15. package/public/placeholder.svg +1 -0
  16. package/public/robots.txt +14 -0
  17. package/src/App.css +42 -0
  18. package/src/App.tsx +32 -0
  19. package/src/admin/convertSchema.ts +83 -0
  20. package/src/admin/factory.ts +12 -0
  21. package/src/admin/introspecter.ts +6 -0
  22. package/src/admin/router.ts +38 -0
  23. package/src/admin/schema.ts +17 -0
  24. package/src/admin/sqlite.ts +73 -0
  25. package/src/admin/types.ts +35 -0
  26. package/src/components/AdminLayout.tsx +19 -0
  27. package/src/components/AdminSidebar.tsx +102 -0
  28. package/src/components/DataTable.tsx +166 -0
  29. package/src/components/ModelForm.tsx +221 -0
  30. package/src/components/NavLink.tsx +28 -0
  31. package/src/components/StatCard.tsx +32 -0
  32. package/src/components/ui/accordion.tsx +52 -0
  33. package/src/components/ui/alert-dialog.tsx +104 -0
  34. package/src/components/ui/alert.tsx +43 -0
  35. package/src/components/ui/aspect-ratio.tsx +5 -0
  36. package/src/components/ui/avatar.tsx +38 -0
  37. package/src/components/ui/badge.tsx +29 -0
  38. package/src/components/ui/breadcrumb.tsx +90 -0
  39. package/src/components/ui/button.tsx +47 -0
  40. package/src/components/ui/calendar.tsx +54 -0
  41. package/src/components/ui/card.tsx +43 -0
  42. package/src/components/ui/carousel.tsx +224 -0
  43. package/src/components/ui/chart.tsx +303 -0
  44. package/src/components/ui/checkbox.tsx +26 -0
  45. package/src/components/ui/collapsible.tsx +9 -0
  46. package/src/components/ui/command.tsx +132 -0
  47. package/src/components/ui/context-menu.tsx +178 -0
  48. package/src/components/ui/dialog.tsx +95 -0
  49. package/src/components/ui/drawer.tsx +87 -0
  50. package/src/components/ui/dropdown-menu.tsx +179 -0
  51. package/src/components/ui/form.tsx +129 -0
  52. package/src/components/ui/hover-card.tsx +27 -0
  53. package/src/components/ui/input-otp.tsx +61 -0
  54. package/src/components/ui/input.tsx +22 -0
  55. package/src/components/ui/label.tsx +17 -0
  56. package/src/components/ui/menubar.tsx +207 -0
  57. package/src/components/ui/navigation-menu.tsx +120 -0
  58. package/src/components/ui/pagination.tsx +81 -0
  59. package/src/components/ui/popover.tsx +29 -0
  60. package/src/components/ui/progress.tsx +23 -0
  61. package/src/components/ui/radio-group.tsx +36 -0
  62. package/src/components/ui/resizable.tsx +37 -0
  63. package/src/components/ui/scroll-area.tsx +38 -0
  64. package/src/components/ui/select.tsx +143 -0
  65. package/src/components/ui/separator.tsx +20 -0
  66. package/src/components/ui/sheet.tsx +107 -0
  67. package/src/components/ui/sidebar.tsx +637 -0
  68. package/src/components/ui/skeleton.tsx +7 -0
  69. package/src/components/ui/slider.tsx +23 -0
  70. package/src/components/ui/sonner.tsx +27 -0
  71. package/src/components/ui/switch.tsx +27 -0
  72. package/src/components/ui/table.tsx +72 -0
  73. package/src/components/ui/tabs.tsx +53 -0
  74. package/src/components/ui/textarea.tsx +21 -0
  75. package/src/components/ui/toast.tsx +111 -0
  76. package/src/components/ui/toaster.tsx +24 -0
  77. package/src/components/ui/toggle-group.tsx +49 -0
  78. package/src/components/ui/toggle.tsx +37 -0
  79. package/src/components/ui/tooltip.tsx +28 -0
  80. package/src/components/ui/use-toast.ts +3 -0
  81. package/src/config/define.ts +6 -0
  82. package/src/config/index.ts +0 -0
  83. package/src/config/load.ts +45 -0
  84. package/src/config/types.ts +5 -0
  85. package/src/hooks/use-mobile.tsx +19 -0
  86. package/src/hooks/use-toast.ts +186 -0
  87. package/src/index.css +142 -0
  88. package/src/lib/models.ts +138 -0
  89. package/src/lib/utils.ts +6 -0
  90. package/src/main.tsx +5 -0
  91. package/src/orm/cli/makemigrations.ts +63 -0
  92. package/src/orm/cli/migrate.ts +127 -0
  93. package/src/orm/cli.ts +30 -0
  94. package/src/orm/core/base-model.ts +6 -0
  95. package/src/orm/core/manager.ts +27 -0
  96. package/src/orm/core/query-builder.ts +74 -0
  97. package/src/orm/db/connection.ts +0 -0
  98. package/src/orm/db/sql-types.ts +72 -0
  99. package/src/orm/db/sqlite.ts +4 -0
  100. package/src/orm/decorators/field.ts +80 -0
  101. package/src/orm/decorators/model.ts +36 -0
  102. package/src/orm/decorators/relations.ts +0 -0
  103. package/src/orm/metadata/field-metadata.ts +0 -0
  104. package/src/orm/metadata/field-types.ts +12 -0
  105. package/src/orm/metadata/get-meta.ts +9 -0
  106. package/src/orm/metadata/index.ts +15 -0
  107. package/src/orm/metadata/keys.ts +2 -0
  108. package/src/orm/metadata/model-registry.ts +53 -0
  109. package/src/orm/metadata/modifiers.ts +26 -0
  110. package/src/orm/metadata/types.ts +45 -0
  111. package/src/orm/migration-engine/diff.ts +243 -0
  112. package/src/orm/migration-engine/operations.ts +186 -0
  113. package/src/orm/schema/build.ts +138 -0
  114. package/src/orm/schema/state.ts +23 -0
  115. package/src/orm/schema/writeMigrations.ts +21 -0
  116. package/src/orm/syncdb.ts +25 -0
  117. package/src/pages/Dashboard.tsx +127 -0
  118. package/src/pages/Index.tsx +18 -0
  119. package/src/pages/ModelPage.tsx +177 -0
  120. package/src/pages/NotFound.tsx +24 -0
  121. package/src/pages/SchemaEditor.tsx +170 -0
  122. package/src/pages/Settings.tsx +166 -0
  123. package/src/server.ts +69 -0
  124. package/src/vite-env.d.ts +1 -0
  125. package/tailwind.config.js +112 -0
  126. package/tailwind.config.ts +114 -0
  127. package/tsconfig.app.json +30 -0
  128. package/tsconfig.json +16 -0
  129. package/tsconfig.node.json +22 -0
  130. package/vite.config.js +23 -0
  131. package/vite.config.ts +18 -0
@@ -0,0 +1,102 @@
1
+ import { NavLink, useLocation } from 'react-router-dom';
2
+ import { Database, LayoutDashboard, Users, FileText, Package, ShoppingCart, Settings, Code } from 'lucide-react';
3
+
4
+ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
5
+ Users,
6
+ FileText,
7
+ Package,
8
+ ShoppingCart,
9
+ };
10
+
11
+ export function AdminSidebar({models}) {
12
+ const location = useLocation();
13
+
14
+ const isActive = (path: string) => location.pathname === path;
15
+ const isModelActive = (name: string) => location.pathname.startsWith(`/models/${name}`);
16
+
17
+ return (
18
+ <aside className="w-64 min-h-screen bg-sidebar border-r border-sidebar-border flex flex-col">
19
+ {/* Logo */}
20
+ <div className="p-4 border-b border-sidebar-border">
21
+ <div className="flex items-center gap-3">
22
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
23
+ <Database className="w-5 h-5 text-primary" />
24
+ </div>
25
+ <div>
26
+ <h1 className="font-semibold text-foreground">DataForge</h1>
27
+ <p className="text-xs text-muted-foreground">ORM Admin Panel</p>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ {/* Navigation */}
33
+ <nav className="flex-1 p-3 space-y-1 scrollbar-thin overflow-y-auto">
34
+ <NavLink
35
+ to="/"
36
+ className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
37
+ isActive('/')
38
+ ? 'bg-sidebar-accent text-primary font-medium'
39
+ : 'text-sidebar-foreground hover:bg-sidebar-accent/50'
40
+ }`}
41
+ >
42
+ <LayoutDashboard className="w-4 h-4" />
43
+ Dashboard
44
+ </NavLink>
45
+
46
+ <NavLink
47
+ to="/schema"
48
+ className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
49
+ isActive('/schema')
50
+ ? 'bg-sidebar-accent text-primary font-medium'
51
+ : 'text-sidebar-foreground hover:bg-sidebar-accent/50'
52
+ }`}
53
+ >
54
+ <Code className="w-4 h-4" />
55
+ Schema Editor
56
+ </NavLink>
57
+
58
+ <div className="pt-4 pb-2">
59
+ <span className="px-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
60
+ Models
61
+ </span>
62
+ </div>
63
+
64
+ {models.map((model) => {
65
+ const Icon = iconMap[model.icon] || Database;
66
+ return (
67
+ <NavLink
68
+ key={model.name}
69
+ to={`/models/${model.name}`}
70
+ className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
71
+ isModelActive(model.name)
72
+ ? 'bg-sidebar-accent text-primary font-medium'
73
+ : 'text-sidebar-foreground hover:bg-sidebar-accent/50'
74
+ }`}
75
+ >
76
+ <Icon className="w-4 h-4" />
77
+ {model.displayName}
78
+ <span className="ml-auto text-xs text-muted-foreground font-mono">
79
+ {model.fields.length}
80
+ </span>
81
+ </NavLink>
82
+ );
83
+ })}
84
+ </nav>
85
+
86
+ {/* Footer */}
87
+ <div className="p-3 border-t border-sidebar-border">
88
+ <NavLink
89
+ to="/settings"
90
+ className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
91
+ isActive('/settings')
92
+ ? 'bg-sidebar-accent text-primary font-medium'
93
+ : 'text-sidebar-foreground hover:bg-sidebar-accent/50'
94
+ }`}
95
+ >
96
+ <Settings className="w-4 h-4" />
97
+ Settings
98
+ </NavLink>
99
+ </div>
100
+ </aside>
101
+ );
102
+ }
@@ -0,0 +1,166 @@
1
+ import { useState } from 'react';
2
+ import { ModelDefinition } from '@/lib/models';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Input } from '@/components/ui/input';
5
+ import { Edit2, Trash2, Search, Plus } from 'lucide-react';
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from '@/components/ui/table';
14
+ import { Badge } from '@/components/ui/badge';
15
+ import { ModelType } from '@/admin/types';
16
+
17
+ interface DataTableProps {
18
+ model: ModelType;
19
+ data: Record<string, unknown>[];
20
+ onEdit?: (record: Record<string, unknown>) => void;
21
+ onDelete?: (record: Record<string, unknown>) => void;
22
+ onAdd?: () => void;
23
+ }
24
+
25
+ export function DataTable({ model, data, onEdit, onDelete, onAdd }: DataTableProps) {
26
+ const [search, setSearch] = useState('');
27
+ const [sortField, setSortField] = useState<string | null>(null);
28
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
29
+
30
+ const filteredData = data.filter(record => {
31
+ if (!search) return true;
32
+ return Object.values(record).some(val =>
33
+ String(val).toLowerCase().includes(search.toLowerCase())
34
+ );
35
+ });
36
+
37
+ const sortedData = sortField
38
+ ? [...filteredData].sort((a, b) => {
39
+ const aVal = a[sortField];
40
+ const bVal = b[sortField];
41
+ if (aVal === bVal) return 0;
42
+ const comparison = aVal! < bVal! ? -1 : 1;
43
+ return sortDir === 'asc' ? comparison : -comparison;
44
+ })
45
+ : filteredData;
46
+
47
+ const handleSort = (field: string) => {
48
+ if (sortField === field) {
49
+ setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
50
+ } else {
51
+ setSortField(field);
52
+ setSortDir('asc');
53
+ }
54
+ };
55
+
56
+ const renderCellValue = (value: unknown, fieldType: string) => {
57
+ if (value === null || value === undefined) {
58
+ return <span className="text-muted-foreground">—</span>;
59
+ }
60
+
61
+ if (typeof value === 'boolean') {
62
+ return (
63
+ <Badge variant={value ? 'default' : 'secondary'} className={value ? 'bg-success text-success-foreground' : ''}>
64
+ {value ? 'Yes' : 'No'}
65
+ </Badge>
66
+ );
67
+ }
68
+
69
+ if (fieldType === 'select') {
70
+ return (
71
+ <Badge variant="outline" className="font-mono text-xs">
72
+ {String(value)}
73
+ </Badge>
74
+ );
75
+ }
76
+
77
+ return String(value);
78
+ };
79
+
80
+ return (
81
+ <div className="space-y-4">
82
+ <div className="flex items-center justify-between gap-4">
83
+ <div className="relative flex-1 max-w-sm">
84
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
85
+ <Input
86
+ placeholder="Search records..."
87
+ value={search}
88
+ onChange={(e) => setSearch(e.target.value)}
89
+ className="pl-9 bg-secondary border-border"
90
+ />
91
+ </div>
92
+ <Button onClick={onAdd} className="gap-2">
93
+ <Plus className="w-4 h-4" />
94
+ Add {model.displayName}
95
+ </Button>
96
+ </div>
97
+
98
+ <div className="admin-card p-0 overflow-hidden">
99
+ <Table>
100
+ <TableHeader>
101
+ <TableRow className="border-border hover:bg-transparent">
102
+ {model.fields.map((field) => (
103
+ <TableHead
104
+ key={field.name}
105
+ onClick={() => handleSort(field.name)}
106
+ className="cursor-pointer hover:text-foreground transition-colors"
107
+ >
108
+ <span className="flex items-center gap-1">
109
+ {field.label}
110
+ {sortField === field.name && (
111
+ <span className="text-primary">
112
+ {sortDir === 'asc' ? '↑' : '↓'}
113
+ </span>
114
+ )}
115
+ </span>
116
+ </TableHead>
117
+ ))}
118
+ <TableHead className="w-24">Actions</TableHead>
119
+ </TableRow>
120
+ </TableHeader>
121
+ <TableBody>
122
+ {sortedData.map((record, idx) => (
123
+ <TableRow key={idx} className="border-border hover:bg-secondary/50">
124
+ {model.fields.map((field) => (
125
+ <TableCell key={field.name} className="font-mono text-sm">
126
+ {renderCellValue(record[field.name], field.type)}
127
+ </TableCell>
128
+ ))}
129
+ <TableCell>
130
+ <div className="flex items-center gap-1">
131
+ <Button
132
+ variant="ghost"
133
+ size="icon"
134
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
135
+ onClick={() => onEdit?.(record)}
136
+ >
137
+ <Edit2 className="w-4 h-4" />
138
+ </Button>
139
+ <Button
140
+ variant="ghost"
141
+ size="icon"
142
+ className="h-8 w-8 text-muted-foreground hover:text-destructive"
143
+ onClick={() => onDelete?.(record)}
144
+ >
145
+ <Trash2 className="w-4 h-4" />
146
+ </Button>
147
+ </div>
148
+ </TableCell>
149
+ </TableRow>
150
+ ))}
151
+ </TableBody>
152
+ </Table>
153
+
154
+ {sortedData.length === 0 && (
155
+ <div className="p-8 text-center text-muted-foreground">
156
+ No records found
157
+ </div>
158
+ )}
159
+ </div>
160
+
161
+ <div className="text-sm text-muted-foreground">
162
+ Showing {sortedData.length} of {data.length} records
163
+ </div>
164
+ </div>
165
+ );
166
+ }
@@ -0,0 +1,221 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Input } from '@/components/ui/input';
4
+ import { Label } from '@/components/ui/label';
5
+ import { Textarea } from '@/components/ui/textarea';
6
+ import { Switch } from '@/components/ui/switch';
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '@/components/ui/select';
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ DialogFooter,
20
+ } from '@/components/ui/dialog';
21
+ import { Save, X } from 'lucide-react';
22
+ import { FieldMeta, ModelType } from '@/admin/types';
23
+
24
+ interface ModelFormProps {
25
+ model: ModelType;
26
+ initialData?: Record<string, unknown>;
27
+ open: boolean;
28
+ onClose: () => void;
29
+ onSave: (data: Record<string, unknown>) => void;
30
+ }
31
+
32
+ export function ModelForm({ model, initialData, open, onClose, onSave }: ModelFormProps) {
33
+ const [formData, setFormData] = useState<Record<string, unknown>>(
34
+ initialData || model.fields.reduce((acc, field) => {
35
+ acc[field.name] = field.default ?? (field.type === 'boolean' ? false : '');
36
+ return acc;
37
+ }, {} as Record<string, unknown>)
38
+ );
39
+
40
+ const handleChange = (field: string, value: unknown) => {
41
+ setFormData(prev => ({ ...prev, [field]: value }));
42
+ };
43
+
44
+ const handleSubmit = (e: React.FormEvent) => {
45
+ e.preventDefault();
46
+ onSave(formData);
47
+ onClose();
48
+ };
49
+
50
+ useEffect(() => {
51
+ if (!open) return;
52
+ if (!model) return;
53
+
54
+ if (initialData) {
55
+ setFormData(initialData);
56
+ return;
57
+ }
58
+
59
+ const emptyData: Record<string, unknown> = {};
60
+ for (const field of model.fields) {
61
+ if (field.type === "primary") continue;
62
+
63
+ if (field.default !== undefined) {
64
+ emptyData[field.name] = field.default;
65
+ } else if (field.type === "boolean") {
66
+ emptyData[field.name] = false;
67
+ } else {
68
+ emptyData[field.name] = "";
69
+ }
70
+ }
71
+
72
+ setFormData(emptyData);
73
+ }, [model, initialData, open]); // 🔑 open is REQUIRED
74
+
75
+
76
+ const renderField = (field: FieldMeta) => {
77
+ const value = formData[field.name];
78
+ switch (field.type) {
79
+ case 'boolean':
80
+ return (
81
+ <div className="flex items-center justify-between">
82
+ <Label htmlFor={field.name} className="text-sm text-muted-foreground">
83
+ {field.label}
84
+ </Label>
85
+ <Switch
86
+ id={field.name}
87
+ checked={value as boolean}
88
+ onCheckedChange={(checked) => handleChange(field.name, checked)}
89
+ />
90
+ </div>
91
+ );
92
+
93
+ case 'text':
94
+ return (
95
+ <div className="space-y-2">
96
+ <Label htmlFor={field.name} className="text-sm text-muted-foreground">
97
+ {field.label}
98
+ {field.required && <span className="text-destructive ml-1">*</span>}
99
+ </Label>
100
+ <Textarea
101
+ id={field.name}
102
+ value={value as string}
103
+ onChange={(e) => handleChange(field.name, e.target.value)}
104
+ className="bg-secondary border-border min-h-[100px]"
105
+ required={field.required}
106
+ />
107
+ </div>
108
+ );
109
+
110
+ case 'select':
111
+ return (
112
+ <div className="space-y-2">
113
+ <Label htmlFor={field.name} className="text-sm text-muted-foreground">
114
+ {field.label}
115
+ {field.required && <span className="text-destructive ml-1">*</span>}
116
+ </Label>
117
+ <Select
118
+ value={value as string}
119
+ onValueChange={(val) => handleChange(field.name, val)}
120
+ >
121
+ <SelectTrigger className="bg-secondary border-border">
122
+ <SelectValue placeholder={`Select ${field.label.toLowerCase()}`} />
123
+ </SelectTrigger>
124
+ <SelectContent>
125
+ {field.options?.map((option) => (
126
+ <SelectItem key={option} value={option}>
127
+ {option}
128
+ </SelectItem>
129
+ ))}
130
+ </SelectContent>
131
+ </Select>
132
+ </div>
133
+ );
134
+
135
+ case 'number':
136
+ return (
137
+ <div className="space-y-2">
138
+ <Label htmlFor={field.name} className="text-sm text-muted-foreground">
139
+ {field.label}
140
+ {field.required && <span className="text-destructive ml-1">*</span>}
141
+ </Label>
142
+ <Input
143
+ id={field.name}
144
+ type="number"
145
+ value={value as number}
146
+ onChange={(e) => handleChange(field.name, Number(e.target.value))}
147
+ className="bg-secondary border-border font-mono"
148
+ required={field.required}
149
+ />
150
+ </div>
151
+ );
152
+
153
+ case 'datetime':
154
+ return (
155
+ <div className="space-y-2">
156
+ <Label htmlFor={field.name} className="text-sm text-muted-foreground">
157
+ {field.label}
158
+ {field.required && <span className="text-destructive ml-1">*</span>}
159
+ </Label>
160
+ <Input
161
+ id={field.name}
162
+ type="date"
163
+ value={value as string}
164
+ onChange={(e) => handleChange(field.name, e.target.value)}
165
+ className="bg-secondary border-border font-mono"
166
+ required={field.required}
167
+ />
168
+ </div>
169
+ );
170
+
171
+ default:
172
+ return (
173
+ <div className="space-y-2">
174
+ <Label htmlFor={field.name} className="text-sm text-muted-foreground">
175
+ {field.label}
176
+ {field.required && <span className="text-destructive ml-1">*</span>}
177
+ </Label>
178
+ <Input
179
+ id={field.name}
180
+ type={field.type === 'email' ? 'email' : 'text'}
181
+ value={value as string}
182
+ onChange={(e) => handleChange(field.name, e.target.value)}
183
+ className="bg-secondary border-border"
184
+ required={field.required}
185
+ maxLength={field.maxLength}
186
+ />
187
+ </div>
188
+ );
189
+ }
190
+ };
191
+ return (
192
+ <Dialog open={open} onOpenChange={onClose}>
193
+ <DialogContent className="bg-card border-border max-w-lg">
194
+ <DialogHeader>
195
+ <DialogTitle className="flex items-center gap-2">
196
+ {initialData ? 'Edit' : 'Add'} {model.displayName}
197
+ </DialogTitle>
198
+ </DialogHeader>
199
+
200
+ <form onSubmit={handleSubmit} className="space-y-4">
201
+ {model.fields.filter(x=>x.options?.autoNowAdd !== true && !x.readonly).filter(x=>!x.primaryKey).map((field) => (
202
+ <div key={field.name}>
203
+ {renderField(field)}
204
+ </div>
205
+ ))}
206
+
207
+ <DialogFooter className="gap-2">
208
+ <Button type="button" variant="outline" onClick={onClose} className="gap-2">
209
+ <X className="w-4 h-4" />
210
+ Cancel
211
+ </Button>
212
+ <Button type="submit" className="gap-2">
213
+ <Save className="w-4 h-4" />
214
+ Save
215
+ </Button>
216
+ </DialogFooter>
217
+ </form>
218
+ </DialogContent>
219
+ </Dialog>
220
+ );
221
+ }
@@ -0,0 +1,28 @@
1
+ import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
2
+ import { forwardRef } from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
6
+ className?: string;
7
+ activeClassName?: string;
8
+ pendingClassName?: string;
9
+ }
10
+
11
+ const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
12
+ ({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
13
+ return (
14
+ <RouterNavLink
15
+ ref={ref}
16
+ to={to}
17
+ className={({ isActive, isPending }) =>
18
+ cn(className, isActive && activeClassName, isPending && pendingClassName)
19
+ }
20
+ {...props}
21
+ />
22
+ );
23
+ },
24
+ );
25
+
26
+ NavLink.displayName = "NavLink";
27
+
28
+ export { NavLink };
@@ -0,0 +1,32 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ interface StatCardProps {
4
+ title: string;
5
+ value: string | number;
6
+ icon: ReactNode;
7
+ trend?: {
8
+ value: number;
9
+ isPositive: boolean;
10
+ };
11
+ }
12
+
13
+ export function StatCard({ title, value, icon, trend }: StatCardProps) {
14
+ return (
15
+ <div className="stat-card animate-fade-in">
16
+ <div className="flex items-start justify-between">
17
+ <div>
18
+ <p className="text-sm text-muted-foreground">{title}</p>
19
+ <p className="text-2xl font-semibold mt-1">{value}</p>
20
+ {trend && (
21
+ <p className={`text-xs mt-2 ${trend.isPositive ? 'text-success' : 'text-destructive'}`}>
22
+ {trend.isPositive ? '↑' : '↓'} {Math.abs(trend.value)}% from last week
23
+ </p>
24
+ )}
25
+ </div>
26
+ <div className="p-2 bg-primary/10 rounded-lg text-primary">
27
+ {icon}
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronDown } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Accordion = AccordionPrimitive.Root;
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
14
+ ));
15
+ AccordionItem.displayName = "AccordionItem";
16
+
17
+ const AccordionTrigger = React.forwardRef<
18
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
19
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
20
+ >(({ className, children, ...props }, ref) => (
21
+ <AccordionPrimitive.Header className="flex">
22
+ <AccordionPrimitive.Trigger
23
+ ref={ref}
24
+ className={cn(
25
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
26
+ className,
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
32
+ </AccordionPrimitive.Trigger>
33
+ </AccordionPrimitive.Header>
34
+ ));
35
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
36
+
37
+ const AccordionContent = React.forwardRef<
38
+ React.ElementRef<typeof AccordionPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <AccordionPrimitive.Content
42
+ ref={ref}
43
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
44
+ {...props}
45
+ >
46
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
47
+ </AccordionPrimitive.Content>
48
+ ));
49
+
50
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
51
+
52
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };