@ramme-io/create-app 1.2.1 → 1.2.5
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 +9 -15
- package/template/package.json +41 -0
- package/template/pkg.json +9 -7
- package/template/src/App.tsx +72 -31
- package/template/src/components/AIChatWidget.tsx +2 -2
- package/template/src/components/AppHeader.tsx +12 -12
- 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/components/dashboard/ChartLine.tsx +28 -0
- package/template/src/components/dashboard/StatCard.tsx +61 -0
- package/template/src/config/app.manifest.ts +3 -1
- package/template/src/config/component-registry.tsx +69 -0
- 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 +28 -8
- package/template/src/engine/renderers/DynamicPage.tsx +150 -0
- package/template/src/engine/runtime/ManifestContext.tsx +79 -0
- package/template/src/{contexts → engine/runtime}/MqttContext.tsx +23 -14
- package/template/src/{contexts → engine/runtime}/SitemapContext.tsx +1 -1
- package/template/src/engine/runtime/data-seeder.ts +47 -0
- package/template/src/{hooks → engine/runtime}/useAction.ts +11 -14
- 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/useDynamicSitemap.tsx +43 -0
- package/template/src/engine/runtime/useJustInTimeSeeder.ts +76 -0
- package/template/src/engine/runtime/useLiveBridge.ts +44 -0
- package/template/src/engine/runtime/useSignal.ts +40 -0
- package/template/src/{generated/hooks.ts → engine/runtime/useSignalStore.ts} +35 -8
- package/template/src/engine/runtime/useWorkflowEngine.ts +89 -0
- 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 +41 -25
- package/template/src/features/developer/GhostOverlay.tsx +114 -0
- 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/features/visualizations/SmartChart.tsx +178 -0
- package/template/src/index.css +1 -1
- package/template/src/main.tsx +27 -15
- package/template/src/templates/dashboard/DashboardLayout.tsx +77 -107
- package/template/src/templates/dashboard/dashboard.sitemap.ts +19 -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/tailwind.config.cjs +10 -9
- package/template/vite.config.ts +0 -11
- 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/components/dev/GhostOverlay.tsx +0 -68
- package/template/src/config/dashboard.layout.ts +0 -110
- package/template/src/contexts/AuthContext.tsx +0 -64
- package/template/src/core/component-registry.tsx +0 -56
- package/template/src/core/data-seeder.ts +0 -35
- package/template/src/data/mockUsers.ts +0 -18
- package/template/src/hooks/useSignal.ts +0 -83
- package/template/src/hooks/useWorkflowEngine.ts +0 -123
- 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/DynamicPage.tsx +0 -95
- 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/{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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Input,
|
|
5
|
+
Card,
|
|
6
|
+
SectionHeader,
|
|
7
|
+
Icon,
|
|
8
|
+
Alert
|
|
9
|
+
} from '@ramme-io/ui';
|
|
10
|
+
import { useAuth } from '../../auth/AuthContext';
|
|
11
|
+
|
|
12
|
+
// 1. REMOVE Zombie Service
|
|
13
|
+
// import { userService } from '../../users/api/user.service';
|
|
14
|
+
|
|
15
|
+
// 2. ADD Engine Hooks & Data
|
|
16
|
+
import { useCrudLocalStorage } from '../../../engine/runtime/useCrudLocalStorage';
|
|
17
|
+
import { SEED_USERS, type User } from '../../../data/mockData';
|
|
18
|
+
|
|
19
|
+
const ProfilePage: React.FC = () => {
|
|
20
|
+
const { user } = useAuth();
|
|
21
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
+
const [success, setSuccess] = useState(false);
|
|
23
|
+
|
|
24
|
+
// 3. CONNECT to Data Lake
|
|
25
|
+
// We only need 'updateItem' here to modify the existing profile
|
|
26
|
+
const { updateItem } = useCrudLocalStorage<User>('ramme_db_users', SEED_USERS);
|
|
27
|
+
|
|
28
|
+
const [formData, setFormData] = useState({
|
|
29
|
+
name: '',
|
|
30
|
+
email: '',
|
|
31
|
+
role: '',
|
|
32
|
+
bio: 'Product Designer based in San Francisco.'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Load current user data into form
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (user) {
|
|
38
|
+
setFormData({
|
|
39
|
+
name: user.name,
|
|
40
|
+
email: user.email,
|
|
41
|
+
role: user.role,
|
|
42
|
+
bio: 'Product Designer based in San Francisco.'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}, [user]);
|
|
46
|
+
|
|
47
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
48
|
+
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleSave = async (e: React.FormEvent) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setIsLoading(true);
|
|
54
|
+
setSuccess(false);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (user?.id) {
|
|
58
|
+
// 4. USE ENGINE to update the record in the Data Lake
|
|
59
|
+
// We merge the existing user object with the new form data
|
|
60
|
+
updateItem({ ...user, ...formData } as User);
|
|
61
|
+
|
|
62
|
+
// 5. REFRESH SESSION (Local hack to update UI header immediately)
|
|
63
|
+
// In a real app, AuthContext would listen to storage changes, but this is fine for a prototype.
|
|
64
|
+
localStorage.setItem('ramme_session', JSON.stringify({ ...user, ...formData }));
|
|
65
|
+
|
|
66
|
+
setSuccess(true);
|
|
67
|
+
setTimeout(() => setSuccess(false), 3000);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(error);
|
|
71
|
+
} finally {
|
|
72
|
+
setIsLoading(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="max-w-4xl space-y-6">
|
|
78
|
+
<SectionHeader title="My Profile" />
|
|
79
|
+
|
|
80
|
+
{success && (
|
|
81
|
+
<Alert variant="info" title="Changes Saved">
|
|
82
|
+
Your profile has been updated successfully.
|
|
83
|
+
</Alert>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
87
|
+
{/* Left Column - Avatar Card */}
|
|
88
|
+
<Card className="p-6 flex flex-col items-center text-center space-y-4 h-fit">
|
|
89
|
+
<div className="relative group cursor-pointer">
|
|
90
|
+
<div className="h-24 w-24 rounded-full bg-primary/10 flex items-center justify-center text-2xl font-bold text-primary border-4 border-background shadow-sm">
|
|
91
|
+
{formData.name.charAt(0)}
|
|
92
|
+
</div>
|
|
93
|
+
<div className="absolute inset-0 bg-black/50 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
|
94
|
+
<Icon name="camera" className="text-white" />
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div>
|
|
98
|
+
<h3 className="font-semibold text-lg">{formData.name}</h3>
|
|
99
|
+
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80 uppercase">
|
|
100
|
+
{formData.role}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="w-full pt-4 border-t">
|
|
104
|
+
<p className="text-xs text-muted-foreground mb-4">Joined December 2024</p>
|
|
105
|
+
<Button variant="outline" className="w-full" size="sm">Change Avatar</Button>
|
|
106
|
+
</div>
|
|
107
|
+
</Card>
|
|
108
|
+
|
|
109
|
+
{/* Right Column - Form */}
|
|
110
|
+
<Card className="md:col-span-2 p-6">
|
|
111
|
+
<form onSubmit={handleSave} className="space-y-4">
|
|
112
|
+
<div className="grid gap-2">
|
|
113
|
+
<h3 className="font-semibold text-lg">Personal Information</h3>
|
|
114
|
+
<p className="text-sm text-muted-foreground">Update your personal details here.</p>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="grid gap-4 py-4">
|
|
118
|
+
<Input
|
|
119
|
+
label="Full Name"
|
|
120
|
+
name="name"
|
|
121
|
+
value={formData.name}
|
|
122
|
+
onChange={handleChange}
|
|
123
|
+
/>
|
|
124
|
+
<Input
|
|
125
|
+
label="Email Address"
|
|
126
|
+
name="email"
|
|
127
|
+
value={formData.email}
|
|
128
|
+
onChange={handleChange}
|
|
129
|
+
/>
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<label className="text-sm font-medium leading-none">Bio</label>
|
|
132
|
+
<textarea
|
|
133
|
+
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
134
|
+
value={formData.bio}
|
|
135
|
+
disabled
|
|
136
|
+
/>
|
|
137
|
+
<p className="text-[0.8rem] text-muted-foreground">Bio editing is disabled in this demo.</p>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="flex justify-end gap-4">
|
|
142
|
+
<Button type="submit" disabled={isLoading}>
|
|
143
|
+
{isLoading ? 'Saving...' : 'Save Changes'}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
</form>
|
|
147
|
+
</Card>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default ProfilePage;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
// --- 1. Import the new generic page ---
|
|
3
|
-
import GenericContentPage from '
|
|
3
|
+
import GenericContentPage from '../../GenericContentPage';
|
|
4
4
|
|
|
5
5
|
// A simple page component for settings
|
|
6
6
|
const TeamPage: React.FC = () => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Sidebar, type SidebarItem, type IconName, Icon } from '@ramme-io/ui';
|
|
4
|
+
import { useSitemap } from '../../engine/runtime/SitemapContext';
|
|
5
|
+
|
|
6
|
+
const Styleguide: React.FC = () => {
|
|
7
|
+
const location = useLocation();
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
const sitemap = useSitemap();
|
|
10
|
+
|
|
11
|
+
// 1. Get Styleguide Section Data
|
|
12
|
+
const styleguideSection = useMemo(() => {
|
|
13
|
+
return sitemap.find(item => item.id === 'styleguide');
|
|
14
|
+
}, [sitemap]);
|
|
15
|
+
|
|
16
|
+
// 2. Transform to Sidebar Items
|
|
17
|
+
const navItems = useMemo<SidebarItem[]>(() => {
|
|
18
|
+
if (!styleguideSection?.children) return [];
|
|
19
|
+
|
|
20
|
+
// ✅ FIX: Remove the explicit ": { ... }" type. Let TS infer 'child' automatically.
|
|
21
|
+
return styleguideSection.children.map((child) => ({
|
|
22
|
+
id: child.id,
|
|
23
|
+
label: child.title,
|
|
24
|
+
// We cast to IconName because we know the string is a valid icon,
|
|
25
|
+
// and we provide a fallback 'hash' if it's undefined.
|
|
26
|
+
icon: (child.icon as IconName) || 'hash',
|
|
27
|
+
href: child.path ? `/docs/styleguide/${child.path}` : '/docs/styleguide',
|
|
28
|
+
}));
|
|
29
|
+
}, [styleguideSection]);
|
|
30
|
+
|
|
31
|
+
// 3. Determine Active Item
|
|
32
|
+
const activeItemId = useMemo(() => {
|
|
33
|
+
const active = navItems.find(item =>
|
|
34
|
+
location.pathname === item.href ||
|
|
35
|
+
(item.href && location.pathname.startsWith(item.href))
|
|
36
|
+
);
|
|
37
|
+
return active?.id;
|
|
38
|
+
}, [location.pathname, navItems]);
|
|
39
|
+
|
|
40
|
+
if (!location.pathname.startsWith('/docs') || !styleguideSection) {
|
|
41
|
+
return <Outlet />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex w-full min-h-[calc(100vh-7rem)]">
|
|
46
|
+
{/* SIDEBAR */}
|
|
47
|
+
<div className="sticky top-[7rem] h-[calc(100vh-7rem)] z-20">
|
|
48
|
+
<Sidebar
|
|
49
|
+
className="h-full border-r border-border bg-card/50 backdrop-blur-sm"
|
|
50
|
+
items={navItems}
|
|
51
|
+
activeItemId={activeItemId}
|
|
52
|
+
onNavigate={(item) => {
|
|
53
|
+
if (item.href) navigate(item.href);
|
|
54
|
+
}}
|
|
55
|
+
logo={
|
|
56
|
+
<div className="flex items-center gap-2 px-1 py-2 text-muted-foreground">
|
|
57
|
+
<Icon name="book-open" className="h-4 w-4" />
|
|
58
|
+
<span className="font-semibold text-xs uppercase tracking-wider">Reference</span>
|
|
59
|
+
</div>
|
|
60
|
+
}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* MAIN CONTENT */}
|
|
65
|
+
<main className="flex-1 min-w-0 bg-background overflow-x-hidden">
|
|
66
|
+
{/* ADDED: Max width constraint + centering */}
|
|
67
|
+
<div className="container max-w-7xl mx-auto p-6 md:p-10 animate-in fade-in duration-500">
|
|
68
|
+
<Outlet />
|
|
69
|
+
</div>
|
|
70
|
+
</main>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default Styleguide;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Drawer, Button, Input, Select, type SelectOption } from '@ramme-io/ui';
|
|
3
|
+
import type { User } from '../../../data/mockData';
|
|
4
|
+
|
|
5
|
+
interface UserDrawerProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
user: User | null;
|
|
9
|
+
onSave: (id: string, data: Partial<User>) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const UserDrawer: React.FC<UserDrawerProps> = ({ isOpen, onClose, user, onSave }) => {
|
|
13
|
+
const [formData, setFormData] = useState<Partial<User>>({
|
|
14
|
+
name: '',
|
|
15
|
+
email: '',
|
|
16
|
+
role: 'viewer',
|
|
17
|
+
status: 'active',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (user) {
|
|
22
|
+
setFormData(user);
|
|
23
|
+
} else {
|
|
24
|
+
setFormData({
|
|
25
|
+
name: '',
|
|
26
|
+
email: '',
|
|
27
|
+
role: 'viewer',
|
|
28
|
+
status: 'active',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}, [user, isOpen]);
|
|
32
|
+
|
|
33
|
+
// Options Configurations
|
|
34
|
+
const roleOptions: SelectOption[] = [
|
|
35
|
+
{ value: 'admin', label: 'Admin' },
|
|
36
|
+
{ value: 'editor', label: 'Editor' },
|
|
37
|
+
{ value: 'viewer', label: 'Viewer' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const statusOptions: SelectOption[] = [
|
|
41
|
+
{ value: 'active', label: 'Active' },
|
|
42
|
+
{ value: 'pending', label: 'Pending' },
|
|
43
|
+
{ value: 'banned', label: 'Banned' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// ✅ HELPER: Find the full object based on the string value
|
|
47
|
+
// This satisfies the TypeScript requirement: value={SelectOption}
|
|
48
|
+
const getOption = (options: SelectOption[], value: string | undefined) => {
|
|
49
|
+
return options.find((opt) => opt.value === value) || null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
53
|
+
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ✅ HELPER: Handle change for both Native Events and Custom Selects
|
|
57
|
+
const handleSelectChange = (name: keyof User, newValue: any) => {
|
|
58
|
+
// If the component returns a SelectOption object (Custom Select)
|
|
59
|
+
if (newValue && typeof newValue === 'object' && 'value' in newValue) {
|
|
60
|
+
setFormData({ ...formData, [name]: newValue.value });
|
|
61
|
+
}
|
|
62
|
+
// If the component returns a standard Event (Native Select)
|
|
63
|
+
else if (newValue?.target) {
|
|
64
|
+
setFormData({ ...formData, [name]: newValue.target.value });
|
|
65
|
+
}
|
|
66
|
+
// Fallback
|
|
67
|
+
else {
|
|
68
|
+
setFormData({ ...formData, [name]: newValue });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const idToSave = user?.id || '';
|
|
75
|
+
onSave(idToSave, formData);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Drawer
|
|
80
|
+
isOpen={isOpen}
|
|
81
|
+
onClose={onClose}
|
|
82
|
+
title={user ? 'Edit User' : 'Add New User'}
|
|
83
|
+
size="md"
|
|
84
|
+
>
|
|
85
|
+
<form onSubmit={handleSubmit} className="flex flex-col h-full">
|
|
86
|
+
<div className="flex-1 space-y-6 p-1">
|
|
87
|
+
<Input
|
|
88
|
+
label="Full Name"
|
|
89
|
+
name="name"
|
|
90
|
+
placeholder="Jane Doe"
|
|
91
|
+
value={formData.name || ''}
|
|
92
|
+
onChange={handleChange}
|
|
93
|
+
required
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<Input
|
|
97
|
+
label="Email Address"
|
|
98
|
+
name="email"
|
|
99
|
+
type="email"
|
|
100
|
+
placeholder="jane@example.com"
|
|
101
|
+
value={formData.email || ''}
|
|
102
|
+
onChange={handleChange}
|
|
103
|
+
required
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<Select
|
|
107
|
+
label="Role"
|
|
108
|
+
options={roleOptions}
|
|
109
|
+
// ✅ FIX: Pass the object, not the string
|
|
110
|
+
value={getOption(roleOptions, formData.role)}
|
|
111
|
+
// ✅ FIX: Handle the update safely
|
|
112
|
+
onChange={(val) => handleSelectChange('role', val)}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<Select
|
|
116
|
+
label="Status"
|
|
117
|
+
options={statusOptions}
|
|
118
|
+
// ✅ FIX: Pass the object, not the string
|
|
119
|
+
value={getOption(statusOptions, formData.status)}
|
|
120
|
+
// ✅ FIX: Handle the update safely
|
|
121
|
+
onChange={(val) => handleSelectChange('status', val)}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="flex justify-end gap-3 mt-8 pt-4 border-t border-border">
|
|
126
|
+
<Button variant="ghost" onClick={onClose} type="button">
|
|
127
|
+
Cancel
|
|
128
|
+
</Button>
|
|
129
|
+
<Button variant="primary" type="submit">
|
|
130
|
+
{user ? 'Save Changes' : 'Create User'}
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
</form>
|
|
134
|
+
</Drawer>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export default UserDrawer;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
PageHeader,
|
|
4
|
+
DataTable,
|
|
5
|
+
Button,
|
|
6
|
+
useToast,
|
|
7
|
+
Badge,
|
|
8
|
+
Input,
|
|
9
|
+
type ColDef,
|
|
10
|
+
type ICellRendererParams,
|
|
11
|
+
} from '@ramme-io/ui';
|
|
12
|
+
|
|
13
|
+
// 1. REMOVE: userService and old User types
|
|
14
|
+
// import { userService } from '../api/user.service';
|
|
15
|
+
// import type { User } from '../api/user.types';
|
|
16
|
+
|
|
17
|
+
// 2. ADD: The Engine Hook & Shared Data
|
|
18
|
+
import { useCrudLocalStorage } from '../../../engine/runtime/useCrudLocalStorage';
|
|
19
|
+
import { SEED_USERS, type User } from '../../../data/mockData';
|
|
20
|
+
|
|
21
|
+
import UserDrawer from '../components/UserDrawer';
|
|
22
|
+
|
|
23
|
+
const UsersPage: React.FC = () => {
|
|
24
|
+
const { addToast } = useToast();
|
|
25
|
+
|
|
26
|
+
// 3. REPLACE: Manual state fetching with the Reactive Engine
|
|
27
|
+
// This automatically loads 'ramme_db_users' and keeps it in sync.
|
|
28
|
+
const {
|
|
29
|
+
data: users,
|
|
30
|
+
createItem,
|
|
31
|
+
updateItem,
|
|
32
|
+
deleteItem
|
|
33
|
+
} = useCrudLocalStorage<User>('ramme_db_users', SEED_USERS);
|
|
34
|
+
|
|
35
|
+
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
36
|
+
const [editingUser, setEditingUser] = useState<User | null>(null);
|
|
37
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
38
|
+
|
|
39
|
+
// (Removed refreshData & useEffect - the hook handles this automatically)
|
|
40
|
+
|
|
41
|
+
const handleOpenDrawer = (user: User | null = null) => {
|
|
42
|
+
setEditingUser(user);
|
|
43
|
+
setIsDrawerOpen(true);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleDelete = (user: User) => {
|
|
47
|
+
if (confirm(`Delete ${user.name}?`)) {
|
|
48
|
+
deleteItem(user.id);
|
|
49
|
+
addToast('User deleted', 'success');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleSave = (_id: string, data: Partial<User>) => {
|
|
54
|
+
try {
|
|
55
|
+
if (editingUser) {
|
|
56
|
+
// Update existing user
|
|
57
|
+
updateItem({ ...editingUser, ...data } as User);
|
|
58
|
+
addToast('User updated', 'success');
|
|
59
|
+
} else {
|
|
60
|
+
// Create new user (Engine handles ID generation)
|
|
61
|
+
createItem({
|
|
62
|
+
...data,
|
|
63
|
+
role: data.role || 'viewer', // Ensure defaults
|
|
64
|
+
status: data.status || 'active',
|
|
65
|
+
joinedAt: new Date().toISOString()
|
|
66
|
+
} as User);
|
|
67
|
+
addToast('User created', 'success');
|
|
68
|
+
}
|
|
69
|
+
// Close drawer
|
|
70
|
+
setIsDrawerOpen(false);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
addToast('Error saving user', 'error');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const columnDefs = useMemo<ColDef[]>(() => [
|
|
77
|
+
{ field: 'name', headerName: 'Name', flex: 1, filter: true },
|
|
78
|
+
{ field: 'email', headerName: 'Email', flex: 1, filter: true },
|
|
79
|
+
{
|
|
80
|
+
field: 'role',
|
|
81
|
+
headerName: 'Role',
|
|
82
|
+
width: 120,
|
|
83
|
+
cellRenderer: (params: any) => (
|
|
84
|
+
<Badge variant={params.value === 'admin' ? 'primary' : 'secondary'}>
|
|
85
|
+
{params.value}
|
|
86
|
+
</Badge>
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
field: 'status',
|
|
91
|
+
width: 120,
|
|
92
|
+
cellRenderer: (params: any) => (
|
|
93
|
+
<Badge variant={params.value === 'active' ? 'success' : 'danger'}>
|
|
94
|
+
{params.value}
|
|
95
|
+
</Badge>
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
headerName: 'Actions',
|
|
100
|
+
width: 120,
|
|
101
|
+
pinned: 'right',
|
|
102
|
+
cellRenderer: (params: ICellRendererParams) => (
|
|
103
|
+
<div className="flex gap-2">
|
|
104
|
+
<Button size="sm" variant="ghost" iconLeft="edit" onClick={() => handleOpenDrawer(params.data)} />
|
|
105
|
+
<Button size="sm" variant="ghost" className="text-red-500 hover:text-red-600" iconLeft="trash-2" onClick={() => handleDelete(params.data)} />
|
|
106
|
+
</div>
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
], [deleteItem]); // Added dependency
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="space-y-6 h-[calc(100vh-140px)] flex flex-col">
|
|
113
|
+
<PageHeader
|
|
114
|
+
title="User Management"
|
|
115
|
+
description="Manage system access and permissions."
|
|
116
|
+
actions={
|
|
117
|
+
<Button variant="primary" iconLeft="plus" onClick={() => handleOpenDrawer()}>
|
|
118
|
+
Add User
|
|
119
|
+
</Button>
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<div className="w-full max-w-sm">
|
|
124
|
+
<Input
|
|
125
|
+
placeholder="Search users..."
|
|
126
|
+
value={searchTerm}
|
|
127
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<DataTable
|
|
132
|
+
rowData={users}
|
|
133
|
+
columnDefs={columnDefs}
|
|
134
|
+
height="100%"
|
|
135
|
+
quickFilterText={searchTerm}
|
|
136
|
+
pagination
|
|
137
|
+
paginationPageSize={10}
|
|
138
|
+
paginationPageSizeSelector={[10, 25, 50]}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<UserDrawer
|
|
142
|
+
isOpen={isDrawerOpen}
|
|
143
|
+
onClose={() => setIsDrawerOpen(false)}
|
|
144
|
+
user={editingUser}
|
|
145
|
+
onSave={handleSave}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default UsersPage;
|