@pattern-stack/frontend-patterns 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.es.js +1 -1
- package/dist/index.js +1 -0
- package/package.json +5 -3
- package/src/App.css +42 -0
- package/src/App.tsx +54 -0
- package/src/__tests__/README.md +221 -0
- package/src/__tests__/atoms/hooks/simple-hooks.test.ts +44 -0
- package/src/__tests__/atoms/ui/button.test.tsx +68 -0
- package/src/__tests__/atoms/utils/simple.test.ts +18 -0
- package/src/__tests__/atoms/utils/utils.test.ts +77 -0
- package/src/__tests__/features/auth/simple-auth.test.tsx +40 -0
- package/src/__tests__/molecules/layout/simple-layout.test.tsx +81 -0
- package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +167 -0
- package/src/__tests__/setup.ts +51 -0
- package/src/__tests__/utils.tsx +123 -0
- package/src/atoms/composed/Accordion/Accordion.tsx +271 -0
- package/src/atoms/composed/Accordion/index.ts +1 -0
- package/src/atoms/composed/Alert/Alert.tsx +132 -0
- package/src/atoms/composed/Alert/index.ts +1 -0
- package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +83 -0
- package/src/atoms/composed/Breadcrumb/index.ts +1 -0
- package/src/atoms/composed/Chart/Chart.tsx +425 -0
- package/src/atoms/composed/Chart/index.ts +2 -0
- package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +72 -0
- package/src/atoms/composed/ColorSwatch/index.ts +1 -0
- package/src/atoms/composed/DarkModeToggle.tsx +66 -0
- package/src/atoms/composed/DataBadge/DataBadge.tsx +81 -0
- package/src/atoms/composed/DataBadge/index.ts +1 -0
- package/src/atoms/composed/DataTable/DataTable.tsx +394 -0
- package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +41 -0
- package/src/atoms/composed/DataTable/index.ts +2 -0
- package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +611 -0
- package/src/atoms/composed/DateTimePicker/index.ts +2 -0
- package/src/atoms/composed/DetailedCard/DetailedCard.tsx +181 -0
- package/src/atoms/composed/DetailedCard/index.ts +2 -0
- package/src/atoms/composed/EmptyState/EmptyState.tsx +90 -0
- package/src/atoms/composed/EmptyState/index.ts +1 -0
- package/src/atoms/composed/FileUpload/FileUpload.tsx +477 -0
- package/src/atoms/composed/FileUpload/index.ts +2 -0
- package/src/atoms/composed/FormField/FormField.tsx +92 -0
- package/src/atoms/composed/FormField/index.ts +1 -0
- package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +37 -0
- package/src/atoms/composed/GlobalSearch/index.ts +1 -0
- package/src/atoms/composed/IconBadge/IconBadge.tsx +95 -0
- package/src/atoms/composed/IconBadge/index.ts +2 -0
- package/src/atoms/composed/Modal/Modal.tsx +223 -0
- package/src/atoms/composed/Modal/index.ts +2 -0
- package/src/atoms/composed/PaletteSwitcher.tsx +386 -0
- package/src/atoms/composed/ProgressBar/ProgressBar.tsx +116 -0
- package/src/atoms/composed/ProgressBar/index.ts +1 -0
- package/src/atoms/composed/StatCard/StatCard.tsx +219 -0
- package/src/atoms/composed/StatCard/index.ts +1 -0
- package/src/atoms/composed/StyleGuide.tsx +717 -0
- package/src/atoms/composed/Toast/Toast.tsx +219 -0
- package/src/atoms/composed/Toast/index.ts +1 -0
- package/src/atoms/composed/Tooltip/Tooltip.tsx +213 -0
- package/src/atoms/composed/Tooltip/index.ts +1 -0
- package/src/atoms/composed/UserAvatar/UserAvatar.tsx +139 -0
- package/src/atoms/composed/UserAvatar/index.ts +1 -0
- package/src/atoms/composed/UserMenu/UserMenu.tsx +16 -0
- package/src/atoms/composed/UserMenu/index.ts +1 -0
- package/src/atoms/composed/index.ts +29 -0
- package/src/atoms/hooks/useApi.ts +80 -0
- package/src/atoms/hooks/useHealth.ts +17 -0
- package/src/atoms/index.ts +13 -0
- package/src/atoms/services/api/client.ts +134 -0
- package/src/atoms/services/auth-service.ts +248 -0
- package/src/atoms/services/health.ts +15 -0
- package/src/atoms/services/index.ts +3 -0
- package/src/atoms/shared/config/constants.ts +17 -0
- package/src/atoms/shared/config/dashboard-sizes.ts +111 -0
- package/src/atoms/shared/config/environment.ts +10 -0
- package/src/atoms/shared/index.ts +4 -0
- package/src/atoms/shared/styles/color-palettes.css +566 -0
- package/src/atoms/types/auth.ts +62 -0
- package/src/atoms/types/generated.ts +1469 -0
- package/src/atoms/types/index.ts +4 -0
- package/src/atoms/types/loading.ts +28 -0
- package/src/atoms/ui/Badge.tsx +30 -0
- package/src/atoms/ui/ErrorBoundary.tsx +59 -0
- package/src/atoms/ui/Select.tsx +53 -0
- package/src/atoms/ui/Switch.tsx +42 -0
- package/src/atoms/ui/Tabs.tsx +118 -0
- package/src/atoms/ui/avatar.tsx +48 -0
- package/src/atoms/ui/button.tsx +70 -0
- package/src/atoms/ui/card.tsx +76 -0
- package/src/atoms/ui/dropdown-menu.tsx +199 -0
- package/src/atoms/ui/index.ts +39 -0
- package/src/atoms/ui/input.tsx +23 -0
- package/src/atoms/ui/label.tsx +23 -0
- package/src/atoms/ui/skeleton.tsx +13 -0
- package/src/atoms/ui/spinner.tsx +49 -0
- package/src/atoms/ui/table.tsx +116 -0
- package/src/atoms/utils/animations.ts +135 -0
- package/src/atoms/utils/tooltip-helpers.ts +140 -0
- package/src/atoms/utils/utils.ts +9 -0
- package/src/features/auth/components/LoginForm.tsx +168 -0
- package/src/features/auth/components/LogoutButton.tsx +19 -0
- package/src/features/auth/components/ProtectedRoute.tsx +60 -0
- package/src/features/auth/components/index.ts +4 -0
- package/src/features/auth/hooks/index.ts +2 -0
- package/src/features/auth/hooks/useAuth.tsx +205 -0
- package/src/features/auth/hooks/usePermissions.ts +35 -0
- package/src/features/auth/index.ts +2 -0
- package/src/features/index.ts +2 -0
- package/src/index.css +704 -0
- package/src/index.ts +13 -0
- package/src/main.tsx +48 -0
- package/src/molecules/.gitkeep +0 -0
- package/src/molecules/forms/FormGroup.tsx +75 -0
- package/src/molecules/forms/SearchInput.tsx +259 -0
- package/src/molecules/forms/index.ts +4 -0
- package/src/molecules/index.ts +4 -0
- package/src/molecules/layout/AppHeader/AppHeader.tsx +42 -0
- package/src/molecules/layout/AppHeader/index.ts +1 -0
- package/src/molecules/layout/AppLayout.tsx +29 -0
- package/src/molecules/layout/PageTemplate.tsx +87 -0
- package/src/molecules/layout/SectionHeader/SectionHeader.tsx +87 -0
- package/src/molecules/layout/SectionHeader/index.ts +1 -0
- package/src/molecules/layout/ShowcaseSection.tsx +57 -0
- package/src/molecules/layout/Sidebar.tsx +144 -0
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +99 -0
- package/src/molecules/layout/SidebarButton/index.ts +1 -0
- package/src/molecules/layout/SidebarContext.tsx +31 -0
- package/src/molecules/layout/index.ts +7 -0
- package/src/molecules/navigation/NavMenu.tsx +188 -0
- package/src/molecules/navigation/Pagination.tsx +172 -0
- package/src/molecules/navigation/index.ts +4 -0
- package/src/organisms/index.ts +5 -0
- package/src/organisms/showcase/ComponentShowcasePage.tsx +2496 -0
- package/src/organisms/showcase/index.ts +1 -0
- package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +242 -0
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +171 -0
- package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +385 -0
- package/src/pages/AdminShowcase/index.tsx +3 -0
- package/src/pages/ComponentShowcase/BadgesShowcase.tsx +188 -0
- package/src/pages/ComponentShowcase/CardsShowcase.tsx +392 -0
- package/src/pages/ComponentShowcase/PalettesShowcase.tsx +207 -0
- package/src/pages/ComponentShowcase/StatesShowcase.tsx +485 -0
- package/src/pages/ComponentShowcase/TablesShowcase.tsx +134 -0
- package/src/pages/ComponentShowcase/TypographyShowcase.tsx +255 -0
- package/src/pages/ComponentShowcase/index.tsx +188 -0
- package/src/pages/index.ts +2 -0
- package/src/templates/AuthTemplate.tsx +216 -0
- package/src/templates/ComponentShowcaseTemplate.tsx +173 -0
- package/src/templates/DashboardTemplate.tsx +232 -0
- package/src/templates/DataTemplate.tsx +319 -0
- package/src/templates/admin/AdminCRUDTemplate.tsx +630 -0
- package/src/templates/admin/AdminDashboardTemplate.tsx +351 -0
- package/src/templates/admin/AdminDetailTemplate.tsx +563 -0
- package/src/templates/admin/index.ts +29 -0
- package/src/templates/factory.tsx +169 -0
- package/src/templates/index.ts +37 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { cn } from '../../atoms/utils/utils';
|
|
3
|
+
import { SectionHeader } from '../../molecules/layout/SectionHeader';
|
|
4
|
+
import { Card } from '../../atoms/ui/card';
|
|
5
|
+
import { Button } from '../../atoms/ui/button';
|
|
6
|
+
import { DataBadge } from '../../atoms/composed/DataBadge';
|
|
7
|
+
import { UserAvatar } from '../../atoms/composed/UserAvatar';
|
|
8
|
+
import { DataTable, type Column } from '../../atoms/composed/DataTable';
|
|
9
|
+
import {
|
|
10
|
+
ArrowLeft,
|
|
11
|
+
Edit,
|
|
12
|
+
Copy,
|
|
13
|
+
ExternalLink,
|
|
14
|
+
History,
|
|
15
|
+
Activity,
|
|
16
|
+
FileText,
|
|
17
|
+
Save,
|
|
18
|
+
X
|
|
19
|
+
} from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
export interface TabConfig {
|
|
22
|
+
/** Tab ID */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Tab label */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Tab icon */
|
|
27
|
+
icon?: React.ReactNode;
|
|
28
|
+
/** Tab content */
|
|
29
|
+
content: React.ReactNode;
|
|
30
|
+
/** Whether tab is disabled */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Badge text for tab */
|
|
33
|
+
badge?: string | number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ActionConfig {
|
|
37
|
+
/** Action ID */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Action label */
|
|
40
|
+
label: string;
|
|
41
|
+
/** Action icon */
|
|
42
|
+
icon?: React.ReactNode;
|
|
43
|
+
/** Action variant */
|
|
44
|
+
variant?: 'default' | 'outline' | 'destructive' | 'secondary';
|
|
45
|
+
/** Action handler */
|
|
46
|
+
onClick: () => void;
|
|
47
|
+
/** Whether action is disabled */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
/** Action permission required */
|
|
50
|
+
permission?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface FieldSection {
|
|
54
|
+
/** Section title */
|
|
55
|
+
title: string;
|
|
56
|
+
/** Section description */
|
|
57
|
+
description?: string;
|
|
58
|
+
/** Section fields */
|
|
59
|
+
fields: DetailField[];
|
|
60
|
+
/** Section category for styling */
|
|
61
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
62
|
+
/** Whether section is collapsible */
|
|
63
|
+
collapsible?: boolean;
|
|
64
|
+
/** Whether section is initially collapsed */
|
|
65
|
+
defaultCollapsed?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DetailField {
|
|
69
|
+
/** Field key */
|
|
70
|
+
key: string;
|
|
71
|
+
/** Field label */
|
|
72
|
+
label: string;
|
|
73
|
+
/** Field type for rendering */
|
|
74
|
+
type?: 'text' | 'email' | 'phone' | 'url' | 'date' | 'datetime' | 'boolean' | 'badge' | 'avatar' | 'json' | 'custom';
|
|
75
|
+
/** Custom render function */
|
|
76
|
+
render?: (value: unknown, data: Record<string, unknown>) => React.ReactNode;
|
|
77
|
+
/** Whether field is editable inline */
|
|
78
|
+
editable?: boolean;
|
|
79
|
+
/** Field validation for editing */
|
|
80
|
+
validation?: Record<string, unknown>;
|
|
81
|
+
/** Copy to clipboard button */
|
|
82
|
+
copyable?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RelatedData {
|
|
86
|
+
/** Related data title */
|
|
87
|
+
title: string;
|
|
88
|
+
/** Related data description */
|
|
89
|
+
description?: string;
|
|
90
|
+
/** Related data items */
|
|
91
|
+
data: Record<string, unknown>[];
|
|
92
|
+
/** Columns for related data table */
|
|
93
|
+
columns: Column<Record<string, unknown>>[];
|
|
94
|
+
/** Actions for related data */
|
|
95
|
+
actions?: ActionConfig[];
|
|
96
|
+
/** Whether to show pagination */
|
|
97
|
+
showPagination?: boolean;
|
|
98
|
+
/** Category for styling */
|
|
99
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AuditEntry {
|
|
103
|
+
/** Entry ID */
|
|
104
|
+
id: string;
|
|
105
|
+
/** Action performed */
|
|
106
|
+
action: string;
|
|
107
|
+
/** User who performed action */
|
|
108
|
+
user: string;
|
|
109
|
+
/** Timestamp */
|
|
110
|
+
timestamp: Date;
|
|
111
|
+
/** Fields changed */
|
|
112
|
+
changes?: Array<{
|
|
113
|
+
field: string;
|
|
114
|
+
oldValue?: unknown;
|
|
115
|
+
newValue?: unknown;
|
|
116
|
+
}>;
|
|
117
|
+
/** Additional metadata */
|
|
118
|
+
metadata?: Record<string, unknown>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface AdminDetailTemplateProps {
|
|
122
|
+
/** Resource title */
|
|
123
|
+
title: string;
|
|
124
|
+
/** Resource subtitle */
|
|
125
|
+
subtitle?: string;
|
|
126
|
+
/** Resource type */
|
|
127
|
+
resourceType: string;
|
|
128
|
+
/** Main resource data */
|
|
129
|
+
data: Record<string, unknown>;
|
|
130
|
+
/** Field sections to display */
|
|
131
|
+
sections: FieldSection[];
|
|
132
|
+
/** Tab configuration */
|
|
133
|
+
tabs?: TabConfig[];
|
|
134
|
+
/** Available actions */
|
|
135
|
+
actions?: ActionConfig[];
|
|
136
|
+
/** Related data sections */
|
|
137
|
+
relatedData?: RelatedData[];
|
|
138
|
+
/** Audit trail */
|
|
139
|
+
auditTrail?: AuditEntry[];
|
|
140
|
+
/** Back navigation handler */
|
|
141
|
+
onBack?: () => void;
|
|
142
|
+
/** Data update handler */
|
|
143
|
+
onUpdate?: (field: string, value: unknown) => Promise<void>;
|
|
144
|
+
/** Whether data is loading */
|
|
145
|
+
isLoading?: boolean;
|
|
146
|
+
/** Additional CSS classes */
|
|
147
|
+
className?: string;
|
|
148
|
+
/** Category for styling */
|
|
149
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
150
|
+
/** Custom sidebar content */
|
|
151
|
+
sidebar?: React.ReactNode;
|
|
152
|
+
/** Whether to show audit trail */
|
|
153
|
+
showAuditTrail?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const AdminDetailTemplate: React.FC<AdminDetailTemplateProps> = ({
|
|
157
|
+
title,
|
|
158
|
+
subtitle,
|
|
159
|
+
resourceType,
|
|
160
|
+
data,
|
|
161
|
+
sections,
|
|
162
|
+
tabs = [],
|
|
163
|
+
actions = [],
|
|
164
|
+
relatedData = [],
|
|
165
|
+
auditTrail = [],
|
|
166
|
+
onBack,
|
|
167
|
+
onUpdate,
|
|
168
|
+
isLoading = false,
|
|
169
|
+
className,
|
|
170
|
+
category = 1,
|
|
171
|
+
sidebar,
|
|
172
|
+
showAuditTrail = true
|
|
173
|
+
}) => {
|
|
174
|
+
const [activeTab, setActiveTab] = useState(tabs[0]?.id || 'overview');
|
|
175
|
+
const [editingField, setEditingField] = useState<string | null>(null);
|
|
176
|
+
const [editValue, setEditValue] = useState<unknown>('');
|
|
177
|
+
|
|
178
|
+
const handleEdit = (field: DetailField, currentValue: unknown) => {
|
|
179
|
+
setEditingField(field.key);
|
|
180
|
+
setEditValue(currentValue);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleSaveEdit = async () => {
|
|
184
|
+
if (editingField && onUpdate) {
|
|
185
|
+
try {
|
|
186
|
+
await onUpdate(editingField, editValue);
|
|
187
|
+
setEditingField(null);
|
|
188
|
+
setEditValue('');
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('Update failed:', error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleCancelEdit = () => {
|
|
196
|
+
setEditingField(null);
|
|
197
|
+
setEditValue('');
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const copyToClipboard = (value: string) => {
|
|
201
|
+
navigator.clipboard.writeText(value);
|
|
202
|
+
// You could show a toast notification here
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const renderFieldValue = (field: DetailField, value: unknown) => {
|
|
206
|
+
// If currently editing this field
|
|
207
|
+
if (editingField === field.key) {
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex items-center gap-2">
|
|
210
|
+
<input
|
|
211
|
+
type="text"
|
|
212
|
+
value={String(editValue)}
|
|
213
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
214
|
+
className="flex-1 px-2 py-1 text-sm border border-input rounded"
|
|
215
|
+
autoFocus
|
|
216
|
+
/>
|
|
217
|
+
<Button size="sm" onClick={handleSaveEdit}>
|
|
218
|
+
<Save className="w-3 h-3" />
|
|
219
|
+
</Button>
|
|
220
|
+
<Button size="sm" variant="outline" onClick={handleCancelEdit}>
|
|
221
|
+
<X className="w-3 h-3" />
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Custom render function
|
|
228
|
+
if (field.render) {
|
|
229
|
+
return field.render(value, data);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Default rendering based on type
|
|
233
|
+
const stringValue = String(value || '');
|
|
234
|
+
let displayValue: React.ReactNode;
|
|
235
|
+
|
|
236
|
+
switch (field.type) {
|
|
237
|
+
case 'email':
|
|
238
|
+
displayValue = value ? (
|
|
239
|
+
<a href={`mailto:${stringValue}`} className="text-category-1 hover:underline">
|
|
240
|
+
{stringValue}
|
|
241
|
+
</a>
|
|
242
|
+
) : '';
|
|
243
|
+
break;
|
|
244
|
+
case 'phone':
|
|
245
|
+
displayValue = value ? (
|
|
246
|
+
<a href={`tel:${stringValue}`} className="text-category-2 hover:underline">
|
|
247
|
+
{stringValue}
|
|
248
|
+
</a>
|
|
249
|
+
) : '';
|
|
250
|
+
break;
|
|
251
|
+
case 'url':
|
|
252
|
+
displayValue = value ? (
|
|
253
|
+
<a href={stringValue} target="_blank" rel="noopener noreferrer" className="text-category-3 hover:underline inline-flex items-center">
|
|
254
|
+
{stringValue} <ExternalLink className="w-3 h-3 ml-1" />
|
|
255
|
+
</a>
|
|
256
|
+
) : '';
|
|
257
|
+
break;
|
|
258
|
+
case 'date':
|
|
259
|
+
displayValue = value ? new Date(stringValue).toLocaleDateString() : '';
|
|
260
|
+
break;
|
|
261
|
+
case 'datetime':
|
|
262
|
+
displayValue = value ? new Date(stringValue).toLocaleString() : '';
|
|
263
|
+
break;
|
|
264
|
+
case 'boolean':
|
|
265
|
+
displayValue = (
|
|
266
|
+
<DataBadge
|
|
267
|
+
variant="status"
|
|
268
|
+
status={value ? 'success' : 'neutral'}
|
|
269
|
+
size="sm"
|
|
270
|
+
>
|
|
271
|
+
{value ? 'Yes' : 'No'}
|
|
272
|
+
</DataBadge>
|
|
273
|
+
);
|
|
274
|
+
break;
|
|
275
|
+
case 'badge':
|
|
276
|
+
displayValue = <DataBadge variant="category" category={1} size="sm">{stringValue}</DataBadge>;
|
|
277
|
+
break;
|
|
278
|
+
case 'avatar':
|
|
279
|
+
displayValue = <UserAvatar />;
|
|
280
|
+
break;
|
|
281
|
+
case 'json':
|
|
282
|
+
displayValue = (
|
|
283
|
+
<pre className="text-xs bg-muted p-2 rounded-md overflow-auto max-w-md">
|
|
284
|
+
{JSON.stringify(value, null, 2)}
|
|
285
|
+
</pre>
|
|
286
|
+
);
|
|
287
|
+
break;
|
|
288
|
+
default:
|
|
289
|
+
displayValue = stringValue;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div className="flex items-center gap-2">
|
|
295
|
+
<span>{displayValue as React.ReactNode}</span>
|
|
296
|
+
{field.copyable && value != null && (
|
|
297
|
+
<Button
|
|
298
|
+
size="sm"
|
|
299
|
+
variant="ghost"
|
|
300
|
+
onClick={() => copyToClipboard(stringValue)}
|
|
301
|
+
className="h-6 w-6 p-0"
|
|
302
|
+
>
|
|
303
|
+
<Copy className="w-3 h-3" />
|
|
304
|
+
</Button>
|
|
305
|
+
)}
|
|
306
|
+
{field.editable && onUpdate && (
|
|
307
|
+
<Button
|
|
308
|
+
size="sm"
|
|
309
|
+
variant="ghost"
|
|
310
|
+
onClick={() => handleEdit(field, value)}
|
|
311
|
+
className="h-6 w-6 p-0"
|
|
312
|
+
>
|
|
313
|
+
<Edit className="w-3 h-3" />
|
|
314
|
+
</Button>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const renderSection = (section: FieldSection) => (
|
|
321
|
+
<Card key={section.title} className="p-6">
|
|
322
|
+
<div className="mb-4">
|
|
323
|
+
<h3 className="text-lg font-semibold text-foreground">{section.title}</h3>
|
|
324
|
+
{section.description && (
|
|
325
|
+
<p className="text-sm text-muted-foreground mt-1">{section.description}</p>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
329
|
+
{section.fields.map((field) => (
|
|
330
|
+
<div key={field.key} className="space-y-1">
|
|
331
|
+
<label className="text-sm font-medium text-muted-foreground">
|
|
332
|
+
{field.label}
|
|
333
|
+
</label>
|
|
334
|
+
<div className="text-sm text-foreground">
|
|
335
|
+
{renderFieldValue(field, data[field.key])}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
))}
|
|
339
|
+
</div>
|
|
340
|
+
</Card>
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const renderAuditTrail = () => (
|
|
344
|
+
<Card className="p-6">
|
|
345
|
+
<div className="mb-4">
|
|
346
|
+
<h3 className="text-lg font-semibold flex items-center">
|
|
347
|
+
<History className="w-5 h-5 mr-2" />
|
|
348
|
+
Audit Trail
|
|
349
|
+
</h3>
|
|
350
|
+
</div>
|
|
351
|
+
<div className="space-y-4">
|
|
352
|
+
{auditTrail.map((entry) => (
|
|
353
|
+
<div key={entry.id} className="flex items-start space-x-3 pb-4 border-b border-border last:border-b-0">
|
|
354
|
+
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
|
|
355
|
+
<Activity className="w-4 h-4 text-muted-foreground" />
|
|
356
|
+
</div>
|
|
357
|
+
<div className="flex-1 min-w-0">
|
|
358
|
+
<div className="flex items-center justify-between">
|
|
359
|
+
<p className="text-sm font-medium text-foreground">
|
|
360
|
+
{entry.action}
|
|
361
|
+
</p>
|
|
362
|
+
<span className="text-xs text-muted-foreground">
|
|
363
|
+
{entry.timestamp.toLocaleString()}
|
|
364
|
+
</span>
|
|
365
|
+
</div>
|
|
366
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
367
|
+
by {entry.user}
|
|
368
|
+
</p>
|
|
369
|
+
{entry.changes && entry.changes.length > 0 && (
|
|
370
|
+
<div className="mt-2 space-y-1">
|
|
371
|
+
{entry.changes.map((change, index) => (
|
|
372
|
+
<div key={index} className="text-xs bg-muted p-2 rounded-md">
|
|
373
|
+
<span className="font-medium">{change.field}:</span>{' '}
|
|
374
|
+
<span className="text-category-8">{String(change.oldValue || 'null')}</span>{' '}
|
|
375
|
+
→{' '}
|
|
376
|
+
<span className="text-category-2">{String(change.newValue || 'null')}</span>
|
|
377
|
+
</div>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
</Card>
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const renderRelatedData = (related: RelatedData) => (
|
|
389
|
+
<Card key={related.title} className="p-6">
|
|
390
|
+
<div className="mb-4 flex items-center justify-between">
|
|
391
|
+
<div>
|
|
392
|
+
<h3 className="text-lg font-semibold">{related.title}</h3>
|
|
393
|
+
{related.description && (
|
|
394
|
+
<p className="text-sm text-muted-foreground mt-1">{related.description}</p>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
{related.actions && related.actions.length > 0 && (
|
|
398
|
+
<div className="flex gap-2">
|
|
399
|
+
{related.actions.map((action) => (
|
|
400
|
+
<Button
|
|
401
|
+
key={action.id}
|
|
402
|
+
variant={action.variant || 'outline'}
|
|
403
|
+
size="sm"
|
|
404
|
+
onClick={action.onClick}
|
|
405
|
+
disabled={action.disabled}
|
|
406
|
+
>
|
|
407
|
+
{action.icon}
|
|
408
|
+
{action.label}
|
|
409
|
+
</Button>
|
|
410
|
+
))}
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
<DataTable
|
|
415
|
+
data={related.data}
|
|
416
|
+
columns={related.columns}
|
|
417
|
+
isLoading={isLoading}
|
|
418
|
+
hover
|
|
419
|
+
/>
|
|
420
|
+
</Card>
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// Build tabs
|
|
424
|
+
const allTabs = [
|
|
425
|
+
{
|
|
426
|
+
id: 'overview',
|
|
427
|
+
label: 'Overview',
|
|
428
|
+
icon: <FileText className="w-4 h-4" />,
|
|
429
|
+
content: (
|
|
430
|
+
<div className="space-y-6">
|
|
431
|
+
{sections.map(renderSection)}
|
|
432
|
+
</div>
|
|
433
|
+
),
|
|
434
|
+
},
|
|
435
|
+
...tabs,
|
|
436
|
+
...(relatedData.length > 0 ? [{
|
|
437
|
+
id: 'related',
|
|
438
|
+
label: 'Related Data',
|
|
439
|
+
icon: <Activity className="w-4 h-4" />,
|
|
440
|
+
content: (
|
|
441
|
+
<div className="space-y-6">
|
|
442
|
+
{relatedData.map(renderRelatedData)}
|
|
443
|
+
</div>
|
|
444
|
+
),
|
|
445
|
+
}] : []),
|
|
446
|
+
...(showAuditTrail && auditTrail.length > 0 ? [{
|
|
447
|
+
id: 'audit',
|
|
448
|
+
label: 'Audit Trail',
|
|
449
|
+
icon: <History className="w-4 h-4" />,
|
|
450
|
+
badge: auditTrail.length,
|
|
451
|
+
content: renderAuditTrail(),
|
|
452
|
+
}] : []),
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<div className={cn('flex flex-col min-h-0 flex-1', className)}>
|
|
457
|
+
{/* Header Section */}
|
|
458
|
+
<div className="flex-shrink-0 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
459
|
+
<div className="container mx-auto px-6 py-6">
|
|
460
|
+
<div className="flex items-start justify-between">
|
|
461
|
+
<div className="flex-1 min-w-0">
|
|
462
|
+
{onBack && (
|
|
463
|
+
<Button
|
|
464
|
+
variant="ghost"
|
|
465
|
+
onClick={onBack}
|
|
466
|
+
className="mb-4 -ml-3"
|
|
467
|
+
>
|
|
468
|
+
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
469
|
+
Back to {resourceType}
|
|
470
|
+
</Button>
|
|
471
|
+
)}
|
|
472
|
+
<SectionHeader
|
|
473
|
+
title={title}
|
|
474
|
+
subtitle={subtitle}
|
|
475
|
+
size="lg"
|
|
476
|
+
className="text-left"
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
{actions.length > 0 && (
|
|
481
|
+
<div className="ml-6 flex-shrink-0 flex items-center gap-2">
|
|
482
|
+
{actions.map((action) => (
|
|
483
|
+
<Button
|
|
484
|
+
key={action.id}
|
|
485
|
+
variant={action.variant || 'outline'}
|
|
486
|
+
onClick={action.onClick}
|
|
487
|
+
disabled={action.disabled}
|
|
488
|
+
>
|
|
489
|
+
{action.icon}
|
|
490
|
+
{action.label}
|
|
491
|
+
</Button>
|
|
492
|
+
))}
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
{/* Main Content */}
|
|
500
|
+
<div className="flex-1 min-h-0 overflow-auto">
|
|
501
|
+
<div className="container mx-auto px-6 py-6">
|
|
502
|
+
<div className={cn(
|
|
503
|
+
'grid gap-6',
|
|
504
|
+
sidebar ? 'grid-cols-1 lg:grid-cols-4' : 'grid-cols-1'
|
|
505
|
+
)}>
|
|
506
|
+
|
|
507
|
+
{/* Main Content */}
|
|
508
|
+
<div className={sidebar ? 'lg:col-span-3' : 'col-span-1'}>
|
|
509
|
+
{allTabs.length > 1 ? (
|
|
510
|
+
<div className="space-y-6">
|
|
511
|
+
{/* Tab Navigation */}
|
|
512
|
+
<div className="border-b border-border">
|
|
513
|
+
<nav className="-mb-px flex space-x-8">
|
|
514
|
+
{allTabs.map((tab) => (
|
|
515
|
+
<button
|
|
516
|
+
key={tab.id}
|
|
517
|
+
onClick={() => setActiveTab(tab.id)}
|
|
518
|
+
className={cn(
|
|
519
|
+
'py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap',
|
|
520
|
+
activeTab === tab.id
|
|
521
|
+
? `border-category-${category} text-category-${category}`
|
|
522
|
+
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
|
523
|
+
)}
|
|
524
|
+
>
|
|
525
|
+
<div className="flex items-center gap-2">
|
|
526
|
+
{tab.icon}
|
|
527
|
+
{tab.label}
|
|
528
|
+
{tab.badge && (
|
|
529
|
+
<span className={cn(
|
|
530
|
+
'ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium',
|
|
531
|
+
`bg-category-${category}/20 text-category-${category}`
|
|
532
|
+
)}>
|
|
533
|
+
{tab.badge}
|
|
534
|
+
</span>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
</button>
|
|
538
|
+
))}
|
|
539
|
+
</nav>
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
{/* Tab Content */}
|
|
543
|
+
<div>
|
|
544
|
+
{allTabs.find(tab => tab.id === activeTab)?.content}
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
) : (
|
|
548
|
+
allTabs[0]?.content
|
|
549
|
+
)}
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
{/* Sidebar */}
|
|
553
|
+
{sidebar && (
|
|
554
|
+
<div className="lg:col-span-1">
|
|
555
|
+
{sidebar}
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Admin template exports
|
|
2
|
+
export {
|
|
3
|
+
AdminDashboardTemplate,
|
|
4
|
+
type AdminDashboardTemplateProps,
|
|
5
|
+
type MetricCard,
|
|
6
|
+
type ChartConfig,
|
|
7
|
+
type ActivityItem,
|
|
8
|
+
type AlertItem
|
|
9
|
+
} from './AdminDashboardTemplate';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
AdminCRUDTemplate,
|
|
13
|
+
type AdminCRUDTemplateProps,
|
|
14
|
+
type ResourceSchema,
|
|
15
|
+
type ResourceField,
|
|
16
|
+
type CRUDPermissions,
|
|
17
|
+
type CRUDActions
|
|
18
|
+
} from './AdminCRUDTemplate';
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
AdminDetailTemplate,
|
|
22
|
+
type AdminDetailTemplateProps,
|
|
23
|
+
type TabConfig,
|
|
24
|
+
type ActionConfig,
|
|
25
|
+
type FieldSection,
|
|
26
|
+
type DetailField,
|
|
27
|
+
type RelatedData,
|
|
28
|
+
type AuditEntry
|
|
29
|
+
} from './AdminDetailTemplate';
|