@ramme-io/create-app 1.2.1 → 1.2.2
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 +1 -2
- package/template/package.json +41 -0
- package/template/pkg.json +1 -1
- package/template/src/App.tsx +62 -31
- package/template/src/components/AIChatWidget.tsx +2 -2
- package/template/src/components/AppHeader.tsx +2 -2
- package/template/src/components/AutoForm.tsx +13 -0
- package/template/src/{pages/styleguide → components}/NotFound.tsx +1 -1
- package/template/src/components/PageTitleUpdater.tsx +2 -2
- package/template/src/components/ProtectedRoute.tsx +18 -1
- package/template/src/components/ScrollToTop.tsx +19 -0
- package/template/src/config/app.manifest.ts +3 -1
- package/template/src/{core → config}/component-registry.tsx +1 -1
- package/template/src/config/navigation.ts +1 -1
- package/template/src/data/mock-charts.ts +32 -28
- package/template/src/{components → engine/renderers}/DynamicBlock.tsx +27 -7
- package/template/src/{pages → engine/renderers}/DynamicPage.tsx +23 -4
- package/template/src/{contexts → engine/runtime}/MqttContext.tsx +25 -11
- package/template/src/{contexts → engine/runtime}/SitemapContext.tsx +1 -1
- package/template/src/{core → engine/runtime}/data-seeder.ts +15 -5
- package/template/src/{hooks → engine/runtime}/useAction.ts +19 -8
- package/template/src/{hooks → engine/runtime}/useCrudLocalStorage.ts +27 -8
- package/template/src/{hooks → engine/runtime}/useDataQuery.ts +15 -1
- package/template/src/engine/runtime/useSignal.ts +51 -0
- package/template/src/{generated/hooks.ts → engine/runtime/useSignalStore.ts} +35 -8
- package/template/src/{hooks → engine/runtime}/useWorkflowEngine.ts +34 -13
- package/template/src/{core → engine/types}/manifest-types.ts +35 -3
- package/template/src/{types → engine/validation}/schema.ts +17 -0
- package/template/src/{pages → features/ai/pages}/AiChat.tsx +1 -1
- package/template/src/features/auth/AuthContext.tsx +118 -0
- package/template/src/features/auth/pages/AuthLayout.tsx +55 -0
- package/template/src/features/auth/pages/LoginPage.tsx +106 -0
- package/template/src/features/auth/pages/SignupPage.tsx +96 -0
- package/template/src/{blocks → features/datagrid}/SmartTable.tsx +4 -6
- package/template/src/{pages → features/onboarding/pages}/Welcome.tsx +0 -1
- package/template/src/features/overview/index.ts +1 -0
- package/template/src/features/overview/pages/OverviewPage.tsx +127 -0
- package/template/src/{pages → features/playground/pages}/AccountingLedgerPage.tsx +1 -1
- package/template/src/{pages/prototypes → features/playground/pages}/ItemSelectorPage.tsx +1 -1
- package/template/src/{pages/settings → features/settings/pages}/BillingPage.tsx +1 -1
- package/template/src/features/settings/pages/ProfilePage.tsx +153 -0
- package/template/src/{pages/settings → features/settings/pages}/TeamPage.tsx +1 -1
- package/template/src/features/styleguide/Styleguide.tsx +75 -0
- package/template/src/features/users/components/UserDrawer.tsx +138 -0
- package/template/src/features/users/index.ts +2 -0
- package/template/src/features/users/pages/UsersPage.tsx +151 -0
- package/template/src/index.css +1 -1
- package/template/src/main.tsx +3 -3
- package/template/src/templates/dashboard/DashboardLayout.tsx +75 -106
- package/template/src/templates/dashboard/dashboard.sitemap.ts +26 -22
- package/template/src/templates/docs/DocsLayout.tsx +49 -38
- package/template/src/templates/docs/docs.sitemap.ts +22 -34
- package/template/src/templates/settings/SettingsLayout.tsx +83 -143
- package/template/src/templates/settings/settings.sitemap.ts +6 -6
- package/template/vite.config.ts +12 -9
- package/template/src/adaptors/.gitkeep +0 -0
- package/template/src/components/LocalSideNav.tsx +0 -120
- package/template/src/components/PageWithSideNav.tsx +0 -69
- package/template/src/config/dashboard.layout.ts +0 -110
- package/template/src/contexts/AuthContext.tsx +0 -64
- package/template/src/data/mockUsers.ts +0 -18
- package/template/src/hooks/useSignal.ts +0 -83
- package/template/src/layouts/DataLayout.tsx +0 -37
- package/template/src/layouts/SideNavLayout.tsx +0 -28
- package/template/src/pages/Dashboard.tsx +0 -60
- package/template/src/pages/DataGridPage.tsx +0 -184
- package/template/src/pages/LoginPage.tsx +0 -58
- package/template/src/pages/settings/ProfilePage.tsx +0 -10
- package/template/src/pages/styleguide/Styleguide.tsx +0 -40
- package/template/src/templates/docs/pages/Introduction.tsx +0 -13
- package/template/src/types/signal.ts +0 -23
- /package/template/src/{core → engine/renderers}/route-generator.tsx +0 -0
- /package/template/src/{core → engine/types}/sitemap-entry.ts +0 -0
- /package/template/src/{pages → features}/GenericContentPage.tsx +0 -0
- /package/template/src/{hooks → features/assistant}/useMockChat.ts +0 -0
- /package/template/src/{components/dev → features/developer}/GhostOverlay.tsx +0 -0
- /package/template/src/{hooks → features/developer}/useDevTools.ts +0 -0
- /package/template/src/{pages → features}/styleguide/sections/charts/ChartsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/colors/ColorsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/elements/ElementsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/feedback/FeedbackSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/forms/FormsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/icons/IconsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/layout/LayoutSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/navigation/NavigationSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/tables/TablesSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/templates/TemplatesSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/theming/ThemingSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/utilities/UtilitiesSection.tsx +0 -0
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Button, Drawer, Icon } from '@ramme-io/ui';
|
|
3
|
-
import LocalSideNav from './LocalSideNav';
|
|
4
|
-
import type { NavItem } from './LocalSideNav';
|
|
5
|
-
|
|
6
|
-
interface PageWithSideNavProps {
|
|
7
|
-
navItems: NavItem[];
|
|
8
|
-
children: React.ReactNode;
|
|
9
|
-
sideNavHeader?: React.ReactNode;
|
|
10
|
-
contentWidth?: 'fixed' | 'full';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const PageWithSideNav: React.FC<PageWithSideNavProps> = ({
|
|
14
|
-
navItems,
|
|
15
|
-
children,
|
|
16
|
-
sideNavHeader,
|
|
17
|
-
contentWidth = 'fixed',
|
|
18
|
-
}) => {
|
|
19
|
-
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
|
|
20
|
-
|
|
21
|
-
const contentContainerClass = contentWidth === 'fixed'
|
|
22
|
-
? 'max-w-7xl mx-auto'
|
|
23
|
-
: '';
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="flex flex-col md:flex-row h-full">
|
|
27
|
-
{/* --- Mobile Header --- */}
|
|
28
|
-
<div className="md:hidden p-4 bg-card border-b border-border flex items-center justify-between sticky top-[65px] z-10">
|
|
29
|
-
{sideNavHeader}
|
|
30
|
-
<Button onClick={() => setIsMobileNavOpen(true)} variant="ghost" size="icon">
|
|
31
|
-
<Icon name="panel-left" />
|
|
32
|
-
</Button>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
{/* --- Desktop Sidebar --- */}
|
|
36
|
-
<aside className="hidden md:flex flex-col w-64 border-r border-border p-4 sticky top-[65px] h-[calc(100vh-65px)]">
|
|
37
|
-
{sideNavHeader}
|
|
38
|
-
<LocalSideNav navItems={navItems} className="mt-1" />
|
|
39
|
-
</aside>
|
|
40
|
-
|
|
41
|
-
{/* --- Mobile Drawer --- */}
|
|
42
|
-
<Drawer
|
|
43
|
-
isOpen={isMobileNavOpen}
|
|
44
|
-
onClose={() => setIsMobileNavOpen(false)}
|
|
45
|
-
position="left"
|
|
46
|
-
>
|
|
47
|
-
<div className="p-4">
|
|
48
|
-
<Button
|
|
49
|
-
onClick={() => setIsMobileNavOpen(false)}
|
|
50
|
-
variant="ghost"
|
|
51
|
-
size="icon"
|
|
52
|
-
className="absolute top-4 right-4"
|
|
53
|
-
>
|
|
54
|
-
<Icon name="x" />
|
|
55
|
-
</Button>
|
|
56
|
-
<div className="mt-2">{sideNavHeader}</div>
|
|
57
|
-
<LocalSideNav navItems={navItems} onLinkClick={() => setIsMobileNavOpen(false)} />
|
|
58
|
-
</div>
|
|
59
|
-
</Drawer>
|
|
60
|
-
|
|
61
|
-
{/* --- Main Content --- */}
|
|
62
|
-
<main className="flex-1 p-6 overflow-y-auto">
|
|
63
|
-
<div className={contentContainerClass}>
|
|
64
|
-
{children}
|
|
65
|
-
</div>
|
|
66
|
-
</main>
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
};
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file dashboard.layout.ts
|
|
3
|
-
* Defines the schema and data for the dynamic dashboard.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// 1. Define the Shape of our "Brain"
|
|
7
|
-
export interface DashboardItem {
|
|
8
|
-
id: string;
|
|
9
|
-
component: string;
|
|
10
|
-
props: Record<string, any>;
|
|
11
|
-
signalId?: string; // <-- Mark as Optional (?)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface DashboardSection {
|
|
15
|
-
id: string;
|
|
16
|
-
title: string;
|
|
17
|
-
type: string;
|
|
18
|
-
columns: number;
|
|
19
|
-
items: DashboardItem[]; // <-- Enforce the type here
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 2. The Data (Typed)
|
|
23
|
-
export const dashboardLayout: DashboardSection[] = [
|
|
24
|
-
{
|
|
25
|
-
id: "section_iot",
|
|
26
|
-
title: "Live Device Status",
|
|
27
|
-
type: "grid",
|
|
28
|
-
columns: 3,
|
|
29
|
-
items: [
|
|
30
|
-
{
|
|
31
|
-
id: "dev_1",
|
|
32
|
-
component: "DeviceCard",
|
|
33
|
-
props: {
|
|
34
|
-
title: "Living Room AC",
|
|
35
|
-
description: "Zone A • Floor 1",
|
|
36
|
-
icon: "thermometer",
|
|
37
|
-
status: "online",
|
|
38
|
-
trend: "Cooling to 70°"
|
|
39
|
-
},
|
|
40
|
-
signalId: "living_room_ac"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: "dev_2",
|
|
44
|
-
component: "DeviceCard",
|
|
45
|
-
props: {
|
|
46
|
-
title: "Air Quality",
|
|
47
|
-
description: "Sensor ID: #8842",
|
|
48
|
-
icon: "droplets",
|
|
49
|
-
status: "active",
|
|
50
|
-
trend: "Stable"
|
|
51
|
-
},
|
|
52
|
-
signalId: "living_room_hum"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: "dev_3",
|
|
56
|
-
component: "DeviceCard",
|
|
57
|
-
props: {
|
|
58
|
-
title: "Main Server",
|
|
59
|
-
description: "192.168.1.42",
|
|
60
|
-
icon: "server",
|
|
61
|
-
status: "online",
|
|
62
|
-
trend: "CPU Load"
|
|
63
|
-
},
|
|
64
|
-
signalId: "server_01"
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: "dev_4",
|
|
68
|
-
component: "DeviceCard",
|
|
69
|
-
props: {
|
|
70
|
-
title: "Front Door",
|
|
71
|
-
description: "Entryway • Camera 01",
|
|
72
|
-
icon: "lock", // This maps to the Lucide icon 'lock'
|
|
73
|
-
status: "offline", // Default state before data loads
|
|
74
|
-
trend: "Locked"
|
|
75
|
-
},
|
|
76
|
-
signalId: "front_door_lock" // <--- We will wire this next
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: "section_metrics",
|
|
82
|
-
title: "Business Overview",
|
|
83
|
-
type: "grid",
|
|
84
|
-
columns: 4,
|
|
85
|
-
items: [
|
|
86
|
-
{
|
|
87
|
-
id: "stat_1",
|
|
88
|
-
component: "StatCard",
|
|
89
|
-
props: {
|
|
90
|
-
title: "Total Users",
|
|
91
|
-
value: "1,234",
|
|
92
|
-
icon: "users",
|
|
93
|
-
changeText: "+10% from last month",
|
|
94
|
-
changeDirection: "positive"
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
id: "stat_2",
|
|
99
|
-
component: "StatCard",
|
|
100
|
-
props: {
|
|
101
|
-
title: "Sales Today",
|
|
102
|
-
value: "$5,678",
|
|
103
|
-
icon: "dollar-sign",
|
|
104
|
-
changeText: "+5% from yesterday",
|
|
105
|
-
changeDirection: "positive"
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
]
|
|
109
|
-
}
|
|
110
|
-
];
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
2
|
-
import { mockUsers, type User } from '../data/mockUsers';
|
|
3
|
-
|
|
4
|
-
interface AuthContextType {
|
|
5
|
-
user: User | null;
|
|
6
|
-
login: (username: string, password?: string) => Promise<User | null>;
|
|
7
|
-
logout: () => void;
|
|
8
|
-
isLoading: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
12
|
-
|
|
13
|
-
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
14
|
-
const [user, setUser] = useState<User | null>(null);
|
|
15
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
// Check for a logged-in user in localStorage on initial load
|
|
19
|
-
const storedUser = localStorage.getItem('authUser');
|
|
20
|
-
if (storedUser) {
|
|
21
|
-
setUser(JSON.parse(storedUser));
|
|
22
|
-
}
|
|
23
|
-
setIsLoading(false);
|
|
24
|
-
}, []);
|
|
25
|
-
|
|
26
|
-
const login = async (username: string, password?: string): Promise<User | null> => {
|
|
27
|
-
// Simulate API call
|
|
28
|
-
return new Promise((resolve) => {
|
|
29
|
-
setTimeout(() => {
|
|
30
|
-
const foundUser = mockUsers.find(
|
|
31
|
-
u => u.username === username && u.password === password
|
|
32
|
-
);
|
|
33
|
-
if (foundUser) {
|
|
34
|
-
const userToStore = { ...foundUser };
|
|
35
|
-
delete userToStore.password; // Don't store password
|
|
36
|
-
localStorage.setItem('authUser', JSON.stringify(userToStore));
|
|
37
|
-
setUser(userToStore);
|
|
38
|
-
resolve(userToStore);
|
|
39
|
-
} else {
|
|
40
|
-
resolve(null);
|
|
41
|
-
}
|
|
42
|
-
}, 500);
|
|
43
|
-
});
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const logout = () => {
|
|
47
|
-
localStorage.removeItem('authUser');
|
|
48
|
-
setUser(null);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
|
|
53
|
-
{children}
|
|
54
|
-
</AuthContext.Provider>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export const useAuth = () => {
|
|
59
|
-
const context = useContext(AuthContext);
|
|
60
|
-
if (context === undefined) {
|
|
61
|
-
throw new Error('useAuth must be used within an AuthProvider');
|
|
62
|
-
}
|
|
63
|
-
return context;
|
|
64
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface User {
|
|
2
|
-
id: number;
|
|
3
|
-
name: string;
|
|
4
|
-
email: string;
|
|
5
|
-
username: string; // <-- Add this
|
|
6
|
-
password?: string; // <-- Add this (optional for security)
|
|
7
|
-
role: 'Admin' | 'Editor' | 'Viewer';
|
|
8
|
-
status: 'Active' | 'Pending' | 'Banned';
|
|
9
|
-
createdAt: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// NOTE: In a real app, passwords would be hashed. This is for simulation only.
|
|
13
|
-
export const mockUsers: User[] = [
|
|
14
|
-
{ id: 1, name: 'Jane Cooper', email: 'jane.cooper@example.com', username: 'jane', password: 'password', role: 'Admin', status: 'Active', createdAt: '2023-01-15T10:00:00Z' },
|
|
15
|
-
{ id: 2, name: 'Cody Fisher', email: 'cody.fisher@example.com', username: 'cody', password: 'password', role: 'Editor', status: 'Active', createdAt: '2023-02-20T11:30:00Z' },
|
|
16
|
-
{ id: 3, name: 'Esther Howard', email: 'esther.howard@example.com', username: 'esther', password: 'password', role: 'Viewer', status: 'Pending', createdAt: '2023-03-05T09:15:00Z' },
|
|
17
|
-
// ... other users
|
|
18
|
-
];
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { useMqtt } from '../contexts/MqttContext';
|
|
3
|
-
import type { Signal } from '../types/signal';
|
|
4
|
-
|
|
5
|
-
interface SignalConfig<T> {
|
|
6
|
-
initialValue?: T;
|
|
7
|
-
min?: number;
|
|
8
|
-
max?: number;
|
|
9
|
-
interval?: number; // Mock mode only
|
|
10
|
-
unit?: string;
|
|
11
|
-
topic?: string; // Real mode only
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function useSignal<T = any>(signalId: string, config: SignalConfig<T> = {}): Signal<T> {
|
|
15
|
-
const {
|
|
16
|
-
initialValue,
|
|
17
|
-
min = -Infinity,
|
|
18
|
-
max = Infinity,
|
|
19
|
-
interval = 2000,
|
|
20
|
-
unit,
|
|
21
|
-
topic
|
|
22
|
-
} = config;
|
|
23
|
-
|
|
24
|
-
const { subscribe, unsubscribe, lastMessage, isConnected } = useMqtt();
|
|
25
|
-
|
|
26
|
-
const [signal, setSignal] = useState<Signal<T>>({
|
|
27
|
-
id: signalId,
|
|
28
|
-
value: initialValue as T,
|
|
29
|
-
unit: unit,
|
|
30
|
-
timestamp: Date.now(),
|
|
31
|
-
status: 'fresh',
|
|
32
|
-
max: max
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// --- REAL MODE: MQTT ---
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
if (!topic || !isConnected) return;
|
|
38
|
-
subscribe(topic);
|
|
39
|
-
return () => unsubscribe(topic);
|
|
40
|
-
}, [topic, isConnected, subscribe, unsubscribe]);
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (!topic || !lastMessage[topic]) return;
|
|
44
|
-
|
|
45
|
-
const rawValue = lastMessage[topic];
|
|
46
|
-
let parsedValue: any = rawValue;
|
|
47
|
-
|
|
48
|
-
// Auto-parse numbers and booleans
|
|
49
|
-
if (!isNaN(Number(rawValue))) parsedValue = Number(rawValue);
|
|
50
|
-
else if (rawValue.toLowerCase() === 'true' || rawValue === 'on') parsedValue = true;
|
|
51
|
-
else if (rawValue.toLowerCase() === 'false' || rawValue === 'off') parsedValue = false;
|
|
52
|
-
|
|
53
|
-
setSignal(prev => ({
|
|
54
|
-
...prev,
|
|
55
|
-
value: parsedValue,
|
|
56
|
-
timestamp: Date.now(),
|
|
57
|
-
status: 'fresh'
|
|
58
|
-
}));
|
|
59
|
-
}, [lastMessage, topic]);
|
|
60
|
-
|
|
61
|
-
// --- MOCK MODE: SIMULATION ---
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (topic) return; // Disable mock if topic exists
|
|
64
|
-
|
|
65
|
-
const timer = setInterval(() => {
|
|
66
|
-
setSignal(prev => {
|
|
67
|
-
let newValue: any = prev.value;
|
|
68
|
-
if (typeof prev.value === 'number') {
|
|
69
|
-
const variance = (Math.random() - 0.5) * 2;
|
|
70
|
-
let nextNum = prev.value + variance;
|
|
71
|
-
if (min !== undefined) nextNum = Math.max(min, nextNum);
|
|
72
|
-
if (max !== undefined) nextNum = Math.min(max, nextNum);
|
|
73
|
-
newValue = Number(nextNum.toFixed(1));
|
|
74
|
-
}
|
|
75
|
-
return { ...prev, value: newValue, timestamp: Date.now(), status: 'fresh' };
|
|
76
|
-
});
|
|
77
|
-
}, interval);
|
|
78
|
-
|
|
79
|
-
return () => clearInterval(timer);
|
|
80
|
-
}, [topic, min, max, interval]);
|
|
81
|
-
|
|
82
|
-
return signal;
|
|
83
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Outlet, useLocation } from 'react-router-dom';
|
|
3
|
-
import { PageWithSideNav } from '../components/PageWithSideNav';
|
|
4
|
-
import { useSitemap } from '../contexts/SitemapContext'; // Import the custom hook
|
|
5
|
-
|
|
6
|
-
const DataLayout: React.FC = () => {
|
|
7
|
-
const location = useLocation();
|
|
8
|
-
const sitemap = useSitemap(); // Consume the sitemap from context
|
|
9
|
-
const isDocsTemplate = location.pathname.startsWith('/docs');
|
|
10
|
-
|
|
11
|
-
if (isDocsTemplate) {
|
|
12
|
-
const dataSitemapSection = sitemap.find(item => item.id === 'data');
|
|
13
|
-
|
|
14
|
-
const navItems = (dataSitemapSection?.children || []).map(child => ({
|
|
15
|
-
label: child.title,
|
|
16
|
-
href: child.path,
|
|
17
|
-
icon: child.icon,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<PageWithSideNav
|
|
22
|
-
sideNavHeader={
|
|
23
|
-
<h2 className="text-lg font-semibold tracking-tight mb-2">
|
|
24
|
-
{dataSitemapSection?.title || 'Data'}
|
|
25
|
-
</h2>
|
|
26
|
-
}
|
|
27
|
-
navItems={navItems}
|
|
28
|
-
>
|
|
29
|
-
<Outlet />
|
|
30
|
-
</PageWithSideNav>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return <Outlet />;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export default DataLayout;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Outlet } from 'react-router-dom';
|
|
3
|
-
import { PageWithSideNav } from '../components/PageWithSideNav';
|
|
4
|
-
import type { NavItem } from '../components/LocalSideNav';
|
|
5
|
-
|
|
6
|
-
interface SideNavLayoutProps {
|
|
7
|
-
navItems: NavItem[];
|
|
8
|
-
sideNavHeader?: React.ReactNode;
|
|
9
|
-
contentWidth?: 'fixed' | 'full';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const SideNavLayout: React.FC<SideNavLayoutProps> = ({
|
|
13
|
-
navItems,
|
|
14
|
-
sideNavHeader,
|
|
15
|
-
contentWidth = 'fixed',
|
|
16
|
-
}) => {
|
|
17
|
-
return (
|
|
18
|
-
<PageWithSideNav
|
|
19
|
-
navItems={navItems}
|
|
20
|
-
sideNavHeader={sideNavHeader}
|
|
21
|
-
contentWidth={contentWidth}
|
|
22
|
-
>
|
|
23
|
-
<Outlet />
|
|
24
|
-
</PageWithSideNav>
|
|
25
|
-
);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export default SideNavLayout;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useLocation } from 'react-router-dom';
|
|
3
|
-
import { PageHeader, Alert } from '@ramme-io/ui';
|
|
4
|
-
import { appManifest } from '../config/app.manifest';
|
|
5
|
-
import { DynamicBlock } from '../components/DynamicBlock';
|
|
6
|
-
|
|
7
|
-
const Dashboard: React.FC = () => {
|
|
8
|
-
const location = useLocation();
|
|
9
|
-
|
|
10
|
-
// 1. Determine current slug from URL
|
|
11
|
-
const pathParts = location.pathname.split('/').filter(Boolean);
|
|
12
|
-
// If path is root or /dashboard, slug is 'dashboard', else take the last part
|
|
13
|
-
const currentSlug = pathParts.length > 1 ? pathParts[pathParts.length - 1] : 'dashboard';
|
|
14
|
-
|
|
15
|
-
// 2. Find the matching Page Definition
|
|
16
|
-
const pageDef = appManifest.pages?.find(p => p.slug === currentSlug)
|
|
17
|
-
|| appManifest.pages?.[0];
|
|
18
|
-
|
|
19
|
-
if (!pageDef) {
|
|
20
|
-
return (
|
|
21
|
-
<div className="p-8">
|
|
22
|
-
<Alert variant="warning" title="Page Not Found">
|
|
23
|
-
Could not find a definition for slug: <code>{currentSlug}</code>
|
|
24
|
-
</Alert>
|
|
25
|
-
</div>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className="space-y-8 relative">
|
|
31
|
-
<PageHeader
|
|
32
|
-
title={pageDef.title}
|
|
33
|
-
description={pageDef.description}
|
|
34
|
-
/>
|
|
35
|
-
|
|
36
|
-
{pageDef.sections.map((section) => (
|
|
37
|
-
<div key={section.id} className="relative">
|
|
38
|
-
{section.title && (
|
|
39
|
-
<h3 className="text-lg font-semibold mb-4 text-foreground">
|
|
40
|
-
{section.title}
|
|
41
|
-
</h3>
|
|
42
|
-
)}
|
|
43
|
-
|
|
44
|
-
<div
|
|
45
|
-
className="grid gap-6"
|
|
46
|
-
style={{
|
|
47
|
-
gridTemplateColumns: `repeat(${section.layout?.columns || 3}, minmax(300px, 1fr))`
|
|
48
|
-
}}
|
|
49
|
-
>
|
|
50
|
-
{section.blocks.map((block) => (
|
|
51
|
-
<DynamicBlock key={block.id} block={block} />
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export default Dashboard;
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
PageHeader, // <-- Add PageHeader back
|
|
4
|
-
DataTable,
|
|
5
|
-
Button,
|
|
6
|
-
Icon,
|
|
7
|
-
Drawer,
|
|
8
|
-
FormTemplate,
|
|
9
|
-
useToast,
|
|
10
|
-
Badge,
|
|
11
|
-
type ColDef,
|
|
12
|
-
type FormField,
|
|
13
|
-
type ICellRendererParams,
|
|
14
|
-
} from '@ramme-io/ui';
|
|
15
|
-
import { useCrudLocalStorage } from '../hooks/useCrudLocalStorage';
|
|
16
|
-
import { mockUsers, type User } from '../data/mockUsers';
|
|
17
|
-
|
|
18
|
-
// --- Custom Cell Renderers (No changes needed here) ---
|
|
19
|
-
const ActionsRenderer: React.FC<ICellRendererParams & { onEdit: (data: User) => void; onDelete: (data: User) => void; }> = ({ data, onEdit, onDelete }) => (
|
|
20
|
-
<div className="flex items-center justify-center gap-2 h-full">
|
|
21
|
-
<Button size="sm" variant="outline" onClick={() => onEdit(data)} aria-label="Edit">
|
|
22
|
-
<Icon name="edit" className="h-4 w-4" />
|
|
23
|
-
</Button>
|
|
24
|
-
<Button size="sm" variant="danger" onClick={() => onDelete(data)} aria-label="Delete">
|
|
25
|
-
<Icon name="trash-2" className="h-4 w-4" />
|
|
26
|
-
</Button>
|
|
27
|
-
</div>
|
|
28
|
-
);
|
|
29
|
-
interface StatusRendererProps extends ICellRendererParams {
|
|
30
|
-
value: User['status'];
|
|
31
|
-
}
|
|
32
|
-
const StatusRenderer: React.FC<StatusRendererProps> = ({ value }) => {
|
|
33
|
-
const variantMap: Record<User['status'], 'success' | 'warning' | 'danger'> = {
|
|
34
|
-
Active: 'success',
|
|
35
|
-
Pending: 'warning',
|
|
36
|
-
Banned: 'danger',
|
|
37
|
-
};
|
|
38
|
-
const variant = variantMap[value] || 'secondary';
|
|
39
|
-
return <Badge variant={variant}>{value}</Badge>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const DataGridPage: React.FC = () => {
|
|
44
|
-
const { addToast } = useToast();
|
|
45
|
-
const { data: users, createItem, updateItem, deleteItem } = useCrudLocalStorage<User>('users', mockUsers);
|
|
46
|
-
|
|
47
|
-
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
48
|
-
const [editingUser, setEditingUser] = useState<User | null>(null);
|
|
49
|
-
|
|
50
|
-
const handleOpenDrawer = (user: User | null = null) => {
|
|
51
|
-
setEditingUser(user);
|
|
52
|
-
setIsDrawerOpen(true);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// ... (handleDelete and handleFormSubmit functions remain the same)
|
|
56
|
-
const handleDelete = (user: User) => {
|
|
57
|
-
if (window.confirm(`Are you sure you want to delete ${user.name}?`)) {
|
|
58
|
-
deleteItem(user.id);
|
|
59
|
-
addToast('User deleted successfully', 'success');
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const handleFormSubmit = (formData: Record<string, any>) => {
|
|
64
|
-
if (editingUser) {
|
|
65
|
-
updateItem({ ...editingUser, ...formData });
|
|
66
|
-
addToast('User updated successfully', 'success');
|
|
67
|
-
} else {
|
|
68
|
-
const newUser: Omit<User, 'id' | 'createdAt'> = {
|
|
69
|
-
...formData,
|
|
70
|
-
} as Omit<User, 'id' | 'createdAt'>;
|
|
71
|
-
createItem({ ...newUser, createdAt: new Date().toISOString() });
|
|
72
|
-
addToast('User created successfully', 'success');
|
|
73
|
-
}
|
|
74
|
-
setIsDrawerOpen(false);
|
|
75
|
-
setEditingUser(null);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// ... (columnDefs and formFields definitions remain the same)
|
|
79
|
-
const columnDefs = useMemo<ColDef[]>(() => [
|
|
80
|
-
{ field: 'id', headerName: 'ID', width: 80, sortable: true },
|
|
81
|
-
{ field: 'name', headerName: 'Name', flex: 2, sortable: true, filter: 'agTextColumnFilter' },
|
|
82
|
-
{ field: 'email', headerName: 'Email', flex: 3, sortable: true, filter: 'agTextColumnFilter' },
|
|
83
|
-
{ field: 'role', headerName: 'Role', flex: 1, filter: 'agTextColumnFilter' },
|
|
84
|
-
{
|
|
85
|
-
field: 'status',
|
|
86
|
-
headerName: 'Status',
|
|
87
|
-
flex: 1,
|
|
88
|
-
cellRenderer: StatusRenderer,
|
|
89
|
-
filter: 'agTextColumnFilter'
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
field: 'createdAt',
|
|
93
|
-
headerName: 'Created At',
|
|
94
|
-
flex: 2,
|
|
95
|
-
sortable: true,
|
|
96
|
-
filter: 'agDateColumnFilter',
|
|
97
|
-
filterParams: {
|
|
98
|
-
comparator: (filterLocalDateAtMidnight: Date, cellValue: string) => {
|
|
99
|
-
if (cellValue == null) return -1;
|
|
100
|
-
const cellDate = new Date(cellValue);
|
|
101
|
-
cellDate.setHours(0,0,0,0);
|
|
102
|
-
if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) return 0;
|
|
103
|
-
return cellDate < filterLocalDateAtMidnight ? -1 : 1;
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
valueFormatter: params => new Date(params.value).toLocaleDateString(),
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
headerName: 'Actions',
|
|
110
|
-
width: 120,
|
|
111
|
-
pinned: 'right',
|
|
112
|
-
cellRenderer: (props: ICellRendererParams) => (
|
|
113
|
-
<ActionsRenderer {...props} onEdit={handleOpenDrawer} onDelete={handleDelete} />
|
|
114
|
-
),
|
|
115
|
-
},
|
|
116
|
-
], [handleDelete]);
|
|
117
|
-
|
|
118
|
-
const formFields: FormField[] = useMemo(() => [
|
|
119
|
-
{ name: 'name', label: 'Full Name', type: 'text', placeholder: 'Jane Doe', required: true, value: editingUser?.name },
|
|
120
|
-
{ name: 'email', label: 'Email Address', type: 'email', placeholder: 'jane.doe@example.com', required: true, value: editingUser?.email },
|
|
121
|
-
{ name: 'username', label: 'Username', type: 'text', placeholder: 'jane.doe', required: true, value: editingUser?.username },
|
|
122
|
-
{
|
|
123
|
-
name: 'role',
|
|
124
|
-
label: 'Role',
|
|
125
|
-
type: 'select',
|
|
126
|
-
options: [{value: 'Admin', label: 'Admin'}, {value: 'Editor', label: 'Editor'}, {value: 'Viewer', label: 'Viewer'}],
|
|
127
|
-
value: editingUser?.role
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
name: 'status',
|
|
131
|
-
label: 'Status',
|
|
132
|
-
type: 'select',
|
|
133
|
-
options: [{value: 'Active', label: 'Active'}, {value: 'Pending', label: 'Pending'}, {value: 'Banned', label: 'Banned'}],
|
|
134
|
-
value: editingUser?.status
|
|
135
|
-
},
|
|
136
|
-
], [editingUser]);
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<div className="space-y-6">
|
|
140
|
-
<PageHeader
|
|
141
|
-
title="User Management"
|
|
142
|
-
description="A complete CRUD example with persisted data and an edit-in-drawer pattern."
|
|
143
|
-
actions={
|
|
144
|
-
<Button
|
|
145
|
-
variant="primary"
|
|
146
|
-
// FIX: Changed from iconBefore to iconLeft and passed the icon name string
|
|
147
|
-
iconLeft="plus"
|
|
148
|
-
onClick={() => handleOpenDrawer()}
|
|
149
|
-
>
|
|
150
|
-
Create User
|
|
151
|
-
</Button>
|
|
152
|
-
}
|
|
153
|
-
/>
|
|
154
|
-
|
|
155
|
-
<DataTable
|
|
156
|
-
rowData={users}
|
|
157
|
-
columnDefs={columnDefs}
|
|
158
|
-
height="calc(100vh - 280px)"
|
|
159
|
-
enableQuickSearch
|
|
160
|
-
rowSelection="multiple"
|
|
161
|
-
pagination
|
|
162
|
-
paginationPageSize={10}
|
|
163
|
-
// FIX: Add the page size selector to resolve the AG Grid warning
|
|
164
|
-
paginationPageSizeSelector={[10, 25, 50]}
|
|
165
|
-
/>
|
|
166
|
-
|
|
167
|
-
<Drawer isOpen={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} title={editingUser ? 'Edit User' : 'Create New User'} size="lg">
|
|
168
|
-
<div className="p-6">
|
|
169
|
-
<FormTemplate
|
|
170
|
-
fields={formFields}
|
|
171
|
-
onSubmit={handleFormSubmit}
|
|
172
|
-
>
|
|
173
|
-
<div className="flex justify-end gap-2 mt-8">
|
|
174
|
-
<Button variant="outline" onClick={() => setIsDrawerOpen(false)}>Cancel</Button>
|
|
175
|
-
<Button type="submit">{editingUser ? 'Update User' : 'Create User'}</Button>
|
|
176
|
-
</div>
|
|
177
|
-
</FormTemplate>
|
|
178
|
-
</div>
|
|
179
|
-
</Drawer>
|
|
180
|
-
</div>
|
|
181
|
-
);
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
export default DataGridPage;
|