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.
Files changed (124) hide show
  1. package/dist/FirebaseOS.d.ts +15 -0
  2. package/dist/firebase-os.cjs.js +2 -17
  3. package/dist/firebase-os.es.js +63 -72
  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 +89 -10
  8. package/src/App.css +184 -0
  9. package/src/App.tsx +214 -0
  10. package/src/FirebaseOS.tsx +80 -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 +227 -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 +401 -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 +372 -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,95 @@
1
+ // @generated-test
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, screen, act } from '@testing-library/react';
4
+ import { BrowserRouter } from 'react-router-dom';
5
+ import { AuthProvider } from '../lib/AuthContext';
6
+ import { ThemeProvider } from '../lib/ThemeContext';
7
+ import { UsersTab } from './Users';
8
+
9
+ // Global mocks
10
+ Object.defineProperty(window, 'matchMedia', {
11
+ writable: true,
12
+ value: vi.fn().mockImplementation(query => ({
13
+ matches: false,
14
+ media: query,
15
+ onchange: null,
16
+ addListener: vi.fn(),
17
+ removeListener: vi.fn(),
18
+ addEventListener: vi.fn(),
19
+ removeEventListener: vi.fn(),
20
+ dispatchEvent: vi.fn(),
21
+ })),
22
+ });
23
+
24
+ vi.mock('../lib/AuthContext', () => ({
25
+ AuthProvider: ({ children }: any) => <>{children}</>,
26
+ useAuth: () => ({
27
+ user: { uid: 'mock-user-123', email: 'test@example.com' },
28
+ userWorkspaces: [],
29
+ activeWorkspace: null,
30
+ activeOrg: null,
31
+ loading: false
32
+ })
33
+ }));
34
+
35
+ vi.mock('../lib/ThemeContext', () => ({
36
+ ThemeProvider: ({ children }: any) => <>{children}</>,
37
+ useTheme: () => ({ themeMode: 'light', setThemeMode: vi.fn(), activeConfig: {} })
38
+ }));
39
+
40
+ vi.mock('firebase/auth', () => ({
41
+ getAuth: vi.fn(() => ({})),
42
+ onAuthStateChanged: vi.fn((auth, cb) => { cb({ uid: 'mock-user-123', email: 'test@example.com', getIdToken: vi.fn(() => Promise.resolve('mock-token')) }); return () => {}; })
43
+ }));
44
+ vi.mock('firebase/firestore', () => ({
45
+ getFirestore: vi.fn(() => ({})),
46
+ collection: vi.fn(),
47
+ doc: vi.fn(),
48
+ setDoc: vi.fn(() => Promise.resolve()),
49
+ addDoc: vi.fn(() => Promise.resolve()),
50
+ updateDoc: vi.fn(() => Promise.resolve()),
51
+ deleteDoc: vi.fn(() => Promise.resolve()),
52
+ query: vi.fn(),
53
+ where: vi.fn(),
54
+ orderBy: vi.fn(),
55
+ limit: vi.fn(),
56
+ getDoc: vi.fn(() => Promise.resolve({ exists: () => true, data: () => ({ role: 'super_admin' }) })),
57
+ getDocs: vi.fn(() => Promise.resolve({ docs: [], forEach: vi.fn() })),
58
+ onSnapshot: vi.fn((...args: any[]) => {
59
+ let cb = args[1];
60
+ if (typeof cb === 'function') {
61
+ cb({docs: [], forEach: vi.fn(), data: () => ({}), exists: () => true});
62
+ }
63
+ return () => {};
64
+ })
65
+ }));
66
+ vi.mock('firebase/storage', () => ({
67
+ getStorage: vi.fn(() => ({})),
68
+ ref: vi.fn(),
69
+ listAll: vi.fn(() => Promise.resolve({ items: [], prefixes: [] })),
70
+ getDownloadURL: vi.fn(() => Promise.resolve('mock-url')),
71
+ getMetadata: vi.fn(() => Promise.resolve({ size: 1024, timeCreated: new Date().toISOString() }))
72
+ }));
73
+
74
+ describe('UsersTab Component', () => {
75
+ it('renders without crashing', async () => {
76
+ // Wrap in standard application providers inside act to process async side effects and prevent warnings
77
+ await act(async () => {
78
+ render(
79
+ <BrowserRouter>
80
+ <AuthProvider>
81
+ <ThemeProvider>
82
+ {/* @ts-ignore */}
83
+ <UsersTab />
84
+ </ThemeProvider>
85
+ </AuthProvider>
86
+ </BrowserRouter>
87
+ );
88
+ // Wait a tick to flush background state updates
89
+ await new Promise(resolve => setTimeout(resolve, 0));
90
+ });
91
+
92
+ // Check if the document has anything rendered without throwing
93
+ expect(document.body).toBeDefined();
94
+ });
95
+ });
@@ -0,0 +1,334 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useParams, useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../lib/AuthContext';
4
+ import { db, storage } from '../lib/firebase';
5
+ import { collection, onSnapshot, doc, updateDoc } from 'firebase/firestore';
6
+ import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
7
+ import { Users as UsersIcon, Loader2, CheckCircle2, User as UserIcon, Camera } from 'lucide-react';
8
+ import { motion, AnimatePresence } from 'framer-motion';
9
+ import { DashboardNav } from '../components/DashboardNav';
10
+
11
+ interface UserProfile {
12
+ id: string;
13
+ email: string;
14
+ displayName?: string;
15
+ role?: string;
16
+ avatar?: string;
17
+ photoURL?: string;
18
+ createdAt?: string;
19
+ inactive?: boolean;
20
+ }
21
+
22
+ export function UsersTab() {
23
+ const { user, userRole } = useAuth();
24
+ const [loadingMembers, setLoadingMembers] = useState(true);
25
+ const navigate = useNavigate();
26
+ const { id: paramUserId } = useParams();
27
+
28
+ const [members, setMembers] = useState<UserProfile[]>([]);
29
+ const [msg, setMsg] = useState<{ text: string, type: 'success' | 'error' } | null>(null);
30
+
31
+ const [editingName, setEditingName] = useState(false);
32
+ const [tempName, setTempName] = useState('');
33
+ const [photoLoading, setPhotoLoading] = useState(false);
34
+
35
+ const selectedMember = paramUserId ? members.find(m => m.id === paramUserId) || null : (members.length > 0 ? members[0] : null);
36
+
37
+ const setSelectedMember = (member: UserProfile | null) => {
38
+ if (member) navigate(`/users/${member.id}`);
39
+ else navigate(`/users`);
40
+ };
41
+
42
+ useEffect(() => {
43
+ setLoadingMembers(true);
44
+ const unsubscribe = onSnapshot(collection(db, 'user_profiles'), (snapshot) => {
45
+ const fetched: UserProfile[] = [];
46
+ snapshot.forEach(docSnap => {
47
+ fetched.push({ id: docSnap.id, ...docSnap.data() } as UserProfile);
48
+ });
49
+ setMembers(fetched);
50
+ setLoadingMembers(false);
51
+
52
+ if (!paramUserId && fetched.length > 0) {
53
+ navigate(`/users/${fetched[0].id}`, { replace: true });
54
+ }
55
+ }, (error) => {
56
+ console.error("Error fetching users:", error);
57
+ setLoadingMembers(false);
58
+ });
59
+
60
+ return () => unsubscribe();
61
+ }, [paramUserId, navigate]);
62
+
63
+ const myProfile = members.find(m => m.id === user?.uid);
64
+
65
+ const canManageTarget = (target: UserProfile) => {
66
+ if (target.id === myProfile?.id) return false;
67
+ if (target.role !== 'admin') return true;
68
+
69
+ // Legacy admins without createdAt are treated as absolute oldest
70
+ if (!myProfile?.createdAt) return true;
71
+ if (!target.createdAt) return false;
72
+
73
+ // Normal comparison: oldest wins
74
+ return new Date(myProfile.createdAt).getTime() <= new Date(target.createdAt).getTime();
75
+ };
76
+
77
+ const handleUpdateRole = async (target: UserProfile, newIsAdmin: boolean) => {
78
+ if (userRole !== 'admin' || !canManageTarget(target)) return;
79
+ try {
80
+ await updateDoc(doc(db, 'user_profiles', target.id), { role: newIsAdmin ? 'admin' : null });
81
+ setMsg({ text: `User role updated successfully`, type: 'success' });
82
+ setTimeout(() => setMsg(null), 3000);
83
+ } catch (err: any) {
84
+ setMsg({ text: err.message, type: 'error' });
85
+ setTimeout(() => setMsg(null), 3000);
86
+ }
87
+ };
88
+
89
+ const handleDeactivate = async (target: UserProfile, deactivated: boolean) => {
90
+ if (userRole !== 'admin' || !canManageTarget(target)) return;
91
+ try {
92
+ await updateDoc(doc(db, 'user_profiles', target.id), { inactive: deactivated });
93
+ setMsg({ text: `User ${deactivated ? 'deactivated' : 'activated'} successfully`, type: 'success' });
94
+ setTimeout(() => setMsg(null), 3000);
95
+ } catch (err: any) {
96
+ setMsg({ text: err.message, type: 'error' });
97
+ setTimeout(() => setMsg(null), 3000);
98
+ }
99
+ };
100
+
101
+ const handleNameSave = async () => {
102
+ setEditingName(false);
103
+ if (!selectedMember || !canManageTarget(selectedMember) || tempName === selectedMember.displayName) return;
104
+ try {
105
+ await updateDoc(doc(db, 'user_profiles', selectedMember.id), { displayName: tempName });
106
+ setMsg({ text: 'Name updated successfully', type: 'success' });
107
+ setTimeout(() => setMsg(null), 3000);
108
+ } catch (err: any) {
109
+ setMsg({ text: err.message, type: 'error' });
110
+ setTimeout(() => setMsg(null), 3000);
111
+ }
112
+ };
113
+
114
+ const handlePhotoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
115
+ const file = e.target.files?.[0];
116
+ if (!file || !selectedMember || !canManageTarget(selectedMember)) return;
117
+ if (file.size > 2 * 1024 * 1024) {
118
+ setMsg({ text: 'File too large.', type: 'error' }); return;
119
+ }
120
+
121
+ setPhotoLoading(true);
122
+ try {
123
+ const storagePath = `user_avatars/${selectedMember.id}/file.png`;
124
+ const fileRef = ref(storage, storagePath);
125
+ await uploadBytes(fileRef, file);
126
+ const newPhotoURL = await getDownloadURL(fileRef);
127
+ await updateDoc(doc(db, 'user_profiles', selectedMember.id), { avatar: newPhotoURL });
128
+ setMsg({ text: 'Avatar successfully updated.', type: 'success' });
129
+ setTimeout(() => setMsg(null), 3000);
130
+ } catch (err: any) {
131
+ setMsg({ text: 'Failed to upload photo.', type: 'error' });
132
+ }
133
+ setPhotoLoading(false);
134
+ };
135
+
136
+ const [showLoader, setShowLoader] = useState(false);
137
+
138
+ useEffect(() => {
139
+ let timer: any;
140
+ if (loadingMembers && members.length === 0) {
141
+ timer = setTimeout(() => setShowLoader(true), 150);
142
+ } else {
143
+ setShowLoader(false);
144
+ }
145
+ return () => clearTimeout(timer);
146
+ }, [loadingMembers, members.length]);
147
+
148
+ if (loadingMembers && members.length === 0) {
149
+ if (!showLoader) return null;
150
+ return (
151
+ <div className="fixed inset-0 z-[100] flex items-center justify-center bg-background/60 backdrop-blur-xl transition-all duration-300">
152
+ <Loader2 className="w-8 h-8 animate-spin text-accent" />
153
+ </div>
154
+ );
155
+ }
156
+
157
+ return (
158
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col">
159
+
160
+ {/* Header */}
161
+ <motion.div
162
+ initial={{ opacity: 0, y: 20 }}
163
+ animate={{ opacity: 1, y: 0 }}
164
+ className="mb-8"
165
+ >
166
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">
167
+ Users
168
+ </h1>
169
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
170
+ <span className="w-8 h-[1px] bg-foreground/10" />
171
+ /users
172
+ </div>
173
+ </motion.div>
174
+
175
+ <div className="flex flex-col gap-6 animate-in fade-in duration-500 pb-20">
176
+
177
+ <AnimatePresence>
178
+ {msg && (
179
+ <motion.div
180
+ initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}
181
+ className={`p-4 text-[14px] font-medium border ${msg.type === 'error' ? 'text-red-500 bg-red-500/10 border-red-500/20 shadow-[0_0_15px_rgba(239,68,68,0.1)]' : 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20 shadow-[0_0_15px_rgba(52,211,153,0.1)]'} rounded-xl flex items-center gap-2`}
182
+ >
183
+ {msg.type === 'success' && <CheckCircle2 className="w-4 h-4 shrink-0" />}
184
+ {msg.text}
185
+ </motion.div>
186
+ )}
187
+ </AnimatePresence>
188
+
189
+ <div className="flex justify-start">
190
+ <DashboardNav />
191
+ </div>
192
+
193
+ <div className="glass-panel rounded-3xl border border-[var(--panel-border)] shadow-sm">
194
+ {members.length === 0 ? (
195
+ <div className="p-8 text-center text-foreground/50 text-[14px]">
196
+ No members found.
197
+ </div>
198
+ ) : (
199
+ <div className="grid grid-cols-1 md:grid-cols-3 min-h-[500px]">
200
+ {/* Left Box: List */}
201
+ <div className="md:col-span-1 border-r md:border-r-[var(--panel-border)] border-b md:border-b-0 border-[var(--panel-border)] flex flex-col hide-scrollbar bg-black/5 dark:bg-white/5 rounded-t-3xl md:rounded-tr-none md:rounded-l-3xl">
202
+ <div className="p-4 border-b border-[var(--panel-border)] font-bold text-[13px] text-foreground/70 uppercase tracking-wider">
203
+ System Users
204
+ </div>
205
+ <div className="flex-1 overflow-y-auto p-4 space-y-2">
206
+ {members.map(member => (
207
+ <div
208
+ key={member.id}
209
+ onClick={() => setSelectedMember(member)}
210
+ className={`p-4 py-5 rounded-2xl border transition-all flex items-center gap-4 cursor-pointer ${selectedMember?.id === member.id ? 'bg-accent/10 border-accent/40 shadow-sm' : 'glass-panel border-transparent hover:border-[var(--panel-border)]'
211
+ } ${member.inactive ? 'opacity-50 grayscale' : ''}`}
212
+ >
213
+ <div className="w-12 h-12 rounded-full overflow-hidden bg-accent/20 shrink-0 flex items-center justify-center border border-[var(--panel-border)]">
214
+ {(member.avatar || member.photoURL) ? (
215
+ <img src={member.avatar || member.photoURL} alt="" className="w-full h-full object-cover" />
216
+ ) : (
217
+ <UserIcon className="w-5 h-5 text-accent" />
218
+ )}
219
+ </div>
220
+ <div className="flex flex-col truncate">
221
+ <span className="font-bold text-[14px] truncate">{member.displayName || 'No Name'}</span>
222
+ <span className="text-[11px] text-foreground/50 truncate font-mono mt-0.5">{member.email}</span>
223
+ </div>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ </div>
228
+
229
+ {/* Right Box: Preview */}
230
+ <div className="md:col-span-2 flex flex-col p-6 md:p-10 relative">
231
+ {selectedMember ? (
232
+ <div className="animate-in fade-in duration-300 w-full max-w-lg">
233
+
234
+ <div className="flex flex-col md:flex-row gap-6 md:gap-8 items-start mb-10">
235
+ <div className="w-24 h-24 rounded-full glass-panel border-4 border-[var(--panel-border)] shadow-xl bg-accent/10 flex items-center justify-center relative overflow-hidden shrink-0 group">
236
+ {photoLoading ? (
237
+ <Loader2 className="w-8 h-8 text-accent animate-spin" />
238
+ ) : (selectedMember.avatar || selectedMember.photoURL) ? (
239
+ <img src={selectedMember.avatar || selectedMember.photoURL} alt="Avatar" className="w-full h-full object-cover" />
240
+ ) : (
241
+ <UserIcon className="w-10 h-10 text-accent" />
242
+ )}
243
+
244
+ {!photoLoading && canManageTarget(selectedMember) && (
245
+ <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-10 backdrop-blur-md">
246
+ <label className="flex flex-col items-center justify-center cursor-pointer p-2 hover:bg-white/20 transition-colors text-white rounded-full" title="Upload Photo">
247
+ <Camera className="w-5 h-5" />
248
+ <input type="file" accept="image/*" onChange={handlePhotoUpload} className="hidden" />
249
+ </label>
250
+ </div>
251
+ )}
252
+ </div>
253
+
254
+ <div className="flex flex-col mt-2 flex-1 w-full relative">
255
+ {editingName ? (
256
+ <input
257
+ autoFocus
258
+ value={tempName}
259
+ onChange={e => setTempName(e.target.value)}
260
+ onBlur={handleNameSave}
261
+ onKeyDown={e => e.key === 'Enter' && handleNameSave()}
262
+ className="text-2xl md:text-3xl font-extrabold mb-1 bg-transparent border-b-2 border-accent outline-none text-foreground w-full py-1"
263
+ />
264
+ ) : (
265
+ <h3
266
+ onDoubleClick={() => {
267
+ if (canManageTarget(selectedMember)) {
268
+ setTempName(selectedMember.displayName || '');
269
+ setEditingName(true);
270
+ }
271
+ }}
272
+ className={`text-2xl md:text-3xl font-extrabold mb-1 select-none ${canManageTarget(selectedMember) ? 'cursor-pointer hover:text-accent transition-colors' : ''}`}
273
+ title={canManageTarget(selectedMember) ? "Double-click to edit" : ""}
274
+ >
275
+ {selectedMember.displayName || 'No Name'}
276
+ </h3>
277
+ )}
278
+ <p className="text-[14px] font-mono text-foreground/50">{selectedMember.email}</p>
279
+
280
+ {!canManageTarget(selectedMember) && selectedMember.id !== myProfile?.id && (
281
+ <div className="mt-3 inline-block self-start px-3 py-1.5 rounded-lg bg-foreground/5 border border-[var(--panel-border)] text-[11px] font-bold text-foreground/50 tracking-wide uppercase">
282
+ Privileged Admin Account
283
+ </div>
284
+ )}
285
+ </div>
286
+ </div>
287
+
288
+ <div className="flex flex-col gap-3">
289
+ <div className="p-4 rounded-2xl glass-panel border border-[var(--panel-border)] flex items-center justify-between">
290
+ <div className="flex flex-col">
291
+ <span className="font-bold text-[14px] text-foreground">Admin Access</span>
292
+ <span className="text-[12px] text-foreground/50">Toggle system privileges</span>
293
+ </div>
294
+ <button
295
+ type="button"
296
+ disabled={!canManageTarget(selectedMember)}
297
+ onClick={() => handleUpdateRole(selectedMember, selectedMember.role !== 'admin')}
298
+ className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors focus:outline-none shadow-inner ${selectedMember.role === 'admin' ? 'bg-accent' : 'bg-foreground/20'} ${!canManageTarget(selectedMember) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
299
+ >
300
+ <span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform shadow-md ${selectedMember.role === 'admin' ? 'translate-x-6' : 'translate-x-1'}`} />
301
+ </button>
302
+ </div>
303
+
304
+ <div className={`p-4 rounded-2xl glass-panel border flex items-center justify-between ${selectedMember.inactive ? 'border-amber-500/30 bg-amber-500/5' : 'border-red-500/20 bg-red-500/5'}`}>
305
+ <div className="flex flex-col">
306
+ <span className={`font-bold text-[14px] ${selectedMember.inactive ? 'text-amber-500' : 'text-red-500'}`}>{selectedMember.inactive ? 'Reactivate User' : 'Deactivate User'}</span>
307
+ <span className={`text-[12px] ${selectedMember.inactive ? 'text-amber-500/70' : 'text-red-500/70'}`}>Block access to this app</span>
308
+ </div>
309
+ <button
310
+ type="button"
311
+ disabled={!canManageTarget(selectedMember)}
312
+ onClick={() => handleDeactivate(selectedMember, !selectedMember.inactive)}
313
+ className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors focus:outline-none shadow-inner ${selectedMember.inactive ? 'bg-amber-500' : 'bg-red-500/20'} ${!canManageTarget(selectedMember) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
314
+ >
315
+ <span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform shadow-md ${selectedMember.inactive ? 'translate-x-6' : 'translate-x-1'}`} />
316
+ </button>
317
+ </div>
318
+ </div>
319
+
320
+ </div>
321
+ ) : (
322
+ <div className="absolute inset-0 flex flex-col items-center justify-center opacity-30 pointer-events-none">
323
+ <UsersIcon className="w-12 h-12 mb-4" />
324
+ <p className="font-bold tracking-widest uppercase text-[12px]">Select a User</p>
325
+ </div>
326
+ )}
327
+ </div>
328
+ </div>
329
+ )}
330
+ </div>
331
+ </div>
332
+ </main>
333
+ );
334
+ }
@@ -0,0 +1,98 @@
1
+ // @generated-test
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, screen, act } from '@testing-library/react';
4
+ import { BrowserRouter } from 'react-router-dom';
5
+ import { AuthProvider } from '../lib/AuthContext';
6
+ import { ThemeProvider } from '../lib/ThemeContext';
7
+ import { Verify } from './Verify';
8
+
9
+ // Global mocks
10
+ Object.defineProperty(window, 'matchMedia', {
11
+ writable: true,
12
+ value: vi.fn().mockImplementation(query => ({
13
+ matches: false,
14
+ media: query,
15
+ onchange: null,
16
+ addListener: vi.fn(),
17
+ removeListener: vi.fn(),
18
+ addEventListener: vi.fn(),
19
+ removeEventListener: vi.fn(),
20
+ dispatchEvent: vi.fn(),
21
+ })),
22
+ });
23
+
24
+ vi.mock('../lib/AuthContext', () => ({
25
+ AuthProvider: ({ children }: any) => <>{children}</>,
26
+ useAuth: () => ({
27
+ user: { uid: 'mock-user-123', email: 'test@example.com' },
28
+ userWorkspaces: [],
29
+ activeWorkspace: null,
30
+ activeOrg: null,
31
+ loading: false
32
+ })
33
+ }));
34
+
35
+ vi.mock('../lib/ThemeContext', () => ({
36
+ ThemeProvider: ({ children }: any) => <>{children}</>,
37
+ useTheme: () => ({ themeMode: 'light', setThemeMode: vi.fn(), activeConfig: {} })
38
+ }));
39
+
40
+ vi.mock('firebase/auth', () => ({
41
+ getAuth: vi.fn(() => ({})),
42
+ onAuthStateChanged: vi.fn((auth, cb) => { cb({ uid: 'mock-user-123', email: 'test@example.com', getIdToken: vi.fn(() => Promise.resolve('mock-token')) }); return () => {}; })
43
+ }));
44
+ vi.mock('firebase/firestore', () => ({
45
+ getFirestore: vi.fn(() => ({})),
46
+ collection: vi.fn(),
47
+ doc: vi.fn(),
48
+ setDoc: vi.fn(() => Promise.resolve()),
49
+ addDoc: vi.fn(() => Promise.resolve()),
50
+ updateDoc: vi.fn(() => Promise.resolve()),
51
+ deleteDoc: vi.fn(() => Promise.resolve()),
52
+ query: vi.fn(),
53
+ where: vi.fn(),
54
+ orderBy: vi.fn(),
55
+ limit: vi.fn(),
56
+ getDoc: vi.fn(() => Promise.resolve({ exists: () => true, data: () => ({ role: 'super_admin' }) })),
57
+ getDocs: vi.fn(() => Promise.resolve({ docs: [], forEach: vi.fn() })),
58
+ onSnapshot: vi.fn((...args: any[]) => {
59
+ let cb = args[1];
60
+ if (typeof args[2] === 'function') {
61
+ cb = args[2];
62
+ }
63
+ if (typeof cb === 'function') {
64
+ cb({docs: [], forEach: vi.fn(), data: () => ({}), exists: () => true});
65
+ }
66
+ return () => {};
67
+ })
68
+ }));
69
+ vi.mock('firebase/storage', () => ({
70
+ getStorage: vi.fn(() => ({})),
71
+ ref: vi.fn(),
72
+ listAll: vi.fn(() => Promise.resolve({ items: [], prefixes: [] })),
73
+ getDownloadURL: vi.fn(() => Promise.resolve('mock-url')),
74
+ getMetadata: vi.fn(() => Promise.resolve({ size: 1024, timeCreated: new Date().toISOString() }))
75
+ }));
76
+
77
+ describe('Verify Component', () => {
78
+ it('renders without crashing', async () => {
79
+ // Wrap in standard application providers inside act to process async side effects and prevent warnings
80
+ await act(async () => {
81
+ render(
82
+ <BrowserRouter>
83
+ <AuthProvider>
84
+ <ThemeProvider>
85
+ {/* @ts-ignore */}
86
+ <Verify />
87
+ </ThemeProvider>
88
+ </AuthProvider>
89
+ </BrowserRouter>
90
+ );
91
+ // Wait a tick to flush background state updates
92
+ await new Promise(resolve => setTimeout(resolve, 0));
93
+ });
94
+
95
+ // Check if the document has anything rendered without throwing
96
+ expect(document.body).toBeDefined();
97
+ });
98
+ });
@@ -0,0 +1,95 @@
1
+ import { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Mail } from 'lucide-react';
4
+ import { useAuth } from '../lib/AuthContext';
5
+ import { Button } from '../components/Button';
6
+ import { sendEmailVerification } from 'firebase/auth';
7
+ import { auth } from '../lib/firebase';
8
+ import { useNavigate } from 'react-router-dom';
9
+
10
+ export function Verify() {
11
+ const { user, refreshUser } = useAuth();
12
+ const navigate = useNavigate();
13
+ const [verifying, setVerifying] = useState(false);
14
+ const [resending, setResending] = useState(false);
15
+ const [msg, setMsg] = useState('');
16
+
17
+ const handleVerify = async () => {
18
+ setVerifying(true);
19
+ setMsg('');
20
+ try {
21
+ if (auth.currentUser) {
22
+ await auth.currentUser.reload();
23
+ if (!auth.currentUser.emailVerified) {
24
+ setMsg('Email is not verified yet. Please check your inbox.');
25
+ setVerifying(false);
26
+ return;
27
+ }
28
+ // Force refresh JWT so the rest of the app sees the verified state
29
+ await auth.currentUser.getIdToken(true);
30
+ await refreshUser();
31
+ navigate('/dashboard');
32
+ }
33
+ } catch (e) {
34
+ setMsg('Something went wrong. Please try again.');
35
+ setVerifying(false);
36
+ }
37
+ };
38
+
39
+ const handleResend = async () => {
40
+ if (user) {
41
+ setResending(true);
42
+ setMsg('');
43
+ try {
44
+ await sendEmailVerification(user);
45
+ setMsg('Verification email sent! Please check your inbox.');
46
+ } catch (err: any) {
47
+ if (err?.code === 'auth/too-many-requests') {
48
+ setMsg('Failed to resend: Please wait a minute before trying again.');
49
+ } else {
50
+ setMsg(err?.message || 'Failed to resend. Please try again later.');
51
+ }
52
+ } finally {
53
+ setResending(false);
54
+ }
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div className="flex-1 flex items-center justify-center w-full z-10 px-4 mb-16">
60
+ <motion.div
61
+ initial={{ opacity: 0, y: 10 }}
62
+ animate={{ opacity: 1, y: 0 }}
63
+ transition={{ duration: 0.5 }}
64
+ className="w-full max-w-[480px] p-6 md:p-8 lg:p-10 rounded-3xl glass-panel relative glow-hover text-center shadow-[0_8px_32px_rgba(0,0,0,0.08)]"
65
+ >
66
+ <div className="absolute -inset-[1px] bg-gradient-to-br from-[var(--primary-glow)] to-transparent opacity-30 blur-[20px] rounded-3xl -z-10" />
67
+
68
+ <div className="w-14 h-14 md:w-16 md:h-16 rounded-2xl glass-panel border border-[var(--panel-border)] flex items-center justify-center mx-auto mb-5 md:mb-6 animate-float shadow-xl">
69
+ <Mail className="w-7 h-7 md:w-8 md:h-8 text-accent opacity-90" />
70
+ </div>
71
+
72
+ <h1 className="text-2xl md:text-3xl font-extrabold mb-3 text-foreground tracking-tight">Verify your email</h1>
73
+ <p className="text-[14px] md:text-[15px] text-foreground/60 mb-6 md:mb-8 leading-[1.6] tracking-wide">
74
+ We've sent a verification link to <span className="font-semibold text-foreground">{user?.email}</span>.
75
+ Please check your inbox (and spam folder) to verify your account and access your dashboard.
76
+ </p>
77
+
78
+ {msg && (
79
+ <div className={`p-3 md:p-4 mb-5 md:mb-6 text-[13px] md:text-[14px] font-medium rounded-xl border ${msg.includes('Failed') || msg.includes('not verified') || msg.includes('wrong') ? 'text-red-400 bg-red-500/10 border-red-500/20' : 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20'}`}>
80
+ {msg}
81
+ </div>
82
+ )}
83
+
84
+ <div className="flex flex-col gap-3 max-w-[360px] mx-auto">
85
+ <Button variant="primary" onClick={handleVerify} isLoading={verifying} className="w-full py-2.5 md:py-3 text-[14px] font-bold tracking-wide rounded-xl transition-all hover:-translate-y-0.5">
86
+ I've verified my email
87
+ </Button>
88
+ <Button variant="ghost" onClick={handleResend} isLoading={resending} className="w-full py-2.5 md:py-3 text-[13px] md:text-[14px] font-semibold tracking-wide rounded-xl transition-all hover:-translate-y-0.5">
89
+ Resend verification link
90
+ </Button>
91
+ </div>
92
+ </motion.div>
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,13 @@
1
+ export * from './pages/publicPage';
2
+ export * from './tabs/calendar/privatecalendar';
3
+ export * from './tabs/calendar/admincalendar';
4
+ export * from './tabs/calendar/publiccalendar';
5
+ export * from './tabs/board/privateboard';
6
+ export * from './tabs/board/adminboard';
7
+ export * from './tabs/board/publicboard';
8
+ export * from './tabs/table/privatetable';
9
+ export * from './tabs/table/admintable';
10
+ export * from './tabs/table/publictable';
11
+ export * from './tabs/crud/shared';
12
+ export * from './tabs/crud/admin';
13
+ export * from './tabs/crud/private';