your-ai-workflow-firebase-os 1.2.7 → 1.2.9
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/dist/ContactPopup-BcjnOBWb.js +7544 -0
- package/dist/ContactPopup-gaUJCR67.cjs +9 -0
- package/dist/FirebaseOS.d.ts +5 -0
- package/dist/PhoneInputField-Bwos3cqE.js +2042 -0
- package/dist/PhoneInputField-CSoNnJ2Y.cjs +1 -0
- package/dist/chunk-DYteiM-1.cjs +1 -0
- package/dist/contactForm.config-CXyq-EbM.cjs +1 -0
- package/dist/contactForm.config-DfZU9Fr1.js +2 -0
- package/dist/lib/ConfigContext.d.ts +5 -0
- package/dist/lib/ThemeContext.d.ts +2 -2
- package/dist/renderer.cjs.js +1 -0
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.es.js +2 -0
- package/dist/supportForm.config-M9zQs6qp.js +2 -0
- package/dist/supportForm.config-haLod4rv.cjs +1 -0
- package/dist/theme.config-0Lvwt5Bo.cjs +1 -0
- package/dist/theme.config-B2y1axZf.js +2 -0
- package/dist/your-ai-workflow-firebase-os.cjs.js +76 -84
- package/dist/your-ai-workflow-firebase-os.css +2 -1
- package/dist/your-ai-workflow-firebase-os.es.js +23988 -33460
- package/package.json +6 -1
- package/scripts/postinstall.js +10 -0
- package/src/FirebaseOS.tsx +7 -1
- package/src/components/ContactPopup.tsx +63 -49
- package/src/components/DashboardNav.tsx +14 -4
- package/src/components/PhoneInputField.tsx +30 -0
- package/src/configs/forms/index.ts +0 -2
- package/src/lib/ConfigContext.tsx +5 -0
- package/src/lib/ThemeContext.tsx +14 -6
- package/src/main.tsx +13 -1
- package/src/pages/DynamicPage.tsx +106 -54
- package/src/pages/FormsAdmin.tsx +37 -6
- package/src/pages/Home.tsx +14 -5
- package/src/pages/PagesAdmin.tsx +37 -6
- package/src/pages/ThemeAdmin.tsx +16 -3
- package/src/renderer.ts +1 -0
- package/dist/contactForm.config-DLeI_ZEj.js +0 -2
- package/dist/contactForm.config-Y2AYSH9s.cjs +0 -1
- package/dist/contactForm.config-ki9OzidM.cjs +0 -1
- package/dist/supportForm.config-74T-eSSH.js +0 -2
- package/dist/supportForm.config-BwZqJ5CZ.cjs +0 -1
- package/dist/supportForm.config-NfLDtLTB.cjs +0 -1
- package/dist/theme.config-BbEzcEDR.js +0 -2
- package/dist/theme.config-DJqqr_kx.cjs +0 -1
- package/dist/theme.config-DKqbHSSM.cjs +0 -1
- package/src/configs/forms/pubForm.config.ts +0 -11
- package/src/configs/forms/userForm.config.ts +0 -11
- /package/dist/{contactForm.config-BHvMbfxM.js → contactForm.config-D9No8cBy.js} +0 -0
- /package/dist/{supportForm.config-D5ZRFZtU.js → supportForm.config-Dgl58gss.js} +0 -0
- /package/dist/{theme.config-q93xMCw6.js → theme.config-D05RQKVy.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "your-ai-workflow-firebase-os",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "A complete Firebase-powered admin OS — one React component.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/your-ai-workflow-firebase-os.cjs.js",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"require": "./dist/your-ai-workflow-firebase-os.cjs.js",
|
|
13
13
|
"types": "./dist/index.d.ts"
|
|
14
14
|
},
|
|
15
|
+
"./renderer": {
|
|
16
|
+
"import": "./dist/renderer.es.js",
|
|
17
|
+
"require": "./dist/renderer.cjs.js",
|
|
18
|
+
"types": "./dist/renderer.d.ts"
|
|
19
|
+
},
|
|
15
20
|
"./styles.css": {
|
|
16
21
|
"default": "./dist/your-ai-workflow-firebase-os.css"
|
|
17
22
|
},
|
package/scripts/postinstall.js
CHANGED
|
@@ -134,6 +134,11 @@ const appContent = `import { FirebaseOS } from 'your-ai-workflow-firebase-os';
|
|
|
134
134
|
import { Home } from './your-ai-workflow-firebase-os/Home';
|
|
135
135
|
import { themeConfig } from './your-ai-workflow-firebase-os/configs/theme.config';
|
|
136
136
|
|
|
137
|
+
// Local Configuration Defaults (The Final Boss)
|
|
138
|
+
import { defaultHomeConfig } from './your-ai-workflow-firebase-os/configs/pages/home.config';
|
|
139
|
+
import { contactFormConfig } from './your-ai-workflow-firebase-os/configs/forms/contactForm.config';
|
|
140
|
+
import { supportFormConfig } from './your-ai-workflow-firebase-os/configs/forms/supportForm.config';
|
|
141
|
+
|
|
137
142
|
// Vite injects these from your .env file at dev/build time.
|
|
138
143
|
const firebaseConfig = {
|
|
139
144
|
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || '',
|
|
@@ -158,6 +163,11 @@ export default function App() {
|
|
|
158
163
|
components={{
|
|
159
164
|
Home: Home
|
|
160
165
|
}}
|
|
166
|
+
defaultConfigs={{
|
|
167
|
+
home: defaultHomeConfig,
|
|
168
|
+
contactForm: contactFormConfig,
|
|
169
|
+
supportForm: supportFormConfig
|
|
170
|
+
}}
|
|
161
171
|
/>
|
|
162
172
|
);
|
|
163
173
|
}
|
package/src/FirebaseOS.tsx
CHANGED
|
@@ -44,6 +44,11 @@ export interface FirebaseOSProps {
|
|
|
44
44
|
TemplateStorage?: React.ComponentType<any>;
|
|
45
45
|
TemplateNone?: React.ComponentType<any>;
|
|
46
46
|
};
|
|
47
|
+
defaultConfigs?: {
|
|
48
|
+
home?: any;
|
|
49
|
+
contactForm?: any;
|
|
50
|
+
supportForm?: any;
|
|
51
|
+
};
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
// ── The Library Component ────────────────────────────────────────────────
|
|
@@ -70,12 +75,13 @@ export function FirebaseOS(props: FirebaseOSProps) {
|
|
|
70
75
|
onAuthChange: props.onAuthChange,
|
|
71
76
|
themeConfig: props.themeConfig,
|
|
72
77
|
components: props.components,
|
|
78
|
+
defaultConfigs: props.defaultConfigs,
|
|
73
79
|
};
|
|
74
80
|
|
|
75
81
|
return (
|
|
76
82
|
<ConfigProvider config={config}>
|
|
77
83
|
<ThemeProvider>
|
|
78
|
-
<div
|
|
84
|
+
<div
|
|
79
85
|
className="firebase-os flex-1 flex flex-col min-h-screen"
|
|
80
86
|
style={{ background: 'var(--bg-gradient)', color: 'var(--fg-color)' }}
|
|
81
87
|
>
|
|
@@ -8,16 +8,7 @@ import { Input } from './Input';
|
|
|
8
8
|
import { Button } from './Button';
|
|
9
9
|
import { CustomSelect } from './CustomSelect';
|
|
10
10
|
import { useTheme } from '../lib/ThemeContext';
|
|
11
|
-
|
|
12
|
-
import { PhoneInput, defaultCountries } from 'react-international-phone';
|
|
13
|
-
import 'react-international-phone/style.css';
|
|
14
|
-
|
|
15
|
-
const popularCountryCodes = ['us', 'gb', 'ca', 'au', 'nz', 'ie', 'ae', 'sg'];
|
|
16
|
-
const customizedCountries = [
|
|
17
|
-
...defaultCountries.filter((c) => popularCountryCodes.includes(c[1])),
|
|
18
|
-
...defaultCountries.filter((c) => !popularCountryCodes.includes(c[1]))
|
|
19
|
-
];
|
|
20
|
-
|
|
11
|
+
import React, { Suspense } from 'react';
|
|
21
12
|
import { doc, getDoc } from 'firebase/firestore';
|
|
22
13
|
import { useNavigate } from 'react-router-dom';
|
|
23
14
|
|
|
@@ -28,30 +19,12 @@ interface ContactPopupProps {
|
|
|
28
19
|
isInline?: boolean;
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<div className="flex flex-col w-full relative z-[99999]">
|
|
34
|
-
<label className="text-[13px] font-medium text-foreground/50 select-none mb-1.5 ml-1">
|
|
35
|
-
{field.label} {field.required && <span className="text-red-500">*</span>}
|
|
36
|
-
</label>
|
|
37
|
-
<PhoneInput
|
|
38
|
-
defaultCountry="us"
|
|
39
|
-
countries={customizedCountries}
|
|
40
|
-
value={value}
|
|
41
|
-
onChange={(val) => onChange(val)}
|
|
42
|
-
forceDialCode={true}
|
|
43
|
-
className="w-full flex items-center rounded-xl text-[15px] text-foreground transition-all duration-300 focus-within:border-accent glow-focus glass-panel overflow-visible relative h-12"
|
|
44
|
-
inputClassName="flex-1 w-full !bg-transparent !border-none !outline-none !shadow-none px-3 text-[15px] text-foreground m-0 p-0 h-full tracking-[0.5px] no-glow"
|
|
45
|
-
countrySelectorStyleProps={{
|
|
46
|
-
buttonClassName: "!bg-transparent !border-0 border-r !border-[var(--panel-border)]/50 !outline-none !shadow-none !px-3 hover:!bg-foreground/5 transition-colors h-full !rounded-xl !rounded-r-none flex items-center justify-center shrink-0",
|
|
47
|
-
}}
|
|
48
|
-
/>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
22
|
+
// Lazy load heavy components only when requested by the form JSON
|
|
23
|
+
const PhoneInputField = React.lazy(() => import('./PhoneInputField'));
|
|
52
24
|
|
|
53
|
-
import { contactFormConfig } from '../configs/forms/contactForm.config';
|
|
54
|
-
import { supportFormConfig } from '../configs/forms/supportForm.config';
|
|
25
|
+
import { contactFormConfig as bundledContactConfig } from '../configs/forms/contactForm.config';
|
|
26
|
+
import { supportFormConfig as bundledSupportConfig } from '../configs/forms/supportForm.config';
|
|
27
|
+
import { useConfig } from '../lib/ConfigContext';
|
|
55
28
|
|
|
56
29
|
export function ContactPopup({ onClose, formId, formConfig: propsConfig, isInline }: ContactPopupProps) {
|
|
57
30
|
const [formData, setFormData] = useState<any>({});
|
|
@@ -62,6 +35,13 @@ export function ContactPopup({ onClose, formId, formConfig: propsConfig, isInlin
|
|
|
62
35
|
const [formConfig, setFormConfig] = useState<any>(propsConfig || null);
|
|
63
36
|
const [loadingConfig, setLoadingConfig] = useState(!propsConfig);
|
|
64
37
|
const navigate = useNavigate();
|
|
38
|
+
const config = useConfig();
|
|
39
|
+
|
|
40
|
+
const getFallback = (id: string) => {
|
|
41
|
+
if (id === 'contact') return config?.defaultConfigs?.contactForm || bundledContactConfig;
|
|
42
|
+
if (id === 'support') return config?.defaultConfigs?.supportForm || bundledSupportConfig;
|
|
43
|
+
return bundledContactConfig;
|
|
44
|
+
};
|
|
65
45
|
|
|
66
46
|
useEffect(() => {
|
|
67
47
|
if (propsConfig) {
|
|
@@ -70,24 +50,57 @@ export function ContactPopup({ onClose, formId, formConfig: propsConfig, isInlin
|
|
|
70
50
|
return;
|
|
71
51
|
}
|
|
72
52
|
|
|
53
|
+
// No formId at all → use contact default instantly
|
|
54
|
+
if (!formId) {
|
|
55
|
+
setFormConfig(getFallback('contact'));
|
|
56
|
+
setLoadingConfig(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// For system forms (contact/support): only query Firestore if
|
|
61
|
+
// the user has actually customized the form (flag set by FormsAdmin on save).
|
|
62
|
+
// Otherwise use the local config file directly — zero network requests.
|
|
63
|
+
if (formId === 'contact' || formId === 'support') {
|
|
64
|
+
const hasCustom = localStorage.getItem(`fbos_custom_form_${formId}`);
|
|
65
|
+
if (!hasCustom) {
|
|
66
|
+
// No customizations — use local config instantly, skip Firestore
|
|
67
|
+
setFormConfig(getFallback(formId));
|
|
68
|
+
setLoadingConfig(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Either a custom form or a system form with customizations → query Firestore
|
|
74
|
+
// First, instantly render from cache to avoid loading spinner
|
|
75
|
+
try {
|
|
76
|
+
const cached = localStorage.getItem(`fbos_form_${formId}`);
|
|
77
|
+
if (cached) {
|
|
78
|
+
setFormConfig(JSON.parse(cached));
|
|
79
|
+
setLoadingConfig(false);
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {}
|
|
82
|
+
|
|
73
83
|
const fetchConfig = async () => {
|
|
74
84
|
try {
|
|
75
|
-
if (!formId) {
|
|
76
|
-
setFormConfig(contactFormConfig);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
85
|
const configDoc = await getDoc(doc(db, 'sys_forms', formId));
|
|
80
86
|
if (configDoc.exists()) {
|
|
81
|
-
|
|
87
|
+
const data = { ...configDoc.data(), id: configDoc.id };
|
|
88
|
+
setFormConfig(data);
|
|
89
|
+
localStorage.setItem(`fbos_form_${formId}`, JSON.stringify(data));
|
|
82
90
|
} else {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// Doc doesn't exist (was reset/deleted) — use local config
|
|
92
|
+
if (formId === 'contact' || formId === 'support') {
|
|
93
|
+
setFormConfig(getFallback(formId));
|
|
94
|
+
// Clear the flag since the doc no longer exists
|
|
95
|
+
localStorage.removeItem(`fbos_custom_form_${formId}`);
|
|
96
|
+
}
|
|
88
97
|
}
|
|
89
98
|
} catch (err) {
|
|
90
99
|
console.error('Failed to load form config:', err);
|
|
100
|
+
// On error, fall back to local config for system forms
|
|
101
|
+
if (formId === 'contact' || formId === 'support') {
|
|
102
|
+
setFormConfig(getFallback(formId));
|
|
103
|
+
}
|
|
91
104
|
} finally {
|
|
92
105
|
setLoadingConfig(false);
|
|
93
106
|
}
|
|
@@ -261,12 +274,13 @@ export function ContactPopup({ onClose, formId, formConfig: propsConfig, isInlin
|
|
|
261
274
|
}).map((field: any, idx: number) => {
|
|
262
275
|
if (field.type === 'tel') {
|
|
263
276
|
return (
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
<Suspense key={idx} fallback={<div className="h-12 w-full glass-panel animate-pulse rounded-xl" />}>
|
|
278
|
+
<PhoneInputField
|
|
279
|
+
field={field}
|
|
280
|
+
value={formData[field.name] || ''}
|
|
281
|
+
onChange={val => setFormData({ ...formData, [field.name]: val })}
|
|
282
|
+
/>
|
|
283
|
+
</Suspense>
|
|
270
284
|
);
|
|
271
285
|
}
|
|
272
286
|
|
|
@@ -44,18 +44,28 @@ export function DashboardNav({ customTabs = [] }: { customTabs?: Omit<TabItem, '
|
|
|
44
44
|
const [tabOrder, setTabOrder] = useState<OrderItem[]>(defaultTabOrder);
|
|
45
45
|
|
|
46
46
|
useEffect(() => {
|
|
47
|
+
// 1. Instant Cache Hydration
|
|
48
|
+
try {
|
|
49
|
+
const cachedTabs = localStorage.getItem('fbos_nav_tabs');
|
|
50
|
+
if (cachedTabs) setTabOverrides(JSON.parse(cachedTabs));
|
|
51
|
+
const cachedOrder = localStorage.getItem('fbos_nav_order');
|
|
52
|
+
if (cachedOrder) setTabOrder(JSON.parse(cachedOrder));
|
|
53
|
+
} catch(e) {}
|
|
54
|
+
|
|
55
|
+
// 2. Background Sync
|
|
47
56
|
const unsubTabs = onSnapshot(collection(db, 'sys_tabs'), (snap) => {
|
|
48
57
|
const overrides: Record<string, any> = {};
|
|
49
58
|
snap.forEach(d => { overrides[d.id] = d.data(); });
|
|
50
59
|
setTabOverrides(overrides);
|
|
60
|
+
localStorage.setItem('fbos_nav_tabs', JSON.stringify(overrides));
|
|
51
61
|
});
|
|
52
62
|
|
|
53
63
|
const unsubOrder = onSnapshot(doc(db, 'sys_configs', 'tab_order'), (d) => {
|
|
54
|
-
|
|
55
|
-
d.exists() && Array.isArray(d.data().order)
|
|
64
|
+
const orderData = d.exists() && Array.isArray(d.data().order)
|
|
56
65
|
? parseOrderItems(d.data().order)
|
|
57
|
-
: defaultTabOrder
|
|
58
|
-
);
|
|
66
|
+
: defaultTabOrder;
|
|
67
|
+
setTabOrder(orderData);
|
|
68
|
+
localStorage.setItem('fbos_nav_order', JSON.stringify(orderData));
|
|
59
69
|
});
|
|
60
70
|
|
|
61
71
|
return () => { unsubTabs(); unsubOrder(); };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PhoneInput, defaultCountries } from 'react-international-phone';
|
|
2
|
+
import 'react-international-phone/style.css';
|
|
3
|
+
|
|
4
|
+
const popularCountryCodes = ['us', 'gb', 'ca', 'au', 'nz', 'ie', 'ae', 'sg'];
|
|
5
|
+
const customizedCountries = [
|
|
6
|
+
...defaultCountries.filter((c) => popularCountryCodes.includes(c[1])),
|
|
7
|
+
...defaultCountries.filter((c) => !popularCountryCodes.includes(c[1]))
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export default function PhoneInputField({ field, value, onChange }: { field: any, value: string, onChange: (val: string) => void }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex flex-col w-full relative z-[99999]">
|
|
13
|
+
<label className="text-[13px] font-medium text-foreground/50 select-none mb-1.5 ml-1">
|
|
14
|
+
{field.label} {field.required && <span className="text-red-500">*</span>}
|
|
15
|
+
</label>
|
|
16
|
+
<PhoneInput
|
|
17
|
+
defaultCountry="us"
|
|
18
|
+
countries={customizedCountries}
|
|
19
|
+
value={value}
|
|
20
|
+
onChange={(val) => onChange(val)}
|
|
21
|
+
forceDialCode={true}
|
|
22
|
+
className="w-full flex items-center rounded-xl text-[15px] text-foreground transition-all duration-300 focus-within:border-accent glow-focus glass-panel overflow-visible relative h-12"
|
|
23
|
+
inputClassName="flex-1 w-full !bg-transparent !border-none !outline-none !shadow-none px-3 text-[15px] text-foreground m-0 p-0 h-full tracking-[0.5px] no-glow"
|
|
24
|
+
countrySelectorStyleProps={{
|
|
25
|
+
buttonClassName: "!bg-transparent !border-0 border-r !border-[var(--panel-border)]/50 !outline-none !shadow-none !px-3 hover:!bg-foreground/5 transition-colors h-full !rounded-xl !rounded-r-none flex items-center justify-center shrink-0",
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { contactFormConfig } from './contactForm.config';
|
|
2
2
|
import { supportFormConfig } from './supportForm.config';
|
|
3
|
-
import { pubFormConfig } from './pubForm.config';
|
|
4
|
-
import { userFormConfig } from './userForm.config';
|
|
5
3
|
|
|
6
4
|
export interface FormField {
|
|
7
5
|
name: string;
|
|
@@ -30,6 +30,11 @@ export interface FirebaseOSConfig {
|
|
|
30
30
|
TemplateStorage?: React.ComponentType<any>;
|
|
31
31
|
TemplateNone?: React.ComponentType<any>;
|
|
32
32
|
};
|
|
33
|
+
defaultConfigs?: {
|
|
34
|
+
home?: any;
|
|
35
|
+
contactForm?: any;
|
|
36
|
+
supportForm?: any;
|
|
37
|
+
};
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const ConfigContext = createContext<FirebaseOSConfig>({});
|
package/src/lib/ThemeContext.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
2
|
-
import { themeConfig } from '../configs/theme.config';
|
|
2
|
+
import { themeConfig as bundledThemeConfig } from '../configs/theme.config';
|
|
3
3
|
import { doc, onSnapshot } from 'firebase/firestore';
|
|
4
4
|
import { db } from './firebase';
|
|
5
|
+
import { useConfig } from './ConfigContext';
|
|
5
6
|
|
|
6
7
|
function deepMerge(target: any, source: any): any {
|
|
7
8
|
if (typeof source !== 'object' || source === null) return source;
|
|
@@ -24,13 +25,16 @@ interface ThemeContextType {
|
|
|
24
25
|
toggleTheme: () => void;
|
|
25
26
|
isSystem: boolean;
|
|
26
27
|
setIsSystem: (is: boolean) => void;
|
|
27
|
-
config: typeof
|
|
28
|
+
config: typeof bundledThemeConfig;
|
|
28
29
|
activeConfig: any;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
32
33
|
|
|
33
34
|
export function ThemeProvider({ children, scopeSelector }: { children: React.ReactNode, scopeSelector?: string }) {
|
|
35
|
+
const libConfig = useConfig();
|
|
36
|
+
const themeConfig = libConfig?.themeConfig || bundledThemeConfig;
|
|
37
|
+
|
|
34
38
|
const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>(() => {
|
|
35
39
|
const saved = localStorage.getItem('theme-mode') as 'light' | 'dark' | 'system' | null;
|
|
36
40
|
return saved || themeConfig.defaultTheme;
|
|
@@ -38,20 +42,24 @@ export function ThemeProvider({ children, scopeSelector }: { children: React.Rea
|
|
|
38
42
|
|
|
39
43
|
const [currentTheme, setCurrentTheme] = useState<Theme>('dark');
|
|
40
44
|
const [activeConfig, setActiveConfig] = useState(() => {
|
|
45
|
+
// Start with local cache if available, otherwise use default
|
|
41
46
|
try {
|
|
42
47
|
const cached = localStorage.getItem('theme_config_cache');
|
|
43
48
|
if (cached) return JSON.parse(cached);
|
|
44
49
|
} catch (e) {}
|
|
45
50
|
return themeConfig;
|
|
46
51
|
});
|
|
47
|
-
|
|
52
|
+
|
|
53
|
+
// Always ready instantly (using default or cache)
|
|
54
|
+
const [isReady, setIsReady] = useState(true);
|
|
48
55
|
|
|
49
56
|
useEffect(() => {
|
|
50
57
|
if (!db) {
|
|
51
58
|
setActiveConfig(themeConfig);
|
|
52
|
-
setIsReady(true);
|
|
53
59
|
return;
|
|
54
60
|
}
|
|
61
|
+
|
|
62
|
+
// Subscribe to Firestore in the background
|
|
55
63
|
const unsub = onSnapshot(doc(db, 'sys_configs', 'theme'), (snap) => {
|
|
56
64
|
if (snap.exists()) {
|
|
57
65
|
const data = snap.data();
|
|
@@ -67,10 +75,10 @@ export function ThemeProvider({ children, scopeSelector }: { children: React.Rea
|
|
|
67
75
|
setThemeMode(merged.defaultTheme);
|
|
68
76
|
}
|
|
69
77
|
} else {
|
|
78
|
+
// Doc was deleted (reset to default)
|
|
70
79
|
setActiveConfig(themeConfig);
|
|
71
|
-
localStorage.
|
|
80
|
+
localStorage.removeItem('theme_config_cache');
|
|
72
81
|
}
|
|
73
|
-
setIsReady(true);
|
|
74
82
|
});
|
|
75
83
|
return () => unsub();
|
|
76
84
|
}, []);
|
package/src/main.tsx
CHANGED
|
@@ -6,12 +6,24 @@ import App from './App.tsx'
|
|
|
6
6
|
import { ThemeProvider } from './lib/ThemeContext.tsx'
|
|
7
7
|
import { ConfigProvider } from './lib/ConfigContext.tsx'
|
|
8
8
|
|
|
9
|
+
import { defaultHomeConfig } from './configs/pages/home.config';
|
|
10
|
+
import { contactFormConfig } from './configs/forms/contactForm.config';
|
|
11
|
+
import { supportFormConfig } from './configs/forms/supportForm.config';
|
|
12
|
+
import { themeConfig } from './configs/theme.config';
|
|
13
|
+
|
|
9
14
|
// Keep body reset in standalone mode (FirebaseOS.tsx doesn't inject this)
|
|
10
15
|
document.body.style.margin = '0';
|
|
11
16
|
|
|
12
17
|
createRoot(document.getElementById('root')!).render(
|
|
13
18
|
<StrictMode>
|
|
14
|
-
<ConfigProvider config={{
|
|
19
|
+
<ConfigProvider config={{
|
|
20
|
+
defaultConfigs: {
|
|
21
|
+
home: defaultHomeConfig,
|
|
22
|
+
contactForm: contactFormConfig,
|
|
23
|
+
supportForm: supportFormConfig
|
|
24
|
+
},
|
|
25
|
+
themeConfig: themeConfig
|
|
26
|
+
}}>
|
|
15
27
|
<ThemeProvider>
|
|
16
28
|
<div className="firebase-os">
|
|
17
29
|
<App />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { Navigate, useLocation } from 'react-router-dom';
|
|
3
|
-
import { collection, getDocs,
|
|
3
|
+
import { collection, doc, getDoc, getDocs, query, where, limit } from 'firebase/firestore';
|
|
4
4
|
import { db } from '../lib/firebase';
|
|
5
5
|
import { ContactPopup } from '../components/ContactPopup';
|
|
6
6
|
import { useAuth } from '../lib/AuthContext';
|
|
@@ -23,37 +23,64 @@ export function DynamicPage() {
|
|
|
23
23
|
const [loading, setLoading] = useState(true);
|
|
24
24
|
|
|
25
25
|
useEffect(() => {
|
|
26
|
+
let isMounted = true;
|
|
27
|
+
|
|
28
|
+
// 1. Instant Cache Render - O(1) lookup
|
|
29
|
+
const cachedMatch = localStorage.getItem(`fbos_route_${location.pathname}`);
|
|
30
|
+
if (cachedMatch) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(cachedMatch);
|
|
33
|
+
setConfig(parsed);
|
|
34
|
+
setLoading(false);
|
|
35
|
+
const newTitle = parsed.pageName || parsed.tabName || parsed.title;
|
|
36
|
+
if (newTitle) document.title = newTitle;
|
|
37
|
+
} catch (e) {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Background Sync with Highly Targeted Queries
|
|
26
41
|
const fetchConfigs = async () => {
|
|
27
|
-
setLoading(true);
|
|
28
42
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
let routeWithoutSlash = location.pathname;
|
|
44
|
+
if (routeWithoutSlash.startsWith('/')) routeWithoutSlash = routeWithoutSlash.substring(1);
|
|
45
|
+
|
|
46
|
+
// Execute targeted queries in parallel to drastically reduce fetching latency
|
|
47
|
+
const [
|
|
48
|
+
docPageReq, docTabReq,
|
|
49
|
+
queryPageReq, queryPageNoSlashReq,
|
|
50
|
+
queryTabReq, queryTabNoSlashReq
|
|
51
|
+
] = await Promise.allSettled([
|
|
52
|
+
getDoc(doc(db, 'sys_pages', routeWithoutSlash)),
|
|
53
|
+
getDoc(doc(db, 'sys_tabs', routeWithoutSlash)),
|
|
54
|
+
getDocs(query(collection(db, 'sys_pages'), where('route', '==', location.pathname), limit(1))),
|
|
55
|
+
getDocs(query(collection(db, 'sys_pages'), where('route', '==', routeWithoutSlash), limit(1))),
|
|
56
|
+
getDocs(query(collection(db, 'sys_tabs'), where('route', '==', location.pathname), limit(1))),
|
|
57
|
+
getDocs(query(collection(db, 'sys_tabs'), where('route', '==', routeWithoutSlash), limit(1)))
|
|
32
58
|
]);
|
|
33
|
-
|
|
59
|
+
|
|
34
60
|
let matchedConfig: any = null;
|
|
35
61
|
let matchedId: string | null = null;
|
|
36
62
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
63
|
+
const assignIfValid = (res: any, isTab: boolean) => {
|
|
64
|
+
if (matchedConfig) return;
|
|
65
|
+
if (res.status === 'fulfilled') {
|
|
66
|
+
if (res.value.exists && res.value.exists()) {
|
|
67
|
+
const d = res.value.data();
|
|
68
|
+
matchedConfig = { ...d, isPrivate: isTab, isAdmin: d.pageType === 'admin', isShared: d.pageType === 'shared' };
|
|
69
|
+
matchedId = res.value.id;
|
|
70
|
+
} else if (res.value.docs && res.value.docs.length > 0) {
|
|
71
|
+
const d = res.value.docs[0].data();
|
|
72
|
+
matchedConfig = { ...d, isPrivate: isTab, isAdmin: d.pageType === 'admin', isShared: d.pageType === 'shared' };
|
|
73
|
+
matchedId = res.value.docs[0].id;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
51
76
|
};
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
assignIfValid(docPageReq, false);
|
|
79
|
+
assignIfValid(queryPageReq, false);
|
|
80
|
+
assignIfValid(queryPageNoSlashReq, false);
|
|
81
|
+
assignIfValid(docTabReq, true);
|
|
82
|
+
assignIfValid(queryTabReq, true);
|
|
83
|
+
assignIfValid(queryTabNoSlashReq, true);
|
|
57
84
|
|
|
58
85
|
// Fallbacks for core pages if not customized yet
|
|
59
86
|
if (!matchedConfig) {
|
|
@@ -66,45 +93,70 @@ export function DynamicPage() {
|
|
|
66
93
|
}
|
|
67
94
|
}
|
|
68
95
|
|
|
96
|
+
if (!isMounted) return;
|
|
97
|
+
|
|
69
98
|
if (matchedConfig) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
if ((matchedId === 'contact' || matchedId === 'support') && !matchedConfig.form) {
|
|
100
|
+
matchedConfig.form = matchedId;
|
|
101
|
+
}
|
|
102
|
+
matchedConfig.pageId = matchedId;
|
|
103
|
+
|
|
104
|
+
// Immediately cache the highly optimized direct-route mapping
|
|
105
|
+
localStorage.setItem(`fbos_route_${location.pathname}`, JSON.stringify(matchedConfig));
|
|
106
|
+
|
|
107
|
+
// Hydrate form config instantly
|
|
108
|
+
if (matchedConfig.form) {
|
|
109
|
+
try {
|
|
110
|
+
const cachedForm = localStorage.getItem(`fbos_form_${matchedConfig.form}`);
|
|
111
|
+
if (cachedForm) matchedConfig.formConfigOverride = JSON.parse(cachedForm);
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
|
|
114
|
+
getDoc(doc(db, 'sys_forms', matchedConfig.form)).then(async (formDoc) => {
|
|
115
|
+
if (!isMounted) return;
|
|
116
|
+
if (formDoc.exists()) {
|
|
117
|
+
const data = { ...formDoc.data(), id: formDoc.id };
|
|
118
|
+
setConfig((prev: any) => ({ ...prev, formConfigOverride: data }));
|
|
119
|
+
localStorage.setItem(`fbos_form_${matchedConfig.form}`, JSON.stringify(data));
|
|
120
|
+
} else {
|
|
121
|
+
let defaultCfg;
|
|
122
|
+
if (matchedConfig.form === 'contact') {
|
|
123
|
+
const { contactFormConfig } = await import('../configs/forms/contactForm.config');
|
|
124
|
+
defaultCfg = contactFormConfig;
|
|
125
|
+
} else if (matchedConfig.form === 'support') {
|
|
126
|
+
const { supportFormConfig } = await import('../configs/forms/supportForm.config');
|
|
127
|
+
defaultCfg = supportFormConfig;
|
|
128
|
+
}
|
|
129
|
+
if (defaultCfg) {
|
|
130
|
+
setConfig((prev: any) => ({ ...prev, formConfigOverride: defaultCfg }));
|
|
131
|
+
localStorage.setItem(`fbos_form_${matchedConfig.form}`, JSON.stringify(defaultCfg));
|
|
132
|
+
}
|
|
87
133
|
}
|
|
88
|
-
|
|
89
|
-
|
|
134
|
+
});
|
|
135
|
+
}
|
|
90
136
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
const newTitle = matchedConfig.pageName || matchedConfig.tabName || matchedConfig.title;
|
|
138
|
+
if (newTitle) {
|
|
139
|
+
document.title = newTitle;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setConfig(matchedConfig);
|
|
143
|
+
setLoading(false);
|
|
98
144
|
} else {
|
|
99
|
-
|
|
145
|
+
setConfig('not-found');
|
|
146
|
+
setLoading(false);
|
|
147
|
+
localStorage.removeItem(`fbos_route_${location.pathname}`);
|
|
100
148
|
}
|
|
149
|
+
|
|
101
150
|
} catch (err) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
151
|
+
if (isMounted && loading) {
|
|
152
|
+
setConfig('not-found');
|
|
153
|
+
setLoading(false);
|
|
154
|
+
}
|
|
105
155
|
}
|
|
106
156
|
};
|
|
107
157
|
fetchConfigs();
|
|
158
|
+
|
|
159
|
+
return () => { isMounted = false; };
|
|
108
160
|
}, [location.pathname]);
|
|
109
161
|
|
|
110
162
|
const [showLoader, setShowLoader] = useState(false);
|