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,407 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Sun, Moon, Database, CheckCircle2, AlertCircle, ShieldCheck, Copy, Check, Loader2, Settings, Mail, MessageCircle } from 'lucide-react';
4
+ import { Link } from 'react-router-dom';
5
+ import { themeConfig } from '../configs/theme.config';
6
+ import { Whatsapp, RefreshDouble } from 'iconoir-react';
7
+
8
+ // ── This page must NOT import firebase.ts, AuthContext, or ThemeContext ──
9
+ // It renders before Firebase is configured.
10
+
11
+ // ── Environment key validation ───────────────────────────────────────────────
12
+
13
+ const REQUIRED_KEYS = [
14
+ { key: 'VITE_FIREBASE_API_KEY', label: 'API Key' },
15
+ { key: 'VITE_FIREBASE_AUTH_DOMAIN', label: 'Auth Domain' },
16
+ { key: 'VITE_FIREBASE_PROJECT_ID', label: 'Project ID' },
17
+ { key: 'VITE_FIREBASE_STORAGE_BUCKET', label: 'Storage Bucket' },
18
+ { key: 'VITE_FIREBASE_MESSAGING_SENDER_ID', label: 'Messaging Sender ID' },
19
+ { key: 'VITE_FIREBASE_APP_ID', label: 'App ID' },
20
+ ];
21
+
22
+ function getEnvVal(key: string): string {
23
+ // Static access is required for Vite to replace env variables at build time.
24
+ let val: any;
25
+ try {
26
+ switch (key) {
27
+ case 'VITE_FIREBASE_API_KEY': val = import.meta.env.VITE_FIREBASE_API_KEY; break;
28
+ case 'VITE_FIREBASE_AUTH_DOMAIN': val = import.meta.env.VITE_FIREBASE_AUTH_DOMAIN; break;
29
+ case 'VITE_FIREBASE_PROJECT_ID': val = import.meta.env.VITE_FIREBASE_PROJECT_ID; break;
30
+ case 'VITE_FIREBASE_STORAGE_BUCKET': val = import.meta.env.VITE_FIREBASE_STORAGE_BUCKET; break;
31
+ case 'VITE_FIREBASE_MESSAGING_SENDER_ID': val = import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID; break;
32
+ case 'VITE_FIREBASE_APP_ID': val = import.meta.env.VITE_FIREBASE_APP_ID; break;
33
+ case 'VITE_ADMIN_EMAILS': val = import.meta.env.VITE_ADMIN_EMAILS; break;
34
+ }
35
+ if (val && typeof val === 'string' && val.trim() !== '') return val;
36
+ } catch {}
37
+
38
+ // Check localStorage for config saved by the setup wizard (library mode fallback)
39
+ try {
40
+ const saved = localStorage.getItem('firebase_config');
41
+ if (saved) {
42
+ const config = JSON.parse(saved);
43
+ const keyMap: Record<string, string> = {
44
+ 'VITE_FIREBASE_API_KEY': 'apiKey',
45
+ 'VITE_FIREBASE_AUTH_DOMAIN': 'authDomain',
46
+ 'VITE_FIREBASE_PROJECT_ID': 'projectId',
47
+ 'VITE_FIREBASE_STORAGE_BUCKET': 'storageBucket',
48
+ 'VITE_FIREBASE_MESSAGING_SENDER_ID': 'messagingSenderId',
49
+ 'VITE_FIREBASE_APP_ID': 'appId',
50
+ 'VITE_ADMIN_EMAILS': 'adminEmails',
51
+ };
52
+ const configKey = keyMap[key];
53
+ if (configKey && config[configKey]) return config[configKey];
54
+ }
55
+ } catch (e) {}
56
+
57
+ return '';
58
+ }
59
+
60
+ // Environment key status
61
+ export function getEnvStatus() {
62
+ const missing: string[] = [];
63
+ for (const { key, label } of REQUIRED_KEYS) {
64
+ const v = getEnvVal(key);
65
+ if (!v || v.trim() === '') {
66
+ console.log('Missing key:', key, v);
67
+ missing.push(label);
68
+ } else {
69
+ console.log('Found key:', key, v);
70
+ }
71
+ }
72
+
73
+ const adminEmails = getEnvVal('VITE_ADMIN_EMAILS');
74
+ const missingAdminEmails = !adminEmails || adminEmails.trim() === '';
75
+
76
+ console.log('Setup complete?', missing.length === 0, missing);
77
+ return { allSet: missing.length === 0, missing, missingAdminEmails };
78
+ }
79
+
80
+ export function isSetupComplete(): boolean {
81
+ return getEnvStatus().allSet;
82
+ }
83
+
84
+ import firestoreRulesRaw from '../../firestore.rules?raw';
85
+ import storageRulesRaw from '../../storage.rules?raw';
86
+
87
+ const firestoreRules = firestoreRulesRaw;
88
+ const storageRules = storageRulesRaw;
89
+
90
+ const envExample = `VITE_FIREBASE_API_KEY=AIzaSyDxJ...
91
+ VITE_FIREBASE_AUTH_DOMAIN=myproject.firebaseapp.com
92
+ VITE_FIREBASE_PROJECT_ID=myproject
93
+ VITE_FIREBASE_STORAGE_BUCKET=myproject.firebasestorage.app
94
+ VITE_FIREBASE_MESSAGING_SENDER_ID=123456789012
95
+ VITE_FIREBASE_APP_ID=1:123456789012:web:abcdef123456
96
+ VITE_ADMIN_EMAILS=your@email.com,other@email.com`;
97
+
98
+ // ── Buttons ───────────────────────────────────────────────────────────────
99
+
100
+ function CopyButton({ code, label }: { code: string; label: string }) {
101
+ const [copied, setCopied] = React.useState(false);
102
+ const handleCopy = () => { navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); };
103
+ return (
104
+ <motion.button
105
+ whileHover={{ scale: 1.02 }}
106
+ whileTap={{ scale: 0.98 }}
107
+ onClick={handleCopy}
108
+ className={`inline-flex items-center gap-2 px-4 py-2 mt-2 rounded-xl text-[13px] font-bold tracking-wide transition-all ${copied ? 'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20' : 'btn-secondary shadow-sm'}`}
109
+ >
110
+ {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
111
+ {copied ? 'Copied to Clipboard!' : label}
112
+ </motion.button>
113
+ );
114
+ }
115
+
116
+ function EnvBlock({ code }: { code: string }) {
117
+ const [copied, setCopied] = React.useState(false);
118
+ const handleCopy = () => { navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); };
119
+ return (
120
+ <div className="my-5 glass-panel border border-[var(--panel-border)] rounded-2xl overflow-hidden shadow-inner bg-background">
121
+ <div className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--panel-border)]/50 bg-foreground/[0.02]">
122
+ <div className="flex items-center gap-2">
123
+ <Settings className="w-3.5 h-3.5 text-foreground/30" />
124
+ <span className="text-[11px] font-bold uppercase tracking-widest text-foreground/30">.env</span>
125
+ </div>
126
+ <button onClick={handleCopy} className={`text-[11px] font-bold uppercase tracking-wider flex items-center gap-1.5 transition-colors ${copied ? 'text-emerald-500' : 'text-accent hover:text-accent-deep'}`}>
127
+ {copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
128
+ {copied ? 'Copied' : 'Copy'}
129
+ </button>
130
+ </div>
131
+ <pre className="p-4 md:p-5 overflow-x-auto text-[13px] leading-[1.8] font-mono styled-scrollbars">
132
+ <code>{code.split('\n').map((line, i) => {
133
+ const eqIdx = line.indexOf('=');
134
+ if (eqIdx === -1) return <span key={i} className="text-foreground/30">{line}{'\n'}</span>;
135
+ const key = line.substring(0, eqIdx);
136
+ const val = line.substring(eqIdx + 1);
137
+ return <span key={i}><span className="text-accent">{key}</span><span className="text-foreground/30">=</span><span className="text-foreground/60">{val}</span>{'\n'}</span>;
138
+ })}</code>
139
+ </pre>
140
+ </div>
141
+ );
142
+ }
143
+
144
+ // ── Step ──────────────────────────────────────────────────────────────────────
145
+
146
+ function Step({ title, children }: { title: string; children: React.ReactNode }) {
147
+ return (
148
+ <div className="flex flex-col relative pb-10 mb-10 border-b border-[var(--panel-border)]/50 last:border-0 last:pb-0 last:mb-0">
149
+ <div className="flex items-center gap-3 mb-4">
150
+ <h3 className="text-[17px] md:text-[19px] font-extrabold text-foreground tracking-tight">{title}</h3>
151
+ </div>
152
+ <div className="text-[14px] md:text-[15px] text-foreground/70 leading-relaxed space-y-4">
153
+ {children}
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ // ── Main Setup Page ──────────────────────────────────────────────────────────
160
+
161
+ interface SetupProps {
162
+ /** When true, renders its own Navbar (pre-Firebase standalone mode) */
163
+ standalone?: boolean;
164
+ }
165
+
166
+ export function Setup({ standalone }: SetupProps) {
167
+ const [darkMode, setDarkMode] = useState<boolean>(() => {
168
+ const saved = localStorage.getItem('theme-mode');
169
+ if (saved === 'light') return false;
170
+ if (saved === 'dark') return true;
171
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
172
+ });
173
+
174
+ const [verifyResult, setVerifyResult] = useState<{ missing: string[]; missingAdminEmails?: boolean } | null>(null);
175
+ const [isVerifying, setIsVerifying] = useState(false);
176
+
177
+ // Only manage the data-theme attribute and favicon when standalone (no ThemeContext)
178
+ useEffect(() => {
179
+ if (!standalone) return;
180
+ document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
181
+ }, [darkMode, standalone]);
182
+
183
+ // Set favicon from themeConfig when standalone
184
+ useEffect(() => {
185
+ if (!standalone) return;
186
+ if (themeConfig.faviconUrl) {
187
+ let favicon = document.querySelector("link[rel*='icon']") as HTMLLinkElement;
188
+ if (!favicon) {
189
+ favicon = document.createElement('link');
190
+ favicon.rel = 'icon';
191
+ document.head.appendChild(favicon);
192
+ }
193
+ favicon.href = themeConfig.faviconUrl;
194
+ }
195
+ }, [standalone]);
196
+
197
+ const handleVerify = () => {
198
+ setIsVerifying(true);
199
+ setVerifyResult(null);
200
+ setTimeout(() => {
201
+ const status = getEnvStatus();
202
+ setVerifyResult({ missing: status.missing, missingAdminEmails: status.missingAdminEmails });
203
+ if (status.allSet) {
204
+ setTimeout(() => { window.location.href = '/'; }, 1800);
205
+ }
206
+ setIsVerifying(false);
207
+ }, 800);
208
+ };
209
+
210
+ return (
211
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col min-h-screen">
212
+
213
+ {/* ── Page heading (identical to ThemeAdmin / PagesAdmin / Calendar) ── */}
214
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-6 flex items-center justify-between">
215
+ <div>
216
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">
217
+ Setup
218
+ </h1>
219
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
220
+ <span className="w-8 h-[1px] bg-foreground/10" />
221
+ /setup
222
+ </div>
223
+ </div>
224
+ </motion.div>
225
+
226
+ {/* ── Content panel ── */}
227
+ <div className="flex flex-col gap-6 flex-1 pb-16">
228
+ <motion.div
229
+ initial={{ opacity: 0, y: 20 }}
230
+ animate={{ opacity: 1, y: 0 }}
231
+ transition={{ delay: 0.1 }}
232
+ className="glass-panel border border-[var(--panel-border)] rounded-3xl p-6 md:p-10 shadow-2xl relative overflow-hidden bg-background flex flex-col"
233
+ >
234
+ {/* Panel header with support buttons */}
235
+ <div className="flex flex-col gap-4 mb-8 pb-6 border-b border-[var(--panel-border)]/50 relative z-10">
236
+ <div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
237
+ <div>
238
+ <h2 className="text-xl font-extrabold text-foreground tracking-tight">Firebase Configuration</h2>
239
+ <p className="text-[14px] font-medium text-foreground/50 mt-2 leading-relaxed">
240
+ Follow these steps to connect your Firebase project. Takes about 5 minutes.
241
+ </p>
242
+ </div>
243
+ <div className="flex items-center gap-2 shrink-0">
244
+ <motion.a
245
+ whileHover={{ scale: 1.05 }}
246
+ whileTap={{ scale: 0.95 }}
247
+ href="mailto:antonina@youraiworkflow.co"
248
+ title="Email Support"
249
+ className="w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer btn-secondary"
250
+ >
251
+ <Mail className="w-4 h-4" />
252
+ </motion.a>
253
+ <motion.a
254
+ whileHover={{ scale: 1.05 }}
255
+ whileTap={{ scale: 0.95 }}
256
+ href="https://wa.me/34632407883"
257
+ target="_blank"
258
+ rel="noopener noreferrer"
259
+ title="WhatsApp Support"
260
+ className="w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer btn-secondary"
261
+ >
262
+ <Whatsapp className="w-4 h-4 text-foreground/80" />
263
+ </motion.a>
264
+ <motion.button
265
+ whileHover={{ scale: 1.05 }}
266
+ whileTap={{ scale: 0.95 }}
267
+ onClick={handleVerify}
268
+ disabled={isVerifying}
269
+ className="h-10 px-4 flex items-center justify-center gap-2 rounded-xl transition-all duration-300 cursor-pointer btn-primary shadow-lg disabled:opacity-50 disabled:cursor-not-allowed ml-2"
270
+ title="Verify Configuration"
271
+ >
272
+ {isVerifying ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshDouble className="w-4 h-4" />}
273
+ <span className="text-[13px] font-bold hidden sm:block">Verify Setup</span>
274
+ </motion.button>
275
+ </div>
276
+ </div>
277
+
278
+ {verifyResult && verifyResult.missing.length > 0 && (
279
+ <motion.div
280
+ initial={{ opacity: 0, y: -10 }}
281
+ animate={{ opacity: 1, y: 0 }}
282
+ className="flex flex-col gap-2 mt-2 bg-red-500/5 border border-red-500/10 p-4 rounded-2xl"
283
+ >
284
+ <div className="flex items-center gap-2.5 text-[13px]">
285
+ <div className="w-8 h-8 rounded-full bg-red-500/10 border border-red-500/20 flex items-center justify-center shrink-0">
286
+ <AlertCircle className="w-4 h-4 text-red-500" />
287
+ </div>
288
+ <span className="font-bold text-foreground/70">Missing values — update <code className="text-accent text-[12px]">.env</code> and restart dev server</span>
289
+ </div>
290
+ <div className="flex flex-wrap gap-1.5 ml-10">
291
+ {verifyResult.missing.map(m => (
292
+ <span key={m} className="px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider rounded-lg bg-red-500/10 border border-red-500/20 text-red-500">{m}</span>
293
+ ))}
294
+ </div>
295
+ </motion.div>
296
+ )}
297
+
298
+ {verifyResult && verifyResult.missing.length === 0 && verifyResult.missingAdminEmails && (
299
+ <motion.div
300
+ initial={{ opacity: 0, y: -10 }}
301
+ animate={{ opacity: 1, y: 0 }}
302
+ className="flex items-center gap-2.5 mt-2 bg-yellow-500/5 border border-yellow-500/10 p-4 rounded-2xl"
303
+ >
304
+ <div className="w-8 h-8 rounded-full bg-yellow-500/10 border border-yellow-500/20 flex items-center justify-center shrink-0">
305
+ <AlertCircle className="w-4 h-4 text-yellow-500" />
306
+ </div>
307
+ <span className="text-[13px] font-bold text-yellow-500">Warning: No Admin Emails configured. App is operational. Redirecting…</span>
308
+ </motion.div>
309
+ )}
310
+
311
+ {verifyResult && verifyResult.missing.length === 0 && !verifyResult.missingAdminEmails && (
312
+ <motion.div
313
+ initial={{ opacity: 0, y: -10 }}
314
+ animate={{ opacity: 1, y: 0 }}
315
+ className="flex items-center gap-2.5 mt-2 bg-emerald-500/5 border border-emerald-500/10 p-4 rounded-2xl"
316
+ >
317
+ <div className="w-8 h-8 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center shrink-0">
318
+ <CheckCircle2 className="w-4 h-4 text-emerald-500" />
319
+ </div>
320
+ <span className="text-[13px] font-bold text-emerald-500">All configured — redirecting…</span>
321
+ </motion.div>
322
+ )}
323
+ </div>
324
+
325
+ {/* ── Steps ── */}
326
+ <div className="flex flex-col relative z-10 p-6 md:p-8 bg-foreground/[0.015] rounded-3xl border border-[var(--panel-border)]/50 mt-4">
327
+
328
+ <Step title="Create a Firebase Project">
329
+ <p>
330
+ 1. Navigate to{' '}
331
+ <a href="https://console.firebase.google.com" target="_blank" rel="noopener noreferrer" className="text-accent underline underline-offset-2 font-semibold hover:text-accent-deep transition-colors">
332
+ console.firebase.google.com
333
+ </a>{' '}
334
+ and click <strong>Add Project</strong>. <br />
335
+ 2. Enter a project name and click <strong>Continue</strong>. <br />
336
+ 3. You can disable Google Analytics or leave it enabled. Click <strong>Create Project</strong>.
337
+ </p>
338
+ </Step>
339
+
340
+ <Step title="Add a Web App">
341
+ <p>
342
+ 1. On your Firebase project dashboard, click the <strong>&lt;/&gt; Web</strong> icon (next to iOS and Android icons). <br />
343
+ 2. Give your app a simple nickname (e.g., <span className="text-accent">MyApp</span>). You don't need to check "Firebase Hosting". <br />
344
+ 3. Click <strong>Register App</strong>. <br />
345
+ 4. Firebase will show you an SDK script block containing an object with keys like <code className="text-[12px] bg-foreground/5 rounded px-2 py-0.5">apiKey</code>, <code className="text-[12px] bg-foreground/5 rounded px-2 py-0.5">authDomain</code>, etc. Keep this tab open.
346
+ </p>
347
+ <pre className="mt-4 p-4 md:p-5 overflow-x-auto text-[13px] leading-[1.8] font-mono styled-scrollbars glass-panel border border-[var(--panel-border)] rounded-2xl bg-background shadow-inner text-foreground/60">
348
+ <code>{`const firebaseConfig = {
349
+ apiKey: "AIzaSy...",
350
+ authDomain: "myproject.firebaseapp.com",
351
+ projectId: "myproject",
352
+ storageBucket: "myproject.firebasestorage.app",
353
+ messagingSenderId: "123456789012",
354
+ appId: "1:123456789012:web:abcdef123456"
355
+ };`}</code>
356
+ </pre>
357
+ </Step>
358
+
359
+ <Step title="Configure your .env file">
360
+ <p>
361
+ Open the <code className="text-accent font-bold text-[13px]">.env</code> file in your source code root. Map the keys from your Firebase Web SDK to the <code className="bg-foreground/5 rounded px-2 py-0.5 font-mono text-[11px]">VITE_ FIREBASE_</code> variables.
362
+ </p>
363
+ <p className="text-[13px] bg-accent/10 border border-accent/20 p-3 rounded-xl mt-2 text-foreground/80">
364
+ <strong>Important:</strong> Provide your own emails in <code className="bg-background/80 rounded px-1.5 py-0.5 font-mono text-[11px]">VITE_ADMIN_EMAILS</code> separated by commas. These users will automatically get full admin privileges upon their first registration. No quotes or spaces!
365
+ </p>
366
+ <EnvBlock code={envExample} />
367
+ </Step>
368
+
369
+ <Step title="Enable Authentication">
370
+ <p>
371
+ 1. In Firebase console Sidebar, go to <strong>Product categories</strong> → <strong>Security</strong> → <strong>Authentication</strong>. Click <strong>Get Started</strong>. <br />
372
+ 2. Click the <strong>Sign-in method</strong> tab. <br />
373
+ 3. Enable <strong>Email/Password</strong> (no need to enable email links). <br />
374
+ 4. Enable <strong>Google</strong>. <br />
375
+ 5. Enable <strong>Anonymous</strong>.
376
+ </p>
377
+ </Step>
378
+
379
+ <Step title="Create Firestore Database">
380
+ <p>
381
+ 1. In the Sidebar, go to <strong>Product categories</strong> → <strong>Database and Storage</strong> → <strong>Firestore</strong>. Click <strong>Create database</strong>. <br />
382
+ 2. Keep the default location and start in <strong>Production mode</strong>. <br />
383
+ 3. Go to the <strong>Rules</strong> tab, paste the rules below, and click <strong>Publish</strong>.
384
+ </p>
385
+ <CopyButton code={firestoreRules} label="Copy Firestore Rules" />
386
+ </Step>
387
+
388
+ <Step title="Configure Storage (Optional but Recommended)">
389
+ <p>
390
+ 1. In the Sidebar, go to <strong>Product categories</strong> → <strong>Database and Storage</strong> → <strong>Storage</strong>. Click <strong>Get started</strong>. Start in production mode. <br />
391
+ 2. Once created, copy the bucket URL (e.g. <code className="text-[12px] bg-foreground/5 rounded px-1.5 py-0.5">your-project.firebasestorage.app</code>) and paste it into <code className="text-[12px] bg-foreground/5 rounded px-1.5 py-0.5">VITE_FIREBASE_STORAGE_BUCKET</code> in your <code className="text-[13px] font-bold">.env</code>. <br />
392
+ 3. In Firebase Storage, go to the <strong>Rules</strong> tab, paste tracking rules, and click <strong>Publish</strong>.
393
+ </p>
394
+ <CopyButton code={storageRules} label="Copy Storage Rules" />
395
+ <p className="text-[12px] mt-4 opacity-60 font-medium border-l-2 border-foreground/20 pl-3">
396
+ Note: Storage rules require the Blaze pay-as-you-go plan, but there are large free tiers so you won't be charged for development usage.
397
+ </p>
398
+ </Step>
399
+
400
+ </div>
401
+
402
+
403
+ </motion.div>
404
+ </div>
405
+ </main>
406
+ );
407
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import { StorageAdmin } from './StorageAdmin';
4
+ import { BrowserRouter } from 'react-router-dom';
5
+ import { AuthProvider } from '../lib/AuthContext';
6
+ import * as firestore from 'firebase/firestore';
7
+
8
+ const mockFile = {
9
+ _id: 'file123',
10
+ fileName: 'test-document.pdf',
11
+ uid: 'user-id',
12
+ uploaderEmail: 'user@example.com',
13
+ fileSize: 1048576, // 1MB
14
+ fileType: 'application/pdf',
15
+ downloadURL: 'https://example.com/test-document.pdf',
16
+ accessPrefix: 'user_'
17
+ };
18
+
19
+ vi.mock('../lib/firebase', () => ({
20
+ auth: {
21
+ currentUser: { uid: 'user-id', email: 'user@example.com' }
22
+ },
23
+ db: {},
24
+ storage: {}
25
+ }));
26
+
27
+ const dummyQuery = { type: 'query' };
28
+ vi.mock('firebase/firestore', () => ({
29
+ getFirestore: vi.fn(() => ({})),
30
+ collection: vi.fn(() => dummyQuery),
31
+ doc: vi.fn(() => dummyQuery),
32
+ setDoc: vi.fn(() => Promise.resolve()),
33
+ updateDoc: vi.fn(() => Promise.resolve()),
34
+ deleteDoc: vi.fn(() => Promise.resolve()),
35
+ addDoc: vi.fn(() => Promise.resolve({ id: 'new-id' })),
36
+ getDocs: vi.fn((...args) => {
37
+ console.log("Mock getDocs called with:", args);
38
+ return Promise.resolve({
39
+ docs: [{ id: mockFile._id, data: () => mockFile }]
40
+ });
41
+ }),
42
+ query: vi.fn(() => dummyQuery),
43
+ where: vi.fn(() => dummyQuery),
44
+ orderBy: vi.fn(() => dummyQuery),
45
+ serverTimestamp: vi.fn(() => ({ toMillis: () => Date.now() })),
46
+ onSnapshot: vi.fn((ref, cb) => {
47
+ // Immediate return
48
+ cb({
49
+ forEach: (docCb: any) => {
50
+ docCb({ id: mockFile._id, data: () => mockFile });
51
+ }
52
+ });
53
+ return vi.fn();
54
+ })
55
+ }));
56
+
57
+ vi.mock('firebase/storage', async () => {
58
+ const actual = await vi.importActual('firebase/storage');
59
+ return {
60
+ ...actual,
61
+ getStorage: vi.fn(),
62
+ ref: vi.fn(),
63
+ uploadBytes: vi.fn(() => Promise.resolve()),
64
+ getDownloadURL: vi.fn(() => Promise.resolve('https://example.com/test-document.pdf')),
65
+ deleteObject: vi.fn(() => Promise.resolve()),
66
+ getBlob: vi.fn(() => Promise.resolve(new Blob(['mock data'], { type: 'application/pdf' })))
67
+ };
68
+ });
69
+
70
+ // Mock clipboard and URL.createObjectURL
71
+ const mockClipboardRead = vi.fn();
72
+ const mockClipboardWrite = vi.fn();
73
+ Object.assign(navigator, {
74
+ clipboard: {
75
+ readText: mockClipboardRead,
76
+ writeText: mockClipboardWrite,
77
+ },
78
+ });
79
+ (globalThis as any).URL.createObjectURL = vi.fn(() => 'blob:mock-url');
80
+ (globalThis as any).URL.revokeObjectURL = vi.fn();
81
+
82
+ vi.mock('../lib/AuthContext', () => ({
83
+ useAuth: () => ({
84
+ userRole: 'admin',
85
+ user: { uid: 'user-id', email: 'user@example.com' },
86
+ activeOrg: { id: 'test-org' },
87
+ activeWorkspace: { id: 'test-ws' },
88
+ authLoading: false
89
+ }),
90
+ AuthProvider: ({ children }: any) => <>{children}</>
91
+ }));
92
+
93
+ vi.mock('../components/DashboardNav', () => ({
94
+ DashboardNav: () => <div data-testid="dashboard-nav">Nav</div>
95
+ }));
96
+
97
+ (globalThis as any).ResizeObserver = class ResizeObserver {
98
+ observe() {}
99
+ unobserve() {}
100
+ disconnect() {}
101
+ };
102
+
103
+ (globalThis as any).IntersectionObserver = class IntersectionObserver {
104
+ root: any = null;
105
+ rootMargin: string = '';
106
+ thresholds: ReadonlyArray<number> = [];
107
+ observe() {}
108
+ unobserve() {}
109
+ disconnect() {}
110
+ takeRecords() { return []; }
111
+ };
112
+
113
+ describe('StorageAdmin Component', () => {
114
+ beforeEach(() => {
115
+ vi.clearAllMocks();
116
+ });
117
+
118
+ const renderStorageAdmin = () => {
119
+ return render(
120
+ <AuthProvider>
121
+ <BrowserRouter>
122
+ <StorageAdmin />
123
+ </BrowserRouter>
124
+ </AuthProvider>
125
+ );
126
+ };
127
+
128
+ it('renders all storage folders for super_admin', async () => {
129
+ renderStorageAdmin();
130
+ expect((await screen.findAllByText('Private'))[0]).toBeInTheDocument();
131
+ expect((await screen.findAllByText('Shared'))[0]).toBeInTheDocument();
132
+ expect((await screen.findAllByText('Public'))[0]).toBeInTheDocument();
133
+ expect((await screen.findAllByText('User Uploads'))[0]).toBeInTheDocument();
134
+ });
135
+
136
+ it('renders correctly', async () => {
137
+ renderStorageAdmin();
138
+ expect(await screen.findByText('Drive')).toBeInTheDocument();
139
+ });
140
+
141
+ it('can open file rename modal safely', async () => {
142
+ renderStorageAdmin();
143
+ expect((await screen.findAllByText('Private'))[0]).toBeInTheDocument();
144
+ });
145
+
146
+ it('opens delete confirmation safely', async () => {
147
+ renderStorageAdmin();
148
+ expect((await screen.findAllByText('Shared'))[0]).toBeInTheDocument();
149
+ });
150
+ });