firebase-os 1.1.4 → 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 -74
  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 +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 { Submissions } from './Submissions';
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('Submissions 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
+ <Submissions />
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,372 @@
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 { MessageSquare, Calendar, Mail, Phone, ChevronDown, HelpCircle, User, Archive, ArchiveRestore, Loader2, FolderArchive } from 'lucide-react';
7
+ import { Link } from 'react-router-dom';
8
+
9
+ interface Submission {
10
+ id: string;
11
+ name: string;
12
+ email: string;
13
+ phone: string;
14
+ questions: string;
15
+ uid: string;
16
+ submittedAt: any;
17
+ avatarUrl?: string;
18
+ isArchived?: boolean;
19
+ [key: string]: any;
20
+ }
21
+
22
+ export function Submissions() {
23
+ const [submissions, setSubmissions] = useState<Submission[]>([]);
24
+ const [loading, setLoading] = useState(true);
25
+ const [expandedId, setExpandedId] = useState<string | null>(null);
26
+ const [showArchived, setShowArchived] = useState(false);
27
+ const [isArchiving, setIsArchiving] = useState<string | null>(null);
28
+
29
+ useEffect(() => {
30
+ const q = query(
31
+ collection(db, 'user_submissions'),
32
+ orderBy('submittedAt', 'desc')
33
+ );
34
+
35
+ const unsubscribe = onSnapshot(q, async (snapshot) => {
36
+ const data: Submission[] = [];
37
+ for (const docSnap of snapshot.docs) {
38
+ const subData = docSnap.data() as Submission;
39
+ let avatarUrl = '';
40
+
41
+ // Attempt to fetch profile avatar if available
42
+ if (subData.uid && subData.uid !== 'guest') {
43
+ try {
44
+ const profileSnap = await getDoc(doc(db, 'user_profiles', subData.uid));
45
+ if (profileSnap.exists()) {
46
+ const pData = profileSnap.data();
47
+ if (pData.avatar) avatarUrl = pData.avatar;
48
+ if (pData.name && !subData.name) subData.name = pData.name;
49
+ if (pData.email && !subData.email) subData.email = pData.email;
50
+ }
51
+ } catch (e) {
52
+ console.error('Failed to fetch avatar', e);
53
+ }
54
+ }
55
+
56
+ data.push({ ...subData, id: docSnap.id, avatarUrl });
57
+ }
58
+ setSubmissions(data);
59
+ setLoading(false);
60
+ }, (error) => {
61
+ console.error("Error fetching submissions:", error);
62
+ setLoading(false);
63
+ });
64
+
65
+ return () => unsubscribe();
66
+ }, []);
67
+
68
+ const formatDate = (timestamp: any) => {
69
+ if (!timestamp) return 'Just now';
70
+ const date = timestamp.toDate ? timestamp.toDate() : new Date(timestamp);
71
+ return new Intl.DateTimeFormat('en-US', {
72
+ month: 'short',
73
+ day: 'numeric',
74
+ hour: 'numeric',
75
+ minute: 'numeric',
76
+ }).format(date);
77
+ };
78
+
79
+ const toggleExpand = (id: string) => {
80
+ setExpandedId(prev => (prev === id ? null : id));
81
+ };
82
+
83
+ const handleArchive = async (id: string, currentStatus: boolean = false) => {
84
+ setIsArchiving(id);
85
+ try {
86
+ await updateDoc(doc(db, 'user_submissions', id), {
87
+ isArchived: !currentStatus
88
+ });
89
+ if (!currentStatus && expandedId === id) {
90
+ setExpandedId(null);
91
+ }
92
+ } catch (e) {
93
+ console.error("Failed to archive/unarchive", e);
94
+ }
95
+ setIsArchiving(null);
96
+ };
97
+
98
+ const activeSubmissions = submissions.filter(s => !s.isArchived);
99
+ const archivedSubmissions = submissions.filter(s => s.isArchived);
100
+
101
+ return (
102
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col">
103
+ <motion.div
104
+ initial={{ opacity: 0, y: 20 }}
105
+ animate={{ opacity: 1, y: 0 }}
106
+ className="mb-8"
107
+ >
108
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">
109
+ Form Submissions
110
+ </h1>
111
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
112
+ <span className="w-8 h-[1px] bg-foreground/10" />
113
+ /submissions
114
+ </div>
115
+ </motion.div>
116
+
117
+ <div className="flex flex-col gap-4 flex-1 min-h-[500px]">
118
+ <div className="flex justify-start">
119
+ <DashboardNav />
120
+ </div>
121
+
122
+ <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)]">
123
+ <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" />
124
+
125
+ <div className="relative z-10 h-full flex flex-col">
126
+ {loading ? (
127
+ <div className="flex-1 flex items-center justify-center min-h-[500px]">
128
+ <div className="w-8 h-8 md:w-10 md:h-10 border-4 border-accent/20 border-t-accent rounded-full animate-spin" />
129
+ </div>
130
+ ) : submissions.length === 0 ? (
131
+ <div className="flex-1 flex flex-col items-center justify-center text-center p-8">
132
+ <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">
133
+ <MessageSquare className="w-6 h-6 outline-none" />
134
+ </div>
135
+ <h2 className="text-2xl font-extrabold text-foreground mb-2">No Submissions</h2>
136
+ <p className="text-[14px] text-foreground/50 font-medium max-w-md">Try submitting a form on the public layout to see it appear here.</p>
137
+ </div>
138
+ ) : (
139
+ <div className="flex flex-col gap-3 pb-8">
140
+ {activeSubmissions.length === 0 && (
141
+ <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">
142
+ <p className="text-[14px] font-bold text-foreground/50">All submissions have been archived.</p>
143
+ </div>
144
+ )}
145
+ <AnimatePresence>
146
+ {activeSubmissions.map((submission, index) => {
147
+ const isExpanded = expandedId === submission.id;
148
+
149
+ return (
150
+ <motion.div
151
+ key={submission.id}
152
+ initial={{ opacity: 0, y: 10 }}
153
+ animate={{ opacity: 1, y: 0 }}
154
+ transition={{ delay: index * 0.05 }}
155
+ className="glass-panel border border-[var(--panel-border)] rounded-2xl flex flex-col overflow-hidden hover:border-accent/40 transition-all duration-300"
156
+ >
157
+ <button
158
+ onClick={() => toggleExpand(submission.id)}
159
+ className="w-full flex items-center justify-between p-4 md:px-6 hover:bg-foreground/[0.02] transition-colors text-left"
160
+ >
161
+ <div className="flex items-center gap-4 flex-1 min-w-0">
162
+ <div className="relative shrink-0">
163
+ <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">
164
+ <Mail className="w-4 h-4" />
165
+ </div>
166
+ </div>
167
+
168
+ <div className="flex flex-col min-w-0 flex-1 justify-center">
169
+ <div className="flex items-center gap-2">
170
+ <span className="font-bold text-[14px] md:text-[15px] text-foreground truncate">{submission.name || submission.full_name || 'Anonymous Submission'}</span>
171
+ {submission.formId && (
172
+ <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">
173
+ {submission.formId.replace(/_/g, ' ')}
174
+ </span>
175
+ )}
176
+ </div>
177
+ <span className="text-[13px] text-foreground/50 truncate flex items-center gap-1.5 font-medium mt-0.5">
178
+ {submission.email || submission.contactEmail || 'No email provided'}
179
+ </span>
180
+ </div>
181
+ </div>
182
+
183
+ <div className="hidden md:flex items-center gap-4 ml-6 shrink-0">
184
+ <div className="text-[12px] font-medium text-foreground/40">
185
+ {formatDate(submission.submittedAt)}
186
+ </div>
187
+ <button
188
+ onClick={(e) => { e.stopPropagation(); handleArchive(submission.id, false); }}
189
+ disabled={isArchiving === submission.id}
190
+ className="p-1.5 rounded-lg text-foreground/30 hover:text-orange-500 hover:bg-orange-500/10 transition-colors"
191
+ title="Archive Submission"
192
+ >
193
+ {isArchiving === submission.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Archive className="w-4 h-4" />}
194
+ </button>
195
+ <div className={`p-1.5 rounded-xl glass-panel transition-transform ${isExpanded ? 'rotate-180 bg-foreground/5' : ''}`}>
196
+ <ChevronDown className="w-4 h-4 text-foreground/50" />
197
+ </div>
198
+ </div>
199
+ </button>
200
+
201
+ <AnimatePresence>
202
+ {isExpanded && (
203
+ <motion.div
204
+ initial={{ height: 0, opacity: 0 }}
205
+ animate={{ height: 'auto', opacity: 1 }}
206
+ exit={{ height: 0, opacity: 0 }}
207
+ className="overflow-hidden"
208
+ >
209
+ <div className="p-4 md:p-6 pt-0 mt-2 border-t border-[var(--panel-border)]/30">
210
+ <div className="md:hidden text-[12px] font-medium text-foreground/40 w-fit mb-4">
211
+ {formatDate(submission.submittedAt)}
212
+ </div>
213
+ {Object.entries(submission)
214
+ .filter(([key]) => !['id', 'uid', 'submittedAt', 'avatarUrl', 'name', 'full_name', 'email', 'contactEmail'].includes(key) && !key.toLowerCase().startsWith('submitter') && key.toLowerCase() !== 'formid')
215
+ .map(([key, val]) => {
216
+ const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
217
+ return (
218
+ <div key={key} className="bg-foreground/[0.02] rounded-2xl p-5 border border-[var(--panel-border)] relative mb-3">
219
+ <div className="flex items-center gap-2 mb-3 text-foreground/60 w-full justify-between">
220
+ <div className="flex items-center gap-2 text-accent">
221
+ <span className="text-[12px] font-bold uppercase tracking-widest text-accent">{formattedKey}</span>
222
+ </div>
223
+ </div>
224
+ <p className="text-[14px] md:text-[15px] text-foreground/80 leading-relaxed whitespace-pre-wrap font-medium">
225
+ {typeof val === 'boolean' ? (val ? 'Yes' : 'No') : String(val || "No data provided")}
226
+ </p>
227
+ </div>
228
+ );
229
+ })}
230
+
231
+ </div>
232
+ </motion.div>
233
+ )}
234
+ </AnimatePresence>
235
+ </motion.div>
236
+ );
237
+ })}
238
+ </AnimatePresence>
239
+
240
+ {/* Archived Folders Toggle */}
241
+ {archivedSubmissions.length > 0 && (
242
+ <div className="mt-6 pt-4 border-t border-[var(--panel-border)]/30">
243
+ <div className="flex justify-center">
244
+ <button
245
+ onClick={() => setShowArchived(!showArchived)}
246
+ 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"
247
+ >
248
+ {showArchived ? 'Hide' : 'View'} {archivedSubmissions.length} Archived Submission{archivedSubmissions.length !== 1 ? 's' : ''}
249
+ <ChevronDown className={`w-3.5 h-3.5 transition-transform duration-300 ${showArchived ? 'rotate-180' : ''}`} />
250
+ </button>
251
+ </div>
252
+
253
+ <AnimatePresence>
254
+ {showArchived && (
255
+ <motion.div
256
+ initial={{ height: 0, opacity: 0 }}
257
+ animate={{ height: 'auto', opacity: 1 }}
258
+ exit={{ height: 0, opacity: 0 }}
259
+ className="overflow-hidden mt-4"
260
+ >
261
+ <div className="flex flex-col gap-3">
262
+ {archivedSubmissions.map((submission, index) => {
263
+ const isExpanded = expandedId === submission.id;
264
+ return (
265
+ <motion.div
266
+ key={submission.id}
267
+ initial={{ opacity: 0, y: 10 }}
268
+ animate={{ opacity: 1, y: 0 }}
269
+ transition={{ delay: index * 0.05 }}
270
+ 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"
271
+ >
272
+ <button
273
+ onClick={() => toggleExpand(submission.id)}
274
+ className="w-full flex items-center justify-between p-4 md:px-6 hover:bg-foreground/[0.02] transition-colors text-left"
275
+ >
276
+ <div className="flex items-center gap-4 flex-1 min-w-0">
277
+ <div className="relative shrink-0 grayscale">
278
+ <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">
279
+ <Mail className="w-4 h-4" />
280
+ </div>
281
+ </div>
282
+
283
+ <div className="flex flex-col min-w-0 flex-1 justify-center">
284
+ <div className="flex items-center gap-2">
285
+ <span className="font-bold text-[14px] md:text-[15px] text-foreground truncate">{submission.name || submission.full_name || 'Anonymous Submission'}</span>
286
+ {submission.formId && (
287
+ <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">
288
+ {submission.formId.replace(/_/g, ' ')}
289
+ </span>
290
+ )}
291
+ </div>
292
+ <span className="text-[13px] text-foreground/50 truncate flex items-center gap-1.5 font-medium mt-0.5">
293
+ {submission.email || submission.contactEmail || 'No email provided'}
294
+ </span>
295
+ </div>
296
+ </div>
297
+
298
+ <div className="hidden md:flex items-center gap-6 ml-6 shrink-0">
299
+ <div className="text-[12px] font-medium text-foreground/40">
300
+ {formatDate(submission.submittedAt)}
301
+ </div>
302
+ <div className={`p-2 rounded-xl glass-panel transition-transform ${isExpanded ? 'rotate-180 bg-foreground/5' : ''}`}>
303
+ <ChevronDown className="w-4 h-4 text-foreground/50" />
304
+ </div>
305
+ </div>
306
+ </button>
307
+
308
+ <AnimatePresence>
309
+ {isExpanded && (
310
+ <motion.div
311
+ initial={{ height: 0, opacity: 0 }}
312
+ animate={{ height: 'auto', opacity: 1 }}
313
+ exit={{ height: 0, opacity: 0 }}
314
+ className="overflow-hidden"
315
+ >
316
+ <div className="p-4 md:p-6 pt-0 mt-2 border-t border-[var(--panel-border)]/30">
317
+ <div className="md:hidden text-[12px] font-medium text-foreground/40 w-fit mb-4">
318
+ {formatDate(submission.submittedAt)}
319
+ </div>
320
+ {Object.entries(submission)
321
+ .filter(([key]) => !['id', 'uid', 'submittedAt', 'avatarUrl', 'name', 'full_name', 'email', 'contactEmail', 'isArchived'].includes(key) && !key.toLowerCase().startsWith('submitter') && key.toLowerCase() !== 'formid')
322
+ .map(([key, val]) => {
323
+ const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
324
+ return (
325
+ <div key={key} className="bg-foreground/[0.02] rounded-2xl p-5 border border-[var(--panel-border)] relative mb-3">
326
+ <div className="flex items-center gap-2 mb-3 text-foreground/60 w-full justify-between">
327
+ <div className="flex items-center gap-2 text-accent">
328
+ <span className="text-[12px] font-bold uppercase tracking-widest text-accent">{formattedKey}</span>
329
+ </div>
330
+ </div>
331
+ <p className="text-[14px] md:text-[15px] text-foreground/80 leading-relaxed whitespace-pre-wrap font-medium">
332
+ {typeof val === 'boolean' ? (val ? 'Yes' : 'No') : String(val || "No data provided")}
333
+ </p>
334
+ </div>
335
+ );
336
+ })}
337
+
338
+ <div className="mt-6 flex justify-end">
339
+ <button
340
+ onClick={() => handleArchive(submission.id, submission.isArchived)}
341
+ disabled={isArchiving === submission.id}
342
+ 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"
343
+ >
344
+ {isArchiving === submission.id ? (
345
+ <Loader2 className="w-4 h-4 animate-spin" />
346
+ ) : (
347
+ <ArchiveRestore className="w-4 h-4" />
348
+ )}
349
+ Unarchive Submission
350
+ </button>
351
+ </div>
352
+ </div>
353
+ </motion.div>
354
+ )}
355
+ </AnimatePresence>
356
+ </motion.div>
357
+ );
358
+ })}
359
+ </div>
360
+ </motion.div>
361
+ )}
362
+ </AnimatePresence>
363
+ </div>
364
+ )}
365
+ </div>
366
+ )}
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </main>
371
+ );
372
+ }
@@ -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 { Templates } from './Templates';
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('Templates 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
+ <Templates />
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,103 @@
1
+ import { motion } from 'framer-motion';
2
+ import { Link } from 'react-router-dom';
3
+ import { LayoutTemplate, AppWindow, CheckCheck, Kanban, Table as TableIcon, Calendar } from 'lucide-react';
4
+
5
+ export function Templates() {
6
+ const templates = [
7
+ {
8
+ id: 'form',
9
+ title: 'Inline Form',
10
+ description: 'A static, embedded contact form designed directly into the page layout.',
11
+ icon: LayoutTemplate,
12
+ path: '/templates/inline_form',
13
+ },
14
+ {
15
+ id: 'popup',
16
+ title: 'Modal Popup Form',
17
+ description: 'A centralized popup contact form that visually blurs the background context.',
18
+ icon: AppWindow,
19
+ path: '/templates/popup_form',
20
+ },
21
+ {
22
+ id: 'confirmation',
23
+ title: 'Action Confirmation',
24
+ description: 'A classic confirmation dialog styled for decisive user actions.',
25
+ icon: CheckCheck,
26
+ path: '/templates/confirmation',
27
+ },
28
+ {
29
+ id: 'board',
30
+ title: 'Board View',
31
+ description: 'A modern, drag-and-drop styled Kanban board aesthetic for task management.',
32
+ icon: Kanban,
33
+ path: '/templates/board',
34
+ },
35
+ {
36
+ id: 'table',
37
+ title: 'Data Table',
38
+ description: 'A clean, column-based data table optimized for large datasets and bulk actions.',
39
+ icon: TableIcon,
40
+ path: '/templates/table',
41
+ },
42
+ {
43
+ id: 'calendar',
44
+ title: 'Calendar View',
45
+ description: 'A structured monthly calendar grid for scheduling and date-based resource planning.',
46
+ icon: Calendar,
47
+ path: '/templates/calendar',
48
+ }
49
+ ];
50
+
51
+ return (
52
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col pt-12">
53
+ <motion.div
54
+ initial={{ opacity: 0, y: 20 }}
55
+ animate={{ opacity: 1, y: 0 }}
56
+ className="mb-10 text-center"
57
+ >
58
+ <h1 className="w-fit mx-auto inline-block text-4xl md:text-5xl lg:text-[4.8rem] leading-[1.1] font-extrabold tracking-tight mb-4 text-gradient animate-float">
59
+ Premium Templates
60
+ </h1>
61
+ <p className="text-foreground/60 text-lg font-medium max-w-2xl mx-auto">
62
+ Explore our collection of fully functional, beautifully designed boilerplate components.
63
+ </p>
64
+ </motion.div>
65
+
66
+ <div className="w-full max-w-4xl mx-auto flex flex-col gap-4 relative">
67
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80%] h-[80%] bg-accent/5 blur-[120px] rounded-full pointer-events-none -z-10" />
68
+
69
+ {templates.map((tpl, i) => {
70
+ const Icon = tpl.icon;
71
+ return (
72
+ <motion.div
73
+ key={tpl.id}
74
+ initial={{ opacity: 0, y: 10 }}
75
+ animate={{ opacity: 1, y: 0 }}
76
+ transition={{ delay: i * 0.05 }}
77
+ >
78
+ <Link
79
+ to={tpl.path}
80
+ className="flex items-center gap-6 w-full p-4 md:px-6 glass-panel border border-[var(--panel-border)] rounded-2xl hover:border-accent/40 transition-all duration-300 group hover:bg-foreground/[0.02]"
81
+ >
82
+ <div className="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center text-accent shrink-0 border border-accent/20 group-hover:scale-105 transition-transform">
83
+ <Icon className="w-5 h-5" />
84
+ </div>
85
+
86
+ <div className="flex flex-col flex-1 min-w-0">
87
+ <h3 className="text-[15px] font-bold text-foreground truncate">{tpl.title}</h3>
88
+ <p className="text-[13px] text-foreground/50 truncate font-medium">
89
+ {tpl.description}
90
+ </p>
91
+ </div>
92
+
93
+ <div className="hidden md:flex items-center gap-2 text-[13px] font-bold px-5 py-2.5 rounded-xl btn-primary shrink-0 transition-transform shadow-lg group-hover:-translate-y-0.5">
94
+ View <span className="font-black leading-none ml-0.5">→</span>
95
+ </div>
96
+ </Link>
97
+ </motion.div>
98
+ );
99
+ })}
100
+ </div>
101
+ </main>
102
+ );
103
+ }