@jmruthers/pace-core 0.5.139 → 0.5.140
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/utils.d.ts +1 -1
- package/dist/utils.js +16 -4
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2 -2
- package/docs/getting-started/examples/basic-auth-app.md +196 -0
- package/docs/getting-started/examples/full-featured-app.md +616 -0
- package/package.json +1 -1
- package/src/utils/performance/bundleAnalysis.ts +17 -3
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# Full-Featured Application
|
|
2
|
+
|
|
3
|
+
A complete meal management application demonstrating all PACE Core features including authentication, RBAC, data tables, forms, and permission enforcement.
|
|
4
|
+
|
|
5
|
+
## 🎯 What This Example Shows
|
|
6
|
+
|
|
7
|
+
- Complete authentication flow with RBAC
|
|
8
|
+
- Multi-tenant organisation support
|
|
9
|
+
- Event management and selection
|
|
10
|
+
- Advanced DataTable with CRUD operations
|
|
11
|
+
- Form validation and error handling
|
|
12
|
+
- Permission enforcement at page and component level
|
|
13
|
+
- Responsive layout with navigation
|
|
14
|
+
- Production-ready patterns
|
|
15
|
+
|
|
16
|
+
## 📁 Project Structure
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/
|
|
20
|
+
├── App.tsx # Main app with all providers
|
|
21
|
+
├── lib/
|
|
22
|
+
│ └── supabase.ts # Supabase client
|
|
23
|
+
├── components/
|
|
24
|
+
│ ├── auth/
|
|
25
|
+
│ │ ├── LoginPage.tsx # Login form
|
|
26
|
+
│ │ └── ProtectedRoute.tsx # Route protection
|
|
27
|
+
│ ├── layout/
|
|
28
|
+
│ │ ├── AppLayout.tsx # Main layout
|
|
29
|
+
│ │ └── Navigation.tsx # Navigation menu
|
|
30
|
+
│ ├── dashboard/
|
|
31
|
+
│ │ └── DashboardPage.tsx # Dashboard with metrics
|
|
32
|
+
│ ├── meals/
|
|
33
|
+
│ │ ├── MealsPage.tsx # Meals list with DataTable
|
|
34
|
+
│ │ ├── MealForm.tsx # Add/edit meal form
|
|
35
|
+
│ │ └── MealModal.tsx # Modal for meal operations
|
|
36
|
+
│ ├── users/
|
|
37
|
+
│ │ ├── UsersPage.tsx # User management
|
|
38
|
+
│ │ └── UserForm.tsx # User creation/editing
|
|
39
|
+
│ └── shared/
|
|
40
|
+
│ ├── LoadingSpinner.tsx # Loading states
|
|
41
|
+
│ └── ErrorBoundary.tsx # Error handling
|
|
42
|
+
├── hooks/
|
|
43
|
+
│ ├── useMeals.ts # Meals data management
|
|
44
|
+
│ └── useUsers.ts # Users data management
|
|
45
|
+
├── types/
|
|
46
|
+
│ ├── meal.ts # Meal type definitions
|
|
47
|
+
│ └── user.ts # User type definitions
|
|
48
|
+
└── utils/
|
|
49
|
+
├── permissions.ts # Permission checking
|
|
50
|
+
└── validation.ts # Form validation schemas
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 🚀 Implementation
|
|
54
|
+
|
|
55
|
+
### 1. App.tsx - Complete Application Setup
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import {
|
|
59
|
+
UnifiedAuthProvider,
|
|
60
|
+
OrganisationProvider,
|
|
61
|
+
EventProvider,
|
|
62
|
+
ErrorBoundary
|
|
63
|
+
} from '@jmruthers/pace-core';
|
|
64
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
65
|
+
import { supabase } from './lib/supabase';
|
|
66
|
+
import { AppRoutes } from './components/AppRoutes';
|
|
67
|
+
|
|
68
|
+
function App() {
|
|
69
|
+
return (
|
|
70
|
+
<ErrorBoundary>
|
|
71
|
+
<UnifiedAuthProvider
|
|
72
|
+
supabaseClient={supabase}
|
|
73
|
+
appName="meal-manager"
|
|
74
|
+
enableRBAC={true}
|
|
75
|
+
requireOrganisationContext={true}
|
|
76
|
+
persistState={true}
|
|
77
|
+
>
|
|
78
|
+
<OrganisationProvider>
|
|
79
|
+
<EventProvider>
|
|
80
|
+
<BrowserRouter>
|
|
81
|
+
<AppRoutes />
|
|
82
|
+
</BrowserRouter>
|
|
83
|
+
</EventProvider>
|
|
84
|
+
</OrganisationProvider>
|
|
85
|
+
</UnifiedAuthProvider>
|
|
86
|
+
</ErrorBoundary>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default App;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. AppRoutes.tsx - Route Configuration
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
97
|
+
import { ProtectedRoute } from './auth/ProtectedRoute';
|
|
98
|
+
import { AppLayout } from './layout/AppLayout';
|
|
99
|
+
import { LoginPage } from './auth/LoginPage';
|
|
100
|
+
import { DashboardPage } from './dashboard/DashboardPage';
|
|
101
|
+
import { MealsPage } from './meals/MealsPage';
|
|
102
|
+
import { UsersPage } from './users/UsersPage';
|
|
103
|
+
|
|
104
|
+
export function AppRoutes() {
|
|
105
|
+
return (
|
|
106
|
+
<Routes>
|
|
107
|
+
<Route path="/login" element={<LoginPage />} />
|
|
108
|
+
|
|
109
|
+
<Route path="/" element={
|
|
110
|
+
<ProtectedRoute>
|
|
111
|
+
<AppLayout />
|
|
112
|
+
</ProtectedRoute>
|
|
113
|
+
}>
|
|
114
|
+
<Route index element={<DashboardPage />} />
|
|
115
|
+
<Route path="meals" element={<MealsPage />} />
|
|
116
|
+
<Route path="users" element={<UsersPage />} />
|
|
117
|
+
</Route>
|
|
118
|
+
|
|
119
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
120
|
+
</Routes>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 3. ProtectedRoute.tsx - Route Protection
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { useUnifiedAuth } from '@jmruthers/pace-core';
|
|
129
|
+
import { Navigate, useLocation } from 'react-router-dom';
|
|
130
|
+
import { LoadingSpinner } from '../shared/LoadingSpinner';
|
|
131
|
+
|
|
132
|
+
interface ProtectedRouteProps {
|
|
133
|
+
children: React.ReactNode;
|
|
134
|
+
requiredPermission?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function ProtectedRoute({ children, requiredPermission }: ProtectedRouteProps) {
|
|
138
|
+
const { user, loading, hasPermission } = useUnifiedAuth();
|
|
139
|
+
const location = useLocation();
|
|
140
|
+
|
|
141
|
+
if (loading) {
|
|
142
|
+
return <LoadingSpinner />;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!user) {
|
|
146
|
+
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (requiredPermission && !hasPermission(requiredPermission)) {
|
|
150
|
+
return <Navigate to="/unauthorized" replace />;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return <>{children}</>;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 4. AppLayout.tsx - Main Layout
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { PaceAppLayout, OrganisationSelector, EventSelector } from '@jmruthers/pace-core';
|
|
161
|
+
import { Outlet } from 'react-router-dom';
|
|
162
|
+
|
|
163
|
+
const navItems = [
|
|
164
|
+
{
|
|
165
|
+
title: 'Dashboard',
|
|
166
|
+
href: '/',
|
|
167
|
+
icon: 'HomeIcon',
|
|
168
|
+
permission: 'read:dashboard'
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
title: 'Meals',
|
|
172
|
+
href: '/meals',
|
|
173
|
+
icon: 'UtensilsIcon',
|
|
174
|
+
permission: 'read:meals'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title: 'Users',
|
|
178
|
+
href: '/users',
|
|
179
|
+
icon: 'UsersIcon',
|
|
180
|
+
permission: 'read:users'
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
export function AppLayout() {
|
|
185
|
+
return (
|
|
186
|
+
<PaceAppLayout
|
|
187
|
+
appName="Meal Manager"
|
|
188
|
+
navItems={navItems}
|
|
189
|
+
enforcePermissions={true}
|
|
190
|
+
defaultPermission="read"
|
|
191
|
+
headerActions={
|
|
192
|
+
<>
|
|
193
|
+
<OrganisationSelector />
|
|
194
|
+
<EventSelector />
|
|
195
|
+
</>
|
|
196
|
+
}
|
|
197
|
+
>
|
|
198
|
+
<Outlet />
|
|
199
|
+
</PaceAppLayout>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 5. DashboardPage.tsx - Dashboard with Metrics
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { Card, CardHeader, CardTitle, CardContent } from '@jmruthers/pace-core';
|
|
208
|
+
import { useUnifiedAuth } from '@jmruthers/pace-core';
|
|
209
|
+
import { useMeals } from '../../hooks/useMeals';
|
|
210
|
+
import { useUsers } from '../../hooks/useUsers';
|
|
211
|
+
|
|
212
|
+
export function DashboardPage() {
|
|
213
|
+
const { user } = useUnifiedAuth();
|
|
214
|
+
const { meals, loading: mealsLoading } = useMeals();
|
|
215
|
+
const { users, loading: usersLoading } = useUsers();
|
|
216
|
+
|
|
217
|
+
const totalMeals = meals?.length || 0;
|
|
218
|
+
const totalUsers = users?.length || 0;
|
|
219
|
+
const thisWeekMeals = meals?.filter(meal => {
|
|
220
|
+
const weekAgo = new Date();
|
|
221
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
222
|
+
return new Date(meal.date) >= weekAgo;
|
|
223
|
+
}).length || 0;
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className="p-6">
|
|
227
|
+
<div className="mb-8">
|
|
228
|
+
<h1 className="text-3xl font-bold text-sec-900">Dashboard</h1>
|
|
229
|
+
<p className="text-sec-600 mt-2">
|
|
230
|
+
Welcome back, {user?.email}. Here's what's happening with your meals.
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
235
|
+
<Card>
|
|
236
|
+
<CardHeader>
|
|
237
|
+
<CardTitle>Total Meals</CardTitle>
|
|
238
|
+
</CardHeader>
|
|
239
|
+
<CardContent>
|
|
240
|
+
<p className="text-3xl font-bold">{mealsLoading ? '...' : totalMeals}</p>
|
|
241
|
+
<p className="text-sm text-sec-600">All time</p>
|
|
242
|
+
</CardContent>
|
|
243
|
+
</Card>
|
|
244
|
+
|
|
245
|
+
<Card>
|
|
246
|
+
<CardHeader>
|
|
247
|
+
<CardTitle>This Week</CardTitle>
|
|
248
|
+
</CardHeader>
|
|
249
|
+
<CardContent>
|
|
250
|
+
<p className="text-3xl font-bold">{mealsLoading ? '...' : thisWeekMeals}</p>
|
|
251
|
+
<p className="text-sm text-sec-600">Last 7 days</p>
|
|
252
|
+
</CardContent>
|
|
253
|
+
</Card>
|
|
254
|
+
|
|
255
|
+
<Card>
|
|
256
|
+
<CardHeader>
|
|
257
|
+
<CardTitle>Total Users</CardTitle>
|
|
258
|
+
</CardHeader>
|
|
259
|
+
<CardContent>
|
|
260
|
+
<p className="text-3xl font-bold">{usersLoading ? '...' : totalUsers}</p>
|
|
261
|
+
<p className="text-sm text-sec-600">Active users</p>
|
|
262
|
+
</CardContent>
|
|
263
|
+
</Card>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* Recent Activity */}
|
|
267
|
+
<Card>
|
|
268
|
+
<CardHeader>
|
|
269
|
+
<CardTitle>Recent Activity</CardTitle>
|
|
270
|
+
</CardHeader>
|
|
271
|
+
<CardContent>
|
|
272
|
+
<p className="text-sec-600">
|
|
273
|
+
Your meal tracking activity will appear here.
|
|
274
|
+
</p>
|
|
275
|
+
</CardContent>
|
|
276
|
+
</Card>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 6. MealsPage.tsx - Advanced DataTable
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
import { useState } from 'react';
|
|
286
|
+
import {
|
|
287
|
+
DataTable,
|
|
288
|
+
Button,
|
|
289
|
+
useToast,
|
|
290
|
+
useUnifiedAuth
|
|
291
|
+
} from '@jmruthers/pace-core';
|
|
292
|
+
import { MealForm } from './MealForm';
|
|
293
|
+
import { useMeals } from '../../hooks/useMeals';
|
|
294
|
+
import { Meal } from '../../types/meal';
|
|
295
|
+
|
|
296
|
+
export function MealsPage() {
|
|
297
|
+
const [showForm, setShowForm] = useState(false);
|
|
298
|
+
const [editingMeal, setEditingMeal] = useState<Meal | null>(null);
|
|
299
|
+
const { meals, loading, createMeal, updateMeal, deleteMeal } = useMeals();
|
|
300
|
+
const { hasPermission } = useUnifiedAuth();
|
|
301
|
+
const { toast } = useToast();
|
|
302
|
+
|
|
303
|
+
const canCreate = hasPermission('create:meals');
|
|
304
|
+
const canEdit = hasPermission('update:meals');
|
|
305
|
+
const canDelete = hasPermission('delete:meals');
|
|
306
|
+
|
|
307
|
+
const columns = [
|
|
308
|
+
{
|
|
309
|
+
accessorKey: 'name',
|
|
310
|
+
header: 'Meal Name',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
accessorKey: 'category',
|
|
314
|
+
header: 'Category',
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
accessorKey: 'calories',
|
|
318
|
+
header: 'Calories',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
accessorKey: 'date',
|
|
322
|
+
header: 'Date',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'actions',
|
|
326
|
+
header: 'Actions',
|
|
327
|
+
cell: ({ row }: any) => (
|
|
328
|
+
<div className="flex gap-2">
|
|
329
|
+
{canEdit && (
|
|
330
|
+
<Button
|
|
331
|
+
variant="outline"
|
|
332
|
+
size="sm"
|
|
333
|
+
onClick={() => setEditingMeal(row.original)}
|
|
334
|
+
>
|
|
335
|
+
Edit
|
|
336
|
+
</Button>
|
|
337
|
+
)}
|
|
338
|
+
{canDelete && (
|
|
339
|
+
<Button
|
|
340
|
+
variant="destructive"
|
|
341
|
+
size="sm"
|
|
342
|
+
onClick={() => handleDelete(row.original.id)}
|
|
343
|
+
>
|
|
344
|
+
Delete
|
|
345
|
+
</Button>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
),
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
const handleDelete = async (id: string) => {
|
|
353
|
+
try {
|
|
354
|
+
await deleteMeal(id);
|
|
355
|
+
toast({
|
|
356
|
+
title: 'Success',
|
|
357
|
+
description: 'Meal deleted successfully',
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
toast({
|
|
361
|
+
title: 'Error',
|
|
362
|
+
description: 'Failed to delete meal',
|
|
363
|
+
variant: 'destructive',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const handleSubmit = async (mealData: Partial<Meal>) => {
|
|
369
|
+
try {
|
|
370
|
+
if (editingMeal) {
|
|
371
|
+
await updateMeal(editingMeal.id, mealData);
|
|
372
|
+
toast({
|
|
373
|
+
title: 'Success',
|
|
374
|
+
description: 'Meal updated successfully',
|
|
375
|
+
});
|
|
376
|
+
} else {
|
|
377
|
+
await createMeal(mealData);
|
|
378
|
+
toast({
|
|
379
|
+
title: 'Success',
|
|
380
|
+
description: 'Meal created successfully',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
setShowForm(false);
|
|
384
|
+
setEditingMeal(null);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
toast({
|
|
387
|
+
title: 'Error',
|
|
388
|
+
description: 'Failed to save meal',
|
|
389
|
+
variant: 'destructive',
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div className="p-6">
|
|
396
|
+
<div className="flex justify-between items-center mb-6">
|
|
397
|
+
<h1 className="text-2xl font-bold">Meals</h1>
|
|
398
|
+
{canCreate && (
|
|
399
|
+
<Button onClick={() => setShowForm(true)}>
|
|
400
|
+
Add Meal
|
|
401
|
+
</Button>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<DataTable
|
|
406
|
+
data={meals || []}
|
|
407
|
+
columns={columns}
|
|
408
|
+
isLoading={loading}
|
|
409
|
+
features={{
|
|
410
|
+
search: true,
|
|
411
|
+
pagination: true,
|
|
412
|
+
}}
|
|
413
|
+
onRowClick={(meal) => canEdit && setEditingMeal(meal)}
|
|
414
|
+
/>
|
|
415
|
+
|
|
416
|
+
{showForm && (
|
|
417
|
+
<MealForm
|
|
418
|
+
meal={editingMeal}
|
|
419
|
+
onSubmit={handleSubmit}
|
|
420
|
+
onCancel={() => {
|
|
421
|
+
setShowForm(false);
|
|
422
|
+
setEditingMeal(null);
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 7. MealForm.tsx - Form with Validation
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
import { useForm } from 'react-hook-form';
|
|
435
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
436
|
+
import { z } from 'zod';
|
|
437
|
+
import {
|
|
438
|
+
Form,
|
|
439
|
+
FormField,
|
|
440
|
+
FormItem,
|
|
441
|
+
FormLabel,
|
|
442
|
+
FormControl,
|
|
443
|
+
FormMessage,
|
|
444
|
+
Button,
|
|
445
|
+
Input,
|
|
446
|
+
Select,
|
|
447
|
+
SelectContent,
|
|
448
|
+
SelectItem,
|
|
449
|
+
SelectTrigger,
|
|
450
|
+
SelectValue,
|
|
451
|
+
Card,
|
|
452
|
+
CardHeader,
|
|
453
|
+
CardTitle,
|
|
454
|
+
CardContent
|
|
455
|
+
} from '@jmruthers/pace-core';
|
|
456
|
+
import { Meal } from '../../types/meal';
|
|
457
|
+
|
|
458
|
+
const mealSchema = z.object({
|
|
459
|
+
name: z.string().min(1, 'Meal name is required'),
|
|
460
|
+
category: z.string().min(1, 'Category is required'),
|
|
461
|
+
calories: z.number().min(1, 'Calories must be greater than 0'),
|
|
462
|
+
date: z.string().min(1, 'Date is required'),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
interface MealFormProps {
|
|
466
|
+
meal?: Meal | null;
|
|
467
|
+
onSubmit: (data: Partial<Meal>) => Promise<void>;
|
|
468
|
+
onCancel: () => void;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function MealForm({ meal, onSubmit, onCancel }: MealFormProps) {
|
|
472
|
+
const form = useForm({
|
|
473
|
+
resolver: zodResolver(mealSchema),
|
|
474
|
+
defaultValues: {
|
|
475
|
+
name: meal?.name || '',
|
|
476
|
+
category: meal?.category || '',
|
|
477
|
+
calories: meal?.calories || 0,
|
|
478
|
+
date: meal?.date || new Date().toISOString().split('T')[0],
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const handleSubmit = async (data: any) => {
|
|
483
|
+
await onSubmit(data);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<Card className="mt-6">
|
|
488
|
+
<CardHeader>
|
|
489
|
+
<CardTitle>{meal ? 'Edit Meal' : 'Add New Meal'}</CardTitle>
|
|
490
|
+
</CardHeader>
|
|
491
|
+
<CardContent>
|
|
492
|
+
<Form {...form}>
|
|
493
|
+
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
|
494
|
+
<FormField
|
|
495
|
+
control={form.control}
|
|
496
|
+
name="name"
|
|
497
|
+
render={({ field }) => (
|
|
498
|
+
<FormItem>
|
|
499
|
+
<FormLabel>Meal Name</FormLabel>
|
|
500
|
+
<FormControl>
|
|
501
|
+
<Input placeholder="Enter meal name" {...field} />
|
|
502
|
+
</FormControl>
|
|
503
|
+
<FormMessage />
|
|
504
|
+
</FormItem>
|
|
505
|
+
)}
|
|
506
|
+
/>
|
|
507
|
+
|
|
508
|
+
<FormField
|
|
509
|
+
control={form.control}
|
|
510
|
+
name="category"
|
|
511
|
+
render={({ field }) => (
|
|
512
|
+
<FormItem>
|
|
513
|
+
<FormLabel>Category</FormLabel>
|
|
514
|
+
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
515
|
+
<FormControl>
|
|
516
|
+
<SelectTrigger>
|
|
517
|
+
<SelectValue placeholder="Select category" />
|
|
518
|
+
</SelectTrigger>
|
|
519
|
+
</FormControl>
|
|
520
|
+
<SelectContent>
|
|
521
|
+
<SelectItem value="breakfast">Breakfast</SelectItem>
|
|
522
|
+
<SelectItem value="lunch">Lunch</SelectItem>
|
|
523
|
+
<SelectItem value="dinner">Dinner</SelectItem>
|
|
524
|
+
<SelectItem value="snack">Snack</SelectItem>
|
|
525
|
+
</SelectContent>
|
|
526
|
+
</Select>
|
|
527
|
+
<FormMessage />
|
|
528
|
+
</FormItem>
|
|
529
|
+
)}
|
|
530
|
+
/>
|
|
531
|
+
|
|
532
|
+
<FormField
|
|
533
|
+
control={form.control}
|
|
534
|
+
name="calories"
|
|
535
|
+
render={({ field }) => (
|
|
536
|
+
<FormItem>
|
|
537
|
+
<FormLabel>Calories</FormLabel>
|
|
538
|
+
<FormControl>
|
|
539
|
+
<Input
|
|
540
|
+
type="number"
|
|
541
|
+
placeholder="Enter calories"
|
|
542
|
+
{...field}
|
|
543
|
+
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
|
544
|
+
/>
|
|
545
|
+
</FormControl>
|
|
546
|
+
<FormMessage />
|
|
547
|
+
</FormItem>
|
|
548
|
+
)}
|
|
549
|
+
/>
|
|
550
|
+
|
|
551
|
+
<FormField
|
|
552
|
+
control={form.control}
|
|
553
|
+
name="date"
|
|
554
|
+
render={({ field }) => (
|
|
555
|
+
<FormItem>
|
|
556
|
+
<FormLabel>Date</FormLabel>
|
|
557
|
+
<FormControl>
|
|
558
|
+
<Input type="date" {...field} />
|
|
559
|
+
</FormControl>
|
|
560
|
+
<FormMessage />
|
|
561
|
+
</FormItem>
|
|
562
|
+
)}
|
|
563
|
+
/>
|
|
564
|
+
|
|
565
|
+
<div className="flex gap-2 pt-4">
|
|
566
|
+
<Button type="submit">
|
|
567
|
+
{meal ? 'Update Meal' : 'Add Meal'}
|
|
568
|
+
</Button>
|
|
569
|
+
<Button type="button" variant="outline" onClick={onCancel}>
|
|
570
|
+
Cancel
|
|
571
|
+
</Button>
|
|
572
|
+
</div>
|
|
573
|
+
</form>
|
|
574
|
+
</Form>
|
|
575
|
+
</CardContent>
|
|
576
|
+
</Card>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## 🔧 Environment Setup
|
|
582
|
+
|
|
583
|
+
Create `.env.local`:
|
|
584
|
+
|
|
585
|
+
```bash
|
|
586
|
+
REACT_APP_SUPABASE_URL=https://your-project.supabase.co
|
|
587
|
+
REACT_APP_SUPABASE_ANON_KEY=your-anon-key-here
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## 🎉 What You Get
|
|
591
|
+
|
|
592
|
+
- ✅ Complete authentication with RBAC
|
|
593
|
+
- ✅ Multi-tenant organisation support
|
|
594
|
+
- ✅ Event management
|
|
595
|
+
- ✅ Advanced DataTable with CRUD
|
|
596
|
+
- ✅ Form validation with Zod
|
|
597
|
+
- ✅ Permission enforcement
|
|
598
|
+
- ✅ Responsive layout
|
|
599
|
+
- ✅ Error handling and loading states
|
|
600
|
+
- ✅ Toast notifications
|
|
601
|
+
- ✅ Production-ready patterns
|
|
602
|
+
|
|
603
|
+
## 🚀 Next Steps
|
|
604
|
+
|
|
605
|
+
- **Customize the design** - Adapt to your brand
|
|
606
|
+
- **Add more features** - Reporting, analytics, integrations
|
|
607
|
+
- **Implement real data** - Connect to your database
|
|
608
|
+
- **Add tests** - Unit and integration testing
|
|
609
|
+
- **Deploy** - Production deployment
|
|
610
|
+
|
|
611
|
+
## 📚 Related Documentation
|
|
612
|
+
|
|
613
|
+
- **[Core Concepts](../core-concepts/)** - Understand authentication and RBAC
|
|
614
|
+
- **[Implementation Guides](../implementation-guides/)** - Advanced patterns
|
|
615
|
+
- **[API Reference](../api-reference/)** - All components and hooks
|
|
616
|
+
- **[Best Practices](../best-practices/)** - Production deployment
|
package/package.json
CHANGED
|
@@ -14,7 +14,13 @@ interface ChunkInfo {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
class BundleAnalyzer {
|
|
17
|
-
private enabled
|
|
17
|
+
private get enabled(): boolean {
|
|
18
|
+
try {
|
|
19
|
+
return import.meta.env?.MODE === 'development';
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
analyzeBundle(): BundleStats | null {
|
|
20
26
|
if (!this.enabled) return null;
|
|
@@ -104,7 +110,11 @@ export const bundleAnalyzer = new BundleAnalyzer();
|
|
|
104
110
|
|
|
105
111
|
// Helper to check if imports are optimized
|
|
106
112
|
export function validateImportPattern(moduleId: string, importedItems: string[]): void {
|
|
107
|
-
|
|
113
|
+
try {
|
|
114
|
+
if (import.meta.env?.MODE !== 'development') return;
|
|
115
|
+
} catch {
|
|
116
|
+
return; // Silently skip in test environments
|
|
117
|
+
}
|
|
108
118
|
|
|
109
119
|
const largeModules = ['lodash', 'moment', 'rxjs'];
|
|
110
120
|
const isLargeModule = largeModules.some(mod => moduleId.includes(mod));
|
|
@@ -121,7 +131,11 @@ export function validateImportPattern(moduleId: string, importedItems: string[])
|
|
|
121
131
|
|
|
122
132
|
// Monitor dynamic imports
|
|
123
133
|
export function trackDynamicImport(moduleName: string): void {
|
|
124
|
-
|
|
134
|
+
try {
|
|
135
|
+
if (import.meta.env?.MODE !== 'development') return;
|
|
136
|
+
} catch {
|
|
137
|
+
return; // Silently skip in test environments
|
|
138
|
+
}
|
|
125
139
|
|
|
126
140
|
// TODO: Replace with proper logging service integration
|
|
127
141
|
// For now, we'll log to console for testing purposes
|