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.
- package/dist/FirebaseOS.d.ts +15 -0
- package/dist/firebase-os.cjs.js +5 -20
- package/dist/firebase-os.es.js +95 -90
- package/dist/index.d.ts +3 -0
- package/dist/lib/ConfigContext.d.ts +12 -0
- package/package.json +3 -2
- package/scripts/postinstall.js +86 -15
- package/src/App.css +184 -0
- package/src/App.tsx +214 -0
- package/src/FirebaseOS.tsx +81 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/AdminNotifications.test.tsx +98 -0
- package/src/components/AdminNotifications.tsx +194 -0
- package/src/components/Button.test.tsx +22 -0
- package/src/components/Button.tsx +53 -0
- package/src/components/ConfirmModal.test.tsx +98 -0
- package/src/components/ConfirmModal.tsx +73 -0
- package/src/components/ContactPopup.test.tsx +98 -0
- package/src/components/ContactPopup.tsx +437 -0
- package/src/components/CustomSelect.test.tsx +47 -0
- package/src/components/CustomSelect.tsx +89 -0
- package/src/components/DashboardNav.test.tsx +98 -0
- package/src/components/DashboardNav.tsx +281 -0
- package/src/components/Input.test.tsx +33 -0
- package/src/components/Input.tsx +61 -0
- package/src/components/JsonEditor.tsx +579 -0
- package/src/components/Navbar.test.tsx +98 -0
- package/src/components/Navbar.tsx +563 -0
- package/src/configs/forms/contactForm.config.ts +15 -0
- package/src/configs/forms/index.ts +29 -0
- package/src/configs/forms/pubForm.config.ts +11 -0
- package/src/configs/forms/supportForm.config.ts +14 -0
- package/src/configs/forms/userForm.config.ts +11 -0
- package/src/configs/pages/admin.config.ts +29 -0
- package/src/configs/pages/contact.config.ts +6 -0
- package/src/configs/pages/home.config.ts +18 -0
- package/src/configs/pages/mem.config.ts +2 -0
- package/src/configs/pages/menuOrders.config.ts +11 -0
- package/src/configs/pages/pub.config.ts +11 -0
- package/src/configs/pages/shared.config.ts +29 -0
- package/src/configs/pages/support.config.ts +7 -0
- package/src/configs/pages/tabOrders.config.ts +33 -0
- package/src/configs/pages/user.config.ts +29 -0
- package/src/configs/theme.config.ts +93 -0
- package/src/index.css +403 -0
- package/src/index.ts +22 -0
- package/src/lib/AuthContext.test.tsx +88 -0
- package/src/lib/AuthContext.tsx +191 -0
- package/src/lib/ConfigContext.tsx +45 -0
- package/src/lib/ThemeContext.tsx +233 -0
- package/src/lib/firebase.ts +91 -0
- package/src/main.tsx +22 -0
- package/src/microcomponents/AdminExampleContent.tsx +44 -0
- package/src/microcomponents/PrivateExampleContent.tsx +39 -0
- package/src/microcomponents/Public.tsx +126 -0
- package/src/microcomponents/SharedExampleContent.tsx +53 -0
- package/src/pages/Dashboard.test.tsx +98 -0
- package/src/pages/Dashboard.tsx +60 -0
- package/src/pages/DynamicPage.tsx +237 -0
- package/src/pages/FormsAdmin.test.tsx +98 -0
- package/src/pages/FormsAdmin.tsx +459 -0
- package/src/pages/Home.test.tsx +98 -0
- package/src/pages/Home.tsx +144 -0
- package/src/pages/Login.test.tsx +98 -0
- package/src/pages/Login.tsx +108 -0
- package/src/pages/PagesAdmin.test.tsx +98 -0
- package/src/pages/PagesAdmin.tsx +1022 -0
- package/src/pages/Profile.test.tsx +98 -0
- package/src/pages/Profile.tsx +319 -0
- package/src/pages/Register.test.tsx +98 -0
- package/src/pages/Register.tsx +116 -0
- package/src/pages/Requests.test.tsx +95 -0
- package/src/pages/Requests.tsx +422 -0
- package/src/pages/ResetPassword.test.tsx +98 -0
- package/src/pages/ResetPassword.tsx +92 -0
- package/src/pages/Settings.test.tsx +98 -0
- package/src/pages/Settings.tsx +393 -0
- package/src/pages/Setup.tsx +407 -0
- package/src/pages/StorageAdmin.test.tsx +150 -0
- package/src/pages/StorageAdmin.tsx +769 -0
- package/src/pages/Submissions.test.tsx +95 -0
- package/src/pages/Submissions.tsx +378 -0
- package/src/pages/Templates.test.tsx +98 -0
- package/src/pages/Templates.tsx +103 -0
- package/src/pages/ThemeAdmin.test.tsx +144 -0
- package/src/pages/ThemeAdmin.tsx +1000 -0
- package/src/pages/Users.test.tsx +95 -0
- package/src/pages/Users.tsx +334 -0
- package/src/pages/Verify.test.tsx +98 -0
- package/src/pages/Verify.tsx +95 -0
- package/src/prompts/index.ts +13 -0
- package/src/prompts/pages/publicPage.ts +44 -0
- package/src/prompts/sharedConstants.ts +12 -0
- package/src/prompts/tabs/board/adminboard.ts +32 -0
- package/src/prompts/tabs/board/privateboard.ts +36 -0
- package/src/prompts/tabs/board/publicboard.ts +36 -0
- package/src/prompts/tabs/calendar/admincalendar.ts +32 -0
- package/src/prompts/tabs/calendar/privatecalendar.ts +36 -0
- package/src/prompts/tabs/calendar/publiccalendar.ts +36 -0
- package/src/prompts/tabs/crud/admin.ts +54 -0
- package/src/prompts/tabs/crud/private.ts +55 -0
- package/src/prompts/tabs/crud/shared.ts +53 -0
- package/src/prompts/tabs/table/admintable.ts +32 -0
- package/src/prompts/tabs/table/privatetable.ts +36 -0
- package/src/prompts/tabs/table/publictable.ts +36 -0
- package/src/setupTests.ts +1 -0
- package/src/templates/AdminPageTemplate.tsx +678 -0
- package/src/templates/PrivatePageTemplate.tsx +594 -0
- package/src/templates/PublicPageTemplate.tsx +92 -0
- package/src/templates/SharedPageTemplate.tsx +551 -0
- package/src/templates/TemplateBoard.test.tsx +106 -0
- package/src/templates/TemplateBoard.tsx +642 -0
- package/src/templates/TemplateCalendar.test.tsx +106 -0
- package/src/templates/TemplateCalendar.tsx +848 -0
- package/src/templates/TemplateConfirmation.test.tsx +106 -0
- package/src/templates/TemplateConfirmation.tsx +145 -0
- package/src/templates/TemplateInlineForm.test.tsx +106 -0
- package/src/templates/TemplateInlineForm.tsx +129 -0
- package/src/templates/TemplatePopupForm.test.tsx +106 -0
- package/src/templates/TemplatePopupForm.tsx +174 -0
- package/src/templates/TemplateTable.test.tsx +106 -0
- 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></> 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
|
+
});
|