@ramme-io/create-app 1.2.0 → 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 +65 -35
- 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/engine/runtime/useSignalStore.ts +94 -0
- package/template/src/engine/runtime/useWorkflowEngine.ts +144 -0
- package/template/src/{core → engine/types}/manifest-types.ts +35 -3
- package/template/src/{types → engine/validation}/schema.ts +53 -2
- 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/features/datagrid/SmartTable.tsx +222 -0
- package/template/src/features/onboarding/pages/Welcome.tsx +161 -0
- 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 +34 -19
- 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/blocks/SmartTable.tsx +0 -191
- 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/generated/hooks.ts +0 -40
- package/template/src/hooks/useSignal.ts +0 -83
- package/template/src/hooks/useWorkflowEngine.ts +0 -6
- 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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
// Import the User type from your mock data to ensure shape consistency
|
|
3
|
+
import type { User } from '../../data/mockData';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file AuthContext.tsx
|
|
7
|
+
* @description The Central Identity Manager.
|
|
8
|
+
* * ARCHITECTURAL ROLE:
|
|
9
|
+
* This context provides global access to the current user's state (Identity).
|
|
10
|
+
* It decouples the UI components from the specific authentication implementation.
|
|
11
|
+
* * KEY FEATURES:
|
|
12
|
+
* 1. **Session Persistence:** Checks localStorage on boot.
|
|
13
|
+
* 2. **Data Lake Connection:** Reads from 'ramme_db_users' to validate real accounts.
|
|
14
|
+
* 3. **Simulation:** Adds artificial latency to test loading states.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ✅ CRITICAL FIX: Match the key used by useCrudLocalStorage and data-seeder
|
|
18
|
+
const USER_DB_KEY = 'ramme_db_users';
|
|
19
|
+
const SESSION_KEY = 'ramme_session';
|
|
20
|
+
|
|
21
|
+
interface AuthContextType {
|
|
22
|
+
user: User | null;
|
|
23
|
+
login: (email: string, password: string) => Promise<void>;
|
|
24
|
+
logout: () => void;
|
|
25
|
+
isAuthenticated: boolean;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
32
|
+
const [user, setUser] = useState<User | null>(null);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
34
|
+
|
|
35
|
+
// Check for existing session on mount
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const storedUser = localStorage.getItem(SESSION_KEY);
|
|
38
|
+
if (storedUser) {
|
|
39
|
+
try {
|
|
40
|
+
setUser(JSON.parse(storedUser));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error("Failed to parse session", e);
|
|
43
|
+
localStorage.removeItem(SESSION_KEY);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const login = async (email: string, password: string) => {
|
|
50
|
+
setIsLoading(true);
|
|
51
|
+
|
|
52
|
+
// ✅ FIX: We now use the 'password' variable to satisfy TypeScript (ts(6133))
|
|
53
|
+
// This also adds a layer of realism to the mock validation.
|
|
54
|
+
if (!password || password.length < 3) {
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
throw new Error("Password must be at least 3 characters");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`🔐 Attempting login for: ${email}`);
|
|
60
|
+
|
|
61
|
+
// Simulate API Delay
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
63
|
+
|
|
64
|
+
// ✅ READ FROM THE CORRECT DATA LAKE
|
|
65
|
+
const storedUsers = localStorage.getItem(USER_DB_KEY);
|
|
66
|
+
|
|
67
|
+
// Debugging: See what is actually in storage
|
|
68
|
+
if (!storedUsers) {
|
|
69
|
+
console.warn(`⚠️ Data Lake '${USER_DB_KEY}' is empty! Did you run the seeder or signup?`);
|
|
70
|
+
} else {
|
|
71
|
+
// Optional: Log count for debugging (remove in production)
|
|
72
|
+
// console.log(`✅ Data Lake found. Users in DB:`, JSON.parse(storedUsers).length);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const users: User[] = storedUsers ? JSON.parse(storedUsers) : [];
|
|
76
|
+
|
|
77
|
+
// Case-insensitive email check
|
|
78
|
+
const foundUser = users.find(u => u.email.toLowerCase() === email.toLowerCase());
|
|
79
|
+
|
|
80
|
+
if (foundUser) {
|
|
81
|
+
console.log("🎉 User found:", foundUser);
|
|
82
|
+
setUser(foundUser);
|
|
83
|
+
localStorage.setItem(SESSION_KEY, JSON.stringify(foundUser));
|
|
84
|
+
setIsLoading(false);
|
|
85
|
+
} else {
|
|
86
|
+
console.error("❌ User NOT found. Available emails:", users.map(u => u.email));
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
throw new Error("Invalid email or password");
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const logout = () => {
|
|
93
|
+
setUser(null);
|
|
94
|
+
localStorage.removeItem(SESSION_KEY);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<AuthContext.Provider
|
|
99
|
+
value={{
|
|
100
|
+
user,
|
|
101
|
+
login,
|
|
102
|
+
logout,
|
|
103
|
+
isAuthenticated: !!user,
|
|
104
|
+
isLoading
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</AuthContext.Provider>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const useAuth = () => {
|
|
113
|
+
const context = useContext(AuthContext);
|
|
114
|
+
if (context === undefined) {
|
|
115
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
116
|
+
}
|
|
117
|
+
return context;
|
|
118
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
import { useTheme, Button, availableThemes, type ThemeConfig } from '@ramme-io/ui';
|
|
4
|
+
|
|
5
|
+
// --- Temporary Theme Switcher (Unchanged) ---
|
|
6
|
+
const MiniThemeSwitcher = () => {
|
|
7
|
+
const { setTheme, customTheme, setCustomTheme } = useTheme();
|
|
8
|
+
|
|
9
|
+
const cyberTheme: ThemeConfig = {
|
|
10
|
+
name: 'Cyberpunk',
|
|
11
|
+
colors: {
|
|
12
|
+
primary: '255 238 88', primaryForeground: '0 0 0',
|
|
13
|
+
secondary: '30 30 35', secondaryForeground: '255 255 255',
|
|
14
|
+
accent: '0 255 255', accentForeground: '0 0 0',
|
|
15
|
+
danger: '255 0 85', dangerForeground: '255 255 255',
|
|
16
|
+
background: '5 5 10', card: '20 20 25', text: '240 240 240',
|
|
17
|
+
border: '255 238 88', muted: '40 40 50', mutedForeground: '150 150 160',
|
|
18
|
+
input: '30 30 35', inputBorder: '80 80 90', ring: '255 238 88'
|
|
19
|
+
},
|
|
20
|
+
borderRadius: '0px',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="absolute top-4 right-4 flex gap-2 z-50">
|
|
25
|
+
{availableThemes.slice(0, 3).map(t => (
|
|
26
|
+
<Button key={t} size="sm" variant="ghost" onClick={() => setTheme(t)} className="opacity-50 hover:opacity-100">
|
|
27
|
+
{t}
|
|
28
|
+
</Button>
|
|
29
|
+
))}
|
|
30
|
+
<Button size="sm" variant="ghost" onClick={() => setCustomTheme(customTheme ? null : cyberTheme)} className="opacity-50 hover:opacity-100">
|
|
31
|
+
AI
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const AuthLayout: React.FC = () => {
|
|
38
|
+
return (
|
|
39
|
+
<div className="min-h-screen w-full flex items-center justify-center bg-background text-foreground relative transition-colors duration-300">
|
|
40
|
+
|
|
41
|
+
{/* Background Pattern */}
|
|
42
|
+
<div className="absolute inset-0 z-0 opacity-[0.03] pointer-events-none bg-[radial-gradient(#888_1px,transparent_1px)] [background-size:16px_16px]" />
|
|
43
|
+
|
|
44
|
+
<MiniThemeSwitcher />
|
|
45
|
+
|
|
46
|
+
{/* ✅ FIX: Removed the outer <Card>, Header, and Footer.
|
|
47
|
+
This is now a "Passthrough" layout. It centers the content
|
|
48
|
+
but lets the Page (LoginPage/SignupPage) own the UI completely.
|
|
49
|
+
*/}
|
|
50
|
+
<div className="z-10 relative w-full flex justify-center p-4">
|
|
51
|
+
<Outlet />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useNavigate, Link } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../AuthContext';
|
|
4
|
+
// 1. Remove 'Card' from imports
|
|
5
|
+
import { Button, Alert, Icon, Input } from '@ramme-io/ui';
|
|
6
|
+
|
|
7
|
+
const LoginPage: React.FC = () => {
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
const { login } = useAuth();
|
|
10
|
+
|
|
11
|
+
const [formData, setFormData] = useState({ email: '', password: '' });
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
|
+
|
|
15
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
16
|
+
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
|
17
|
+
if (error) setError(null);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setIsLoading(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
await login(formData.email, formData.password);
|
|
26
|
+
navigate('/dashboard');
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
setError(err.message || 'Invalid email or password.');
|
|
29
|
+
} finally {
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-screen items-center justify-center bg-background p-4 transition-colors duration-300">
|
|
36
|
+
|
|
37
|
+
{/* 2. REPLACED <Card> with a native <div>
|
|
38
|
+
- Removes conflicting library styles (border-border vs border-t-primary).
|
|
39
|
+
- Adds 'sm:w-[400px]' for better mobile responsiveness.
|
|
40
|
+
- Adds 'bg-card' explicitly for theme support.
|
|
41
|
+
*/}
|
|
42
|
+
<div className="w-full sm:w-[400px] p-8 rounded-xl shadow-2xl border-t-4 border-t-primary bg-card ring-1 ring-black/5 dark:ring-white/10">
|
|
43
|
+
|
|
44
|
+
{/* Header */}
|
|
45
|
+
<div className="text-center mb-8">
|
|
46
|
+
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mb-4 text-primary transition-transform hover:scale-110">
|
|
47
|
+
<Icon name="layout-template" size={24} />
|
|
48
|
+
</div>
|
|
49
|
+
<h1 className="text-2xl font-bold text-foreground tracking-tight">Welcome Back</h1>
|
|
50
|
+
<p className="text-sm text-muted-foreground mt-2">Enter your credentials to access your account</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Error State */}
|
|
54
|
+
{error && (
|
|
55
|
+
<Alert variant="danger" title="Access Denied" className="mb-6 animate-in fade-in slide-in-from-top-2">
|
|
56
|
+
{error}
|
|
57
|
+
</Alert>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{/* Form */}
|
|
61
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
62
|
+
<Input
|
|
63
|
+
name="email"
|
|
64
|
+
label="Email Address"
|
|
65
|
+
type="email"
|
|
66
|
+
placeholder="alex@example.com"
|
|
67
|
+
required
|
|
68
|
+
autoComplete="email"
|
|
69
|
+
value={formData.email}
|
|
70
|
+
onChange={handleChange}
|
|
71
|
+
// Explicit background to prevent transparency issues
|
|
72
|
+
className="bg-background"
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<div className="space-y-1">
|
|
76
|
+
<Input
|
|
77
|
+
name="password"
|
|
78
|
+
label="Password"
|
|
79
|
+
type="password"
|
|
80
|
+
placeholder="••••••••"
|
|
81
|
+
required
|
|
82
|
+
autoComplete="current-password"
|
|
83
|
+
value={formData.password}
|
|
84
|
+
onChange={handleChange}
|
|
85
|
+
className="bg-background"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<Button type="submit" loading={isLoading} className="w-full mt-2 font-medium" size="lg">
|
|
90
|
+
Sign In
|
|
91
|
+
</Button>
|
|
92
|
+
</form>
|
|
93
|
+
|
|
94
|
+
{/* Footer */}
|
|
95
|
+
<div className="mt-8 pt-6 border-t border-border text-center text-sm">
|
|
96
|
+
<span className="text-muted-foreground">Don't have an account? </span>
|
|
97
|
+
<Link to="/signup" className="font-semibold text-primary hover:text-primary/80 transition-colors">
|
|
98
|
+
Create one
|
|
99
|
+
</Link>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default LoginPage;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useNavigate, Link } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../AuthContext';
|
|
4
|
+
import { useCrudLocalStorage } from '../../../engine/runtime/useCrudLocalStorage';
|
|
5
|
+
import { Button, Card, FormTemplate, Alert, Icon, type FormField } from '@ramme-io/ui';
|
|
6
|
+
// Import SEED_USERS to safe-guard initialization if the DB is empty
|
|
7
|
+
import { SEED_USERS, type User } from '../../../data/mockData';
|
|
8
|
+
|
|
9
|
+
const SignupPage: React.FC = () => {
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
const { login } = useAuth();
|
|
12
|
+
|
|
13
|
+
// ✅ DATA HOOK: Direct access to the User Data Lake
|
|
14
|
+
// This ensures new users are saved to 'ramme_db_users' alongside seed data
|
|
15
|
+
const { data: users, createItem } = useCrudLocalStorage<User>('ramme_db_users', SEED_USERS);
|
|
16
|
+
|
|
17
|
+
const [error, setError] = useState<string | null>(null);
|
|
18
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
19
|
+
|
|
20
|
+
const handleSignup = async (formData: Record<string, any>) => {
|
|
21
|
+
setIsLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
|
|
24
|
+
// 1. Validation: Check for duplicates
|
|
25
|
+
if (users.some(u => u.email.toLowerCase() === formData.email.toLowerCase())) {
|
|
26
|
+
setError('An account with this email already exists.');
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// 2. Create the User Record in the Data Lake
|
|
33
|
+
// We cast to 'User' because we handle the 'id' generation in createItem
|
|
34
|
+
const newUser = {
|
|
35
|
+
name: formData.name,
|
|
36
|
+
email: formData.email,
|
|
37
|
+
role: 'viewer', // Default role for new signups
|
|
38
|
+
status: 'active',
|
|
39
|
+
joinedAt: new Date().toISOString().split('T')[0],
|
|
40
|
+
// In a real app, you'd hash the password here.
|
|
41
|
+
// For this prototype, the presence of the record implies access.
|
|
42
|
+
} as User;
|
|
43
|
+
|
|
44
|
+
createItem(newUser);
|
|
45
|
+
|
|
46
|
+
// 3. Auto-Login immediately after creation
|
|
47
|
+
await login(formData.email, formData.password);
|
|
48
|
+
navigate('/dashboard');
|
|
49
|
+
|
|
50
|
+
} catch (err: any) {
|
|
51
|
+
setError(err.message || 'Failed to create account.');
|
|
52
|
+
} finally {
|
|
53
|
+
setIsLoading(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const formFields: FormField[] = [
|
|
58
|
+
{ name: 'name', label: 'Full Name', type: 'text', placeholder: 'Jane Doe', required: true },
|
|
59
|
+
{ name: 'email', label: 'Email Address', type: 'email', placeholder: 'jane@example.com', required: true },
|
|
60
|
+
{ name: 'password', label: 'Password', type: 'password', placeholder: 'Min. 3 characters', required: true },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
|
65
|
+
<Card className="w-full max-w-md p-8 shadow-xl border-t-4 border-t-secondary">
|
|
66
|
+
<div className="text-center mb-8">
|
|
67
|
+
<div className="mx-auto w-12 h-12 bg-secondary/10 rounded-full flex items-center justify-center mb-4 text-secondary">
|
|
68
|
+
<Icon name="user-plus" size={24} />
|
|
69
|
+
</div>
|
|
70
|
+
<h1 className="text-3xl font-bold text-foreground">Create Account</h1>
|
|
71
|
+
<p className="text-muted-foreground mt-2">Join the platform to get started</p>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{error && <Alert variant="danger" title="Registration Failed" className="mb-4">{error}</Alert>}
|
|
75
|
+
|
|
76
|
+
<FormTemplate
|
|
77
|
+
fields={formFields}
|
|
78
|
+
onSubmit={handleSignup}
|
|
79
|
+
>
|
|
80
|
+
<Button type="submit" loading={isLoading} className="w-full mt-4" size="lg" variant="secondary">
|
|
81
|
+
Create Account
|
|
82
|
+
</Button>
|
|
83
|
+
</FormTemplate>
|
|
84
|
+
|
|
85
|
+
<div className="mt-6 text-center text-sm">
|
|
86
|
+
<span className="text-muted-foreground">Already have an account? </span>
|
|
87
|
+
<Link to="/login" className="font-semibold text-primary hover:underline">
|
|
88
|
+
Sign In
|
|
89
|
+
</Link>
|
|
90
|
+
</div>
|
|
91
|
+
</Card>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default SignupPage;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DataTable,
|
|
4
|
+
Button,
|
|
5
|
+
Icon,
|
|
6
|
+
Card,
|
|
7
|
+
useToast,
|
|
8
|
+
SearchInput,
|
|
9
|
+
type ColDef,
|
|
10
|
+
type GridApi
|
|
11
|
+
} from '@ramme-io/ui';
|
|
12
|
+
import { getResourceMeta, getMockData } from '../../data/mockData';
|
|
13
|
+
import { AutoForm } from '../../components/AutoForm';
|
|
14
|
+
import { useCrudLocalStorage } from '../../engine/runtime/useCrudLocalStorage';
|
|
15
|
+
|
|
16
|
+
interface SmartTableProps {
|
|
17
|
+
dataId: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
initialFilter?: Record<string, any>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SmartTable: React.FC<SmartTableProps> = ({
|
|
23
|
+
dataId,
|
|
24
|
+
title}) => {
|
|
25
|
+
const { addToast } = useToast();
|
|
26
|
+
|
|
27
|
+
// 1. DATA KERNEL
|
|
28
|
+
const meta = getResourceMeta(dataId);
|
|
29
|
+
const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
|
|
30
|
+
|
|
31
|
+
// We use the CRUD hook to persist changes to localStorage
|
|
32
|
+
const {
|
|
33
|
+
data: rowData,
|
|
34
|
+
createItem,
|
|
35
|
+
updateItem,
|
|
36
|
+
deleteItem
|
|
37
|
+
} = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
|
|
38
|
+
|
|
39
|
+
// 2. UI STATE
|
|
40
|
+
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
41
|
+
const [currentRecord, setCurrentRecord] = useState<any>(null);
|
|
42
|
+
const [gridApi, setGridApi] = useState<GridApi | null>(null);
|
|
43
|
+
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
44
|
+
const [quickFilterText, setQuickFilterText] = useState('');
|
|
45
|
+
|
|
46
|
+
// 3. COLUMN DEFINITIONS
|
|
47
|
+
const columns = useMemo<ColDef[]>(() => {
|
|
48
|
+
if (!meta?.fields) return [];
|
|
49
|
+
|
|
50
|
+
const generatedCols: ColDef[] = meta.fields.map((f: any) => {
|
|
51
|
+
const col: ColDef = {
|
|
52
|
+
field: f.key,
|
|
53
|
+
headerName: f.label,
|
|
54
|
+
filter: true,
|
|
55
|
+
sortable: true,
|
|
56
|
+
resizable: true,
|
|
57
|
+
flex: 1,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Smart Formatting based on type
|
|
61
|
+
if (f.type === 'currency') {
|
|
62
|
+
col.valueFormatter = (p: any) => p.value ? `$${Number(p.value).toLocaleString()}` : '';
|
|
63
|
+
}
|
|
64
|
+
if (f.type === 'date') {
|
|
65
|
+
col.valueFormatter = (p: any) => p.value ? new Date(p.value).toLocaleDateString() : '';
|
|
66
|
+
}
|
|
67
|
+
if (f.type === 'status') {
|
|
68
|
+
col.cellRenderer = (p: any) => {
|
|
69
|
+
const statusColors: any = {
|
|
70
|
+
active: 'bg-green-100 text-green-800',
|
|
71
|
+
paid: 'bg-green-100 text-green-800',
|
|
72
|
+
pending: 'bg-yellow-100 text-yellow-800',
|
|
73
|
+
inactive: 'bg-slate-100 text-slate-600',
|
|
74
|
+
overdue: 'bg-red-100 text-red-800'
|
|
75
|
+
};
|
|
76
|
+
const colorClass = statusColors[String(p.value).toLowerCase()] || 'bg-slate-100 text-slate-800';
|
|
77
|
+
return (
|
|
78
|
+
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
|
|
79
|
+
{p.value}
|
|
80
|
+
</span>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return col;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Add Checkbox Selection to the first column
|
|
88
|
+
if (generatedCols.length > 0) {
|
|
89
|
+
generatedCols[0].headerCheckboxSelection = true;
|
|
90
|
+
generatedCols[0].checkboxSelection = true;
|
|
91
|
+
generatedCols[0].minWidth = 180;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add Actions Column
|
|
95
|
+
generatedCols.push({
|
|
96
|
+
headerName: "Actions",
|
|
97
|
+
field: "id",
|
|
98
|
+
width: 100,
|
|
99
|
+
pinned: 'right',
|
|
100
|
+
cellRenderer: (params: any) => (
|
|
101
|
+
<div className="flex items-center gap-1">
|
|
102
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e) => { e.stopPropagation(); setCurrentRecord(params.data); setIsEditOpen(true); }}>
|
|
103
|
+
<Icon name="edit-2" size={14} className="text-slate-500" />
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return generatedCols;
|
|
110
|
+
}, [meta]);
|
|
111
|
+
|
|
112
|
+
// 4. HANDLERS
|
|
113
|
+
const onGridReady = useCallback((params: any) => {
|
|
114
|
+
setGridApi(params.api);
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const onSelectionChanged = useCallback(() => {
|
|
118
|
+
if (gridApi) {
|
|
119
|
+
setSelectedRows(gridApi.getSelectedRows());
|
|
120
|
+
}
|
|
121
|
+
}, [gridApi]);
|
|
122
|
+
|
|
123
|
+
const handleBulkDelete = () => {
|
|
124
|
+
if (confirm(`Delete ${selectedRows.length} items?`)) {
|
|
125
|
+
selectedRows.forEach(row => deleteItem(row.id));
|
|
126
|
+
setSelectedRows([]);
|
|
127
|
+
addToast(`${selectedRows.length} items deleted`, 'success');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleSave = (record: any) => {
|
|
132
|
+
if (record.id && currentRecord?.id) {
|
|
133
|
+
updateItem(record);
|
|
134
|
+
addToast('Item updated', 'success');
|
|
135
|
+
} else {
|
|
136
|
+
const { id, ...newItem } = record;
|
|
137
|
+
createItem(newItem);
|
|
138
|
+
addToast('Item created', 'success');
|
|
139
|
+
}
|
|
140
|
+
setIsEditOpen(false);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// --- RENDER ---
|
|
144
|
+
return (
|
|
145
|
+
<Card className="flex flex-col h-[600px] border border-border shadow-sm overflow-hidden bg-card">
|
|
146
|
+
|
|
147
|
+
{/* HEADER TOOLBAR */}
|
|
148
|
+
<div className="p-4 border-b border-border flex justify-between items-center gap-4 bg-muted/5">
|
|
149
|
+
|
|
150
|
+
{/* Left: Title or Bulk Actions */}
|
|
151
|
+
{selectedRows.length > 0 ? (
|
|
152
|
+
<div className="flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200">
|
|
153
|
+
<span className="bg-primary text-primary-foreground text-xs font-bold px-2 py-1 rounded-md">
|
|
154
|
+
{selectedRows.length} Selected
|
|
155
|
+
</span>
|
|
156
|
+
<Button size="sm" variant="danger" onClick={handleBulkDelete} iconLeft="trash-2">
|
|
157
|
+
Delete Selected
|
|
158
|
+
</Button>
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="flex items-center gap-2">
|
|
162
|
+
<div className="p-2 bg-primary/10 rounded-md text-primary">
|
|
163
|
+
<Icon name="table" size={18} />
|
|
164
|
+
</div>
|
|
165
|
+
<div>
|
|
166
|
+
<h3 className="text-base font-bold text-foreground leading-tight">
|
|
167
|
+
{title || meta?.name || dataId}
|
|
168
|
+
</h3>
|
|
169
|
+
<p className="text-xs text-muted-foreground">
|
|
170
|
+
{rowData.length} records found
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{/* Right: Actions & Filter */}
|
|
177
|
+
<div className="flex items-center gap-2">
|
|
178
|
+
<div className="w-64">
|
|
179
|
+
<SearchInput
|
|
180
|
+
placeholder="Quick search..."
|
|
181
|
+
value={quickFilterText}
|
|
182
|
+
onChange={(e) => {
|
|
183
|
+
setQuickFilterText(e.target.value);
|
|
184
|
+
gridApi?.setQuickFilter(e.target.value);
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="h-6 w-px bg-border mx-1" />
|
|
189
|
+
<Button size="sm" variant="primary" iconLeft="plus" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
|
|
190
|
+
Add New
|
|
191
|
+
</Button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* AG GRID */}
|
|
196
|
+
<div className="flex-1 w-full bg-card relative">
|
|
197
|
+
<DataTable
|
|
198
|
+
rowData={rowData}
|
|
199
|
+
columnDefs={columns}
|
|
200
|
+
onGridReady={onGridReady}
|
|
201
|
+
onSelectionChanged={onSelectionChanged}
|
|
202
|
+
rowSelection="multiple"
|
|
203
|
+
pagination={true}
|
|
204
|
+
paginationPageSize={10}
|
|
205
|
+
headerHeight={48}
|
|
206
|
+
rowHeight={48}
|
|
207
|
+
enableCellTextSelection={true}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* EDIT DRAWER */}
|
|
212
|
+
<AutoForm
|
|
213
|
+
isOpen={isEditOpen}
|
|
214
|
+
onClose={() => setIsEditOpen(false)}
|
|
215
|
+
onSubmit={handleSave}
|
|
216
|
+
title={meta?.name || 'Item'}
|
|
217
|
+
fields={meta?.fields || []}
|
|
218
|
+
initialData={currentRecord}
|
|
219
|
+
/>
|
|
220
|
+
</Card>
|
|
221
|
+
);
|
|
222
|
+
};
|