@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
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo } from 'react';
|
|
2
|
-
import { DataTable, Button, Icon, Card, useToast, type ColDef } from '@ramme-io/ui';
|
|
3
|
-
import { getResourceMeta, getMockData } from '../data/mockData';
|
|
4
|
-
import { AutoForm } from '../components/AutoForm';
|
|
5
|
-
// ✅ Import types that now definitely exist
|
|
6
|
-
import { useDataQuery, type FilterOption, type SortOption } from '../hooks/useDataQuery';
|
|
7
|
-
import { useCrudLocalStorage } from '../hooks/useCrudLocalStorage';
|
|
8
|
-
|
|
9
|
-
interface SmartTableProps {
|
|
10
|
-
dataId: string;
|
|
11
|
-
title?: string;
|
|
12
|
-
initialFilter?: Record<string, any>;
|
|
13
|
-
initialSort?: SortOption;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const SmartTable: React.FC<SmartTableProps> = ({
|
|
17
|
-
dataId,
|
|
18
|
-
title,
|
|
19
|
-
initialFilter,
|
|
20
|
-
initialSort
|
|
21
|
-
}) => {
|
|
22
|
-
const { addToast } = useToast();
|
|
23
|
-
|
|
24
|
-
// 1. STATE
|
|
25
|
-
const [page, setPage] = useState(1);
|
|
26
|
-
const [pageSize] = useState(10); // Kept as state in case we add a selector later
|
|
27
|
-
|
|
28
|
-
// 2. METADATA
|
|
29
|
-
const meta = getResourceMeta(dataId);
|
|
30
|
-
|
|
31
|
-
// 3. STORAGE LAYER
|
|
32
|
-
// Seed data if local storage is empty
|
|
33
|
-
const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
|
|
34
|
-
|
|
35
|
-
const {
|
|
36
|
-
data: rawData,
|
|
37
|
-
createItem,
|
|
38
|
-
updateItem
|
|
39
|
-
} = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
|
|
40
|
-
|
|
41
|
-
// 4. LOGIC LAYER
|
|
42
|
-
const activeFilters = useMemo<FilterOption[]>(() => {
|
|
43
|
-
if (!initialFilter) return [];
|
|
44
|
-
return Object.entries(initialFilter).map(([key, value]) => ({
|
|
45
|
-
field: key,
|
|
46
|
-
operator: 'equals',
|
|
47
|
-
value
|
|
48
|
-
}));
|
|
49
|
-
}, [initialFilter]);
|
|
50
|
-
|
|
51
|
-
const { data: processedRows, total, pageCount } = useDataQuery(rawData, {
|
|
52
|
-
filters: activeFilters,
|
|
53
|
-
sort: initialSort, // Using the prop directly for now (Fixes 'setSort' unused)
|
|
54
|
-
page,
|
|
55
|
-
pageSize
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// 5. UI STATE
|
|
59
|
-
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
60
|
-
const [currentRecord, setCurrentRecord] = useState<any>(null);
|
|
61
|
-
|
|
62
|
-
// 6. COLUMNS
|
|
63
|
-
const columns = useMemo(() => {
|
|
64
|
-
if (meta?.fields) {
|
|
65
|
-
const cols: ColDef[] = meta.fields.map((f: any) => {
|
|
66
|
-
const colDef: ColDef = {
|
|
67
|
-
field: f.key,
|
|
68
|
-
headerName: f.label,
|
|
69
|
-
filter: true,
|
|
70
|
-
sortable: true,
|
|
71
|
-
flex: 1,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
if (f.key.endsWith('Id')) {
|
|
75
|
-
const collectionName = f.key.replace('Id', 's');
|
|
76
|
-
const relatedData = getMockData(collectionName);
|
|
77
|
-
if (relatedData) {
|
|
78
|
-
colDef.valueFormatter = (params) => {
|
|
79
|
-
const match = relatedData.find((item: any) => item.id === params.value);
|
|
80
|
-
return match ? (match.name || match.title || params.value) : params.value;
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (f.type === 'currency') colDef.valueFormatter = (p: any) => p.value ? `$${p.value}` : '';
|
|
86
|
-
if (f.type === 'date') colDef.valueFormatter = (p: any) => p.value ? new Date(p.value).toLocaleDateString() : '';
|
|
87
|
-
|
|
88
|
-
if (f.type === 'status') {
|
|
89
|
-
colDef.cellRenderer = (p: any) => (
|
|
90
|
-
<span className={`px-2 py-1 rounded-full text-xs font-medium border
|
|
91
|
-
${['active', 'paid'].includes(p.value?.toLowerCase()) ? 'bg-green-100 text-green-800 border-green-200' :
|
|
92
|
-
['pending'].includes(p.value?.toLowerCase()) ? 'bg-yellow-100 text-yellow-800 border-yellow-200' : 'bg-slate-100 text-slate-800 border-slate-200'}`}>
|
|
93
|
-
{p.value}
|
|
94
|
-
</span>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
return colDef;
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
cols.push({
|
|
101
|
-
headerName: "Actions",
|
|
102
|
-
field: "id",
|
|
103
|
-
width: 100,
|
|
104
|
-
pinned: 'right',
|
|
105
|
-
cellRenderer: (params: any) => (
|
|
106
|
-
<Button variant="ghost" size="sm" onClick={() => { setCurrentRecord(params.data); setIsEditOpen(true); }}>
|
|
107
|
-
<Icon name="edit-2" className="w-4 h-4 text-slate-500" />
|
|
108
|
-
</Button>
|
|
109
|
-
)
|
|
110
|
-
});
|
|
111
|
-
return cols;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (processedRows.length > 0) {
|
|
115
|
-
return Object.keys(processedRows[0]).map(k => ({ field: k, headerName: k.toUpperCase(), flex: 1 }));
|
|
116
|
-
}
|
|
117
|
-
return [];
|
|
118
|
-
}, [meta, processedRows]);
|
|
119
|
-
|
|
120
|
-
const handleSave = (record: any) => {
|
|
121
|
-
if (record.id && currentRecord?.id) {
|
|
122
|
-
updateItem(record);
|
|
123
|
-
addToast('Record updated successfully', 'success');
|
|
124
|
-
} else {
|
|
125
|
-
const { id, ...newItem } = record;
|
|
126
|
-
createItem(newItem);
|
|
127
|
-
addToast('New record created', 'success');
|
|
128
|
-
}
|
|
129
|
-
setIsEditOpen(false);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<Card className="p-0 overflow-hidden border border-border flex flex-col h-full min-h-[500px]">
|
|
134
|
-
<div className="p-4 border-b border-border flex justify-between items-center bg-muted/20">
|
|
135
|
-
<div className="flex items-center gap-2">
|
|
136
|
-
<h3 className="text-lg font-semibold text-foreground">
|
|
137
|
-
{title || meta?.name || dataId}
|
|
138
|
-
</h3>
|
|
139
|
-
<span className="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">
|
|
140
|
-
{total} records
|
|
141
|
-
</span>
|
|
142
|
-
</div>
|
|
143
|
-
<Button size="sm" variant="outline" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
|
|
144
|
-
<Icon name="plus" className="mr-2 w-4 h-4" /> Add Item
|
|
145
|
-
</Button>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<div className="flex-1 w-full bg-card relative">
|
|
149
|
-
<DataTable
|
|
150
|
-
rowData={processedRows}
|
|
151
|
-
columnDefs={columns}
|
|
152
|
-
pagination={false} // We handle pagination logic manually below
|
|
153
|
-
/>
|
|
154
|
-
|
|
155
|
-
{/* Pagination Controls */}
|
|
156
|
-
<div className="p-2 border-t border-border flex justify-between items-center text-sm">
|
|
157
|
-
<span className="text-muted-foreground">
|
|
158
|
-
Page {page} of {pageCount || 1}
|
|
159
|
-
</span>
|
|
160
|
-
<div className="flex gap-2">
|
|
161
|
-
<Button
|
|
162
|
-
variant="ghost"
|
|
163
|
-
size="sm"
|
|
164
|
-
disabled={page === 1}
|
|
165
|
-
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
166
|
-
>
|
|
167
|
-
Prev
|
|
168
|
-
</Button>
|
|
169
|
-
<Button
|
|
170
|
-
variant="ghost"
|
|
171
|
-
size="sm"
|
|
172
|
-
disabled={page >= pageCount}
|
|
173
|
-
onClick={() => setPage(p => p + 1)}
|
|
174
|
-
>
|
|
175
|
-
Next
|
|
176
|
-
</Button>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<AutoForm
|
|
182
|
-
isOpen={isEditOpen}
|
|
183
|
-
onClose={() => setIsEditOpen(false)}
|
|
184
|
-
onSubmit={handleSave}
|
|
185
|
-
title={meta?.name || 'Item'}
|
|
186
|
-
fields={meta?.fields || []}
|
|
187
|
-
initialData={currentRecord}
|
|
188
|
-
/>
|
|
189
|
-
</Card>
|
|
190
|
-
);
|
|
191
|
-
};
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { NavLink, useLocation } from 'react-router-dom';
|
|
3
|
-
import { Icon, type IconName } from '@ramme-io/ui';
|
|
4
|
-
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
-
|
|
6
|
-
export interface NavItem {
|
|
7
|
-
label: string;
|
|
8
|
-
href: string;
|
|
9
|
-
icon?: IconName;
|
|
10
|
-
children?: NavItem[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface LocalSideNavProps {
|
|
14
|
-
navItems: NavItem[];
|
|
15
|
-
className?: string;
|
|
16
|
-
onLinkClick?: () => void; // 1. Add the optional prop
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const LocalSideNav: React.FC<LocalSideNavProps> = ({ navItems, className, onLinkClick }) => { // 2. Destructure prop
|
|
20
|
-
const location = useLocation();
|
|
21
|
-
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>({});
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
const currentParent = navItems.find(item =>
|
|
25
|
-
item.children?.some(child => location.pathname === child.href)
|
|
26
|
-
);
|
|
27
|
-
if (currentParent) {
|
|
28
|
-
setExpandedItems(prev => ({ ...prev, [currentParent.href]: true }));
|
|
29
|
-
}
|
|
30
|
-
}, [location.pathname, navItems]);
|
|
31
|
-
|
|
32
|
-
const handleToggle = (href: string) => {
|
|
33
|
-
setExpandedItems(prev => ({ ...prev, [href]: !prev[href] }));
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const childNavLinkClasses = ({ isActive }: { isActive: boolean }) =>
|
|
37
|
-
`flex items-center gap-2 text-sm transition-colors duration-200 py-1 ${
|
|
38
|
-
isActive
|
|
39
|
-
? 'text-primary font-medium'
|
|
40
|
-
: 'text-muted-foreground hover:text-text'
|
|
41
|
-
}`;
|
|
42
|
-
|
|
43
|
-
const renderNavLinks = (items: NavItem[]) => {
|
|
44
|
-
return items.map(item => {
|
|
45
|
-
const isExpanded = !!expandedItems[item.href];
|
|
46
|
-
|
|
47
|
-
if (item.children) {
|
|
48
|
-
return (
|
|
49
|
-
//Section link
|
|
50
|
-
<li key={item.href}>
|
|
51
|
-
<button
|
|
52
|
-
onClick={() => handleToggle(item.href)}
|
|
53
|
-
className="flex w-full items-center justify-between gap-2 py-1 font-normal text-text transition-colors duration-200 hover:text-primary"
|
|
54
|
-
>
|
|
55
|
-
<span className="flex items-center gap-2">
|
|
56
|
-
{item.icon && <Icon name={item.icon} className="h-4 w-4" />}
|
|
57
|
-
<span className="text-md">{item.label}</span>
|
|
58
|
-
</span>
|
|
59
|
-
<Icon
|
|
60
|
-
name="chevron-right"
|
|
61
|
-
className={`h-4 w-4 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
|
|
62
|
-
/>
|
|
63
|
-
</button>
|
|
64
|
-
<AnimatePresence initial={false}>
|
|
65
|
-
{isExpanded && (
|
|
66
|
-
<motion.ul
|
|
67
|
-
key="content"
|
|
68
|
-
initial="collapsed"
|
|
69
|
-
animate="open"
|
|
70
|
-
exit="collapsed"
|
|
71
|
-
variants={{
|
|
72
|
-
open: { opacity: 1, height: 'auto' },
|
|
73
|
-
collapsed: { opacity: 0, height: 0 },
|
|
74
|
-
}}
|
|
75
|
-
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
|
76
|
-
className="space-y-1 overflow-hidden border-l border-border pl-5 mt-1 ml-1"
|
|
77
|
-
>
|
|
78
|
-
{item.children.map(child => (
|
|
79
|
-
<li key={child.href}>
|
|
80
|
-
<NavLink
|
|
81
|
-
to={child.href}
|
|
82
|
-
className={childNavLinkClasses}
|
|
83
|
-
onClick={onLinkClick} // 3. Add onClick to child NavLink
|
|
84
|
-
>
|
|
85
|
-
{child.label}
|
|
86
|
-
</NavLink>
|
|
87
|
-
</li>
|
|
88
|
-
))}
|
|
89
|
-
</motion.ul>
|
|
90
|
-
)}
|
|
91
|
-
</AnimatePresence>
|
|
92
|
-
</li>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<li key={item.href}>
|
|
98
|
-
<NavLink
|
|
99
|
-
to={item.href}
|
|
100
|
-
className="flex items-center gap-2 py-1 text-lg font-semibold"
|
|
101
|
-
onClick={onLinkClick} // 3. Add onClick to top-level NavLink
|
|
102
|
-
>
|
|
103
|
-
{item.icon && <Icon name={item.icon} className="h-4 w-4" />}
|
|
104
|
-
{item.label}
|
|
105
|
-
</NavLink>
|
|
106
|
-
</li>
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<nav className={`w-full ${className || ''}`}>
|
|
113
|
-
<ul className="space-y-1">
|
|
114
|
-
{renderNavLinks(navItems)}
|
|
115
|
-
</ul>
|
|
116
|
-
</nav>
|
|
117
|
-
);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export default LocalSideNav;
|
|
@@ -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,40 +0,0 @@
|
|
|
1
|
-
import { useSignal } from '../hooks/useSignal';
|
|
2
|
-
|
|
3
|
-
export const useGeneratedSignals = () => {
|
|
4
|
-
|
|
5
|
-
// 🟢 REAL: Connected to public MQTT test broker
|
|
6
|
-
const living_room_ac = useSignal('living_room_ac', {
|
|
7
|
-
initialValue: 72,
|
|
8
|
-
min: 60,
|
|
9
|
-
max: 90,
|
|
10
|
-
unit: '°F',
|
|
11
|
-
topic: 'ramme/test/temp' // <--- The magic link
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
// 🟠 MOCK: Simulation Mode
|
|
15
|
-
const living_room_hum = useSignal('living_room_hum', {
|
|
16
|
-
initialValue: 45,
|
|
17
|
-
min: 40,
|
|
18
|
-
max: 60,
|
|
19
|
-
unit: '%'
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const server_01 = useSignal('server_01', {
|
|
23
|
-
initialValue: 42,
|
|
24
|
-
min: 10,
|
|
25
|
-
max: 95,
|
|
26
|
-
unit: '%'
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const front_door_lock = useSignal('front_door_lock', {
|
|
30
|
-
initialValue: 'LOCKED',
|
|
31
|
-
unit: ''
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
living_room_ac,
|
|
36
|
-
living_room_hum,
|
|
37
|
-
server_01,
|
|
38
|
-
front_door_lock,
|
|
39
|
-
};
|
|
40
|
-
};
|
|
@@ -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
|
-
}
|