@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.
- package/package.json +14 -9
- package/template/package-lock.json +5474 -0
- package/template/src/components/layout/StandardPageLayout.tsx +41 -0
- package/template/src/engine/renderers/DynamicPage.tsx +25 -35
- package/template/src/features/GenericContentPage.tsx +19 -14
- package/template/src/features/auth/pages/LoginPage.tsx +1 -1
- package/template/src/features/datagrid/SmartTable.tsx +117 -43
- package/template/src/features/overview/pages/OverviewPage.tsx +57 -69
- package/template/src/features/settings/pages/ProfilePage.tsx +70 -80
- package/template/src/features/styleguide/Styleguide.tsx +8 -9
- package/template/src/features/users/pages/UsersPage.tsx +41 -52
- package/template/src/templates/dashboard/DashboardLayout.tsx +8 -11
- package/template/DESIGNER_GUIDE.md +0 -43
|
@@ -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,
|
|
2
|
-
import {
|
|
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';
|
|
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
|
-
//
|
|
9
|
-
//
|
|
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;
|
|
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
|
-
<
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
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
|
-
</
|
|
138
|
+
</StandardPageLayout>
|
|
149
139
|
);
|
|
150
140
|
};
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
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;
|
|
7
|
+
children?: React.ReactNode;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
const GenericContentPage: React.FC<GenericContentPageProps> = ({ pageTitle, children }) => {
|
|
10
11
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
210
|
+
Add
|
|
211
211
|
</Button>
|
|
212
212
|
</div>
|
|
213
213
|
</div>
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
<div className="
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
</
|
|
123
|
-
</
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
</Card>
|
|
111
|
+
</div>
|
|
124
112
|
</div>
|
|
125
|
-
</
|
|
113
|
+
</StandardPageLayout>
|
|
126
114
|
);
|
|
127
115
|
};
|