@ramme-io/create-app 1.2.8 → 1.2.10

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.
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { PageHeader } from '@ramme-io/ui';
3
+
4
+ interface StandardPageLayoutProps {
5
+ title: string;
6
+ description?: string;
7
+ children: React.ReactNode;
8
+ actions?: React.ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export const StandardPageLayout: React.FC<StandardPageLayoutProps> = ({
13
+ title,
14
+ description,
15
+ children,
16
+ actions,
17
+ className,
18
+ }) => {
19
+ return (
20
+ <div className={`flex flex-col h-full min-h-[calc(100vh-64px)] animate-in fade-in duration-300 ${className || ''}`}>
21
+
22
+ {/* 1. Standardized Header Area (Sticky) */}
23
+ <div className="px-4 py-4 md:px-8 border-b border-border bg-background/95 backdrop-blur-sm sticky top-0 z-10">
24
+
25
+ {/* Breadcrumbs removed to prevent Router Context crash */}
26
+
27
+ <PageHeader
28
+ title={title}
29
+ description={description}
30
+ actions={actions}
31
+ className="p-0 border-none"
32
+ />
33
+ </div>
34
+
35
+ {/* 2. Standardized Content Area */}
36
+ <div className="flex-1 p-4 md:p-8 w-full max-w-[1600px] mx-auto">
37
+ {children}
38
+ </div>
39
+ </div>
40
+ );
41
+ };
@@ -1,23 +1,23 @@
1
- import React, { Component, type ErrorInfo, useMemo } from 'react';
2
- import { PageHeader, Alert, Badge } from '@ramme-io/ui';
1
+ import React, { Component, useMemo } from 'react';
2
+ import { Alert, Badge } from '@ramme-io/ui';
3
3
  import { useManifest, useBridgeStatus } from '../runtime/ManifestContext';
4
4
  import { getComponent } from '../../config/component-registry';
5
- import { getMockData } from '../../data/mockData'; // Keep as fallback
5
+ import { getMockData } from '../../data/mockData';
6
6
  import { Wifi, WifiOff, AlertTriangle, Loader2, Database, Wand2 } from 'lucide-react';
7
+ // ✅ Import Core Layout
8
+ import { StandardPageLayout } from '../../components/layout/StandardPageLayout';
7
9
 
8
- // --- 🛠️ JIT DATA GENERATOR ---
9
- // This ensures the preview ALWAYS has data, even for new resources not yet in the DB.
10
+ // ... (Keep existing generateJitData & BlockErrorBoundary logic unchanged) ...
11
+ // (Re-pasting helper functions omitted for brevity, assume they are present)
10
12
  const generateJitData = (resourceDef: any, count = 10) => {
11
13
  if (!resourceDef) return [];
12
-
13
14
  return Array.from({ length: count }).map((_, i) => {
14
15
  const row: any = { id: i + 1 };
15
16
  resourceDef.fields.forEach((f: any) => {
16
- // Intelligent Mocking based on field type
17
17
  if (f.type === 'status') {
18
18
  row[f.key] = ['Active', 'Pending', 'Closed', 'Archived'][Math.floor(Math.random() * 4)];
19
19
  } else if (f.type === 'boolean') {
20
- row[f.key] = Math.random() > 0.3; // 70% true
20
+ row[f.key] = Math.random() > 0.3;
21
21
  } else if (f.type === 'number' || f.type === 'currency') {
22
22
  row[f.key] = Math.floor(Math.random() * 1000) + 100;
23
23
  } else if (f.type === 'date') {
@@ -32,7 +32,6 @@ const generateJitData = (resourceDef: any, count = 10) => {
32
32
  });
33
33
  };
34
34
 
35
- // --- ERROR BOUNDARY ---
36
35
  class BlockErrorBoundary extends Component<{ children: React.ReactNode }, { hasError: boolean, error: string }> {
37
36
  constructor(props: any) { super(props); this.state = { hasError: false, error: '' }; }
38
37
  static getDerivedStateFromError(error: any) { return { hasError: true, error: error.message }; }
@@ -48,7 +47,6 @@ class BlockErrorBoundary extends Component<{ children: React.ReactNode }, { hasE
48
47
  }
49
48
  }
50
49
 
51
- // --- MAIN RENDERER ---
52
50
  export const DynamicPage = ({ pageId }: { pageId: string }) => {
53
51
  const manifest = useManifest();
54
52
  const status = useBridgeStatus();
@@ -64,22 +62,21 @@ export const DynamicPage = ({ pageId }: { pageId: string }) => {
64
62
  );
65
63
  }
66
64
 
65
+ // Define the "Live Mode" badge as an action element for the header
66
+ const statusBadge = (
67
+ <div className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-bold border ${isLive ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500'}`}>
68
+ {isLive ? <Wifi size={14} className="text-green-600 animate-pulse"/> : <WifiOff size={14} />}
69
+ {isLive ? 'LIVE PREVIEW' : 'STATIC MODE'}
70
+ </div>
71
+ );
72
+
67
73
  return (
68
- <div className="space-y-8 animate-in fade-in duration-300 pb-20 p-6 md:p-8">
69
- {/* Header */}
70
- <div className="flex flex-col md:flex-row md:items-start justify-between gap-4 border-b border-border pb-6">
71
- <div>
72
- <h1 className="text-3xl font-bold tracking-tight text-foreground">{page.title}</h1>
73
- {page.description && <p className="text-muted-foreground text-lg">{page.description}</p>}
74
- </div>
75
- <div className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-bold border ${isLive ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500'}`}>
76
- {isLive ? <Wifi size={14} className="text-green-600 animate-pulse"/> : <WifiOff size={14} />}
77
- {isLive ? 'LIVE PREVIEW' : 'STATIC MODE'}
78
- </div>
79
- </div>
80
-
81
- {/* Grid Layout */}
82
- <div className="grid gap-8">
74
+ <StandardPageLayout
75
+ title={page.title}
76
+ description={page.description}
77
+ actions={statusBadge}
78
+ >
79
+ <div className="grid gap-6 md:gap-8">
83
80
  {page.sections?.map((section: any) => (
84
81
  <section key={section.id} className="space-y-4">
85
82
  {section.title && (
@@ -87,30 +84,24 @@ export const DynamicPage = ({ pageId }: { pageId: string }) => {
87
84
  <h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground">{section.title}</h3>
88
85
  </div>
89
86
  )}
90
- <div className="grid gap-6" style={{ gridTemplateColumns: `repeat(${section.layout?.columns || 1}, minmax(0, 1fr))` }}>
87
+ <div className="grid gap-4 md:gap-6" style={{ gridTemplateColumns: `repeat(${section.layout?.columns || 1}, minmax(0, 1fr))` }}>
91
88
  {section.blocks.map((block: any) => {
92
89
  const Component = getComponent(block.type);
93
90
  const safeDataId = block.props.dataId?.toLowerCase();
94
91
 
95
- // --- 🛡️ HYBRID DATA STRATEGY ---
96
92
  let resolvedData: any[] = [];
97
93
  let isGenerated = false;
98
94
  let autoColumns = undefined;
99
95
 
100
96
  if (safeDataId) {
101
- // 1. Try Local Storage first
102
97
  resolvedData = getMockData(safeDataId);
103
-
104
- // 2. If empty, Generate JIT Data from Manifest
105
98
  if (!resolvedData || resolvedData.length === 0) {
106
99
  const resourceDef = manifest.resources?.find((r: any) => r.id.toLowerCase() === safeDataId);
107
100
  if (resourceDef) {
108
101
  resolvedData = generateJitData(resourceDef);
109
- isGenerated = true; // Flag for debug overlay
102
+ isGenerated = true;
110
103
  }
111
104
  }
112
-
113
- // 3. Auto-Generate Table Columns
114
105
  const resourceDef = manifest.resources?.find((r: any) => r.id.toLowerCase() === safeDataId);
115
106
  if (resourceDef && !block.props.columnDefs) {
116
107
  autoColumns = resourceDef.fields.map((f: any) => ({
@@ -123,7 +114,6 @@ export const DynamicPage = ({ pageId }: { pageId: string }) => {
123
114
  return (
124
115
  <div key={block.id} style={{ gridColumn: `span ${block.layout?.colSpan || 1}`, gridRow: `span ${block.layout?.rowSpan || 1}` }} className="relative group">
125
116
  <BlockErrorBoundary>
126
- {/* Debug Overlay */}
127
117
  <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 pointer-events-none">
128
118
  <div className={`text-white text-[10px] px-2 py-1 rounded backdrop-blur-md flex items-center gap-1 ${isGenerated ? 'bg-amber-600/90' : 'bg-black/80'}`}>
129
119
  {block.type}
@@ -145,6 +135,6 @@ export const DynamicPage = ({ pageId }: { pageId: string }) => {
145
135
  </section>
146
136
  ))}
147
137
  </div>
148
- </div>
138
+ </StandardPageLayout>
149
139
  );
150
140
  };
@@ -1,26 +1,31 @@
1
1
  import React from 'react';
2
- import { PageHeader } from '@ramme-io/ui'; // Assuming we have this
2
+ // Import Core Layout
3
+ import { StandardPageLayout } from '../components/layout/StandardPageLayout';
3
4
 
4
5
  interface GenericContentPageProps {
5
6
  pageTitle: string;
6
- children?: React.ReactNode; // Allow for optional custom content later
7
+ children?: React.ReactNode;
7
8
  }
8
9
 
9
10
  const GenericContentPage: React.FC<GenericContentPageProps> = ({ pageTitle, children }) => {
10
11
  return (
11
- <div>
12
- <PageHeader title={pageTitle} />
13
- <div className="mt-6">
14
- {children ? (
15
- children
16
- ) : (
17
- <p className="text-muted-foreground">
18
- This is a placeholder page for "{pageTitle}". Replace this content in{' '}
19
- <code>src/pages/YourSpecificPage.tsx</code>.
12
+ <StandardPageLayout
13
+ title={pageTitle}
14
+ description={`This is a placeholder page for "${pageTitle}".`}
15
+ >
16
+ {children ? (
17
+ children
18
+ ) : (
19
+ <div className="p-12 border-2 border-dashed border-border rounded-lg bg-muted/10 text-center flex flex-col items-center justify-center">
20
+ <p className="text-muted-foreground text-lg mb-2">
21
+ This page is ready for content.
20
22
  </p>
21
- )}
22
- </div>
23
- </div>
23
+ <p className="text-sm text-muted-foreground">
24
+ Replace this file in <code>src/pages/YourSpecificPage.tsx</code>.
25
+ </p>
26
+ </div>
27
+ )}
28
+ </StandardPageLayout>
24
29
  );
25
30
  };
26
31
 
@@ -89,7 +89,7 @@ const LoginPage: React.FC = () => {
89
89
  {/* Footer */}
90
90
  <div className="mt-8 pt-6 border-t border-border text-center text-sm">
91
91
  <span className="text-muted-foreground">Don't have an account? </span>
92
- [cite_start]{/* ✅ CRITICAL FIX: Updated path to /auth/signup [cite: 1937] */}
92
+
93
93
  <Link to="/auth/signup" className="font-semibold text-primary hover:text-primary/80 transition-colors">
94
94
  Create one
95
95
  </Link>
@@ -4,6 +4,7 @@ import {
4
4
  Button,
5
5
  Icon,
6
6
  Card,
7
+ Badge,
7
8
  useToast,
8
9
  SearchInput,
9
10
  type ColDef,
@@ -14,7 +15,8 @@ import { getResourceMeta } from '../../data/mockData';
14
15
  import { AutoForm } from '../../components/AutoForm';
15
16
  import { useCrudLocalStorage } from '../../engine/runtime/useCrudLocalStorage';
16
17
  import { useManifest } from '../../engine/runtime/ManifestContext';
17
- import type { ResourceDefinition } from '../../engine/validation/schema';
18
+ // IMPORT FieldDefinition specifically
19
+ import type { ResourceDefinition, FieldDefinition } from '../../engine/validation/schema';
18
20
 
19
21
  interface SmartTableProps {
20
22
  dataId: string;
@@ -24,38 +26,27 @@ interface SmartTableProps {
24
26
 
25
27
  export const SmartTable: React.FC<SmartTableProps> = ({
26
28
  dataId,
27
- title}) => {
29
+ title
30
+ }) => {
28
31
  const { addToast } = useToast();
29
-
30
32
  const manifest = useManifest();
31
33
 
32
- // FIX: Normalize 'meta' to always match 'ResourceDefinition'
34
+ // --- 1. METADATA RESOLUTION ---
33
35
  const meta = useMemo<ResourceDefinition | null>(() => {
34
- // 1. Try Dynamic Manifest (Preview Mode)
35
36
  const dynamicResource = manifest.resources?.find((r: ResourceDefinition) => r.id === dataId);
36
-
37
- if (dynamicResource) {
38
- return dynamicResource;
39
- }
37
+ if (dynamicResource) return dynamicResource;
40
38
 
41
- // 2. Try Static Data (Deployed Mode)
42
39
  const staticMeta = getResourceMeta(dataId);
43
-
44
40
  if (staticMeta) {
45
- // ⚠️ Type Patch: Inject 'id' so it satisfies ResourceDefinition
46
- // The static file uses the object key as the ID, but the type expects it inline.
47
41
  return {
48
42
  ...staticMeta,
49
43
  id: dataId,
50
- // Ensure 'type' strings from mockData match the Zod enum if needed,
51
- // but typically 'text' | 'number' overlaps fine.
52
44
  } as unknown as ResourceDefinition;
53
45
  }
54
-
55
46
  return null;
56
47
  }, [manifest, dataId]);
57
48
 
58
- // JIT Seeder now receives a valid ResourceDefinition
49
+ // --- 2. DATA HYDRATION ---
59
50
  const seedData = useJustInTimeSeeder(dataId, meta);
60
51
 
61
52
  const {
@@ -65,18 +56,18 @@ export const SmartTable: React.FC<SmartTableProps> = ({
65
56
  deleteItem
66
57
  } = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
67
58
 
68
- // --- UI STATE ---
59
+ // --- 3. UI STATE ---
69
60
  const [isEditOpen, setIsEditOpen] = useState(false);
70
61
  const [currentRecord, setCurrentRecord] = useState<any>(null);
71
62
  const [gridApi, setGridApi] = useState<GridApi | null>(null);
72
63
  const [selectedRows, setSelectedRows] = useState<any[]>([]);
73
64
  const [quickFilterText, setQuickFilterText] = useState('');
74
65
 
75
- // --- COLUMN DEFINITIONS ---
66
+ // --- 4. COLUMN DEFINITIONS (Desktop) ---
76
67
  const columns = useMemo<ColDef[]>(() => {
77
68
  if (!meta?.fields) return [];
78
69
 
79
- const generatedCols: ColDef[] = meta.fields.map((f: any) => {
70
+ const generatedCols: ColDef[] = meta.fields.map((f: FieldDefinition) => {
80
71
  const col: ColDef = {
81
72
  field: f.key,
82
73
  headerName: f.label,
@@ -125,7 +116,8 @@ export const SmartTable: React.FC<SmartTableProps> = ({
125
116
  pinned: 'right',
126
117
  cellRenderer: (params: any) => (
127
118
  <div className="flex items-center gap-1">
128
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e) => { e.stopPropagation(); setCurrentRecord(params.data); setIsEditOpen(true); }}>
119
+ {/* FIXED: Explicit React.MouseEvent type */}
120
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e: React.MouseEvent) => { e.stopPropagation(); setCurrentRecord(params.data); setIsEditOpen(true); }}>
129
121
  <Icon name="edit-2" size={14} className="text-slate-500" />
130
122
  </Button>
131
123
  </div>
@@ -135,7 +127,12 @@ export const SmartTable: React.FC<SmartTableProps> = ({
135
127
  return generatedCols;
136
128
  }, [meta]);
137
129
 
138
- // --- HANDLERS ---
130
+ // --- 5. FIELD HELPERS (Mobile) ---
131
+ // ✅ FIXED: Explicit FieldDefinition type for 'f'
132
+ const titleField = useMemo(() => meta?.fields.find((f: FieldDefinition) => f.type === 'text' && f.key !== 'id') || meta?.fields[0], [meta]);
133
+ const statusField = useMemo(() => meta?.fields.find((f: FieldDefinition) => f.type === 'status'), [meta]);
134
+
135
+ // --- 6. HANDLERS ---
139
136
  const onGridReady = useCallback((params: any) => {
140
137
  setGridApi(params.api);
141
138
  }, []);
@@ -169,14 +166,15 @@ export const SmartTable: React.FC<SmartTableProps> = ({
169
166
  return (
170
167
  <Card className="flex flex-col h-[600px] border border-border shadow-sm overflow-hidden bg-card">
171
168
 
172
- <div className="p-4 border-b border-border flex justify-between items-center gap-4 bg-muted/5">
169
+ {/* --- HEADER --- */}
170
+ <div className="p-4 border-b border-border flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-muted/5">
173
171
  {selectedRows.length > 0 ? (
174
172
  <div className="flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200">
175
173
  <span className="bg-primary text-primary-foreground text-xs font-bold px-2 py-1 rounded-md">
176
174
  {selectedRows.length} Selected
177
175
  </span>
178
176
  <Button size="sm" variant="danger" onClick={handleBulkDelete} iconLeft="trash-2">
179
- Delete Selected
177
+ Delete
180
178
  </Button>
181
179
  </div>
182
180
  ) : (
@@ -195,36 +193,112 @@ export const SmartTable: React.FC<SmartTableProps> = ({
195
193
  </div>
196
194
  )}
197
195
 
198
- <div className="flex items-center gap-2">
199
- <div className="w-64">
196
+ <div className="flex items-center gap-2 w-full sm:w-auto">
197
+ <div className="w-full sm:w-64">
200
198
  <SearchInput
201
199
  placeholder="Quick search..."
202
200
  value={quickFilterText}
203
- onChange={(e) => {
201
+ // FIXED: Explicit ChangeEvent type
202
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
204
203
  setQuickFilterText(e.target.value);
205
- gridApi?.updateGridOptions({ quickFilterText: e.target.value }); }}
204
+ gridApi?.updateGridOptions({ quickFilterText: e.target.value });
205
+ }}
206
206
  />
207
207
  </div>
208
- <div className="h-6 w-px bg-border mx-1" />
208
+ <div className="h-6 w-px bg-border mx-1 hidden sm:block" />
209
209
  <Button size="sm" variant="primary" iconLeft="plus" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
210
- Add New
210
+ Add
211
211
  </Button>
212
212
  </div>
213
213
  </div>
214
214
 
215
- <div className="flex-1 w-full bg-card relative">
216
- <DataTable
217
- rowData={rowData}
218
- columnDefs={columns}
219
- onGridReady={onGridReady}
220
- onSelectionChanged={onSelectionChanged}
221
- rowSelection="multiple"
222
- pagination={true}
223
- paginationPageSize={10}
224
- headerHeight={48}
225
- rowHeight={48}
226
- enableCellTextSelection={true}
227
- />
215
+ {/* --- CONTENT AREA --- */}
216
+ <div className="flex-1 w-full bg-card relative overflow-hidden">
217
+
218
+ {/* 🖥️ DESKTOP VIEW: The Power Grid */}
219
+ <div className="hidden md:block h-full">
220
+ <DataTable
221
+ rowData={rowData}
222
+ columnDefs={columns}
223
+ onGridReady={onGridReady}
224
+ onSelectionChanged={onSelectionChanged}
225
+ rowSelection="multiple"
226
+ pagination={true}
227
+ paginationPageSize={10}
228
+ headerHeight={48}
229
+ rowHeight={48}
230
+ enableCellTextSelection={true}
231
+ />
232
+ </div>
233
+
234
+ {/* 📱 MOBILE VIEW: The Card List */}
235
+ <div className="block md:hidden h-full overflow-y-auto p-4 space-y-3 bg-muted/5">
236
+ {rowData.map((row) => (
237
+ <div key={row.id} className="bg-background border border-border rounded-lg p-4 shadow-sm relative group">
238
+
239
+ {/* Header: Title + Status */}
240
+ <div className="flex justify-between items-start mb-2">
241
+ <div>
242
+ <h4 className="font-semibold text-foreground">
243
+ {titleField ? row[titleField.key] : row.id}
244
+ </h4>
245
+ <p className="text-xs text-muted-foreground font-mono mt-0.5 opacity-70">{row.id}</p>
246
+ </div>
247
+ {statusField && (
248
+ <Badge variant={
249
+ ['active', 'paid'].includes(String(row[statusField.key]).toLowerCase()) ? 'success' :
250
+ ['pending'].includes(String(row[statusField.key]).toLowerCase()) ? 'warning' : 'secondary'
251
+ }>
252
+ {row[statusField.key]}
253
+ </Badge>
254
+ )}
255
+ </div>
256
+
257
+ {/* Body: Detailed Fields */}
258
+ <div className="space-y-1 text-sm text-muted-foreground mb-4">
259
+ {meta?.fields
260
+ // ✅ FIXED: Explicit FieldDefinition type for 'f'
261
+ .filter((f: FieldDefinition) => f.key !== titleField?.key && f.key !== statusField?.key && f.key !== 'id')
262
+ .slice(0, 3)
263
+ .map((f: FieldDefinition) => (
264
+ <div key={f.key} className="flex justify-between border-b border-dashed border-border/50 pb-1 last:border-0">
265
+ <span className="opacity-70">{f.label}:</span>
266
+ <span className="font-medium text-foreground">
267
+ {f.type === 'currency' ? `$${Number(row[f.key]).toLocaleString()}` :
268
+ f.type === 'date' ? new Date(row[f.key]).toLocaleDateString() : row[f.key]}
269
+ </span>
270
+ </div>
271
+ ))}
272
+ </div>
273
+
274
+ {/* Footer: Actions */}
275
+ <div className="flex gap-2 pt-2 border-t border-border/50">
276
+ <Button
277
+ size="sm"
278
+ variant="outline"
279
+ className="flex-1 h-8 text-xs"
280
+ onClick={() => { setCurrentRecord(row); setIsEditOpen(true); }}
281
+ >
282
+ <Icon name="edit-2" size={12} className="mr-2"/> Edit
283
+ </Button>
284
+ <Button
285
+ size="sm"
286
+ variant="ghost"
287
+ className="text-destructive hover:bg-destructive/10 px-3 h-8"
288
+ onClick={() => { if(confirm('Delete?')) deleteItem(row.id); }}
289
+ >
290
+ <Icon name="trash-2" size={14} />
291
+ </Button>
292
+ </div>
293
+
294
+ </div>
295
+ ))}
296
+
297
+ {rowData.length === 0 && (
298
+ <div className="text-center p-8 text-muted-foreground">No records found.</div>
299
+ )}
300
+ </div>
301
+
228
302
  </div>
229
303
 
230
304
  <AutoForm
@@ -1,21 +1,16 @@
1
1
  import React from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
3
  import {
4
- PageHeader,
5
4
  Card,
6
5
  BarChart,
7
6
  Button,
8
7
  Icon,
9
8
  Badge
10
9
  } from '@ramme-io/ui';
11
-
12
- // 1. REMOVE: The Zombie Service Import
13
- // import { userService } from '../../users';
14
-
15
- // 2. ADD: The Engine Hook & Shared Data
16
- // (Adjust path '../../engine/...' if needed based on your folder structure)
17
10
  import { useCrudLocalStorage } from '../../../engine/runtime/useCrudLocalStorage';
18
11
  import { SEED_USERS, type User } from '../../../data/mockData';
12
+ // ✅ Import Core Layout
13
+ import { StandardPageLayout } from '../../../components/layout/StandardPageLayout';
19
14
 
20
15
  // Mock Data for Chart
21
16
  const revenueData = [
@@ -28,7 +23,6 @@ const revenueData = [
28
23
  { name: 'Sun', revenue: 3490, cost: 4300 },
29
24
  ];
30
25
 
31
- // Recreating the StatCard locally (since the old one was custom)
32
26
  const StatCard = ({ title, value, trend, icon, onClick, className }: any) => (
33
27
  <Card
34
28
  className={`p-6 flex items-center justify-between space-x-4 transition-all hover:border-primary/50 ${className || ''}`}
@@ -50,78 +44,72 @@ const StatCard = ({ title, value, trend, icon, onClick, className }: any) => (
50
44
 
51
45
  export const OverviewPage: React.FC = () => {
52
46
  const navigate = useNavigate();
53
-
54
- // 3. REPLACE: Manual fetching with the Reactive Engine
55
- // This automatically connects to 'ramme_db_users' and keeps the count live
56
47
  const { data: users } = useCrudLocalStorage<User>('ramme_db_users', SEED_USERS);
57
-
58
- // Calculate count directly from the hook data
59
48
  const userCount = users.length;
60
49
 
61
50
  return (
62
- <div className="space-y-8 pb-10">
63
- <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
64
- <div>
65
- <PageHeader
66
- title="Dashboard"
67
- description="Overview of your application performance."
68
- />
69
- </div>
51
+ <StandardPageLayout
52
+ title="Dashboard"
53
+ description="Overview of your application performance."
54
+ actions={
70
55
  <div className="flex gap-2">
71
56
  <Button variant="outline" iconLeft="download">Export</Button>
72
57
  <Button iconLeft="plus">New Report</Button>
73
58
  </div>
74
- </div>
75
-
76
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
77
- <StatCard
78
- title="Total Users"
79
- value={userCount} // ✅ Uses live data from Data Lake
80
- trend={+12.5}
81
- icon="users"
82
- className="cursor-pointer bg-primary/5 border-primary/20"
83
- onClick={() => navigate('/dashboard/users')}
84
- />
85
- <StatCard title="Total Revenue" value="$45,231" trend={+20.1} icon="dollar-sign" />
86
- <StatCard title="Sales" value="+12,234" trend={+19} icon="credit-card" />
87
- <StatCard title="Active Now" value="+573" trend={-4} icon="activity" />
88
- </div>
59
+ }
60
+ >
61
+ <div className="space-y-8">
62
+ {/* Stat Cards */}
63
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
64
+ <StatCard
65
+ title="Total Users"
66
+ value={userCount}
67
+ trend={+12.5}
68
+ icon="users"
69
+ className="cursor-pointer bg-primary/5 border-primary/20"
70
+ onClick={() => navigate('/dashboard/users')}
71
+ />
72
+ <StatCard title="Total Revenue" value="$45,231" trend={+20.1} icon="dollar-sign" />
73
+ <StatCard title="Sales" value="+12,234" trend={+19} icon="credit-card" />
74
+ <StatCard title="Active Now" value="+573" trend={-4} icon="activity" />
75
+ </div>
89
76
 
90
- {/* Main Content Grid */}
91
- <div className="grid grid-cols-1 lg:grid-cols-7 gap-8">
92
- <Card className="lg:col-span-4 p-6">
93
- <div className="flex items-center justify-between mb-6">
94
- <h3 className="font-semibold text-lg">Revenue Overview</h3>
95
- <Badge variant="outline">Weekly</Badge>
96
- </div>
97
- <div className="h-[350px] w-full">
98
- <BarChart
99
- data={revenueData}
100
- dataKeyX="name"
101
- barKeys={['revenue', 'cost']}
102
- />
103
- </div>
104
- </Card>
77
+ {/* Main Content Grid */}
78
+ <div className="grid grid-cols-1 lg:grid-cols-7 gap-8">
79
+ <Card className="lg:col-span-4 p-6">
80
+ <div className="flex items-center justify-between mb-6">
81
+ <h3 className="font-semibold text-lg">Revenue Overview</h3>
82
+ <Badge variant="outline">Weekly</Badge>
83
+ </div>
84
+ <div className="h-[350px] w-full">
85
+ <BarChart
86
+ data={revenueData}
87
+ dataKeyX="name"
88
+ barKeys={['revenue', 'cost']}
89
+ />
90
+ </div>
91
+ </Card>
105
92
 
106
- <Card className="lg:col-span-3 p-6 flex flex-col">
107
- <h3 className="font-semibold text-lg mb-4">Recent Activity</h3>
108
- <div className="space-y-6 overflow-y-auto pr-2">
109
- {[1, 2, 3].map((i) => (
110
- <div key={i} className="flex items-start gap-4">
111
- <span className="relative flex h-2 w-2 mt-2">
112
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
113
- <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
114
- </span>
115
- <div className="space-y-1">
116
- <p className="text-sm font-medium leading-none">System Alert</p>
117
- <p className="text-xs text-muted-foreground">Database backup completed.</p>
118
- <p className="text-xs text-muted-foreground pt-1">Just now</p>
93
+ <Card className="lg:col-span-3 p-6 flex flex-col">
94
+ <h3 className="font-semibold text-lg mb-4">Recent Activity</h3>
95
+ <div className="space-y-6 overflow-y-auto pr-2">
96
+ {[1, 2, 3].map((i) => (
97
+ <div key={i} className="flex items-start gap-4">
98
+ <span className="relative flex h-2 w-2 mt-2">
99
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
100
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
101
+ </span>
102
+ <div className="space-y-1">
103
+ <p className="text-sm font-medium leading-none">System Alert</p>
104
+ <p className="text-xs text-muted-foreground">Database backup completed.</p>
105
+ <p className="text-xs text-muted-foreground pt-1">Just now</p>
106
+ </div>
119
107
  </div>
120
- </div>
121
- ))}
122
- </div>
123
- </Card>
108
+ ))}
109
+ </div>
110
+ </Card>
111
+ </div>
124
112
  </div>
125
- </div>
113
+ </StandardPageLayout>
126
114
  );
127
115
  };