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,848 @@
1
+ import { motion, AnimatePresence } from 'framer-motion';
2
+ import { ArrowLeft, ChevronLeft, ChevronRight, Calendar as CalendarIcon, Clock, Plus, LayoutList, X, Trash, Edit2, Mail, Loader2, Check, Bot } from 'lucide-react';
3
+ import { Link } from 'react-router-dom';
4
+ import { Button } from '../components/Button';
5
+ import { Input } from '../components/Input';
6
+ import { ConfirmModal } from '../components/ConfirmModal';
7
+ import { DashboardNav } from '../components/DashboardNav';
8
+ import { useState, useEffect } from 'react';
9
+ import { useAuth } from '../lib/AuthContext';
10
+ import { db, auth } from '../lib/firebase';
11
+ import { collection, addDoc, onSnapshot, query, where, orderBy, doc, deleteDoc, updateDoc, setDoc, serverTimestamp, getDocs, getDoc } from 'firebase/firestore';
12
+ import { privateCalendarPrompt, adminCalendarPrompt, publicCalendarPrompt } from '../prompts';
13
+ import { onAuthStateChanged } from 'firebase/auth';
14
+
15
+ type CalendarMode = 'public' | 'private' | 'shared' | 'admin_only';
16
+
17
+ interface CalEvent {
18
+ id: string;
19
+ dateStr: string;
20
+ title: string;
21
+ description?: string;
22
+ time: string;
23
+ duration?: string;
24
+ color?: string;
25
+ uid?: string;
26
+ creatorEmail?: string;
27
+ creatorName?: string;
28
+ creatorAvatar?: string;
29
+ customFields?: Record<string, string>;
30
+ }
31
+
32
+ interface ModalData {
33
+ id?: string;
34
+ uid?: string;
35
+ dateStr: string;
36
+ title: string;
37
+ description: string;
38
+ time: string;
39
+ duration: string;
40
+ color: string;
41
+ guestEmail: string;
42
+ customFields: Record<string, string>;
43
+ }
44
+
45
+ const pastelColors = [
46
+ { text: 'text-red-400', bgCircle: 'bg-red-400', bgCard: 'bg-red-500/[0.08]', borderCard: 'border-red-500/30', bgLight: 'bg-red-500/15', bgHover: 'hover:bg-red-500', bgAlt: 'bg-red-500/[0.04]', name: 'Red' },
47
+ { text: 'text-orange-400', bgCircle: 'bg-orange-400', bgCard: 'bg-orange-500/[0.08]', borderCard: 'border-orange-500/30', bgLight: 'bg-orange-500/15', bgHover: 'hover:bg-orange-500', bgAlt: 'bg-orange-500/[0.04]', name: 'Orange' },
48
+ { text: 'text-yellow-400', bgCircle: 'bg-yellow-400', bgCard: 'bg-yellow-500/[0.08]', borderCard: 'border-yellow-500/30', bgLight: 'bg-yellow-500/15', bgHover: 'hover:bg-yellow-500', bgAlt: 'bg-yellow-500/[0.04]', name: 'Yellow' },
49
+ { text: 'text-green-400', bgCircle: 'bg-green-400', bgCard: 'bg-green-500/[0.08]', borderCard: 'border-green-500/30', bgLight: 'bg-green-500/15', bgHover: 'hover:bg-green-500', bgAlt: 'bg-green-500/[0.04]', name: 'Green' },
50
+ { text: 'text-blue-400', bgCircle: 'bg-blue-400', bgCard: 'bg-blue-500/[0.08]', borderCard: 'border-blue-500/30', bgLight: 'bg-blue-500/15', bgHover: 'hover:bg-blue-500', bgAlt: 'bg-blue-500/[0.04]', name: 'Blue' },
51
+ { text: 'text-accent', bgCircle: 'bg-accent', bgCard: 'bg-accent/[0.08]', borderCard: 'border-accent/30', bgLight: 'bg-accent/15', bgHover: 'hover:bg-accent', bgAlt: 'bg-accent/[0.04]', name: 'Purple' },
52
+ { text: 'text-pink-400', bgCircle: 'bg-pink-400', bgCard: 'bg-pink-500/[0.08]', borderCard: 'border-pink-500/30', bgLight: 'bg-pink-500/15', bgHover: 'hover:bg-pink-500', bgAlt: 'bg-pink-500/[0.04]', name: 'Pink' },
53
+ { text: 'text-foreground/60',bgCircle: 'bg-foreground/30',bgCard: 'bg-foreground/[0.04]', borderCard: 'border-[var(--panel-border)]/50', bgLight: 'bg-foreground/15', bgHover: 'hover:bg-foreground/30', bgAlt: 'bg-foreground/[0.02]', name: 'Default' },
54
+ ];
55
+
56
+ export interface CalendarProps {
57
+ title?: string;
58
+ subtitle?: string;
59
+ backUrl?: string;
60
+ eventsCollection?: string;
61
+ defaultTimeFormat?: string;
62
+ config?: any;
63
+ }
64
+
65
+ const BLANK_MODAL: ModalData = { dateStr: '', title: '', description: '', time: '12:00', duration: '60', color: '', guestEmail: '', customFields: {} };
66
+
67
+ export function TemplateCalendar({
68
+ title = 'Calendar View',
69
+ subtitle = '/templates/calendar',
70
+ backUrl = '/templates',
71
+ eventsCollection = 'pub_events',
72
+ defaultTimeFormat = '12h',
73
+ config,
74
+ }: CalendarProps) {
75
+
76
+ // ── Tab mode detection ─────────────────────────────────────────────────────
77
+ const isTabMode = !!(config?.isPrivate || config?.isAdmin || config?.isShared);
78
+ const isAdmin = config?.isAdmin;
79
+ const isShared = config?.isShared;
80
+ const isPrivate = config?.isPrivate && !isAdmin && !isShared;
81
+
82
+ // Config-driven settings (with sensible defaults for standalone page mode)
83
+ const showButton = config?.showButton !== false;
84
+ const buttonText = config?.buttonText || '+';
85
+ const buttonStyle = config?.buttonStyle || 'primary';
86
+ const showViewsButton = config?.showViewsButton !== false;
87
+ const viewsButtonStyle = config?.viewsButtonStyle || 'secondary';
88
+ const showPrompt = config?.showPrompt !== false;
89
+ const defaultView = config?.defaultView || 'calendar';
90
+ const customFieldsDef: string[] = config?.customFields || [];
91
+
92
+ // ── Mode detection from collection prefix ──────────────────────────────────
93
+ const mode: CalendarMode = eventsCollection.startsWith('user_') ? 'private'
94
+ : eventsCollection.startsWith('mem_') ? 'shared'
95
+ : eventsCollection.startsWith('admin_') ? 'admin_only'
96
+ : 'public';
97
+
98
+ // ── State ──────────────────────────────────────────────────────────────────
99
+ const [view, setView] = useState<'month' | 'list'>(() => {
100
+ try {
101
+ const saved = localStorage.getItem(`cal_view_${eventsCollection}`);
102
+ if (saved === 'list' || saved === 'month') return saved as 'month' | 'list';
103
+ } catch {}
104
+ return defaultView === 'board' ? 'list' : 'month';
105
+ });
106
+ const [availableForms, setAvailableForms] = useState<string[]>([]);
107
+ const [events, setEvents] = useState<CalEvent[]>([]);
108
+ const [showModal, setShowModal] = useState(false);
109
+ const [viewingEvent, setViewingEvent] = useState<CalEvent | null>(null);
110
+ const [isSaving, setIsSaving] = useState(false);
111
+ const [isDeleting, setIsDeleting] = useState(false);
112
+ const [confirmDeleteEvent, setConfirmDeleteEvent] = useState<CalEvent | null>(null);
113
+ const [modalData, setModalData] = useState<ModalData>(BLANK_MODAL);
114
+ const [currentDate, setCurrentDate] = useState(new Date());
115
+ const [copied, setCopied] = useState(false);
116
+
117
+ // Auth
118
+ const [currentUser, setCurrentUser] = useState<any>(null);
119
+ const [userProfile, setUserProfile] = useState<any>(null);
120
+ const [authReady, setAuthReady] = useState(false);
121
+
122
+ // 12/24h toggle
123
+ const [use24Hour, setUse24Hour] = useState(() => {
124
+ try { return localStorage.getItem('calendar_use_24h') === 'true'; } catch { return defaultTimeFormat === '24h'; }
125
+ });
126
+ const handleToggle24Hour = () => {
127
+ const next = !use24Hour;
128
+ setUse24Hour(next);
129
+ try { localStorage.setItem('calendar_use_24h', String(next)); } catch {}
130
+ };
131
+
132
+ useEffect(() => {
133
+ try { localStorage.setItem(`cal_view_${eventsCollection}`, view); } catch {}
134
+
135
+ // Fetch forms only for relevant modes
136
+ if (mode === 'public' || mode === 'private') {
137
+ getDocs(collection(db, 'sys_forms')).then(snap => {
138
+ const type = mode === 'public' ? 'public' : 'private';
139
+ setAvailableForms(snap.docs.filter(d => d.data().formType === type).map(doc => doc.id));
140
+ });
141
+ } else {
142
+ setAvailableForms([]);
143
+ }
144
+ }, [view, eventsCollection, mode]);
145
+
146
+ // Save preferred view
147
+ const handleToggleView = () => {
148
+ const next = view === 'month' ? 'list' : 'month';
149
+ setView(next);
150
+ try { localStorage.setItem(`cal_view_${eventsCollection}`, next); } catch {}
151
+ };
152
+
153
+ // Session-remembered email for public mode
154
+ const getRememberedEmail = () => { try { return sessionStorage.getItem('cal_guest_email') || ''; } catch { return ''; } };
155
+ const rememberEmail = (e: string) => { try { sessionStorage.setItem('cal_guest_email', e); } catch {} };
156
+
157
+ // ── Auth listener ──────────────────────────────────────────────────────────
158
+ useEffect(() => {
159
+ const unsub = onAuthStateChanged(auth, async (user) => {
160
+ setCurrentUser(user);
161
+ if (user && mode === 'admin_only') {
162
+ try {
163
+ const snap = await getDoc(doc(db, 'user_profiles', user.uid));
164
+ if (snap.exists()) setUserProfile(snap.data());
165
+ } catch {}
166
+ }
167
+ setAuthReady(true);
168
+ });
169
+ return unsub;
170
+ }, [mode]);
171
+
172
+ // ── Events subscription ────────────────────────────────────────────────────
173
+ useEffect(() => {
174
+ if (!authReady) return;
175
+ if (mode !== 'public' && !currentUser) { setEvents([]); return; }
176
+
177
+ const q = (mode === 'private' && currentUser)
178
+ ? query(collection(db, eventsCollection), where('uid', '==', currentUser.uid))
179
+ : collection(db, eventsCollection);
180
+
181
+ const unsub = onSnapshot(q as any, (snap: any) => {
182
+ const fetched: CalEvent[] = [];
183
+ snap.forEach((d: any) => fetched.push({ id: d.id, ...d.data() } as CalEvent));
184
+ setEvents(fetched);
185
+ }, (err: any) => console.error('Calendar events:', err));
186
+
187
+ return () => unsub();
188
+ }, [eventsCollection, mode, currentUser?.uid, authReady]);
189
+
190
+ // ── Helpers ────────────────────────────────────────────────────────────────
191
+ const isOwner = (ev: CalEvent) => !!currentUser && currentUser.uid === ev.uid;
192
+
193
+ // Show edit/delete: private = always (personal cal), shared/admin = owner only, public = never
194
+ const canActOn = (ev: CalEvent) => {
195
+ if (mode === 'public') return false;
196
+ if (mode === 'private') return !!currentUser;
197
+ return isOwner(ev);
198
+ };
199
+
200
+ const formatTime = (t: string) => {
201
+ if (!t) return '';
202
+ if (use24Hour) return t;
203
+ const [h, m] = t.split(':');
204
+ let hInt = parseInt(h, 10);
205
+ const ampm = hInt >= 12 ? 'PM' : 'AM';
206
+ hInt = hInt % 12 || 12;
207
+ return `${hInt}:${m} ${ampm}`;
208
+ };
209
+
210
+ const formatDuration = (s?: string) => {
211
+ if (!s) return '60m';
212
+ const m = parseInt(s, 10);
213
+ if (isNaN(m)) return s;
214
+ if (m >= 60) return m % 60 === 0 ? `${m / 60}h` : `${Math.floor(m / 60)}h ${m % 60}m`;
215
+ return `${m}m`;
216
+ };
217
+
218
+ const to12hParts = (t: string) => {
219
+ const [hRaw, mRaw] = (t || '12:00').split(':');
220
+ let h = parseInt(hRaw, 10) || 0;
221
+ const m = parseInt(mRaw, 10) || 0;
222
+ const ampm = h >= 12 ? 'PM' : 'AM';
223
+ h = h % 12 || 12;
224
+ return { h, m, ampm };
225
+ };
226
+
227
+ const from12hParts = (h: number, m: number, ampm: string) => {
228
+ let h24 = h % 12;
229
+ if (ampm === 'PM') h24 += 12;
230
+ return `${h24.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
231
+ };
232
+
233
+ const getLocalStr = (d: Date) => {
234
+ const off = d.getTimezoneOffset() * 60000;
235
+ return new Date(d.getTime() - off).toISOString().split('T')[0];
236
+ };
237
+
238
+ const getTodayStr = () => getLocalStr(new Date());
239
+
240
+ // ── Calendar grid ──────────────────────────────────────────────────────────
241
+ const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
242
+ const monthEnd = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
243
+ const startDOW = monthStart.getDay();
244
+ const daysInMon = monthEnd.getDate();
245
+ const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
246
+ const dates = Array.from({ length: 42 }, (_, i) => {
247
+ const n = i - startDOW + 1;
248
+ const d = new Date(currentDate.getFullYear(), currentDate.getMonth(), n);
249
+ return { day: d.getDate(), dateStr: getLocalStr(d), isCurrentMonth: n > 0 && n <= daysInMon };
250
+ });
251
+
252
+ const handlePrev = () => setCurrentDate(view === 'month'
253
+ ? new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)
254
+ : new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1));
255
+
256
+ const handleNext = () => setCurrentDate(view === 'month'
257
+ ? new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)
258
+ : new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1));
259
+
260
+ // ── Add / Edit ─────────────────────────────────────────────────────────────
261
+ const promptAddEvent = (dateStr?: string) => {
262
+ if (mode !== 'public' && !currentUser) return;
263
+ const tmrw = new Date(); tmrw.setDate(tmrw.getDate() + 1);
264
+ const h = new Date().getHours().toString().padStart(2, '0');
265
+ setModalData({ ...BLANK_MODAL, dateStr: dateStr || getLocalStr(tmrw), time: `${h}:00`, guestEmail: mode === 'public' ? getRememberedEmail() : '', customFields: {} });
266
+ setShowModal(true);
267
+ };
268
+
269
+ const promptEditEvent = (ev: CalEvent) => {
270
+ let t = ev.time;
271
+ if (t) { const [hh, mm] = t.split(':'); if (hh && mm) t = `${hh.padStart(2,'0')}:${mm}`; }
272
+ setModalData({ id: ev.id, uid: ev.uid, dateStr: ev.dateStr, title: ev.title, description: ev.description || '', time: t || '12:00', duration: ev.duration || '60', color: ev.color || '', guestEmail: ev.creatorEmail || '', customFields: ev.customFields || {} });
273
+ setShowModal(true);
274
+ };
275
+
276
+ const handleSaveEvent = async (e: React.FormEvent) => {
277
+ e.preventDefault();
278
+ if (!modalData.title.trim()) return;
279
+ setIsSaving(true);
280
+ try {
281
+ const payload: any = {
282
+ dateStr: modalData.dateStr,
283
+ title: modalData.title.trim(),
284
+ description: modalData.description.trim(),
285
+ time: modalData.time,
286
+ duration: modalData.duration || '60',
287
+ };
288
+ if (modalData.color) payload.color = modalData.color;
289
+
290
+ // Save custom fields
291
+ if (customFieldsDef.length > 0) {
292
+ payload.customFields = modalData.customFields;
293
+ }
294
+
295
+ // Creator fields — only on new events
296
+ if (!modalData.id) {
297
+ if (mode === 'public') {
298
+ const email = modalData.guestEmail.trim();
299
+ payload.creatorEmail = email;
300
+ rememberEmail(email);
301
+ } else if (mode === 'private' || mode === 'shared') {
302
+ payload.uid = currentUser?.uid;
303
+ payload.creatorEmail = currentUser?.email || '';
304
+ payload.creatorName = currentUser?.displayName || '';
305
+ payload.creatorAvatar = currentUser?.photoURL || '';
306
+ } else if (mode === 'admin_only') {
307
+ payload.uid = currentUser?.uid;
308
+ payload.creatorEmail = userProfile?.email || currentUser?.email || '';
309
+ payload.creatorName = userProfile?.displayName || userProfile?.name || '';
310
+ payload.creatorAvatar = userProfile?.avatar || userProfile?.avatarUrl || currentUser?.photoURL || '';
311
+ }
312
+ }
313
+
314
+ if (modalData.id) {
315
+ await setDoc(doc(db, eventsCollection, modalData.id), payload, { merge: true });
316
+ if (viewingEvent?.id === modalData.id) setViewingEvent({ ...viewingEvent, ...payload } as CalEvent);
317
+ } else {
318
+ await setDoc(doc(db, eventsCollection, `ev_${Date.now()}`), payload, { merge: true });
319
+ }
320
+ setShowModal(false);
321
+ } catch (err) { console.error(err); }
322
+ setIsSaving(false);
323
+ };
324
+
325
+ const handleDeleteEvent = async () => {
326
+ if (!confirmDeleteEvent) return;
327
+ setIsDeleting(true);
328
+ try {
329
+ await deleteDoc(doc(db, eventsCollection, confirmDeleteEvent.id));
330
+ if (viewingEvent?.id === confirmDeleteEvent.id) setViewingEvent(null);
331
+ setConfirmDeleteEvent(null);
332
+ setShowModal(false);
333
+ } catch {}
334
+ setIsDeleting(false);
335
+ };
336
+
337
+ // Creator avatar block shown inside the view modal
338
+ const renderCreator = (ev: CalEvent) => {
339
+ if (mode === 'private') return null;
340
+ if (!ev.creatorEmail && !ev.creatorName) return null;
341
+ const letter = (ev.creatorName || ev.creatorEmail || '?').charAt(0).toUpperCase();
342
+ const col = pastelColors.find(c => c.text === ev.color);
343
+ return (
344
+ <div className="flex items-center gap-3 mt-4 p-3 rounded-xl bg-foreground/[0.03] border border-[var(--panel-border)]/50">
345
+ {ev.creatorAvatar
346
+ ? <img src={ev.creatorAvatar} alt="" className="w-8 h-8 rounded-full object-cover shrink-0" />
347
+ : <div className={`w-8 h-8 rounded-full ${col?.bgCircle || 'bg-accent'} flex items-center justify-center text-[13px] font-extrabold text-white shrink-0`}>{letter}</div>
348
+ }
349
+ <div className="flex flex-col min-w-0">
350
+ {ev.creatorName && <span className="text-[13px] font-bold text-foreground/90 truncate">{ev.creatorName}</span>}
351
+ {ev.creatorEmail && <span className="text-[12px] text-foreground/50 truncate">{ev.creatorEmail}</span>}
352
+ </div>
353
+ </div>
354
+ );
355
+ };
356
+
357
+ // Creator badge for card (compact) — shared/admin only
358
+ const renderCardCreator = (ev: CalEvent) => {
359
+ if (mode === 'private' || mode === 'public') return null;
360
+ if (!ev.creatorName && !ev.creatorEmail) return null;
361
+ const name = ev.creatorName || ev.creatorEmail?.split('@')[0] || '';
362
+ const initials = name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2);
363
+ return (
364
+ <div className="flex items-center gap-2 mt-2 pt-2 border-t border-[var(--panel-border)]/30">
365
+ {ev.creatorAvatar ? (
366
+ <img src={ev.creatorAvatar} alt="" className="w-5 h-5 rounded-full object-cover shrink-0" />
367
+ ) : (
368
+ <div className="w-5 h-5 rounded-full bg-accent/15 text-accent flex items-center justify-center text-[8px] font-extrabold shrink-0">
369
+ {initials}
370
+ </div>
371
+ )}
372
+ <div className="flex items-center gap-1 min-w-0 flex-1">
373
+ <span className="text-[11px] font-semibold text-foreground/40 truncate">{name}</span>
374
+ {ev.creatorEmail && <span className="text-[10px] text-foreground/25 truncate">· {ev.creatorEmail}</span>}
375
+ </div>
376
+ </div>
377
+ );
378
+ };
379
+
380
+ // Prompt text
381
+ const getPrompt = () => {
382
+ if (mode === 'admin_only') return adminCalendarPrompt(config, title, eventsCollection, availableForms);
383
+ if (mode === 'private') return privateCalendarPrompt(config, title, eventsCollection, availableForms);
384
+ return publicCalendarPrompt(config, title, eventsCollection, availableForms);
385
+ };
386
+ const promptText = getPrompt();
387
+
388
+ const handleCopyPrompt = () => {
389
+ navigator.clipboard.writeText(promptText);
390
+ setCopied(true);
391
+ setTimeout(() => setCopied(false), 2000);
392
+ };
393
+
394
+ // ── Auth guard (non-public modes) ─────────────────────────────────────────
395
+ if (authReady && mode !== 'public' && !currentUser) {
396
+ return (
397
+ <main className="flex-1 w-full flex items-center justify-center min-h-screen p-8">
398
+ <div className="text-center glass-panel border border-[var(--panel-border)] rounded-3xl p-12 max-w-sm w-full">
399
+ <div className="w-16 h-16 rounded-full bg-accent/10 border border-accent/20 flex items-center justify-center mx-auto mb-4">
400
+ <Mail className="w-7 h-7 text-accent" />
401
+ </div>
402
+ <h2 className="text-2xl font-extrabold mb-2">Sign in required</h2>
403
+ <p className="text-foreground/50 text-[14px] mb-6">You need to be signed in to access this calendar.</p>
404
+ <Link to="/login" className="inline-flex items-center justify-center gap-2 px-6 py-2.5 rounded-xl btn-primary font-bold text-[14px] w-full">Sign In</Link>
405
+ </div>
406
+ </main>
407
+ );
408
+ }
409
+
410
+ if (!authReady && mode !== 'public') {
411
+ return (
412
+ <main className="flex-1 w-full flex items-center justify-center min-h-screen">
413
+ <Loader2 className="w-8 h-8 animate-spin text-accent" />
414
+ </main>
415
+ );
416
+ }
417
+
418
+ const canAdd = (mode === 'public' || !!currentUser) && showButton;
419
+
420
+ // ── Shared calendar toolbar (inside container) ─────────────────────────────
421
+ const CalendarToolbar = () => (
422
+ <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 p-6 md:px-8 md:py-6 border-b border-[var(--panel-border)]/50 bg-background/50 shrink-0">
423
+ <div className="flex-1 min-w-0">
424
+ <h2 className="text-xl font-extrabold text-foreground tracking-tight truncate">
425
+ {config?.tabTitle || config?.tabName || title}
426
+ </h2>
427
+ <p className="text-[13px] font-medium text-foreground/50 mt-1">
428
+ {events.length} {events.length === 1 ? 'event' : 'events'} · {view === 'month' ? 'Month' : 'Board'} View
429
+ </p>
430
+ </div>
431
+ <div className="flex items-center gap-2 shrink-0">
432
+ {/* Prompt Copy */}
433
+ {showPrompt && (
434
+ <motion.button
435
+ whileHover={{ scale: 1.05 }}
436
+ whileTap={{ scale: 0.95 }}
437
+ onClick={handleCopyPrompt}
438
+ title={copied ? 'Copied!' : 'Copy Developer Prompt'}
439
+ className={`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer ${copied ? 'bg-emerald-500/10 text-emerald-500' : 'btn-secondary'}`}
440
+ >
441
+ {copied ? <Check className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
442
+ </motion.button>
443
+ )}
444
+ {/* 12h/24h toggle */}
445
+ <motion.button
446
+ whileHover={{ scale: 1.05 }}
447
+ whileTap={{ scale: 0.95 }}
448
+ onClick={handleToggle24Hour}
449
+ className="w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer btn-secondary text-[12px] font-extrabold"
450
+ >
451
+ {use24Hour ? '24h' : '12h'}
452
+ </motion.button>
453
+ {/* View toggle */}
454
+ {showViewsButton && (
455
+ <motion.button
456
+ whileHover={{ scale: 1.05 }}
457
+ whileTap={{ scale: 0.95 }}
458
+ onClick={handleToggleView}
459
+ title={view === 'month' ? 'Switch to Board' : 'Switch to Calendar'}
460
+ className={`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer ${viewsButtonStyle === 'primary' ? 'btn-primary' : 'btn-secondary'}`}
461
+ >
462
+ {view === 'month' ? <LayoutList className="w-4 h-4" /> : <CalendarIcon className="w-4 h-4" />}
463
+ </motion.button>
464
+ )}
465
+ {/* Add event */}
466
+ {canAdd && (
467
+ <motion.button
468
+ whileHover={{ scale: 1.05 }}
469
+ whileTap={{ scale: 0.95 }}
470
+ onClick={() => promptAddEvent()}
471
+ className={`h-10 flex items-center justify-center gap-2 rounded-xl transition-all duration-300 cursor-pointer ${buttonText === '+' ? 'w-10' : 'px-5'} ${buttonStyle === 'primary' ? 'btn-primary' : 'btn-secondary'}`}
472
+ >
473
+ {buttonText === '+' ? <Plus className="w-4 h-4" /> : <span className="text-[13px] font-bold">{buttonText}</span>}
474
+ </motion.button>
475
+ )}
476
+ </div>
477
+ </div>
478
+ );
479
+
480
+ // ── Month/list internal toolbar ────────────────────────────────────────────
481
+ const MonthToolbar = () => (
482
+ <div className="p-4 md:p-6 border-b border-[var(--panel-border)]/50 flex items-center justify-between bg-foreground/[0.01]">
483
+ <h3 className="text-xl font-extrabold tracking-tight ml-2">
484
+ {view === 'month'
485
+ ? currentDate.toLocaleString('default', { month: 'long', year: 'numeric' })
486
+ : currentDate.toLocaleString('default', { month: 'long', day: 'numeric', year: 'numeric' })}
487
+ </h3>
488
+ <div className="flex items-center gap-1">
489
+ <button onClick={() => setCurrentDate(new Date())} className="px-4 py-2 text-[13px] font-bold border border-[var(--panel-border)] rounded-xl mr-2 hover:bg-foreground/5 transition-colors">Today</button>
490
+ <button onClick={handlePrev} className="p-2 rounded-xl text-foreground/50 hover:text-foreground hover:bg-foreground/5 transition-colors border border-transparent hover:border-[var(--panel-border)]"><ChevronLeft className="w-5 h-5" /></button>
491
+ <button onClick={handleNext} className="p-2 rounded-xl text-foreground/50 hover:text-foreground hover:bg-foreground/5 transition-colors border border-transparent hover:border-[var(--panel-border)]"><ChevronRight className="w-5 h-5" /></button>
492
+ </div>
493
+ </div>
494
+ );
495
+
496
+ // ── Calendar Content (shared between tab and standalone) ───────────────────
497
+ const CalendarContent = () => (
498
+ <>
499
+ {MonthToolbar()}
500
+ {view === 'month' ? (
501
+ <div className="overflow-x-auto w-full flex-1">
502
+ <div className="min-w-[800px] h-full flex flex-col">
503
+ <div className="grid grid-cols-7 border-b border-[var(--panel-border)]/50 bg-foreground/[0.02]">
504
+ {days.map(d => <div key={d} className="py-3 text-center text-[12px] font-bold uppercase tracking-wider text-foreground/40">{d}</div>)}
505
+ </div>
506
+ <div className="grid grid-cols-7 auto-rows-auto bg-[var(--panel-border)] gap-[1px] relative flex-1">
507
+ {dates.map((dateObj, idx) => {
508
+ const isToday = dateObj.dateStr === getTodayStr();
509
+ const dayEvents = events.filter(e => e.dateStr === dateObj.dateStr);
510
+ return (
511
+ <div
512
+ key={idx}
513
+ onClick={() => dateObj.isCurrentMonth && canAdd && promptAddEvent(dateObj.dateStr)}
514
+ className={`min-h-[140px] p-2 bg-[var(--panel-bg)] flex flex-col relative group
515
+ ${!dateObj.isCurrentMonth ? 'opacity-30 bg-background/50 pointer-events-none' : canAdd ? 'hover:bg-foreground/[0.03] transition-colors cursor-cell' : ''}`}
516
+ >
517
+ <div className="flex justify-between items-start mb-2 shrink-0">
518
+ <span className={`w-8 h-8 rounded-full flex items-center justify-center text-[14px] font-bold ${isToday ? 'bg-accent text-white shadow-lg shadow-accent/20' : 'text-foreground/70 group-hover:text-foreground'}`}>
519
+ {dateObj.day}
520
+ </span>
521
+ </div>
522
+ <div className="flex flex-col gap-1.5 flex-1 w-full relative z-10 pb-1">
523
+ {dayEvents.map(ev => {
524
+ const col = pastelColors.find(c => c.text === ev.color);
525
+ const showEdit = canActOn(ev);
526
+ return (
527
+ <div key={ev.id} className={`px-2 py-1.5 rounded-lg border transition-colors shadow-sm cursor-pointer group/ev relative ${col ? col.bgCard + ' ' + col.borderCard : 'bg-accent/20 border-accent/30 hover:bg-accent/40'}`} onClick={e => { e.stopPropagation(); setViewingEvent(ev); }}>
528
+ <p className={`text-[12px] font-bold truncate leading-tight ${showEdit ? 'pr-5' : ''} ${col ? col.text : 'text-foreground'}`}>{ev.title}</p>
529
+ <p className="text-[10px] text-foreground/70 mt-0.5">{formatTime(ev.time)}</p>
530
+ {showEdit && (
531
+ <button onClick={e => { e.stopPropagation(); promptEditEvent(ev); }} className="absolute top-1 right-1 opacity-0 group-hover/ev:opacity-100 p-1 text-foreground/60 hover:bg-foreground/10 hover:text-foreground transition-all rounded">
532
+ <Edit2 className="w-3 h-3" />
533
+ </button>
534
+ )}
535
+ </div>
536
+ );
537
+ })}
538
+ </div>
539
+ </div>
540
+ );
541
+ })}
542
+ </div>
543
+ <style>{`
544
+ .styled-scrollbars::-webkit-scrollbar{width:4px}
545
+ .styled-scrollbars::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--accent-color) 30%,transparent);border-radius:4px}
546
+ input[type="time"]::-webkit-calendar-picker-indicator{display:none!important}
547
+ input[type="date"]{color-scheme:dark}
548
+ input[type="date"]::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.5;transition:opacity .2s}
549
+ input[type="date"]::-webkit-calendar-picker-indicator:hover{opacity:1}
550
+ `}</style>
551
+ </div>
552
+ </div>
553
+ ) : (
554
+ /* Board/List view */
555
+ <div className="flex flex-col p-4 md:p-8 overflow-y-auto w-full flex-1 gap-4">
556
+ {events
557
+ .filter(e => e.dateStr === getLocalStr(currentDate))
558
+ .sort((a, b) => a.time.localeCompare(b.time))
559
+ .map(ev => {
560
+ const evDate = new Date(ev.dateStr + 'T12:00:00');
561
+ const col = pastelColors.find(c => c.text === ev.color);
562
+ const showActions = canActOn(ev);
563
+ return (
564
+ <div key={ev.id} onClick={() => setViewingEvent(ev)} className={`p-4 rounded-2xl border flex flex-col cursor-pointer transition-colors group ${col ? col.bgCard + ' ' + col.borderCard + ' hover:brightness-110' : 'bg-foreground/[0.01] border-[var(--panel-border)]/70 hover:border-accent/40 hover:bg-foreground/[0.02]'}`}>
565
+ <div className="flex items-center gap-3">
566
+ <div className="flex items-center gap-3 shrink-0">
567
+ <div className={`w-11 h-11 md:w-12 md:h-12 rounded-xl flex flex-col items-center justify-center border transition-colors shadow-sm ${col ? col.bgCard + ' ' + col.borderCard : 'bg-foreground/5 border-[var(--panel-border)] group-hover:bg-accent/10 group-hover:border-accent/30'}`}>
568
+ <span className={`text-[10px] font-bold uppercase leading-none mb-0.5 ${col ? col.text : 'text-foreground/40 group-hover:text-accent/60'}`}>{evDate.toLocaleString('default', { month: 'short' })}</span>
569
+ <span className={`text-[18px] font-black leading-none ${col ? col.text : 'group-hover:text-accent'}`}>{evDate.getDate()}</span>
570
+ </div>
571
+ <div className="h-6 w-px bg-[var(--panel-border)] hidden sm:block group-hover:bg-accent/30 transition-colors" />
572
+ </div>
573
+ <div className="flex flex-col gap-1 min-w-0 flex-1">
574
+ <h4 className={`text-[15px] md:text-[16px] font-extrabold tracking-tight truncate ${col ? col.text : 'text-foreground group-hover:text-accent'}`}>{ev.title}</h4>
575
+ <div className="flex items-center gap-3 text-[12px] text-foreground/50 font-medium">
576
+ <span>{formatTime(ev.time)}</span>
577
+ <span className="flex items-center gap-1"><Clock className="w-3 h-3" />{formatDuration(ev.duration)}</span>
578
+ </div>
579
+ </div>
580
+ {showActions && (
581
+ <div className="ml-auto pl-2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
582
+ <button onClick={e => { e.stopPropagation(); promptEditEvent(ev); }} className="p-2.5 rounded-xl text-foreground/50 hover:bg-foreground/10 hover:text-foreground transition-colors"><Edit2 className="w-4 h-4" /></button>
583
+ <button onClick={e => { e.stopPropagation(); setConfirmDeleteEvent(ev); }} className="p-2.5 rounded-xl text-foreground/30 hover:bg-red-500/10 hover:text-red-500 transition-colors"><Trash className="w-4 h-4" /></button>
584
+ </div>
585
+ )}
586
+ </div>
587
+ {/* Creator badge for shared/admin */}
588
+ {isTabMode && renderCardCreator(ev)}
589
+ </div>
590
+ );
591
+ })}
592
+ {events.filter(e => e.dateStr === getLocalStr(currentDate)).length === 0 && (
593
+ <div className="p-12 text-center text-foreground/40 font-medium">No events scheduled for this day.</div>
594
+ )}
595
+ </div>
596
+ )}
597
+ </>
598
+ );
599
+
600
+ // ── Modals (shared) ────────────────────────────────────────────────────────
601
+ const Modals = () => (
602
+ <AnimatePresence>
603
+ {/* View event modal */}
604
+ {viewingEvent && (
605
+ <div key="view-modal" className="fixed inset-0 z-[100] flex items-center justify-center p-4">
606
+ <motion.div onClick={() => setViewingEvent(null)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 bg-background/80 backdrop-blur-xl cursor-pointer" />
607
+ <motion.div
608
+ initial={{ opacity: 0, scale: 0.95, y: 10 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 10 }}
609
+ className={`w-full max-w-[500px] p-6 sm:p-8 rounded-3xl glass-panel border shadow-2xl relative z-10 ${pastelColors.find(c => c.text === viewingEvent.color)?.bgCard || 'bg-background'} ${pastelColors.find(c => c.text === viewingEvent.color)?.borderCard || 'border-[var(--panel-border)]'}`}
610
+ >
611
+ <div className="absolute top-4 right-4 flex items-center gap-1 bg-background/50 backdrop-blur-3xl p-1 rounded-2xl border border-[var(--panel-border)]/50 shadow-sm z-50">
612
+ {canActOn(viewingEvent) && (
613
+ <>
614
+ <button onClick={() => { setViewingEvent(null); promptEditEvent(viewingEvent); }} className="p-2 rounded-xl text-foreground/50 hover:bg-foreground/10 hover:text-foreground transition-all" title="Edit"><Edit2 className="w-4 h-4" /></button>
615
+ <button onClick={() => { setConfirmDeleteEvent(viewingEvent); setViewingEvent(null); }} className="p-2 rounded-xl text-foreground/50 hover:bg-red-500/10 hover:text-red-500 transition-all" title="Delete"><Trash className="w-4 h-4" /></button>
616
+ <div className="w-px h-6 bg-[var(--panel-border)]/50 mx-1" />
617
+ </>
618
+ )}
619
+ <button onClick={() => setViewingEvent(null)} className="p-2 rounded-xl text-foreground/40 hover:bg-foreground/10 hover:text-foreground transition-all"><X className="w-5 h-5" /></button>
620
+ </div>
621
+
622
+ <div className="mb-6 pr-32">
623
+ <div className="flex items-center gap-2 mb-3">
624
+ <span className={`w-2.5 h-2.5 rounded-full ${pastelColors.find(c => c.text === viewingEvent.color)?.bgCircle || 'bg-accent'}`} />
625
+ <span className={`text-[11px] font-bold tracking-[0.2em] uppercase ${pastelColors.find(c => c.text === viewingEvent.color)?.text || 'text-accent'}`}>Event Details</span>
626
+ </div>
627
+ <h3 className={`text-3xl font-black mb-3 tracking-tight leading-tight break-words ${pastelColors.find(c => c.text === viewingEvent.color)?.text || 'text-foreground'}`}>{viewingEvent.title}</h3>
628
+ <div className="flex flex-col gap-2 mt-4">
629
+ <div className="flex items-center gap-2.5 text-[14px] text-foreground/70 font-medium">
630
+ <div className="w-8 h-8 rounded-full bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0"><CalendarIcon className="w-4 h-4 text-foreground/50" /></div>
631
+ {new Date(viewingEvent.dateStr + 'T12:00:00').toLocaleDateString('default', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
632
+ </div>
633
+ <div className="flex items-center gap-2.5 text-[14px] text-foreground/70 font-medium">
634
+ <div className="w-8 h-8 rounded-full bg-foreground/5 border border-[var(--panel-border)] flex items-center justify-center shrink-0"><Clock className="w-4 h-4 text-foreground/50" /></div>
635
+ {formatTime(viewingEvent.time)} <span className="text-foreground/40 text-[12px] ml-1">({formatDuration(viewingEvent.duration)})</span>
636
+ </div>
637
+ </div>
638
+ {renderCreator(viewingEvent)}
639
+ </div>
640
+
641
+ <div className="w-full h-px bg-gradient-to-r from-[var(--panel-border)]/0 via-[var(--panel-border)] to-[var(--panel-border)]/0 mb-6" />
642
+
643
+ {viewingEvent.description ? (
644
+ <div className="bg-foreground/[0.02] border border-[var(--panel-border)]/50 rounded-2xl py-3 px-4 mb-4 w-full overflow-hidden">
645
+ <div className="text-[14px] text-foreground/80 font-medium whitespace-pre-wrap leading-tight break-words">{viewingEvent.description}</div>
646
+ </div>
647
+ ) : (
648
+ <div className="p-4 mb-4 rounded-2xl border border-dashed border-[var(--panel-border)] bg-foreground/[0.01] text-center">
649
+ <p className="text-[12px] font-medium text-foreground/40">No description provided for this event.</p>
650
+ </div>
651
+ )}
652
+
653
+ {/* Custom fields */}
654
+ {viewingEvent.customFields && Object.entries(viewingEvent.customFields).filter(([, v]) => v).length > 0 && (
655
+ <div className="flex flex-col gap-2 mb-2">
656
+ {Object.entries(viewingEvent.customFields).filter(([, v]) => v).map(([key, value]) => (
657
+ <div key={key} className="flex items-start gap-2 text-[13px]">
658
+ <span className="font-bold text-foreground/50 capitalize shrink-0">{key}:</span>
659
+ <span className="text-foreground/80 font-medium break-words min-w-0">{value}</span>
660
+ </div>
661
+ ))}
662
+ </div>
663
+ )}
664
+ </motion.div>
665
+ </div>
666
+ )}
667
+
668
+ {/* Add / Edit modal */}
669
+ {showModal && (
670
+ <div key="edit-modal" className="fixed inset-0 z-[100] flex items-center justify-center p-4">
671
+ <motion.div onClick={() => setShowModal(false)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 bg-background/80 backdrop-blur-xl cursor-pointer" />
672
+ <motion.div
673
+ initial={{ opacity: 0, scale: 0.95, y: 10 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 10 }}
674
+ className="w-full max-w-[500px] p-6 sm:p-8 rounded-3xl glass-panel border border-[var(--panel-border)] shadow-2xl relative z-10 max-h-[90vh] overflow-y-auto styled-scrollbars"
675
+ >
676
+ <div className="absolute top-4 right-4 flex items-center gap-1">
677
+ {modalData.id && canActOn({ uid: modalData.uid } as CalEvent) && (
678
+ <button onClick={() => { setShowModal(false); setConfirmDeleteEvent({ id: modalData.id!, ...modalData } as any); }} className="p-2 rounded-xl text-foreground/30 hover:bg-red-500/10 hover:text-red-500 transition-all"><Trash className="w-4 h-4" /></button>
679
+ )}
680
+ <button onClick={() => setShowModal(false)} className="p-2 rounded-xl text-foreground/40 hover:bg-foreground/10 hover:text-foreground transition-all"><X className="w-5 h-5" /></button>
681
+ </div>
682
+
683
+ <div className="mb-6 pr-12">
684
+ <h3 className="text-2xl font-extrabold mb-1 tracking-tight">{modalData.id ? 'Edit Event' : 'Add Event'}</h3>
685
+ <p className="text-[14px] text-foreground/50">{modalData.id ? 'Modify your scheduled calendar block.' : 'Schedule a new calendar block.'}</p>
686
+ </div>
687
+
688
+ <form onSubmit={handleSaveEvent} className="space-y-4">
689
+ {mode === 'public' && !modalData.id && (
690
+ <Input label="Your Email" type="email" value={modalData.guestEmail} onChange={e => setModalData({ ...modalData, guestEmail: e.target.value })} placeholder="you@example.com" required />
691
+ )}
692
+
693
+ <Input label="Event Title" value={modalData.title} onChange={e => setModalData({ ...modalData, title: e.target.value })} placeholder="e.g. Design Sync" required />
694
+
695
+ <div className="flex flex-col w-full relative">
696
+ <label className="text-[13px] font-medium text-foreground/50 select-none mb-1.5 ml-1">Description</label>
697
+ <textarea value={modalData.description} onChange={e => setModalData({ ...modalData, description: e.target.value })} placeholder="e.g. Discussing the new features..." className="block min-h-[80px] w-full rounded-xl text-[15px] text-foreground px-4 py-3 transition-all duration-300 placeholder:text-foreground/40 focus:outline-none focus:border-accent glow-focus glass-panel styled-scrollbars resize-none" />
698
+ </div>
699
+
700
+ {/* Custom fields */}
701
+ {customFieldsDef.map(fieldName => (
702
+ <Input
703
+ key={fieldName}
704
+ label={fieldName}
705
+ value={modalData.customFields[fieldName] || ''}
706
+ onChange={e => setModalData({ ...modalData, customFields: { ...modalData.customFields, [fieldName]: e.target.value } })}
707
+ placeholder={`Enter ${fieldName.toLowerCase()}...`}
708
+ />
709
+ ))}
710
+
711
+ <style>{`
712
+ input[type="date"]::-webkit-calendar-picker-indicator{background:transparent;bottom:0;color:transparent;cursor:pointer;height:auto;left:0;position:absolute;right:0;top:0;width:auto;z-index:10}
713
+ input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
714
+ input[type="number"]{-moz-appearance:textfield}
715
+ `}</style>
716
+
717
+ <div className="relative group/date flex flex-col w-full">
718
+ <Input type="date" label="Date" required value={modalData.dateStr} onChange={e => setModalData({ ...modalData, dateStr: e.target.value })} className="pr-10" />
719
+ <div className="absolute right-4 bottom-4 pointer-events-none group-focus-within/date:text-accent text-foreground/40 transition-colors"><CalendarIcon className="w-4 h-4" /></div>
720
+ </div>
721
+
722
+ <div className="grid grid-cols-2 gap-4">
723
+ {use24Hour ? (
724
+ <Input type="time" label="Time" value={modalData.time} onChange={e => setModalData({ ...modalData, time: e.target.value })} required />
725
+ ) : (() => {
726
+ const { h, m, ampm } = to12hParts(modalData.time);
727
+ return (
728
+ <div className="flex flex-col w-full">
729
+ <label className="text-[13px] font-medium text-foreground/50 select-none mb-1.5 ml-1">Time</label>
730
+ <div className="flex items-center h-12 glass-panel border border-[var(--panel-border)] rounded-xl px-3 gap-1 focus-within:border-accent transition-colors">
731
+ <input type="number" min={1} max={12} value={h} onChange={e => setModalData({ ...modalData, time: from12hParts(Math.min(12, Math.max(1, parseInt(e.target.value) || 12)), m, ampm) })} className="w-8 text-center bg-transparent focus:outline-none text-foreground font-bold text-[15px] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" />
732
+ <span className="text-foreground/40 font-bold text-[15px] select-none">:</span>
733
+ <input type="number" min={0} max={59} value={m.toString().padStart(2,'0')} onChange={e => setModalData({ ...modalData, time: from12hParts(h, Math.min(59, Math.max(0, parseInt(e.target.value) || 0)), ampm) })} className="w-8 text-center bg-transparent focus:outline-none text-foreground font-bold text-[15px] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" />
734
+ <button type="button" onClick={() => setModalData({ ...modalData, time: from12hParts(h, m, ampm === 'AM' ? 'PM' : 'AM') })} className="ml-auto px-2 py-0.5 rounded-md bg-accent/10 text-accent text-[11px] font-extrabold uppercase tracking-wider hover:bg-accent/20 transition-colors shrink-0">{ampm}</button>
735
+ </div>
736
+ </div>
737
+ );
738
+ })()}
739
+ <Input type="number" min="1" max="1440" label="Duration (min)" value={modalData.duration} onChange={e => setModalData({ ...modalData, duration: e.target.value })} placeholder="60" required />
740
+ </div>
741
+
742
+ <div className="flex flex-col gap-2">
743
+ <label className="text-[11px] font-bold text-foreground/50 uppercase tracking-widest mt-1">Event Color</label>
744
+ <div className="flex gap-2 items-center relative z-10 w-full h-[46px]">
745
+ {pastelColors.map(c => (
746
+ <button key={c.name} type="button" onClick={() => setModalData({ ...modalData, color: modalData.color === c.text ? '' : c.text })} className={`w-6 h-6 rounded-full shrink-0 ${c.bgCircle} hover:scale-110 transition-transform flex items-center justify-center border border-[var(--panel-border)]`}>
747
+ {modalData.color === c.text && <div className="w-2.5 h-2.5 rounded-full bg-background" />}
748
+ </button>
749
+ ))}
750
+ </div>
751
+ </div>
752
+
753
+ <div className="pt-2">
754
+ <Button type="submit" isLoading={isSaving} className="w-full py-3">{modalData.id ? 'Save Changes' : 'Create Event'}</Button>
755
+ </div>
756
+ </form>
757
+ </motion.div>
758
+ </div>
759
+ )}
760
+
761
+ <ConfirmModal
762
+ isOpen={!!confirmDeleteEvent}
763
+ title="Confirm Deletion"
764
+ message={<>Are you entirely sure you wish to delete <strong>{confirmDeleteEvent?.title}</strong>? This action is permanent.</>}
765
+ onConfirm={handleDeleteEvent}
766
+ onCancel={() => { setConfirmDeleteEvent(null); }}
767
+ isProcessing={isDeleting}
768
+ confirmText="Yes, Delete"
769
+ />
770
+ </AnimatePresence>
771
+ );
772
+
773
+ // ── TAB MODE rendering (admin/shared/private) ─────────────────────────────
774
+ if (isTabMode) {
775
+ return (
776
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col">
777
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-6 flex items-center justify-between">
778
+ <div>
779
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">
780
+ {config?.tabTitle || config?.tabName || title}
781
+ </h1>
782
+ {subtitle && (
783
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
784
+ <span className="w-8 h-[1px] bg-foreground/10" />{subtitle}
785
+ </div>
786
+ )}
787
+ </div>
788
+ </motion.div>
789
+
790
+ <div className="flex flex-col gap-6 flex-1 pb-16">
791
+ <DashboardNav />
792
+
793
+ <motion.div
794
+ initial={{ opacity: 0, y: 20 }}
795
+ animate={{ opacity: 1, y: 0 }}
796
+ transition={{ delay: 0.1 }}
797
+ className="flex flex-col glass-panel border border-[var(--panel-border)] rounded-3xl overflow-hidden flex-1 shadow-2xl bg-background"
798
+ >
799
+ {CalendarToolbar()}
800
+ {CalendarContent()}
801
+ </motion.div>
802
+ </div>
803
+
804
+ {Modals()}
805
+ </main>
806
+ );
807
+ }
808
+
809
+ // ── STANDALONE PAGE rendering (public / templates preview) ─────────────────
810
+ return (
811
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col pt-12 min-h-screen">
812
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-4">
813
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">{title}</h1>
814
+ {subtitle && (
815
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
816
+ <span className="w-8 h-[1px] bg-foreground/10" />{subtitle}
817
+ </div>
818
+ )}
819
+ </motion.div>
820
+
821
+ <div className="flex justify-between items-center mb-6 border-b border-[var(--panel-border)] pb-6 relative z-20">
822
+ {backUrl
823
+ ? <Link to={backUrl} className="px-5 py-2 border border-[var(--panel-border)] rounded-xl text-[13px] font-bold hover:bg-foreground/5 transition-all flex items-center gap-2"><ArrowLeft className="w-4 h-4" /> Go Back</Link>
824
+ : <div />}
825
+ <div className="flex gap-2">
826
+ <Button variant="secondary" onClick={handleToggle24Hour} className="px-3 h-10 text-[12px] font-bold rounded-xl flex items-center justify-center border-[var(--panel-border)] border shrink-0 hover:bg-foreground/5 shadow-sm text-foreground/70">
827
+ {use24Hour ? '24h' : '12h'}
828
+ </Button>
829
+ <Button variant="secondary" onClick={handleToggleView} className="w-10 h-10 p-0 text-[14px] font-bold rounded-xl flex items-center justify-center border-[var(--panel-border)] border shrink-0">
830
+ {view === 'month' ? <LayoutList className="w-4 h-4 text-foreground/70" /> : <CalendarIcon className="w-4 h-4 text-foreground/70" />}
831
+ </Button>
832
+ {canAdd && (
833
+ <Button onClick={() => promptAddEvent()} className="px-6 py-2.5 text-[14px] font-bold rounded-xl shadow-lg flex items-center gap-2">
834
+ <Plus className="w-4 h-4" /> Event
835
+ </Button>
836
+ )}
837
+ </div>
838
+ </div>
839
+
840
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="glass-panel border border-[var(--panel-border)] rounded-3xl overflow-hidden shadow-2xl relative flex flex-col flex-1">
841
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-accent/5 blur-[120px] rounded-full pointer-events-none -z-10" />
842
+ {CalendarContent()}
843
+ </motion.div>
844
+
845
+ {Modals()}
846
+ </main>
847
+ );
848
+ }