@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.
Files changed (92) hide show
  1. package/package.json +1 -2
  2. package/template/package.json +41 -0
  3. package/template/pkg.json +1 -1
  4. package/template/src/App.tsx +65 -35
  5. package/template/src/components/AIChatWidget.tsx +2 -2
  6. package/template/src/components/AppHeader.tsx +2 -2
  7. package/template/src/components/AutoForm.tsx +13 -0
  8. package/template/src/{pages/styleguide → components}/NotFound.tsx +1 -1
  9. package/template/src/components/PageTitleUpdater.tsx +2 -2
  10. package/template/src/components/ProtectedRoute.tsx +18 -1
  11. package/template/src/components/ScrollToTop.tsx +19 -0
  12. package/template/src/config/app.manifest.ts +3 -1
  13. package/template/src/{core → config}/component-registry.tsx +1 -1
  14. package/template/src/config/navigation.ts +1 -1
  15. package/template/src/data/mock-charts.ts +32 -28
  16. package/template/src/{components → engine/renderers}/DynamicBlock.tsx +27 -7
  17. package/template/src/{pages → engine/renderers}/DynamicPage.tsx +23 -4
  18. package/template/src/{contexts → engine/runtime}/MqttContext.tsx +25 -11
  19. package/template/src/{contexts → engine/runtime}/SitemapContext.tsx +1 -1
  20. package/template/src/{core → engine/runtime}/data-seeder.ts +15 -5
  21. package/template/src/{hooks → engine/runtime}/useAction.ts +19 -8
  22. package/template/src/{hooks → engine/runtime}/useCrudLocalStorage.ts +27 -8
  23. package/template/src/{hooks → engine/runtime}/useDataQuery.ts +15 -1
  24. package/template/src/engine/runtime/useSignal.ts +51 -0
  25. package/template/src/engine/runtime/useSignalStore.ts +94 -0
  26. package/template/src/engine/runtime/useWorkflowEngine.ts +144 -0
  27. package/template/src/{core → engine/types}/manifest-types.ts +35 -3
  28. package/template/src/{types → engine/validation}/schema.ts +53 -2
  29. package/template/src/{pages → features/ai/pages}/AiChat.tsx +1 -1
  30. package/template/src/features/auth/AuthContext.tsx +118 -0
  31. package/template/src/features/auth/pages/AuthLayout.tsx +55 -0
  32. package/template/src/features/auth/pages/LoginPage.tsx +106 -0
  33. package/template/src/features/auth/pages/SignupPage.tsx +96 -0
  34. package/template/src/features/datagrid/SmartTable.tsx +222 -0
  35. package/template/src/features/onboarding/pages/Welcome.tsx +161 -0
  36. package/template/src/features/overview/index.ts +1 -0
  37. package/template/src/features/overview/pages/OverviewPage.tsx +127 -0
  38. package/template/src/{pages → features/playground/pages}/AccountingLedgerPage.tsx +1 -1
  39. package/template/src/{pages/prototypes → features/playground/pages}/ItemSelectorPage.tsx +1 -1
  40. package/template/src/{pages/settings → features/settings/pages}/BillingPage.tsx +1 -1
  41. package/template/src/features/settings/pages/ProfilePage.tsx +153 -0
  42. package/template/src/{pages/settings → features/settings/pages}/TeamPage.tsx +1 -1
  43. package/template/src/features/styleguide/Styleguide.tsx +75 -0
  44. package/template/src/features/users/components/UserDrawer.tsx +138 -0
  45. package/template/src/features/users/index.ts +2 -0
  46. package/template/src/features/users/pages/UsersPage.tsx +151 -0
  47. package/template/src/index.css +1 -1
  48. package/template/src/main.tsx +3 -3
  49. package/template/src/templates/dashboard/DashboardLayout.tsx +75 -106
  50. package/template/src/templates/dashboard/dashboard.sitemap.ts +34 -19
  51. package/template/src/templates/docs/DocsLayout.tsx +49 -38
  52. package/template/src/templates/docs/docs.sitemap.ts +22 -34
  53. package/template/src/templates/settings/SettingsLayout.tsx +83 -143
  54. package/template/src/templates/settings/settings.sitemap.ts +6 -6
  55. package/template/vite.config.ts +12 -9
  56. package/template/src/adaptors/.gitkeep +0 -0
  57. package/template/src/blocks/SmartTable.tsx +0 -191
  58. package/template/src/components/LocalSideNav.tsx +0 -120
  59. package/template/src/components/PageWithSideNav.tsx +0 -69
  60. package/template/src/config/dashboard.layout.ts +0 -110
  61. package/template/src/contexts/AuthContext.tsx +0 -64
  62. package/template/src/data/mockUsers.ts +0 -18
  63. package/template/src/generated/hooks.ts +0 -40
  64. package/template/src/hooks/useSignal.ts +0 -83
  65. package/template/src/hooks/useWorkflowEngine.ts +0 -6
  66. package/template/src/layouts/DataLayout.tsx +0 -37
  67. package/template/src/layouts/SideNavLayout.tsx +0 -28
  68. package/template/src/pages/Dashboard.tsx +0 -60
  69. package/template/src/pages/DataGridPage.tsx +0 -184
  70. package/template/src/pages/LoginPage.tsx +0 -58
  71. package/template/src/pages/settings/ProfilePage.tsx +0 -10
  72. package/template/src/pages/styleguide/Styleguide.tsx +0 -40
  73. package/template/src/templates/docs/pages/Introduction.tsx +0 -13
  74. package/template/src/types/signal.ts +0 -23
  75. /package/template/src/{core → engine/renderers}/route-generator.tsx +0 -0
  76. /package/template/src/{core → engine/types}/sitemap-entry.ts +0 -0
  77. /package/template/src/{pages → features}/GenericContentPage.tsx +0 -0
  78. /package/template/src/{hooks → features/assistant}/useMockChat.ts +0 -0
  79. /package/template/src/{components/dev → features/developer}/GhostOverlay.tsx +0 -0
  80. /package/template/src/{hooks → features/developer}/useDevTools.ts +0 -0
  81. /package/template/src/{pages → features}/styleguide/sections/charts/ChartsSection.tsx +0 -0
  82. /package/template/src/{pages → features}/styleguide/sections/colors/ColorsSection.tsx +0 -0
  83. /package/template/src/{pages → features}/styleguide/sections/elements/ElementsSection.tsx +0 -0
  84. /package/template/src/{pages → features}/styleguide/sections/feedback/FeedbackSection.tsx +0 -0
  85. /package/template/src/{pages → features}/styleguide/sections/forms/FormsSection.tsx +0 -0
  86. /package/template/src/{pages → features}/styleguide/sections/icons/IconsSection.tsx +0 -0
  87. /package/template/src/{pages → features}/styleguide/sections/layout/LayoutSection.tsx +0 -0
  88. /package/template/src/{pages → features}/styleguide/sections/navigation/NavigationSection.tsx +0 -0
  89. /package/template/src/{pages → features}/styleguide/sections/tables/TablesSection.tsx +0 -0
  90. /package/template/src/{pages → features}/styleguide/sections/templates/TemplatesSection.tsx +0 -0
  91. /package/template/src/{pages → features}/styleguide/sections/theming/ThemingSection.tsx +0 -0
  92. /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
- }