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.
Files changed (50) hide show
  1. package/dist/ContactPopup-BcjnOBWb.js +7544 -0
  2. package/dist/ContactPopup-gaUJCR67.cjs +9 -0
  3. package/dist/FirebaseOS.d.ts +5 -0
  4. package/dist/PhoneInputField-Bwos3cqE.js +2042 -0
  5. package/dist/PhoneInputField-CSoNnJ2Y.cjs +1 -0
  6. package/dist/chunk-DYteiM-1.cjs +1 -0
  7. package/dist/contactForm.config-CXyq-EbM.cjs +1 -0
  8. package/dist/contactForm.config-DfZU9Fr1.js +2 -0
  9. package/dist/lib/ConfigContext.d.ts +5 -0
  10. package/dist/lib/ThemeContext.d.ts +2 -2
  11. package/dist/renderer.cjs.js +1 -0
  12. package/dist/renderer.d.ts +1 -0
  13. package/dist/renderer.es.js +2 -0
  14. package/dist/supportForm.config-M9zQs6qp.js +2 -0
  15. package/dist/supportForm.config-haLod4rv.cjs +1 -0
  16. package/dist/theme.config-0Lvwt5Bo.cjs +1 -0
  17. package/dist/theme.config-B2y1axZf.js +2 -0
  18. package/dist/your-ai-workflow-firebase-os.cjs.js +76 -84
  19. package/dist/your-ai-workflow-firebase-os.css +2 -1
  20. package/dist/your-ai-workflow-firebase-os.es.js +23988 -33460
  21. package/package.json +6 -1
  22. package/scripts/postinstall.js +10 -0
  23. package/src/FirebaseOS.tsx +7 -1
  24. package/src/components/ContactPopup.tsx +63 -49
  25. package/src/components/DashboardNav.tsx +14 -4
  26. package/src/components/PhoneInputField.tsx +30 -0
  27. package/src/configs/forms/index.ts +0 -2
  28. package/src/lib/ConfigContext.tsx +5 -0
  29. package/src/lib/ThemeContext.tsx +14 -6
  30. package/src/main.tsx +13 -1
  31. package/src/pages/DynamicPage.tsx +106 -54
  32. package/src/pages/FormsAdmin.tsx +37 -6
  33. package/src/pages/Home.tsx +14 -5
  34. package/src/pages/PagesAdmin.tsx +37 -6
  35. package/src/pages/ThemeAdmin.tsx +16 -3
  36. package/src/renderer.ts +1 -0
  37. package/dist/contactForm.config-DLeI_ZEj.js +0 -2
  38. package/dist/contactForm.config-Y2AYSH9s.cjs +0 -1
  39. package/dist/contactForm.config-ki9OzidM.cjs +0 -1
  40. package/dist/supportForm.config-74T-eSSH.js +0 -2
  41. package/dist/supportForm.config-BwZqJ5CZ.cjs +0 -1
  42. package/dist/supportForm.config-NfLDtLTB.cjs +0 -1
  43. package/dist/theme.config-BbEzcEDR.js +0 -2
  44. package/dist/theme.config-DJqqr_kx.cjs +0 -1
  45. package/dist/theme.config-DKqbHSSM.cjs +0 -1
  46. package/src/configs/forms/pubForm.config.ts +0 -11
  47. package/src/configs/forms/userForm.config.ts +0 -11
  48. /package/dist/{contactForm.config-BHvMbfxM.js → contactForm.config-D9No8cBy.js} +0 -0
  49. /package/dist/{supportForm.config-D5ZRFZtU.js → supportForm.config-Dgl58gss.js} +0 -0
  50. /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.7",
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
  },
@@ -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
  }
@@ -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
- function PhoneInputField({ field, value, onChange }: { field: any, value: string, onChange: (val: string) => void }) {
32
- return (
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
- setFormConfig({ ...configDoc.data(), id: configDoc.id });
87
+ const data = { ...configDoc.data(), id: configDoc.id };
88
+ setFormConfig(data);
89
+ localStorage.setItem(`fbos_form_${formId}`, JSON.stringify(data));
82
90
  } else {
83
- if (formId === 'contact') {
84
- setFormConfig(contactFormConfig);
85
- } else if (formId === 'support') {
86
- setFormConfig(supportFormConfig);
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
- <PhoneInputField
265
- key={idx}
266
- field={field}
267
- value={formData[field.name] || ''}
268
- onChange={val => setFormData({ ...formData, [field.name]: val })}
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
- setTabOrder(
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>({});
@@ -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 themeConfig;
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
- const [isReady, setIsReady] = useState(() => !!localStorage.getItem('theme_config_cache'));
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.setItem('theme_config_cache', JSON.stringify(themeConfig));
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, doc, getDoc } from 'firebase/firestore';
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
- const [pubConfigsSnap, pubTabsSnap] = await Promise.all([
30
- getDocs(collection(db, 'sys_pages')),
31
- getDocs(collection(db, 'sys_tabs'))
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 checkSnap = (snap: any, isPrivate: boolean) => {
38
- snap.forEach((docSnap: any) => {
39
- const d = docSnap.data();
40
- let route = d.route;
41
- if (route && !route.startsWith('/')) route = '/' + route;
42
-
43
- // System Pages have strict document IDs tracking their canonical path (e.g. 'contact')
44
- const pathMatchesSystemId = ('/' + docSnap.id) === location.pathname;
45
-
46
- if (route === location.pathname || pathMatchesSystemId) {
47
- matchedConfig = { ...d, isPrivate, isAdmin: d.pageType === 'admin', isShared: d.pageType === 'shared' };
48
- matchedId = docSnap.id;
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
- checkSnap(pubConfigsSnap, false);
54
- if (!matchedConfig) {
55
- checkSnap(pubTabsSnap, true);
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
- if ((matchedId === 'contact' || matchedId === 'support') && !matchedConfig.form) {
71
- matchedConfig.form = matchedId;
72
- }
73
- matchedConfig.pageId = matchedId;
74
-
75
- // Hydrate form config immediately to prevent secondary loaders flashing on transitions
76
- if (matchedConfig.form) {
77
- const formDoc = await getDoc(doc(db, 'sys_forms', matchedConfig.form));
78
- if (formDoc.exists()) {
79
- matchedConfig.formConfigOverride = { ...formDoc.data(), id: formDoc.id };
80
- } else {
81
- if (matchedConfig.form === 'contact') {
82
- const { contactFormConfig } = await import('../configs/forms/contactForm.config');
83
- matchedConfig.formConfigOverride = contactFormConfig;
84
- } else if (matchedConfig.form === 'support') {
85
- const { supportFormConfig } = await import('../configs/forms/supportForm.config');
86
- matchedConfig.formConfigOverride = supportFormConfig;
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
- // Update Document Title matching pageName/tabName globally
92
- const newTitle = matchedConfig.pageName || matchedConfig.tabName || matchedConfig.title;
93
- if (newTitle) {
94
- document.title = newTitle;
95
- }
96
-
97
- setConfig(matchedConfig);
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
- setConfig('not-found');
145
+ setConfig('not-found');
146
+ setLoading(false);
147
+ localStorage.removeItem(`fbos_route_${location.pathname}`);
100
148
  }
149
+
101
150
  } catch (err) {
102
- setConfig('not-found');
103
- } finally {
104
- setLoading(false);
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);