firebase-os 1.1.3 → 1.1.5
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/FirebaseOS.d.ts +15 -0
- package/dist/firebase-os.cjs.js +2 -17
- package/dist/firebase-os.es.js +63 -72
- package/dist/index.d.ts +3 -0
- package/dist/lib/ConfigContext.d.ts +12 -0
- package/package.json +3 -2
- package/scripts/postinstall.js +89 -10
- package/src/App.css +184 -0
- package/src/App.tsx +214 -0
- package/src/FirebaseOS.tsx +80 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/AdminNotifications.test.tsx +98 -0
- package/src/components/AdminNotifications.tsx +194 -0
- package/src/components/Button.test.tsx +22 -0
- package/src/components/Button.tsx +53 -0
- package/src/components/ConfirmModal.test.tsx +98 -0
- package/src/components/ConfirmModal.tsx +73 -0
- package/src/components/ContactPopup.test.tsx +98 -0
- package/src/components/ContactPopup.tsx +437 -0
- package/src/components/CustomSelect.test.tsx +47 -0
- package/src/components/CustomSelect.tsx +89 -0
- package/src/components/DashboardNav.test.tsx +98 -0
- package/src/components/DashboardNav.tsx +281 -0
- package/src/components/Input.test.tsx +33 -0
- package/src/components/Input.tsx +61 -0
- package/src/components/JsonEditor.tsx +579 -0
- package/src/components/Navbar.test.tsx +98 -0
- package/src/components/Navbar.tsx +563 -0
- package/src/configs/forms/contactForm.config.ts +15 -0
- package/src/configs/forms/index.ts +29 -0
- package/src/configs/forms/pubForm.config.ts +11 -0
- package/src/configs/forms/supportForm.config.ts +14 -0
- package/src/configs/forms/userForm.config.ts +11 -0
- package/src/configs/pages/admin.config.ts +29 -0
- package/src/configs/pages/contact.config.ts +6 -0
- package/src/configs/pages/home.config.ts +18 -0
- package/src/configs/pages/mem.config.ts +2 -0
- package/src/configs/pages/menuOrders.config.ts +11 -0
- package/src/configs/pages/pub.config.ts +11 -0
- package/src/configs/pages/shared.config.ts +29 -0
- package/src/configs/pages/support.config.ts +7 -0
- package/src/configs/pages/tabOrders.config.ts +33 -0
- package/src/configs/pages/user.config.ts +29 -0
- package/src/configs/theme.config.ts +93 -0
- package/src/index.css +403 -0
- package/src/index.ts +22 -0
- package/src/lib/AuthContext.test.tsx +88 -0
- package/src/lib/AuthContext.tsx +191 -0
- package/src/lib/ConfigContext.tsx +45 -0
- package/src/lib/ThemeContext.tsx +227 -0
- package/src/lib/firebase.ts +91 -0
- package/src/main.tsx +22 -0
- package/src/microcomponents/AdminExampleContent.tsx +44 -0
- package/src/microcomponents/PrivateExampleContent.tsx +39 -0
- package/src/microcomponents/Public.tsx +126 -0
- package/src/microcomponents/SharedExampleContent.tsx +53 -0
- package/src/pages/Dashboard.test.tsx +98 -0
- package/src/pages/Dashboard.tsx +60 -0
- package/src/pages/DynamicPage.tsx +237 -0
- package/src/pages/FormsAdmin.test.tsx +98 -0
- package/src/pages/FormsAdmin.tsx +459 -0
- package/src/pages/Home.test.tsx +98 -0
- package/src/pages/Home.tsx +144 -0
- package/src/pages/Login.test.tsx +98 -0
- package/src/pages/Login.tsx +108 -0
- package/src/pages/PagesAdmin.test.tsx +98 -0
- package/src/pages/PagesAdmin.tsx +1022 -0
- package/src/pages/Profile.test.tsx +98 -0
- package/src/pages/Profile.tsx +319 -0
- package/src/pages/Register.test.tsx +98 -0
- package/src/pages/Register.tsx +116 -0
- package/src/pages/Requests.test.tsx +95 -0
- package/src/pages/Requests.tsx +422 -0
- package/src/pages/ResetPassword.test.tsx +98 -0
- package/src/pages/ResetPassword.tsx +92 -0
- package/src/pages/Settings.test.tsx +98 -0
- package/src/pages/Settings.tsx +393 -0
- package/src/pages/Setup.tsx +401 -0
- package/src/pages/StorageAdmin.test.tsx +150 -0
- package/src/pages/StorageAdmin.tsx +769 -0
- package/src/pages/Submissions.test.tsx +95 -0
- package/src/pages/Submissions.tsx +372 -0
- package/src/pages/Templates.test.tsx +98 -0
- package/src/pages/Templates.tsx +103 -0
- package/src/pages/ThemeAdmin.test.tsx +144 -0
- package/src/pages/ThemeAdmin.tsx +1000 -0
- package/src/pages/Users.test.tsx +95 -0
- package/src/pages/Users.tsx +334 -0
- package/src/pages/Verify.test.tsx +98 -0
- package/src/pages/Verify.tsx +95 -0
- package/src/prompts/index.ts +13 -0
- package/src/prompts/pages/publicPage.ts +44 -0
- package/src/prompts/sharedConstants.ts +12 -0
- package/src/prompts/tabs/board/adminboard.ts +32 -0
- package/src/prompts/tabs/board/privateboard.ts +36 -0
- package/src/prompts/tabs/board/publicboard.ts +36 -0
- package/src/prompts/tabs/calendar/admincalendar.ts +32 -0
- package/src/prompts/tabs/calendar/privatecalendar.ts +36 -0
- package/src/prompts/tabs/calendar/publiccalendar.ts +36 -0
- package/src/prompts/tabs/crud/admin.ts +54 -0
- package/src/prompts/tabs/crud/private.ts +55 -0
- package/src/prompts/tabs/crud/shared.ts +53 -0
- package/src/prompts/tabs/table/admintable.ts +32 -0
- package/src/prompts/tabs/table/privatetable.ts +36 -0
- package/src/prompts/tabs/table/publictable.ts +36 -0
- package/src/setupTests.ts +1 -0
- package/src/templates/AdminPageTemplate.tsx +678 -0
- package/src/templates/PrivatePageTemplate.tsx +594 -0
- package/src/templates/PublicPageTemplate.tsx +92 -0
- package/src/templates/SharedPageTemplate.tsx +551 -0
- package/src/templates/TemplateBoard.test.tsx +106 -0
- package/src/templates/TemplateBoard.tsx +642 -0
- package/src/templates/TemplateCalendar.test.tsx +106 -0
- package/src/templates/TemplateCalendar.tsx +848 -0
- package/src/templates/TemplateConfirmation.test.tsx +106 -0
- package/src/templates/TemplateConfirmation.tsx +145 -0
- package/src/templates/TemplateInlineForm.test.tsx +106 -0
- package/src/templates/TemplateInlineForm.tsx +129 -0
- package/src/templates/TemplatePopupForm.test.tsx +106 -0
- package/src/templates/TemplatePopupForm.tsx +174 -0
- package/src/templates/TemplateTable.test.tsx +106 -0
- 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
|
+
};
|