firebase-os 1.1.4 → 1.1.6

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 (124) hide show
  1. package/dist/FirebaseOS.d.ts +15 -0
  2. package/dist/firebase-os.cjs.js +5 -20
  3. package/dist/firebase-os.es.js +95 -90
  4. package/dist/index.d.ts +3 -0
  5. package/dist/lib/ConfigContext.d.ts +12 -0
  6. package/package.json +3 -2
  7. package/scripts/postinstall.js +86 -15
  8. package/src/App.css +184 -0
  9. package/src/App.tsx +214 -0
  10. package/src/FirebaseOS.tsx +81 -0
  11. package/src/assets/hero.png +0 -0
  12. package/src/assets/react.svg +1 -0
  13. package/src/assets/vite.svg +1 -0
  14. package/src/components/AdminNotifications.test.tsx +98 -0
  15. package/src/components/AdminNotifications.tsx +194 -0
  16. package/src/components/Button.test.tsx +22 -0
  17. package/src/components/Button.tsx +53 -0
  18. package/src/components/ConfirmModal.test.tsx +98 -0
  19. package/src/components/ConfirmModal.tsx +73 -0
  20. package/src/components/ContactPopup.test.tsx +98 -0
  21. package/src/components/ContactPopup.tsx +437 -0
  22. package/src/components/CustomSelect.test.tsx +47 -0
  23. package/src/components/CustomSelect.tsx +89 -0
  24. package/src/components/DashboardNav.test.tsx +98 -0
  25. package/src/components/DashboardNav.tsx +281 -0
  26. package/src/components/Input.test.tsx +33 -0
  27. package/src/components/Input.tsx +61 -0
  28. package/src/components/JsonEditor.tsx +579 -0
  29. package/src/components/Navbar.test.tsx +98 -0
  30. package/src/components/Navbar.tsx +563 -0
  31. package/src/configs/forms/contactForm.config.ts +15 -0
  32. package/src/configs/forms/index.ts +29 -0
  33. package/src/configs/forms/pubForm.config.ts +11 -0
  34. package/src/configs/forms/supportForm.config.ts +14 -0
  35. package/src/configs/forms/userForm.config.ts +11 -0
  36. package/src/configs/pages/admin.config.ts +29 -0
  37. package/src/configs/pages/contact.config.ts +6 -0
  38. package/src/configs/pages/home.config.ts +18 -0
  39. package/src/configs/pages/mem.config.ts +2 -0
  40. package/src/configs/pages/menuOrders.config.ts +11 -0
  41. package/src/configs/pages/pub.config.ts +11 -0
  42. package/src/configs/pages/shared.config.ts +29 -0
  43. package/src/configs/pages/support.config.ts +7 -0
  44. package/src/configs/pages/tabOrders.config.ts +33 -0
  45. package/src/configs/pages/user.config.ts +29 -0
  46. package/src/configs/theme.config.ts +93 -0
  47. package/src/index.css +403 -0
  48. package/src/index.ts +22 -0
  49. package/src/lib/AuthContext.test.tsx +88 -0
  50. package/src/lib/AuthContext.tsx +191 -0
  51. package/src/lib/ConfigContext.tsx +45 -0
  52. package/src/lib/ThemeContext.tsx +233 -0
  53. package/src/lib/firebase.ts +91 -0
  54. package/src/main.tsx +22 -0
  55. package/src/microcomponents/AdminExampleContent.tsx +44 -0
  56. package/src/microcomponents/PrivateExampleContent.tsx +39 -0
  57. package/src/microcomponents/Public.tsx +126 -0
  58. package/src/microcomponents/SharedExampleContent.tsx +53 -0
  59. package/src/pages/Dashboard.test.tsx +98 -0
  60. package/src/pages/Dashboard.tsx +60 -0
  61. package/src/pages/DynamicPage.tsx +237 -0
  62. package/src/pages/FormsAdmin.test.tsx +98 -0
  63. package/src/pages/FormsAdmin.tsx +459 -0
  64. package/src/pages/Home.test.tsx +98 -0
  65. package/src/pages/Home.tsx +144 -0
  66. package/src/pages/Login.test.tsx +98 -0
  67. package/src/pages/Login.tsx +108 -0
  68. package/src/pages/PagesAdmin.test.tsx +98 -0
  69. package/src/pages/PagesAdmin.tsx +1022 -0
  70. package/src/pages/Profile.test.tsx +98 -0
  71. package/src/pages/Profile.tsx +319 -0
  72. package/src/pages/Register.test.tsx +98 -0
  73. package/src/pages/Register.tsx +116 -0
  74. package/src/pages/Requests.test.tsx +95 -0
  75. package/src/pages/Requests.tsx +422 -0
  76. package/src/pages/ResetPassword.test.tsx +98 -0
  77. package/src/pages/ResetPassword.tsx +92 -0
  78. package/src/pages/Settings.test.tsx +98 -0
  79. package/src/pages/Settings.tsx +393 -0
  80. package/src/pages/Setup.tsx +407 -0
  81. package/src/pages/StorageAdmin.test.tsx +150 -0
  82. package/src/pages/StorageAdmin.tsx +769 -0
  83. package/src/pages/Submissions.test.tsx +95 -0
  84. package/src/pages/Submissions.tsx +378 -0
  85. package/src/pages/Templates.test.tsx +98 -0
  86. package/src/pages/Templates.tsx +103 -0
  87. package/src/pages/ThemeAdmin.test.tsx +144 -0
  88. package/src/pages/ThemeAdmin.tsx +1000 -0
  89. package/src/pages/Users.test.tsx +95 -0
  90. package/src/pages/Users.tsx +334 -0
  91. package/src/pages/Verify.test.tsx +98 -0
  92. package/src/pages/Verify.tsx +95 -0
  93. package/src/prompts/index.ts +13 -0
  94. package/src/prompts/pages/publicPage.ts +44 -0
  95. package/src/prompts/sharedConstants.ts +12 -0
  96. package/src/prompts/tabs/board/adminboard.ts +32 -0
  97. package/src/prompts/tabs/board/privateboard.ts +36 -0
  98. package/src/prompts/tabs/board/publicboard.ts +36 -0
  99. package/src/prompts/tabs/calendar/admincalendar.ts +32 -0
  100. package/src/prompts/tabs/calendar/privatecalendar.ts +36 -0
  101. package/src/prompts/tabs/calendar/publiccalendar.ts +36 -0
  102. package/src/prompts/tabs/crud/admin.ts +54 -0
  103. package/src/prompts/tabs/crud/private.ts +55 -0
  104. package/src/prompts/tabs/crud/shared.ts +53 -0
  105. package/src/prompts/tabs/table/admintable.ts +32 -0
  106. package/src/prompts/tabs/table/privatetable.ts +36 -0
  107. package/src/prompts/tabs/table/publictable.ts +36 -0
  108. package/src/setupTests.ts +1 -0
  109. package/src/templates/AdminPageTemplate.tsx +678 -0
  110. package/src/templates/PrivatePageTemplate.tsx +594 -0
  111. package/src/templates/PublicPageTemplate.tsx +92 -0
  112. package/src/templates/SharedPageTemplate.tsx +551 -0
  113. package/src/templates/TemplateBoard.test.tsx +106 -0
  114. package/src/templates/TemplateBoard.tsx +642 -0
  115. package/src/templates/TemplateCalendar.test.tsx +106 -0
  116. package/src/templates/TemplateCalendar.tsx +848 -0
  117. package/src/templates/TemplateConfirmation.test.tsx +106 -0
  118. package/src/templates/TemplateConfirmation.tsx +145 -0
  119. package/src/templates/TemplateInlineForm.test.tsx +106 -0
  120. package/src/templates/TemplateInlineForm.tsx +129 -0
  121. package/src/templates/TemplatePopupForm.test.tsx +106 -0
  122. package/src/templates/TemplatePopupForm.tsx +174 -0
  123. package/src/templates/TemplateTable.test.tsx +106 -0
  124. package/src/templates/TemplateTable.tsx +675 -0
@@ -0,0 +1,563 @@
1
+ import { Database, User as UserIcon, Menu, Sun, Moon, Users, Settings, HelpCircle, LayoutTemplate, Mail, LogIn, HardDrive, LayoutDashboard, ChevronDown, Layers, MoreHorizontal } from 'lucide-react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../lib/AuthContext';
4
+ import { useState, useEffect, useRef } from 'react';
5
+ import { motion, AnimatePresence } from 'framer-motion';
6
+ import { useTheme } from '../lib/ThemeContext';
7
+ // import { themeConfig } from '../theme.config'; // We now use dynamic activeConfig
8
+ import { ContactPopup } from './ContactPopup';
9
+ import { AdminNotifications } from './AdminNotifications';
10
+ import { db } from '../lib/firebase';
11
+ import { collection, onSnapshot, doc } from 'firebase/firestore';
12
+ import { defaultMenuOrder } from '../configs/pages/menuOrders.config';
13
+ import type { OrderItem } from '../configs/pages/tabOrders.config';
14
+ import { parseOrderItems } from '../configs/pages/tabOrders.config';
15
+ import { defaultSupportConfig } from '../configs/pages/support.config';
16
+
17
+ export interface BaseMenuItem {
18
+ label: string;
19
+ path?: string;
20
+ action?: () => void;
21
+ submenu?: { label: string; path?: string; action?: () => void }[];
22
+ }
23
+
24
+ // System menu stubs — always available even if not in Firestore
25
+ const systemMenuDefaults: Record<string, { label: string; path: string }> = {
26
+ templates: { label: 'Templates', path: '/templates' },
27
+ setup: { label: 'Setup', path: '/setup' },
28
+ contact: { label: 'Contact', path: '/contact' },
29
+ };
30
+
31
+ export function Navbar({ customMenu, standalone }: { customMenu?: BaseMenuItem[], standalone?: boolean }) {
32
+ const { user, userRole, signOut } = useAuth();
33
+ const navigate = useNavigate();
34
+ const [dropdownOpen, setDropdownOpen] = useState<'user' | 'nav' | 'guest' | 'overflow' | false>(false);
35
+ const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
36
+ const [contactOpen, setContactOpen] = useState(false);
37
+ const [supportOpen, setSupportOpen] = useState(false);
38
+ const [photoError, setPhotoError] = useState(false);
39
+ const displayName = user?.displayName || user?.email?.split('@')[0] || 'User';
40
+ const capitalizedName = displayName.charAt(0).toUpperCase() + displayName.slice(1);
41
+
42
+ const { theme, toggleTheme, activeConfig } = useTheme();
43
+
44
+ const handleSignOut = async () => {
45
+ await signOut();
46
+ navigate('/');
47
+ setDropdownOpen(false);
48
+ };
49
+
50
+ // ── Public menu: reads from sys_pages + sys_configs/page_order ──────────
51
+ const [pageInfoMap, setPageInfoMap] = useState<Record<string, { label: string; path: string }>>({ ...systemMenuDefaults });
52
+ const [menuOrder, setMenuOrder] = useState<OrderItem[]>(defaultMenuOrder);
53
+
54
+ useEffect(() => {
55
+ if (!db) return;
56
+
57
+ // 1. Page info from sys_pages (same source as Settings.tsx)
58
+ const unsubPages = onSnapshot(collection(db, 'sys_pages'), (snap) => {
59
+ const map: Record<string, { label: string; path: string }> = { ...systemMenuDefaults };
60
+ snap.forEach(d => {
61
+ const val = d.data();
62
+ if (val.enabled === false) return;
63
+ const label = val.pageName || val.title || systemMenuDefaults[d.id]?.label || d.id;
64
+ const path = val.route || (d.id === 'home' ? '/' : '/' + d.id);
65
+ map[d.id] = { label, path };
66
+ });
67
+ setPageInfoMap(map);
68
+ });
69
+
70
+ // 2. Order + visibility from sys_configs/page_order (same source as Settings.tsx)
71
+ const unsubOrder = onSnapshot(doc(db, 'sys_configs', 'page_order'), (d) => {
72
+ setMenuOrder(
73
+ d.exists() && Array.isArray(d.data().order)
74
+ ? parseOrderItems(d.data().order)
75
+ : defaultMenuOrder
76
+ );
77
+ });
78
+
79
+ const unsubSupport = onSnapshot(doc(db, 'sys_tabs', 'support'), (d) => {
80
+ if (d.exists() && d.data().pageName) {
81
+ setSupportConfig(d.data().pageName);
82
+ }
83
+ });
84
+
85
+ return () => { unsubPages(); unsubOrder(); unsubSupport(); };
86
+ }, []);
87
+
88
+ const [supportConfig, setSupportConfig] = useState(defaultSupportConfig.tabName);
89
+
90
+ // Build ordered, visibility-filtered menu (excluding 'home' — that's the logo link)
91
+ const builtMenu: BaseMenuItem[] = (() => {
92
+ const hiddenIds = new Set(menuOrder.filter(o => o.hidden).map(o => o.id));
93
+ const placed = new Set<string>();
94
+ const items: BaseMenuItem[] = [];
95
+
96
+ // Follow the order list, skip hidden + home
97
+ menuOrder.forEach(o => {
98
+ if (o.hidden || o.id === 'home') return;
99
+ const info = pageInfoMap[o.id];
100
+ if (info) {
101
+ items.push({ label: info.label, path: info.path });
102
+ placed.add(o.id);
103
+ }
104
+ });
105
+
106
+ // Append any pages not yet in the order list (new custom pages)
107
+ Object.entries(pageInfoMap).forEach(([id, info]) => {
108
+ if (!placed.has(id) && !hiddenIds.has(id) && id !== 'home' && info.path !== '/' && info.path !== '/support') {
109
+ items.push({ label: info.label, path: info.path });
110
+ }
111
+ });
112
+
113
+ return items;
114
+ })();
115
+
116
+ const menuItemsToUse = customMenu || builtMenu;
117
+
118
+ const guestNavRef = useRef<HTMLDivElement>(null);
119
+ const measureRef = useRef<HTMLDivElement>(null);
120
+ const [maxVisible, setMaxVisible] = useState(menuItemsToUse.length);
121
+ const navKey = menuItemsToUse.map(o => o.label).join('|');
122
+
123
+ useEffect(() => {
124
+ if (standalone || user) return;
125
+
126
+ const measure = () => {
127
+ const container = guestNavRef.current;
128
+ const measureRow = measureRef.current;
129
+ if (!container || !measureRow) return;
130
+
131
+ const containerWidth = container.clientWidth;
132
+ const moreButtonWidth = 50;
133
+ const gap = 28; // gap-7 = 28px
134
+ const tabEls = measureRow.querySelectorAll<HTMLElement>('[data-measure-nav]');
135
+ if (tabEls.length === 0) { setMaxVisible(menuItemsToUse.length); return; }
136
+
137
+ let checkWidth = 0;
138
+ let allFit = true;
139
+ for (let i = 0; i < tabEls.length; i++) {
140
+ checkWidth += tabEls[i].offsetWidth + (i > 0 ? gap : 0);
141
+ if (checkWidth > containerWidth) { allFit = false; break; }
142
+ }
143
+ if (allFit) { setMaxVisible(menuItemsToUse.length); return; }
144
+
145
+ let totalWidth = 0;
146
+ let fitCount = 0;
147
+ for (let i = 0; i < tabEls.length; i++) {
148
+ const tabWidth = tabEls[i].offsetWidth;
149
+ const neededWidth = totalWidth + tabWidth + (i > 0 ? gap : 0);
150
+ if (neededWidth + moreButtonWidth > containerWidth) break;
151
+ totalWidth = neededWidth;
152
+ fitCount++;
153
+ }
154
+ setMaxVisible(Math.max(fitCount, 1));
155
+ };
156
+
157
+ const raf = requestAnimationFrame(measure);
158
+ if (typeof ResizeObserver === 'undefined') return () => cancelAnimationFrame(raf);
159
+ const ro = new ResizeObserver(() => requestAnimationFrame(measure));
160
+ if (guestNavRef.current) ro.observe(guestNavRef.current);
161
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); };
162
+ }, [navKey, standalone, user, menuItemsToUse.length]);
163
+
164
+ const visibleMenuItems = menuItemsToUse.slice(0, maxVisible);
165
+ const hiddenMenuItems = menuItemsToUse.slice(maxVisible);
166
+
167
+ return (
168
+ <nav className="flex items-center justify-between w-full max-w-7xl mx-auto z-40 p-4 md:p-10 relative">
169
+ {/* Logo */}
170
+ <div className="flex items-center gap-2 md:gap-4 z-50">
171
+ <Link to="/" className="flex items-center gap-2 md:gap-3 group cursor-pointer z-50">
172
+ <div className="w-9 h-9 md:w-10 md:h-10 rounded-xl glass-panel flex items-center justify-center glow-hover shadow-lg transition-transform duration-500 group-hover:rotate-[360deg] group-active:scale-95 overflow-hidden">
173
+ {activeConfig?.logoUrl ? (
174
+ <img src={activeConfig.logoUrl} alt="Logo" className="w-full h-full object-cover" />
175
+ ) : (
176
+ <Database className="w-4 h-4 md:w-5 md:h-5 text-accent" />
177
+ )}
178
+ </div>
179
+ <span className="hidden lg:block font-bold text-[22px] tracking-tight text-gradient transition-colors duration-300 drop-shadow-sm">
180
+ {activeConfig?.appName || ''}
181
+ </span>
182
+ </Link>
183
+ </div>
184
+
185
+
186
+
187
+ <AnimatePresence>
188
+ {contactOpen && <ContactPopup formId="contact" onClose={() => setContactOpen(false)} />}
189
+ </AnimatePresence>
190
+ <AnimatePresence>
191
+ {supportOpen && <ContactPopup formId="support" onClose={() => setSupportOpen(false)} />}
192
+ </AnimatePresence>
193
+
194
+ {/* Right Auth Block */}
195
+ <div className="flex items-center gap-2 md:gap-4 z-50 relative ml-auto flex-1 justify-end">
196
+ {!standalone && !user && (
197
+ <>
198
+ <div
199
+ ref={measureRef}
200
+ aria-hidden="true"
201
+ className="flex items-center gap-7 whitespace-nowrap px-1"
202
+ style={{ position: 'fixed', top: -9999, left: 0, visibility: 'hidden', height: 0, pointerEvents: 'none' }}
203
+ >
204
+ {menuItemsToUse.map((item, idx) => (
205
+ <span key={item.path || idx} data-measure-nav className="text-[13px] md:text-[14px] font-bold tracking-wide flex items-center gap-1.5 shrink-0">
206
+ {item.label}
207
+ {item.submenu && <ChevronDown className="w-3 h-3" />}
208
+ </span>
209
+ ))}
210
+ </div>
211
+
212
+ <div className="hidden sm:flex items-center gap-7 mr-6 w-full max-w-full justify-end relative" ref={guestNavRef}>
213
+ {visibleMenuItems.map((item, idx) => (
214
+ <div
215
+ key={`${item.path || 'no-path'}-${idx}`}
216
+ className="relative shrink-0"
217
+ onMouseEnter={() => setActiveSubmenu(item.label)}
218
+ onMouseLeave={() => setActiveSubmenu(null)}
219
+ >
220
+ {item.path ? (
221
+ <Link
222
+ to={item.path}
223
+ className="text-[13px] md:text-[14px] font-bold tracking-wide text-foreground/50 hover:text-accent transition-all cursor-pointer flex items-center gap-1.5"
224
+ >
225
+ {item.label}
226
+ {item.submenu && <ChevronDown className="w-3 h-3" />}
227
+ </Link>
228
+ ) : (
229
+ <button
230
+ onClick={item.action}
231
+ className="text-[13px] md:text-[14px] font-bold tracking-wide text-foreground/50 hover:text-accent transition-all flex items-center gap-1.5"
232
+ >
233
+ {item.label}
234
+ {item.submenu && <ChevronDown className="w-3 h-3" />}
235
+ </button>
236
+ )}
237
+
238
+ {item.submenu && activeSubmenu === item.label && (
239
+ <div className="absolute top-[100%] left-0 pt-2 z-50">
240
+ <motion.div
241
+ initial={{ opacity: 0, y: 5 }} animate={{ opacity: 1, y: 0 }}
242
+ className="min-w-[180px] max-h-[300px] overflow-y-auto styled-scrollbars bg-background/95 backdrop-blur-xl border border-[var(--panel-border)] rounded-2xl shadow-xl flex flex-col py-2"
243
+ >
244
+ {item.submenu.map((sub, sIdx) => (
245
+ sub.path ? (
246
+ <Link key={sIdx} to={sub.path} className="px-5 py-3 text-[13px] font-medium text-foreground/70 hover:text-foreground hover:bg-foreground/5 transition-colors">
247
+ {sub.label}
248
+ </Link>
249
+ ) : (
250
+ <button key={sIdx} onClick={sub.action} className="px-5 py-3 text-left w-full text-[13px] font-medium text-foreground/70 hover:text-foreground hover:bg-foreground/5 transition-colors">
251
+ {sub.label}
252
+ </button>
253
+ )
254
+ ))}
255
+ </motion.div>
256
+ </div>
257
+ )}
258
+ </div>
259
+ ))}
260
+
261
+ {hiddenMenuItems.length > 0 && (
262
+ <div
263
+ className="relative flex items-center shrink-0"
264
+ onMouseEnter={() => setDropdownOpen('overflow')}
265
+ >
266
+ <button
267
+ onClick={() => setDropdownOpen(dropdownOpen === 'overflow' ? false : 'overflow')}
268
+ className={`p-1 rounded-lg transition-colors flex items-center ${dropdownOpen === 'overflow' ? 'bg-foreground/5 text-accent' : 'text-foreground/50 hover:bg-foreground/5 hover:text-accent'}`}
269
+ >
270
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="w-5 h-5"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
271
+ </button>
272
+
273
+ <AnimatePresence>
274
+ {dropdownOpen === 'overflow' && (
275
+ <>
276
+ <div className="fixed inset-0 z-40" onClick={() => setDropdownOpen(false)} />
277
+ <motion.div
278
+ initial={{ opacity: 0, y: 5, scale: 0.98 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 5, scale: 0.98 }}
279
+ className="absolute top-[100%] pt-1 right-0 min-w-[200px] max-h-[350px] overflow-y-auto styled-scrollbars bg-background border border-[var(--panel-border)] rounded-2xl shadow-xl flex flex-col py-2 z-50 origin-top-right"
280
+ >
281
+ {hiddenMenuItems.map((sub, sIdx) => (
282
+ sub.path ? (
283
+ <Link key={sIdx} to={sub.path} onClick={() => setDropdownOpen(false)} className="px-5 py-3 text-[13px] font-bold tracking-wide text-foreground/70 hover:text-foreground hover:bg-foreground/5 transition-colors">
284
+ {sub.label}
285
+ </Link>
286
+ ) : (
287
+ <button key={sIdx} onClick={() => { if(sub.action) sub.action(); setDropdownOpen(false); }} className="px-5 py-3 text-left w-full text-[13px] font-bold tracking-wide text-foreground/70 hover:text-foreground hover:bg-foreground/5 transition-colors">
288
+ {sub.label}
289
+ </button>
290
+ )
291
+ ))}
292
+ </motion.div>
293
+ </>
294
+ )}
295
+ </AnimatePresence>
296
+ </div>
297
+ )}
298
+ </div>
299
+
300
+ {activeConfig?.showThemeToggle && (
301
+ <button
302
+ onClick={toggleTheme}
303
+ className="p-2 mr-2 rounded-xl glass-panel hover:bg-foreground/5 transition-all text-accent border border-[var(--panel-border)] hover:border-accent/40 shrink-0"
304
+ aria-label="Toggle Theme"
305
+ >
306
+ {theme === 'dark' ? <Sun className="w-4 h-4 md:w-5 md:h-5" /> : <Moon className="w-4 h-4 md:w-5 md:h-5" />}
307
+ </button>
308
+ )}
309
+
310
+ <div className="flex items-center gap-2 shrink-0">
311
+ <Link to="/login" className="hidden sm:flex text-[14px] font-bold tracking-wide text-foreground/80 hover:text-accent transition-colors px-3 py-1.5 shrink-0">
312
+ Sign In
313
+ </Link>
314
+ <Link to="/register" className="hidden sm:flex text-[13px] md:text-[14px] font-bold tracking-wide px-4 py-1.5 md:px-5 md:py-2 rounded-xl btn-primary transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5 active:scale-95 items-center justify-center ml-2">
315
+ Get Started
316
+ </Link>
317
+
318
+ {/* Mobile Guest Hamburger */}
319
+ <div className="relative sm:hidden ml-1">
320
+ <button
321
+ onClick={() => setDropdownOpen(dropdownOpen === 'guest' ? false : 'guest')}
322
+ className={`p-2 rounded-xl glass-panel glow-hover border transition-all ${dropdownOpen === 'guest' ? 'border-accent bg-foreground/5' : 'border-[var(--panel-border)] hover:bg-foreground/5'}`}
323
+ >
324
+ <Menu className="w-5 h-5 text-accent/80" />
325
+ </button>
326
+
327
+ <AnimatePresence>
328
+ {dropdownOpen === 'guest' && (
329
+ <>
330
+ <div className="fixed inset-0 z-40" onClick={() => setDropdownOpen(false)} />
331
+ <motion.div
332
+ initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }}
333
+ className="absolute top-full right-0 mt-3 w-[220px] p-2 glass-panel border border-[var(--panel-border)] rounded-2xl shadow-2xl z-50 flex flex-col gap-1 origin-top-right"
334
+ >
335
+ <Link to="/templates" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
336
+ <LayoutTemplate className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Templates</span>
337
+ </Link>
338
+ <Link to="/setup" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
339
+ <Settings className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Setup</span>
340
+ </Link>
341
+ <Link to="/contact" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all w-full text-left">
342
+ <Mail className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Contact</span>
343
+ </Link>
344
+ <div className="pt-2 mt-1 border-t border-[var(--panel-border)]/50 flex flex-col gap-1">
345
+ <Link to="/login" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all w-full">
346
+ <LogIn className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Sign In</span>
347
+ </Link>
348
+ <Link to="/register" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl btn-primary w-full shadow-lg hover:-translate-y-0.5 transition-transform text-white">
349
+ <span className="text-[14px] font-bold mx-auto">Get Started</span>
350
+ </Link>
351
+ </div>
352
+ </motion.div>
353
+ </>
354
+ )}
355
+ </AnimatePresence>
356
+ </div>
357
+ </div>
358
+ </>
359
+ )}
360
+
361
+ {!standalone && user && (
362
+ <div className="flex items-center gap-3">
363
+ <div className="flex items-center gap-1 sm:gap-2 mr-1">
364
+ {userRole === 'admin' && <AdminNotifications />}
365
+
366
+ {activeConfig?.showThemeToggle && (
367
+ <button
368
+ onClick={toggleTheme}
369
+ className="p-2 ml-1 rounded-xl glass-panel hover:bg-foreground/5 transition-all text-accent border border-[var(--panel-border)] hover:border-accent/40"
370
+ aria-label="Toggle Theme"
371
+ >
372
+ {theme === 'dark' ? <Sun className="w-4 h-4 md:w-5 md:h-5" /> : <Moon className="w-4 h-4 md:w-5 md:h-5" />}
373
+ </button>
374
+ )}
375
+ </div>
376
+
377
+ <div className="relative z-50 flex items-center gap-3">
378
+ <div className="relative flex-shrink-0">
379
+ <button
380
+ onClick={() => setDropdownOpen(dropdownOpen === 'user' ? false : 'user')}
381
+ className={`flex items-center gap-2 group p-1 sm:pr-2.5 rounded-full glass-panel hover:bg-foreground/5 transition-all duration-300 border ${dropdownOpen === 'user' ? 'border-accent/40 bg-foreground/5' : 'border-transparent hover:border-[var(--panel-border)]'}`}
382
+ >
383
+ <div className="w-8 h-8 md:w-9 md:h-9 rounded-full bg-accent/10 flex items-center justify-center overflow-hidden border border-[var(--panel-border)] group-hover:border-accent/30 transition-colors shrink-0 text-accent">
384
+ {user.photoURL && !photoError ? (
385
+ <img src={user.photoURL} alt="Profile" className="w-full h-full object-cover" onError={() => setPhotoError(true)} />
386
+ ) : (
387
+ <UserIcon className="w-4 h-4 md:w-5 md:h-5 text-accent" />
388
+ )}
389
+ </div>
390
+ <div className="hidden sm:flex flex-col text-left mr-1 justify-center min-w-[70px] max-w-[160px]">
391
+ <span className="text-[12px] font-extrabold leading-tight text-foreground truncate w-full">
392
+ {capitalizedName}
393
+ </span>
394
+ <span className="text-[10px] font-mono tracking-wider leading-tight text-accent truncate w-full mt-0.5 uppercase font-bold">
395
+ {userRole === 'admin' ? 'Admin' : 'Free User'}
396
+ </span>
397
+ </div>
398
+ </button>
399
+
400
+ <AnimatePresence>
401
+ {dropdownOpen === 'user' && (
402
+ <>
403
+ <div className="fixed inset-0 z-40" onClick={() => setDropdownOpen(false)} />
404
+ <motion.div
405
+ initial={{ opacity: 0, y: -5, scale: 0.98 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, scale: 0.98, y: -5 }}
406
+ className="absolute top-full mt-2 right-0 w-[280px] md:w-[320px] glass-panel border border-[var(--panel-border)] rounded-2xl p-2 shadow-2xl z-50 flex flex-col origin-top-right"
407
+ >
408
+ <div className="p-3 border-b border-[var(--panel-border)]/50 mb-1 flex flex-col gap-1.5">
409
+ <span className="font-bold text-[15px] text-foreground truncate">{capitalizedName}</span>
410
+ <span className="text-[12px] text-foreground/50 truncate font-mono">{user.email}</span>
411
+ </div>
412
+
413
+ <div className="py-1">
414
+ <Link to="/profile" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3">
415
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
416
+ <UserIcon className="w-3 h-3 text-accent" />
417
+ </span>
418
+ Profile Settings
419
+ </Link>
420
+ <Link to="/drive" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3">
421
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
422
+ <HardDrive className="w-3 h-3 text-accent" />
423
+ </span>
424
+ Drive
425
+ </Link>
426
+
427
+ {userRole === 'admin' && (
428
+ <>
429
+ <Link to="/users" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3">
430
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
431
+ <Users className="w-3 h-3 text-accent" />
432
+ </span>
433
+ Users
434
+ </Link>
435
+ <Link to="/submissions" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3">
436
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
437
+ <Mail className="w-3 h-3 text-accent" />
438
+ </span>
439
+ Submissions
440
+ </Link>
441
+ <Link to="/requests" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3">
442
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
443
+ <HelpCircle className="w-3 h-3 text-accent" />
444
+ </span>
445
+ Requests
446
+ </Link>
447
+ <Link to="/settings" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3">
448
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
449
+ <Settings className="w-3 h-3 text-accent" />
450
+ </span>
451
+ Settings
452
+ </Link>
453
+ <Link to="/pages" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3">
454
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
455
+ <Layers className="w-3 h-3 text-accent" />
456
+ </span>
457
+ Pages
458
+ </Link>
459
+ </>
460
+ )}
461
+
462
+ {userRole !== 'admin' && (
463
+ <Link to="/support" onClick={() => setDropdownOpen(false)} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-semibold tracking-wide hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all gap-3 text-left">
464
+ <span className="w-5 h-5 rounded-md bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0">
465
+ <HelpCircle className="w-3 h-3 text-accent" />
466
+ </span>
467
+ {supportConfig}
468
+ </Link>
469
+ )}
470
+ </div>
471
+
472
+ <div className="pt-1 mt-1 border-t border-[var(--panel-border)]/50">
473
+ <button onClick={handleSignOut} className="flex items-center w-full px-3 py-2.5 rounded-xl text-[13px] font-bold tracking-wide hover:bg-red-500/10 hover:text-red-500 text-foreground/70 transition-all gap-3">
474
+ <span className="w-5 h-5 rounded-md bg-red-500/5 flex items-center justify-center shrink-0 text-red-500">
475
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="w-3 h-3"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
476
+ </span>
477
+ Sign Out
478
+ </button>
479
+ </div>
480
+ </motion.div>
481
+ </>
482
+ )}
483
+ </AnimatePresence>
484
+ </div>
485
+
486
+ {/* Mobile hamburger for users (only shown if needed, e.g., missing top nav links) */}
487
+ <div className="relative sm:hidden">
488
+ <button
489
+ onClick={() => setDropdownOpen(dropdownOpen === 'nav' ? false : 'nav')}
490
+ className={`p-2 rounded-xl glass-panel glow-hover border transition-all ${dropdownOpen === 'nav' ? 'border-accent bg-foreground/5' : 'border-[var(--panel-border)] hover:bg-foreground/5'}`}
491
+ >
492
+ <Menu className="w-5 h-5 text-accent/80" />
493
+ </button>
494
+
495
+ <AnimatePresence>
496
+ {dropdownOpen === 'nav' && (
497
+ <>
498
+ <div className="fixed inset-0 z-40" onClick={() => setDropdownOpen(false)} />
499
+ <motion.div
500
+ initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }}
501
+ className="absolute top-full right-0 mt-3 w-[220px] p-2 glass-panel border border-[var(--panel-border)] rounded-2xl shadow-2xl z-50 flex flex-col gap-1 origin-top-right"
502
+ >
503
+ <Link to="/profile" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
504
+ <UserIcon className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Profile Settings</span>
505
+ </Link>
506
+ <Link to="/drive" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
507
+ <HardDrive className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Drive</span>
508
+ </Link>
509
+ <Link to="/dashboard" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
510
+ <LayoutDashboard className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Dashboard</span>
511
+ </Link>
512
+ {userRole === 'admin' && (
513
+ <>
514
+ <Link to="/users" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
515
+ <Users className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Users</span>
516
+ </Link>
517
+ <Link to="/submissions" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
518
+ <Mail className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Submissions</span>
519
+ </Link>
520
+ <Link to="/requests" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
521
+ <HelpCircle className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Requests</span>
522
+ </Link>
523
+ <Link to="/settings" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all">
524
+ <Settings className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Settings</span>
525
+ </Link>
526
+ </>
527
+ )}
528
+ {userRole !== 'admin' && (
529
+ <Link to="/support" onClick={() => setDropdownOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-foreground/5 text-foreground/80 hover:text-foreground transition-all w-full text-left">
530
+ <HelpCircle className="w-4 h-4 text-accent/70" /> <span className="text-[14px] font-semibold">Support</span>
531
+ </Link>
532
+ )}
533
+ <div className="pt-1 mt-1 border-t border-[var(--panel-border)]/50">
534
+ <button onClick={handleSignOut} className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-red-500/10 text-red-500/80 hover:text-red-500 transition-all w-full">
535
+ <span className="text-[14px] font-semibold">Sign Out</span>
536
+ </button>
537
+ </div>
538
+ </motion.div>
539
+ </>
540
+ )}
541
+ </AnimatePresence>
542
+ </div>
543
+ </div>
544
+ </div>
545
+ )}
546
+
547
+ {standalone && (
548
+ <>
549
+ {activeConfig?.showThemeToggle && (
550
+ <button
551
+ onClick={toggleTheme}
552
+ className="p-2 ml-1 rounded-xl glass-panel hover:bg-foreground/5 transition-all text-accent border border-[var(--panel-border)] hover:border-accent/40"
553
+ aria-label="Toggle Theme"
554
+ >
555
+ {theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
556
+ </button>
557
+ )}
558
+ </>
559
+ )}
560
+ </div>
561
+ </nav>
562
+ );
563
+ }
@@ -0,0 +1,15 @@
1
+ export const contactFormConfig = {
2
+ systemName: 'contact',
3
+ formType: 'public',
4
+ submitPage: 'submissions',
5
+ redirectTo: '/',
6
+ title: 'Get in Touch',
7
+ buttonName: 'Contact Us',
8
+ submitText: 'Send Message',
9
+ fields: [
10
+ { name: 'name', type: 'text', label: 'Full Name', placeholder: 'John Doe', required: true },
11
+ { name: 'email', type: 'email', label: 'Email Address', placeholder: 'john@example.com', required: true },
12
+ { name: 'phone', type: 'tel', label: 'Phone Number', placeholder: '+1 (555) 000-0000', required: false },
13
+ { name: 'questions', type: 'textarea', label: 'Your Questions', placeholder: 'How can we help you?', required: true }
14
+ ]
15
+ };
@@ -0,0 +1,29 @@
1
+ import { contactFormConfig } from './contactForm.config';
2
+ import { supportFormConfig } from './supportForm.config';
3
+ import { pubFormConfig } from './pubForm.config';
4
+ import { userFormConfig } from './userForm.config';
5
+
6
+ export interface FormField {
7
+ name: string;
8
+ type: string;
9
+ label: string;
10
+ placeholder?: string;
11
+ required: boolean;
12
+ }
13
+
14
+ export interface FormConfig {
15
+ title: string;
16
+ systemName: string;
17
+ formType: 'public' | 'private';
18
+ submitPage: string;
19
+ redirectTo: string;
20
+ buttonName?: string;
21
+ submitText: string;
22
+ enabled?: boolean;
23
+ fields: FormField[];
24
+ }
25
+
26
+ export const defaultForms: Record<string, FormConfig> = {
27
+ contact: contactFormConfig as FormConfig,
28
+ support: supportFormConfig as FormConfig,
29
+ };
@@ -0,0 +1,11 @@
1
+ export const pubFormConfig = {
2
+ systemName: 'pubForm',
3
+ formType: 'public',
4
+ submitPage: 'submissions',
5
+ redirectTo: '',
6
+ title: 'Public Form',
7
+ description: 'Please describe your request',
8
+ buttonName: 'Open Form',
9
+ submitText: 'Submit',
10
+ fields: []
11
+ };
@@ -0,0 +1,14 @@
1
+ export const supportFormConfig = {
2
+ systemName: 'support',
3
+ formType: 'private',
4
+ submitPage: 'requests',
5
+ redirectTo: '/dashboard',
6
+ title: 'Request Assistance',
7
+ buttonName: 'Get Assistance',
8
+ submitText: 'Submit Request',
9
+ fields: [
10
+ { name: 'topic', type: 'text', label: 'Topic', placeholder: 'E.g. Password reset issue', required: true },
11
+ { name: 'contactEmail', type: 'email', label: 'Contact Email', placeholder: 'your@email.com', required: true },
12
+ { name: 'description', type: 'textarea', label: 'Description', placeholder: 'Please describe your issue in detail...', required: true }
13
+ ]
14
+ };
@@ -0,0 +1,11 @@
1
+ export const userFormConfig = {
2
+ systemName: 'userForm',
3
+ formType: 'private',
4
+ submitPage: 'requests',
5
+ redirectTo: '',
6
+ title: 'User Form',
7
+ description: 'How can we help you today?',
8
+ buttonName: 'Open Form',
9
+ submitText: 'Submit',
10
+ fields: []
11
+ };