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,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 { Requests } from './Requests';
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('Requests 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
+ <Requests />
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,422 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { db } from '../lib/firebase';
4
+ import { collection, query, orderBy, onSnapshot, doc, getDoc, updateDoc } from 'firebase/firestore';
5
+ import { DashboardNav } from '../components/DashboardNav';
6
+ import { HelpCircle, Calendar, Mail, AlertCircle, ChevronDown, ChevronUp, User, Archive, ArchiveRestore, Loader2, FolderArchive } from 'lucide-react';
7
+ import { Link } from 'react-router-dom';
8
+
9
+ interface UserRequest {
10
+ id: string;
11
+ name: string;
12
+ email?: string;
13
+ contactEmail?: string;
14
+ topic?: string;
15
+ description: string;
16
+ uid: string;
17
+ submittedAt: any;
18
+ avatarUrl?: string;
19
+ isArchived?: boolean;
20
+ [key: string]: any;
21
+ }
22
+
23
+ export function Requests() {
24
+ const [requests, setRequests] = useState<UserRequest[]>([]);
25
+ const [loading, setLoading] = useState(true);
26
+ const [expandedId, setExpandedId] = useState<string | null>(null);
27
+ const [showArchived, setShowArchived] = useState(false);
28
+ const [isArchiving, setIsArchiving] = useState<string | null>(null);
29
+
30
+ useEffect(() => {
31
+ const q = query(
32
+ collection(db, 'user_requests'),
33
+ orderBy('submittedAt', 'desc')
34
+ );
35
+
36
+ const unsubscribe = onSnapshot(q, async (snapshot) => {
37
+ const data: UserRequest[] = [];
38
+ for (const docSnap of snapshot.docs) {
39
+ const reqData = docSnap.data() as UserRequest;
40
+ let avatarUrl = '';
41
+
42
+ // Attempt to fetch profile avatar if available
43
+ if (reqData.uid && reqData.uid !== 'guest') {
44
+ try {
45
+ const profileSnap = await getDoc(doc(db, 'user_profiles', reqData.uid));
46
+ if (profileSnap.exists()) {
47
+ const pData = profileSnap.data();
48
+ if (pData.avatar || pData.photoURL) avatarUrl = pData.avatar || pData.photoURL;
49
+ if (pData.displayName && !reqData.name) reqData.name = pData.displayName;
50
+ if (pData.email && !reqData.email) reqData.email = pData.email;
51
+ if (pData.email && !reqData.contactEmail) reqData.contactEmail = pData.email;
52
+ }
53
+ } catch (e) {
54
+ console.error('Failed to fetch avatar', e);
55
+ }
56
+ }
57
+
58
+ data.push({ ...reqData, id: docSnap.id, avatarUrl });
59
+ }
60
+ setRequests(data);
61
+ setLoading(false);
62
+ }, (error) => {
63
+ console.error("Error fetching requests:", error);
64
+ setLoading(false);
65
+ });
66
+
67
+ return () => unsubscribe();
68
+ }, []);
69
+
70
+ const formatDate = (timestamp: any) => {
71
+ if (!timestamp) return 'Just now';
72
+ const date = timestamp.toDate ? timestamp.toDate() : new Date(timestamp);
73
+ return new Intl.DateTimeFormat('en-US', {
74
+ month: 'short',
75
+ day: 'numeric',
76
+ hour: 'numeric',
77
+ minute: 'numeric',
78
+ }).format(date);
79
+ };
80
+
81
+ const toggleExpand = (id: string) => {
82
+ setExpandedId(prev => (prev === id ? null : id));
83
+ };
84
+
85
+ const handleArchive = async (id: string, currentStatus: boolean = false) => {
86
+ setIsArchiving(id);
87
+ try {
88
+ await updateDoc(doc(db, 'user_requests', id), {
89
+ isArchived: !currentStatus
90
+ });
91
+ if (!currentStatus && expandedId === id) {
92
+ setExpandedId(null);
93
+ }
94
+ } catch (e) {
95
+ console.error("Failed to archive/unarchive", e);
96
+ }
97
+ setIsArchiving(null);
98
+ };
99
+
100
+ const activeRequests = requests.filter(r => !r.isArchived);
101
+ const archivedRequests = requests.filter(r => r.isArchived);
102
+
103
+ return (
104
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col">
105
+ <motion.div
106
+ initial={{ opacity: 0, y: 20 }}
107
+ animate={{ opacity: 1, y: 0 }}
108
+ className="mb-8"
109
+ >
110
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">
111
+ User Requests
112
+ </h1>
113
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
114
+ <span className="w-8 h-[1px] bg-foreground/10" />
115
+ /requests
116
+ </div>
117
+ </motion.div>
118
+
119
+ <div className="flex flex-col gap-4 flex-1 min-h-[500px]">
120
+ <div className="flex justify-start">
121
+ <DashboardNav />
122
+ </div>
123
+
124
+ <div className="w-full flex-1 glass-panel rounded-3xl p-6 md:p-10 relative overflow-hidden border border-[var(--panel-border)] shadow-2xl bg-[var(--panel-bg)]">
125
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-accent/5 blur-[100px] rounded-full pointer-events-none" />
126
+
127
+ <div className="relative z-10 h-full flex flex-col">
128
+ {loading ? (
129
+ <div className="flex-1 flex items-center justify-center min-h-[500px]">
130
+ <div className="w-8 h-8 md:w-10 md:h-10 border-4 border-accent/20 border-t-accent rounded-full animate-spin" />
131
+ </div>
132
+ ) : requests.length === 0 ? (
133
+ <div className="flex-1 flex flex-col items-center justify-center text-center p-8">
134
+ <div className="w-16 h-16 rounded-3xl bg-accent/10 border border-accent/20 flex items-center justify-center text-accent mx-auto mb-6 shadow-inner">
135
+ <HelpCircle className="w-6 h-6 outline-none" />
136
+ </div>
137
+ <h2 className="text-2xl font-extrabold text-foreground mb-2">No User Requests</h2>
138
+ <p className="text-[14px] text-foreground/50 font-medium max-w-md">User support tickets will appear here once submitted.</p>
139
+ </div>
140
+ ) : (
141
+ <div className="flex flex-col gap-3 pb-8">
142
+ {activeRequests.length === 0 && (
143
+ <div className="bg-foreground/[0.02] border border-dashed border-[var(--panel-border)] rounded-2xl flex flex-col items-center justify-center p-8 text-center mb-4">
144
+ <p className="text-[14px] font-bold text-foreground/50">All requests have been archived.</p>
145
+ </div>
146
+ )}
147
+ <AnimatePresence>
148
+ {activeRequests.map((request, index) => {
149
+ const isExpanded = expandedId === request.id;
150
+
151
+ return (
152
+ <motion.div
153
+ key={request.id}
154
+ initial={{ opacity: 0, y: 10 }}
155
+ animate={{ opacity: 1, y: 0 }}
156
+ transition={{ delay: index * 0.05 }}
157
+ className="glass-panel border border-[var(--panel-border)] rounded-2xl flex flex-col overflow-hidden hover:border-accent/40 transition-all duration-300"
158
+ >
159
+ <button
160
+ onClick={() => toggleExpand(request.id)}
161
+ className="w-full flex items-center justify-between p-4 md:px-6 hover:bg-foreground/[0.02] transition-colors text-left"
162
+ >
163
+ <div className="flex items-center gap-4 flex-1 min-w-0">
164
+ <div className="relative shrink-0">
165
+ {(request.avatarUrl || request.submitterAvatar) ? (
166
+ <img src={request.avatarUrl || request.submitterAvatar} alt={request.submitterName || request.name} className="w-10 h-10 rounded-full object-cover border border-[var(--panel-border)] shadow-sm" />
167
+ ) : (
168
+ <div className="w-10 h-10 rounded-full bg-accent/10 text-accent flex items-center justify-center font-bold border border-accent/20 shadow-sm uppercase">
169
+ {(request.submitterName || request.name || request.contactEmail || request.email || 'U')[0]}
170
+ </div>
171
+ )}
172
+ </div>
173
+
174
+ <div className="flex flex-col min-w-0 flex-1">
175
+ {request.uid !== 'guest' ? (
176
+ <Link to={`/users/${request.uid}`} onClick={(e: any) => e.stopPropagation()} className="w-fit font-bold text-[14px] md:text-[15px] text-foreground truncate hover:text-accent flex items-center gap-2">
177
+ {request.submitterName || request.name || 'Anonymous User'}
178
+ {request.formId && (
179
+ <span className="text-[10px] font-bold uppercase tracking-widest text-accent bg-accent/10 px-2 py-0.5 rounded-md border border-accent/20 shrink-0">
180
+ {request.formId.replace(/_/g, ' ')}
181
+ </span>
182
+ )}
183
+ </Link>
184
+ ) : (
185
+ <div className="flex items-center gap-2">
186
+ <span className="font-bold text-[14px] md:text-[15px] text-foreground truncate">{request.name || 'Anonymous User'}</span>
187
+ {request.formId && (
188
+ <span className="text-[10px] font-bold uppercase tracking-widest text-accent bg-accent/10 px-2 py-0.5 rounded-md border border-accent/20 shrink-0">
189
+ {request.formId.replace(/_/g, ' ')}
190
+ </span>
191
+ )}
192
+ </div>
193
+ )}
194
+ <span className="text-[13px] text-foreground/50 truncate flex items-center gap-1.5 font-medium mt-0.5">
195
+ <Mail className="w-3.5 h-3.5" /> {request.contactEmail || request.email || 'No email provided'}
196
+ </span>
197
+ </div>
198
+ </div>
199
+
200
+ <div className="hidden md:flex items-center gap-4 ml-6 shrink-0">
201
+ <div className="text-[12px] font-medium text-foreground/40">
202
+ {formatDate(request.submittedAt)}
203
+ </div>
204
+ <button
205
+ onClick={(e) => { e.stopPropagation(); handleArchive(request.id, false); }}
206
+ disabled={isArchiving === request.id}
207
+ className="p-1.5 rounded-lg text-foreground/30 hover:text-orange-500 hover:bg-orange-500/10 transition-colors"
208
+ title="Archive Request"
209
+ >
210
+ {isArchiving === request.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Archive className="w-4 h-4" />}
211
+ </button>
212
+ <div className={`p-1.5 rounded-xl glass-panel transition-transform ${isExpanded ? 'rotate-180 bg-foreground/5' : ''}`}>
213
+ <ChevronDown className="w-4 h-4 text-foreground/50" />
214
+ </div>
215
+ </div>
216
+ </button>
217
+
218
+ <AnimatePresence>
219
+ {isExpanded && (
220
+ <motion.div
221
+ initial={{ height: 0, opacity: 0 }}
222
+ animate={{ height: 'auto', opacity: 1 }}
223
+ exit={{ height: 0, opacity: 0 }}
224
+ className="overflow-hidden"
225
+ >
226
+ <div className="p-4 md:p-6 pt-0 mt-2 border-t border-[var(--panel-border)]/30">
227
+ <div className="md:hidden text-[12px] font-medium text-foreground/40 w-fit mb-4">
228
+ {formatDate(request.submittedAt)}
229
+ </div>
230
+ {request.topic && (
231
+ <div className="flex items-center gap-2 mb-4 text-[13px] text-foreground/70 bg-foreground/5 px-4 py-2.5 rounded-xl w-fit border border-[var(--panel-border)]">
232
+ <AlertCircle className="w-4 h-4 text-accent" />
233
+ <span className="font-medium text-accent">Topic:</span>
234
+ <span className="font-bold">{request.topic}</span>
235
+ </div>
236
+ )}
237
+
238
+
239
+ {Object.entries(request)
240
+ .filter(([key]) => !['id', 'uid', 'submittedAt', 'avatarUrl', 'name', 'full_name', 'email', 'contactEmail', 'topic'].includes(key) && !key.toLowerCase().startsWith('submitter') && key.toLowerCase() !== 'formid')
241
+ .map(([key, val]) => {
242
+ const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
243
+ return (
244
+ <div key={key} className="bg-foreground/[0.02] rounded-2xl p-5 border border-[var(--panel-border)] relative mb-3">
245
+ <div className="flex items-center gap-2 mb-3 text-foreground/60 w-full justify-between">
246
+ <div className="flex items-center gap-2 text-accent">
247
+ <span className="text-[12px] font-bold uppercase tracking-widest text-accent">{formattedKey}</span>
248
+ </div>
249
+ </div>
250
+ <p className="text-[14px] md:text-[15px] text-foreground/80 leading-relaxed whitespace-pre-wrap font-medium">
251
+ {typeof val === 'boolean' ? (val ? 'Yes' : 'No') : String(val || "No data provided")}
252
+ </p>
253
+ </div>
254
+ );
255
+ })}
256
+
257
+ </div>
258
+ </motion.div>
259
+ )}
260
+ </AnimatePresence>
261
+ </motion.div>
262
+ );
263
+ })}
264
+ </AnimatePresence>
265
+
266
+ {/* Archived Folders Toggle */}
267
+ {archivedRequests.length > 0 && (
268
+ <div className="mt-6 pt-4 border-t border-[var(--panel-border)]/30">
269
+ <div className="flex justify-center">
270
+ <button
271
+ onClick={() => setShowArchived(!showArchived)}
272
+ className="flex items-center gap-2 text-[11px] font-bold text-foreground/40 hover:text-foreground/70 transition-colors py-2 uppercase tracking-widest outline-none"
273
+ >
274
+ {showArchived ? 'Hide' : 'View'} {archivedRequests.length} Archived Request{archivedRequests.length !== 1 ? 's' : ''}
275
+ <ChevronDown className={`w-3.5 h-3.5 transition-transform duration-300 ${showArchived ? 'rotate-180' : ''}`} />
276
+ </button>
277
+ </div>
278
+
279
+ <AnimatePresence>
280
+ {showArchived && (
281
+ <motion.div
282
+ initial={{ height: 0, opacity: 0 }}
283
+ animate={{ height: 'auto', opacity: 1 }}
284
+ exit={{ height: 0, opacity: 0 }}
285
+ className="overflow-hidden mt-4"
286
+ >
287
+ <div className="flex flex-col gap-3">
288
+ {archivedRequests.map((request, index) => {
289
+ const isExpanded = expandedId === request.id;
290
+ return (
291
+ <motion.div
292
+ key={request.id}
293
+ initial={{ opacity: 0, y: 10 }}
294
+ animate={{ opacity: 1, y: 0 }}
295
+ transition={{ delay: index * 0.05 }}
296
+ className="glass-panel border border-[var(--panel-border)] rounded-2xl flex flex-col overflow-hidden opacity-50 grayscale hover:opacity-100 hover:grayscale-0 hover:border-accent/40 transition-all duration-300"
297
+ >
298
+ <button
299
+ onClick={() => toggleExpand(request.id)}
300
+ className="w-full flex items-center justify-between p-4 md:px-6 hover:bg-foreground/[0.02] transition-colors text-left"
301
+ >
302
+ <div className="flex items-center gap-4 flex-1 min-w-0">
303
+ <div className="relative shrink-0 grayscale">
304
+ {(request.avatarUrl || request.submitterAvatar) ? (
305
+ <img src={request.avatarUrl || request.submitterAvatar} alt={request.submitterName || request.name} className="w-10 h-10 rounded-full object-cover border border-[var(--panel-border)] shadow-sm" />
306
+ ) : (
307
+ <div className="w-10 h-10 rounded-full bg-accent/10 text-accent flex items-center justify-center font-bold border border-accent/20 shadow-sm uppercase">
308
+ {(request.submitterName || request.name || request.contactEmail || request.email || 'U')[0]}
309
+ </div>
310
+ )}
311
+ </div>
312
+
313
+ <div className="flex flex-col min-w-0 flex-1">
314
+ {request.uid !== 'guest' ? (
315
+ <div className="w-fit font-bold text-[14px] md:text-[15px] text-foreground truncate flex items-center gap-2">
316
+ {request.submitterName || request.name || 'Anonymous User'}
317
+ {request.formId && (
318
+ <span className="text-[10px] font-bold uppercase tracking-widest text-accent bg-accent/10 px-2 py-0.5 rounded-md border border-accent/20 shrink-0">
319
+ {request.formId.replace(/_/g, ' ')}
320
+ </span>
321
+ )}
322
+ </div>
323
+ ) : (
324
+ <div className="flex items-center gap-2">
325
+ <span className="font-bold text-[14px] md:text-[15px] text-foreground truncate">{request.name || 'Anonymous User'}</span>
326
+ {request.formId && (
327
+ <span className="text-[10px] font-bold uppercase tracking-widest text-accent bg-accent/10 px-2 py-0.5 rounded-md border border-accent/20 shrink-0">
328
+ {request.formId.replace(/_/g, ' ')}
329
+ </span>
330
+ )}
331
+ </div>
332
+ )}
333
+ <span className="text-[13px] text-foreground/50 truncate flex items-center gap-1.5 font-medium mt-0.5">
334
+ <Mail className="w-3.5 h-3.5" /> {request.contactEmail || request.email || 'No email provided'}
335
+ </span>
336
+ </div>
337
+ </div>
338
+
339
+ <div className="hidden md:flex items-center gap-6 ml-6 shrink-0">
340
+ <div className="text-[12px] font-medium text-foreground/40">
341
+ {formatDate(request.submittedAt)}
342
+ </div>
343
+ <div className={`p-2 rounded-xl glass-panel transition-transform ${isExpanded ? 'rotate-180 bg-foreground/5' : ''}`}>
344
+ <ChevronDown className="w-4 h-4 text-foreground/50" />
345
+ </div>
346
+ </div>
347
+ </button>
348
+
349
+ <AnimatePresence>
350
+ {isExpanded && (
351
+ <motion.div
352
+ initial={{ height: 0, opacity: 0 }}
353
+ animate={{ height: 'auto', opacity: 1 }}
354
+ exit={{ height: 0, opacity: 0 }}
355
+ className="overflow-hidden"
356
+ >
357
+ <div className="p-4 md:p-6 pt-0 mt-2 border-t border-[var(--panel-border)]/30">
358
+ <div className="md:hidden text-[12px] font-medium text-foreground/40 w-fit mb-4">
359
+ {formatDate(request.submittedAt)}
360
+ </div>
361
+ {request.topic && (
362
+ <div className="flex items-center gap-2 mb-4 text-[13px] text-foreground/70 bg-foreground/5 px-4 py-2.5 rounded-xl w-fit border border-[var(--panel-border)]">
363
+ <AlertCircle className="w-4 h-4 text-accent" />
364
+ <span className="font-medium text-accent">Topic:</span>
365
+ <span className="font-bold">{request.topic}</span>
366
+ </div>
367
+ )}
368
+
369
+ {Object.entries(request)
370
+ .filter(([key]) => !['id', 'uid', 'submittedAt', 'avatarUrl', 'name', 'full_name', 'email', 'contactEmail', 'topic', 'isArchived'].includes(key) && !key.toLowerCase().startsWith('submitter') && key.toLowerCase() !== 'formid')
371
+ .map(([key, val]) => {
372
+ const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
373
+ return (
374
+ <div key={key} className="bg-foreground/[0.02] rounded-2xl p-5 border border-[var(--panel-border)] relative mb-3">
375
+ <div className="flex items-center gap-2 mb-3 text-foreground/60 w-full justify-between">
376
+ <div className="flex items-center gap-2 text-accent">
377
+ <span className="text-[12px] font-bold uppercase tracking-widest text-accent">{formattedKey}</span>
378
+ </div>
379
+ </div>
380
+ <p className="text-[14px] md:text-[15px] text-foreground/80 leading-relaxed whitespace-pre-wrap font-medium">
381
+ {typeof val === 'boolean' ? (val ? 'Yes' : 'No') : String(val || "No data provided")}
382
+ </p>
383
+ </div>
384
+ );
385
+ })}
386
+
387
+ <div className="mt-6 flex justify-end">
388
+ <button
389
+ onClick={() => handleArchive(request.id, request.isArchived)}
390
+ disabled={isArchiving === request.id}
391
+ className="flex items-center gap-2 px-4 py-2.5 rounded-xl text-[13px] font-bold text-foreground/70 hover:text-emerald-500 bg-foreground/5 hover:bg-emerald-500/10 transition-colors border border-transparent shadow-sm"
392
+ >
393
+ {isArchiving === request.id ? (
394
+ <Loader2 className="w-4 h-4 animate-spin" />
395
+ ) : (
396
+ <ArchiveRestore className="w-4 h-4" />
397
+ )}
398
+ Unarchive Request
399
+ </button>
400
+ </div>
401
+ </div>
402
+ </motion.div>
403
+ )}
404
+ </AnimatePresence>
405
+ </motion.div>
406
+ );
407
+ })}
408
+ </div>
409
+ </motion.div>
410
+ )}
411
+ </AnimatePresence>
412
+ </div>
413
+ )}
414
+
415
+ </div>
416
+ )}
417
+ </div>
418
+ </div>
419
+ </div>
420
+ </main>
421
+ );
422
+ }
@@ -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 { ResetPassword } from './ResetPassword';
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('ResetPassword 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
+ <ResetPassword />
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
+ });