@ramme-io/create-app 1.1.9 → 1.2.0
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 +4 -3
- package/template/pkg.json +16 -13
- package/template/src/App.tsx +14 -7
- package/template/src/blocks/SmartTable.tsx +191 -0
- package/template/src/components/AutoForm.tsx +128 -0
- package/template/src/components/DynamicBlock.tsx +37 -31
- package/template/src/components/dev/GhostOverlay.tsx +26 -59
- package/template/src/config/app.manifest.ts +48 -48
- package/template/src/core/component-registry.tsx +21 -41
- package/template/src/core/data-seeder.ts +35 -0
- package/template/src/data/mockData.ts +163 -34
- package/template/src/generated/hooks.ts +34 -55
- package/template/src/hooks/useDataQuery.ts +84 -0
- package/template/src/hooks/useSignal.ts +43 -33
- package/template/src/hooks/useWorkflowEngine.ts +6 -0
- package/template/src/pages/Dashboard.tsx +43 -90
- package/template/src/pages/DynamicPage.tsx +54 -22
- package/template/src/templates/dashboard/DashboardLayout.tsx +2 -0
- package/template/src/templates/dashboard/dashboard.sitemap.ts +23 -73
- package/template/src/types/schema.ts +84 -31
|
@@ -2,55 +2,55 @@ import type { AppSpecification } from '../types/schema';
|
|
|
2
2
|
|
|
3
3
|
export const appManifest: AppSpecification = {
|
|
4
4
|
meta: {
|
|
5
|
-
name: "
|
|
6
|
-
version: "
|
|
7
|
-
description: "
|
|
8
|
-
author: "Ramme Builder"
|
|
5
|
+
name: "Ramme System Check",
|
|
6
|
+
version: "1.0.0",
|
|
7
|
+
description: "Verifying the Smart Runtime.",
|
|
8
|
+
author: "Ramme Builder",
|
|
9
9
|
},
|
|
10
10
|
config: {
|
|
11
|
-
theme: '
|
|
12
|
-
mockMode: false
|
|
11
|
+
theme: 'system',
|
|
12
|
+
mockMode: false,
|
|
13
|
+
brokerUrl: 'wss://test.mosquitto.org:8081',
|
|
13
14
|
},
|
|
14
|
-
domain: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
15
|
+
domain: { signals: [], entities: [] },
|
|
16
|
+
pages: [
|
|
17
|
+
{
|
|
18
|
+
id: "dashboard",
|
|
19
|
+
slug: "dashboard",
|
|
20
|
+
title: "System Status",
|
|
21
|
+
description: "Verifying Week 1 Logic Engine",
|
|
22
|
+
sections: [
|
|
23
|
+
{
|
|
24
|
+
id: "sect-users",
|
|
25
|
+
title: "User Management (CRUD Test)",
|
|
26
|
+
layout: { columns: 1 },
|
|
27
|
+
blocks: [
|
|
28
|
+
{
|
|
29
|
+
id: "table_users",
|
|
30
|
+
type: "SmartTable",
|
|
31
|
+
props: {
|
|
32
|
+
title: "Active Users",
|
|
33
|
+
dataId: "users" // ✅ Points to SEED_USERS
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "sect-invoices",
|
|
40
|
+
title: "Invoices (Relational Test)",
|
|
41
|
+
layout: { columns: 1 },
|
|
42
|
+
blocks: [
|
|
43
|
+
{
|
|
44
|
+
id: "table_invoices",
|
|
45
|
+
type: "SmartTable",
|
|
46
|
+
props: {
|
|
47
|
+
title: "Recent Invoices",
|
|
48
|
+
dataId: "invoices" // ✅ Points to SEED_INVOICES
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
56
|
};
|
|
@@ -5,65 +5,45 @@ import {
|
|
|
5
5
|
BarChart,
|
|
6
6
|
LineChart,
|
|
7
7
|
PieChart,
|
|
8
|
-
DataTable,
|
|
8
|
+
DataTable,
|
|
9
9
|
Card,
|
|
10
10
|
Alert,
|
|
11
11
|
EmptyState,
|
|
12
|
+
ToggleSwitch
|
|
12
13
|
} from '@ramme-io/ui';
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
15
|
+
// ✅ IMPORT YOUR CUSTOM COMPONENT
|
|
16
|
+
import { SmartTable } from '../blocks/SmartTable';
|
|
17
|
+
|
|
18
18
|
export const COMPONENT_REGISTRY: Record<string, React.FC<any>> = {
|
|
19
|
-
//
|
|
19
|
+
// IoT Primitives
|
|
20
20
|
DeviceCard,
|
|
21
|
+
|
|
22
|
+
// Data Display
|
|
21
23
|
StatCard,
|
|
22
24
|
BarChart,
|
|
23
25
|
LineChart,
|
|
24
26
|
PieChart,
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
// Tables
|
|
29
|
+
DataTable, // The raw grid
|
|
30
|
+
|
|
31
|
+
// ✅ FIX: Map "SmartTable" to the actual SmartTable component
|
|
32
|
+
// (Previously it was aliased to DataTable, which broke the UI)
|
|
33
|
+
SmartTable: SmartTable,
|
|
34
|
+
|
|
35
|
+
// Layout & Feedback
|
|
26
36
|
Card,
|
|
27
37
|
Alert,
|
|
28
38
|
EmptyState,
|
|
29
|
-
|
|
30
|
-
// --- 🛡️ ROBUSTNESS ALIASES ---
|
|
31
|
-
// These mappings allow the manifest to use lowercase or alternate names
|
|
32
|
-
// without crashing the application.
|
|
33
39
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
'Table': DataTable,
|
|
37
|
-
'grid': DataTable,
|
|
38
|
-
|
|
39
|
-
// Charts
|
|
40
|
-
'chart': BarChart, // Default generic chart to BarChart
|
|
41
|
-
'line': LineChart,
|
|
42
|
-
'bar': BarChart,
|
|
43
|
-
'pie': PieChart,
|
|
44
|
-
|
|
45
|
-
// IoT Fallbacks
|
|
46
|
-
'DataCard': DeviceCard,
|
|
47
|
-
'GaugeCard': DeviceCard,
|
|
48
|
-
'ToggleCard': DeviceCard,
|
|
49
|
-
'SliderCard': DeviceCard,
|
|
50
|
-
'SparklineCard': DeviceCard,
|
|
40
|
+
// Forms/Controls
|
|
41
|
+
ToggleSwitch
|
|
51
42
|
};
|
|
52
43
|
|
|
53
|
-
/**
|
|
54
|
-
* Helper to safely resolve a component.
|
|
55
|
-
* Returns a fallback if the component name is unknown.
|
|
56
|
-
*/
|
|
57
44
|
export const getComponent = (name: string) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// 2. If not found, try PascalCase (e.g. "deviceCard" -> "DeviceCard")
|
|
62
|
-
if (!Component) {
|
|
63
|
-
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
64
|
-
Component = COMPONENT_REGISTRY[pascalName];
|
|
65
|
-
}
|
|
66
|
-
|
|
45
|
+
const Component = COMPONENT_REGISTRY[name];
|
|
46
|
+
|
|
67
47
|
if (!Component) {
|
|
68
48
|
console.warn(`[Registry] Unknown component type: "${name}"`);
|
|
69
49
|
return () => (
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// ✅ Match the export from your new mockData.ts
|
|
2
|
+
import { DATA_REGISTRY } from '../data/mockData';
|
|
3
|
+
|
|
4
|
+
const DB_PREFIX = 'ramme_db_';
|
|
5
|
+
|
|
6
|
+
export const initializeDataLake = () => {
|
|
7
|
+
if (typeof window === 'undefined') return;
|
|
8
|
+
|
|
9
|
+
console.groupCollapsed('🌊 [Data Lake] Initialization');
|
|
10
|
+
|
|
11
|
+
Object.entries(DATA_REGISTRY).forEach(([key, seedData]) => {
|
|
12
|
+
const storageKey = `${DB_PREFIX}${key}`;
|
|
13
|
+
const existing = localStorage.getItem(storageKey);
|
|
14
|
+
|
|
15
|
+
if (!existing) {
|
|
16
|
+
console.log(`✨ Seeding collection: ${key} (${seedData.length} records)`);
|
|
17
|
+
localStorage.setItem(storageKey, JSON.stringify(seedData));
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`✅ Collection exists: ${key}`);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
console.groupEnd();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Utility to clear the lake (useful for a "Reset Data" button)
|
|
28
|
+
*/
|
|
29
|
+
export const resetDataLake = () => {
|
|
30
|
+
// ✅ FIX: Use DATA_REGISTRY (the new name)
|
|
31
|
+
Object.keys(DATA_REGISTRY).forEach((key) => {
|
|
32
|
+
localStorage.removeItem(`${DB_PREFIX}${key}`);
|
|
33
|
+
});
|
|
34
|
+
window.location.reload();
|
|
35
|
+
};
|
|
@@ -1,43 +1,172 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file src/data/mockData.ts
|
|
3
|
+
* @description The "Golden Copy" of seed data.
|
|
4
|
+
* These records are injected into localStorage when the app first launches.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ✅ 1. Interface for the Metadata (Schema)
|
|
8
|
+
export interface ResourceMeta {
|
|
9
|
+
name: string;
|
|
10
|
+
fields: {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
type: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
defaultValue?: any;
|
|
16
|
+
description?: string;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- DATA INTERFACES ---
|
|
21
|
+
|
|
22
|
+
export interface User {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
role: 'admin' | 'editor' | 'viewer';
|
|
27
|
+
status: 'active' | 'pending' | 'banned';
|
|
28
|
+
avatar?: string;
|
|
29
|
+
joinedAt: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Product {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
category: string;
|
|
36
|
+
price: number;
|
|
37
|
+
stock: number;
|
|
38
|
+
status: 'in_stock' | 'low_stock' | 'out_of_stock';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Invoice {
|
|
42
|
+
id: string;
|
|
43
|
+
userId: string; // Foreign Key -> User
|
|
44
|
+
amount: number;
|
|
45
|
+
status: 'paid' | 'pending' | 'overdue';
|
|
46
|
+
date: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Review {
|
|
50
|
+
id: string;
|
|
51
|
+
productId: string; // Foreign Key -> Product
|
|
52
|
+
rating: number;
|
|
53
|
+
comment: string;
|
|
54
|
+
author: string;
|
|
55
|
+
date: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ActivityLog {
|
|
59
|
+
id: string;
|
|
60
|
+
action: string;
|
|
61
|
+
entity: string;
|
|
62
|
+
timestamp: string;
|
|
63
|
+
user: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- SEED DATA ---
|
|
67
|
+
|
|
68
|
+
export const SEED_USERS: User[] = [
|
|
69
|
+
{ id: 'usr_1', name: 'Alex Carter', email: 'alex@example.com', role: 'admin', status: 'active', joinedAt: '2023-01-15' },
|
|
70
|
+
{ id: 'usr_2', name: 'Sarah Jenkins', email: 'sarah@example.com', role: 'editor', status: 'active', joinedAt: '2023-03-22' },
|
|
71
|
+
{ id: 'usr_3', name: 'Mike Ross', email: 'mike@example.com', role: 'viewer', status: 'pending', joinedAt: '2023-05-10' },
|
|
72
|
+
{ id: 'usr_4', name: 'Emily Blunt', email: 'emily@example.com', role: 'editor', status: 'banned', joinedAt: '2023-06-01' },
|
|
12
73
|
];
|
|
13
74
|
|
|
14
|
-
export const
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{ make: 'BMW', model: 'i4', price: 51400, electric: true },
|
|
75
|
+
export const SEED_PRODUCTS: Product[] = [
|
|
76
|
+
{ id: 'prod_1', name: 'ErgoChair Pro', category: 'Furniture', price: 450, stock: 12, status: 'in_stock' },
|
|
77
|
+
{ id: 'prod_2', name: 'Standing Desk', category: 'Furniture', price: 600, stock: 3, status: 'low_stock' },
|
|
78
|
+
{ id: 'prod_3', name: 'Monitor Arm', category: 'Accessories', price: 120, stock: 0, status: 'out_of_stock' },
|
|
79
|
+
{ id: 'prod_4', name: 'Mechanical Keyboard', category: 'Electronics', price: 180, stock: 25, status: 'in_stock' },
|
|
20
80
|
];
|
|
21
81
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
{
|
|
27
|
-
{ time: "8am", value: 3 },
|
|
28
|
-
{ time: "12pm", value: 5 },
|
|
29
|
-
{ time: "4pm", value: 2 },
|
|
30
|
-
{ time: "8pm", value: 3 }
|
|
82
|
+
export const SEED_INVOICES: Invoice[] = [
|
|
83
|
+
{ id: 'inv_001', userId: 'usr_2', amount: 450.00, status: 'paid', date: '2023-10-01' },
|
|
84
|
+
{ id: 'inv_002', userId: 'usr_1', amount: 1200.50, status: 'pending', date: '2023-10-05' },
|
|
85
|
+
{ id: 'inv_003', userId: 'usr_3', amount: 180.00, status: 'overdue', date: '2023-09-15' },
|
|
86
|
+
{ id: 'inv_004', userId: 'usr_2', amount: 600.00, status: 'paid', date: '2023-10-10' },
|
|
31
87
|
];
|
|
32
88
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
89
|
+
export const SEED_REVIEWS: Review[] = [
|
|
90
|
+
{ id: 'rev_1', productId: 'prod_1', rating: 5, comment: 'Life changing comfort!', author: 'Alex Carter', date: '2023-09-01' },
|
|
91
|
+
{ id: 'rev_2', productId: 'prod_4', rating: 4, comment: 'Clicky but loud.', author: 'Mike Ross', date: '2023-09-10' },
|
|
92
|
+
{ id: 'rev_3', productId: 'prod_2', rating: 2, comment: 'Wobbles at max height.', author: 'Sarah Jenkins', date: '2023-09-12' },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export const SEED_LOGS: ActivityLog[] = [
|
|
96
|
+
{ id: 'log_1', action: 'User Created', entity: 'User', user: 'System', timestamp: '2023-10-01T10:00:00Z' },
|
|
97
|
+
{ id: 'log_2', action: 'Invoice Paid', entity: 'Invoice', user: 'Alex Carter', timestamp: '2023-10-02T14:30:00Z' },
|
|
98
|
+
{ id: 'log_3', action: 'Stock Updated', entity: 'Product', user: 'Sarah Jenkins', timestamp: '2023-10-03T09:15:00Z' },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// --- REGISTRIES ---
|
|
102
|
+
|
|
103
|
+
// 1. Data Registry (The Records)
|
|
104
|
+
export const DATA_REGISTRY: Record<string, any[]> = {
|
|
105
|
+
users: SEED_USERS,
|
|
106
|
+
products: SEED_PRODUCTS,
|
|
107
|
+
invoices: SEED_INVOICES,
|
|
108
|
+
reviews: SEED_REVIEWS,
|
|
109
|
+
logs: SEED_LOGS,
|
|
39
110
|
};
|
|
40
111
|
|
|
41
|
-
|
|
42
|
-
|
|
112
|
+
// 2. Metadata Registry (The Schema/Definitions)
|
|
113
|
+
export const RESOURCE_METADATA: Record<string, ResourceMeta> = {
|
|
114
|
+
users: {
|
|
115
|
+
name: 'Users',
|
|
116
|
+
fields: [
|
|
117
|
+
{ key: 'name', label: 'Name', type: 'text', required: true },
|
|
118
|
+
{ key: 'email', label: 'Email', type: 'email', required: true },
|
|
119
|
+
{ key: 'role', label: 'Role', type: 'status' },
|
|
120
|
+
{ key: 'status', label: 'Status', type: 'status' },
|
|
121
|
+
{ key: 'joinedAt', label: 'Joined', type: 'date' },
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
products: {
|
|
125
|
+
name: 'Products',
|
|
126
|
+
fields: [
|
|
127
|
+
{ key: 'name', label: 'Product Name', type: 'text', required: true },
|
|
128
|
+
{ key: 'category', label: 'Category', type: 'text' },
|
|
129
|
+
{ key: 'price', label: 'Price', type: 'currency' },
|
|
130
|
+
{ key: 'stock', label: 'Stock', type: 'number' },
|
|
131
|
+
{ key: 'status', label: 'Availability', type: 'status' },
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
invoices: {
|
|
135
|
+
name: 'Invoices',
|
|
136
|
+
fields: [
|
|
137
|
+
// ✅ FIX: Changed label from 'User ID' to 'User'
|
|
138
|
+
{ key: 'userId', label: 'User', type: 'text', required: true },
|
|
139
|
+
{ key: 'amount', label: 'Amount', type: 'currency' },
|
|
140
|
+
{ key: 'status', label: 'Status', type: 'status' },
|
|
141
|
+
{ key: 'date', label: 'Date', type: 'date' },
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
reviews: {
|
|
145
|
+
name: 'Reviews',
|
|
146
|
+
fields: [
|
|
147
|
+
// ✅ FIX: Changed label from 'Product ID' to 'Product'
|
|
148
|
+
{ key: 'productId', label: 'Product', type: 'text' },
|
|
149
|
+
{ key: 'rating', label: 'Rating', type: 'number' },
|
|
150
|
+
{ key: 'comment', label: 'Comment', type: 'text' },
|
|
151
|
+
{ key: 'author', label: 'Author', type: 'text' },
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
logs: {
|
|
155
|
+
name: 'Activity Logs',
|
|
156
|
+
fields: [
|
|
157
|
+
{ key: 'action', label: 'Action', type: 'text' },
|
|
158
|
+
{ key: 'entity', label: 'Entity', type: 'text' },
|
|
159
|
+
{ key: 'user', label: 'User', type: 'text' },
|
|
160
|
+
{ key: 'timestamp', label: 'Time', type: 'date' },
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// --- HELPERS ---
|
|
166
|
+
|
|
167
|
+
export const getMockData = (id: string) => DATA_REGISTRY[id] || [];
|
|
168
|
+
|
|
169
|
+
// ✅ Updated to return ResourceMeta or null (instead of never/null)
|
|
170
|
+
export const getResourceMeta = (id: string): ResourceMeta | null => {
|
|
171
|
+
return RESOURCE_METADATA[id] || null;
|
|
43
172
|
};
|
|
@@ -1,61 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
// GENERATED ADAPTER CODE
|
|
3
|
-
// Config: LIVE MODE (HTTP)
|
|
4
|
-
// ------------------------------------------------------------------
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useSignal } from '../hooks/useSignal';
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
15
12
|
});
|
|
16
13
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
});
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const json = await response.json();
|
|
31
|
-
|
|
32
|
-
// Update Signals based on the paths defined in manifest
|
|
33
|
-
setSignals(prev => ({
|
|
34
|
-
...prev,
|
|
35
|
-
'mrr_stripe': {
|
|
36
|
-
value: getNestedValue(json, 'data.finance.mrr'),
|
|
37
|
-
unit: 'USD',
|
|
38
|
-
status: 'fresh'
|
|
39
|
-
},
|
|
40
|
-
'active_users': {
|
|
41
|
-
value: getNestedValue(json, 'data.users.total'),
|
|
42
|
-
unit: '',
|
|
43
|
-
status: 'fresh'
|
|
44
|
-
}
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
console.error("API Polling Error:", err);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
22
|
+
const server_01 = useSignal('server_01', {
|
|
23
|
+
initialValue: 42,
|
|
24
|
+
min: 10,
|
|
25
|
+
max: 95,
|
|
26
|
+
unit: '%'
|
|
27
|
+
});
|
|
51
28
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const interval = setInterval(pollEndpoints, 5000);
|
|
57
|
-
return () => clearInterval(interval);
|
|
58
|
-
}, []);
|
|
29
|
+
const front_door_lock = useSignal('front_door_lock', {
|
|
30
|
+
initialValue: 'LOCKED',
|
|
31
|
+
unit: ''
|
|
32
|
+
});
|
|
59
33
|
|
|
60
|
-
return
|
|
61
|
-
|
|
34
|
+
return {
|
|
35
|
+
living_room_ac,
|
|
36
|
+
living_room_hum,
|
|
37
|
+
server_01,
|
|
38
|
+
front_door_lock,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
// --- 1. Export the Missing Types (Fixes ts(2305)) ---
|
|
4
|
+
|
|
5
|
+
export type SortDirection = 'asc' | 'desc';
|
|
6
|
+
|
|
7
|
+
export interface SortOption {
|
|
8
|
+
field: string;
|
|
9
|
+
direction: SortDirection;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FilterOption {
|
|
13
|
+
field: string;
|
|
14
|
+
operator: 'equals' | 'contains' | 'gt' | 'lt' | 'neq';
|
|
15
|
+
value: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueryOptions {
|
|
19
|
+
filters?: FilterOption[];
|
|
20
|
+
sort?: SortOption;
|
|
21
|
+
page?: number;
|
|
22
|
+
pageSize?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface QueryResult<T> {
|
|
26
|
+
data: T[];
|
|
27
|
+
total: number;
|
|
28
|
+
pageCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- 2. Update the Hook Signature (Fixes ts(2345)) ---
|
|
32
|
+
|
|
33
|
+
export function useDataQuery<T>(
|
|
34
|
+
rawData: T[], // <--- Now accepts an Array, NOT a string ID
|
|
35
|
+
options: QueryOptions = {}
|
|
36
|
+
): QueryResult<T> {
|
|
37
|
+
const { filters, sort, page = 1, pageSize = 10 } = options;
|
|
38
|
+
|
|
39
|
+
// A. Filtering Logic
|
|
40
|
+
const filteredData = useMemo(() => {
|
|
41
|
+
if (!filters || filters.length === 0) return rawData;
|
|
42
|
+
|
|
43
|
+
return rawData.filter((item: any) => {
|
|
44
|
+
return filters.every((filter) => {
|
|
45
|
+
const itemValue = item[filter.field];
|
|
46
|
+
|
|
47
|
+
switch (filter.operator) {
|
|
48
|
+
case 'equals': return itemValue == filter.value;
|
|
49
|
+
case 'neq': return itemValue != filter.value;
|
|
50
|
+
case 'contains':
|
|
51
|
+
return String(itemValue).toLowerCase().includes(String(filter.value).toLowerCase());
|
|
52
|
+
case 'gt': return itemValue > filter.value;
|
|
53
|
+
case 'lt': return itemValue < filter.value;
|
|
54
|
+
default: return true;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}, [rawData, filters]);
|
|
59
|
+
|
|
60
|
+
// B. Sorting Logic
|
|
61
|
+
const sortedData = useMemo(() => {
|
|
62
|
+
if (!sort) return filteredData;
|
|
63
|
+
|
|
64
|
+
return [...filteredData].sort((a: any, b: any) => {
|
|
65
|
+
const aValue = a[sort.field];
|
|
66
|
+
const bValue = b[sort.field];
|
|
67
|
+
if (aValue < bValue) return sort.direction === 'asc' ? -1 : 1;
|
|
68
|
+
if (aValue > bValue) return sort.direction === 'asc' ? 1 : -1;
|
|
69
|
+
return 0;
|
|
70
|
+
});
|
|
71
|
+
}, [filteredData, sort]);
|
|
72
|
+
|
|
73
|
+
// C. Pagination Logic
|
|
74
|
+
const paginatedResult = useMemo(() => {
|
|
75
|
+
const startIndex = (page - 1) * pageSize;
|
|
76
|
+
return sortedData.slice(startIndex, startIndex + pageSize);
|
|
77
|
+
}, [sortedData, page, pageSize]);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
data: paginatedResult,
|
|
81
|
+
total: filteredData.length,
|
|
82
|
+
pageCount: Math.ceil(filteredData.length / pageSize),
|
|
83
|
+
};
|
|
84
|
+
}
|