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,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 { Profile } from './Profile';
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('Profile 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
+ <Profile />
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,319 @@
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { useAuth } from '../lib/AuthContext';
4
+ import { storage, auth, db } from '../lib/firebase';
5
+ import { updateProfile, verifyBeforeUpdateEmail, updatePassword, fetchSignInMethodsForEmail } from 'firebase/auth';
6
+ import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage';
7
+ import { doc, updateDoc, collection, getDocs, query, where, writeBatch } from 'firebase/firestore';
8
+ import { Input } from '../components/Input';
9
+ import { Button } from '../components/Button';
10
+ import { User, CheckCircle2, AlertCircle, Camera, Trash2, Loader2 } from 'lucide-react';
11
+ import { DashboardNav } from '../components/DashboardNav';
12
+
13
+ export function Profile() {
14
+ const { user, refreshUser } = useAuth();
15
+
16
+ const [name, setName] = useState(user?.displayName || '');
17
+ const [email, setEmail] = useState(user?.email || '');
18
+ const [photoURL, setPhotoURL] = useState(user?.photoURL || '');
19
+
20
+ const [loading, setLoading] = useState(false);
21
+ const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
22
+
23
+ const [password, setPassword] = useState('');
24
+ const [confirmPassword, setConfirmPassword] = useState('');
25
+ const [photoError, setPhotoError] = useState(false);
26
+ const [photoLoading, setPhotoLoading] = useState(false);
27
+ const [imageFetching, setImageFetching] = useState(true);
28
+
29
+ React.useEffect(() => {
30
+ setName(user?.displayName || '');
31
+ setEmail(user?.email || '');
32
+ setPhotoURL(user?.photoURL || '');
33
+ }, [user?.uid, user?.displayName, user?.email, user?.photoURL]);
34
+
35
+ // Propagate avatar changes to all shared/admin records owned by this user
36
+ const propagateAvatarToRecords = async (newAvatarURL: string) => {
37
+ if (!user) return;
38
+ try {
39
+ // Find all mem_ and admin_ collections that might contain this user's records
40
+ // We target the known file collections + dynamically query tab records
41
+ const collectionsToUpdate: string[] = ['mem_files', 'admin_files'];
42
+
43
+ // Also find mem_* and admin_* record collections from sys_tabs
44
+ const tabsSnap = await getDocs(collection(db, 'sys_tabs'));
45
+ tabsSnap.forEach(d => {
46
+ const data = d.data();
47
+ const tabName = (data.tabName || d.id).toLowerCase().replace(/[^a-z0-9]+/g, '_');
48
+ if (data.pageType === 'shared') collectionsToUpdate.push(`mem_${tabName}_records`);
49
+ if (data.pageType === 'admin') collectionsToUpdate.push(`admin_${tabName}_records`);
50
+ });
51
+
52
+ for (const col of collectionsToUpdate) {
53
+ try {
54
+ const q = query(collection(db, col), where('uid', '==', user.uid));
55
+ const snap = await getDocs(q);
56
+ if (snap.empty) continue;
57
+ const batch = writeBatch(db);
58
+ snap.docs.forEach(d => batch.update(d.ref, { creatorAvatar: newAvatarURL }));
59
+ await batch.commit();
60
+ } catch (e) {
61
+ // Collection might not exist yet — that's fine
62
+ }
63
+ }
64
+ } catch (e) {
65
+ console.error('Error propagating avatar to records:', e);
66
+ }
67
+ };
68
+
69
+ const handlePhotoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
70
+ const file = e.target.files?.[0];
71
+ if (!file || !user) return;
72
+
73
+ // Size limit (2MB) - Responsive for faster uploads
74
+ if (file.size > 2 * 1024 * 1024) {
75
+ setMsg({ text: 'File too large. Please select an image under 2MB.', type: 'error' });
76
+ return;
77
+ }
78
+
79
+ setPhotoLoading(true);
80
+ setPhotoError(false);
81
+ setImageFetching(true);
82
+ try {
83
+ const storagePath = `user_avatars/${user.uid}/file.png`;
84
+ const fileRef = ref(storage, storagePath);
85
+
86
+ // Upload to Storage
87
+ await uploadBytes(fileRef, file, {
88
+ customMetadata: { ownerId: user.uid }
89
+ });
90
+ const newPhotoURL = await getDownloadURL(fileRef);
91
+
92
+ // Update Firebase Auth Profile
93
+ await updateProfile(user, { photoURL: newPhotoURL });
94
+
95
+ // Update Firestore User Profile
96
+ const userRef = doc(db, 'user_profiles', user.uid);
97
+ await updateDoc(userRef, {
98
+ avatar: newPhotoURL
99
+ });
100
+
101
+ setPhotoURL(newPhotoURL);
102
+ await refreshUser();
103
+ // Propagate to shared/admin records in background
104
+ propagateAvatarToRecords(newPhotoURL);
105
+ setMsg({ text: 'Avatar successfully updated!', type: 'success' });
106
+ } catch (err) {
107
+ console.error(err);
108
+ setMsg({ text: 'Failed to upload photo.', type: 'error' });
109
+ } finally {
110
+ setPhotoLoading(false);
111
+ }
112
+ };
113
+
114
+ const handlePhotoDelete = async () => {
115
+ if (!user) return;
116
+ setPhotoLoading(true);
117
+ setPhotoError(false);
118
+ setImageFetching(false);
119
+ try {
120
+ const storagePath = `user_avatars/${user.uid}/file.png`;
121
+ const fileRef = ref(storage, storagePath);
122
+ await deleteObject(fileRef).catch((e) => console.error(e));
123
+
124
+ // Update Auth
125
+ await updateProfile(user, { photoURL: '' });
126
+
127
+ // Update Firestore
128
+ const userRef = doc(db, 'user_profiles', user.uid);
129
+ await updateDoc(userRef, { avatar: '' });
130
+
131
+ setPhotoURL('');
132
+ await refreshUser();
133
+ // Clear avatar in shared/admin records
134
+ propagateAvatarToRecords('');
135
+ setMsg({ text: 'Avatar removed.', type: 'success' });
136
+ } catch (err) {
137
+ console.error(err);
138
+ setMsg({ text: 'Failed to delete photo.', type: 'error' });
139
+ } finally {
140
+ setPhotoLoading(false);
141
+ }
142
+ };
143
+
144
+ const handleSave = async (e: React.FormEvent) => {
145
+ e.preventDefault();
146
+ if (!user) return;
147
+
148
+ setLoading(true);
149
+ setMsg(null);
150
+
151
+ try {
152
+ // Update display name
153
+ if (name !== user.displayName) {
154
+ await updateProfile(user, { displayName: name });
155
+ }
156
+
157
+ // Update email (requires re-verification)
158
+ let emailUpdated = false;
159
+ if (email !== user.email) {
160
+ const methods = await fetchSignInMethodsForEmail(auth, email);
161
+ if (methods && methods.length > 0) {
162
+ throw new Error('An account with this email already exists.');
163
+ }
164
+ await verifyBeforeUpdateEmail(user, email);
165
+ emailUpdated = true;
166
+ }
167
+
168
+ // Update password
169
+ if (password) {
170
+ if (password !== confirmPassword) {
171
+ throw new Error('Passwords do not match.');
172
+ }
173
+ if (password.length < 6) {
174
+ throw new Error('Password must be at least 6 characters.');
175
+ }
176
+ await updatePassword(user, password);
177
+ setPassword('');
178
+ setConfirmPassword('');
179
+ }
180
+
181
+ await refreshUser();
182
+
183
+ if (emailUpdated) {
184
+ setMsg({ text: 'Profile updated. A verification link has been sent to your new email address.', type: 'success' });
185
+ } else {
186
+ setMsg({ text: 'Profile successfully updated.', type: 'success' });
187
+ }
188
+ } catch (err: any) {
189
+ console.error(err);
190
+ if (err.code === 'auth/requires-recent-login') {
191
+ setMsg({ text: 'For security, please sign out and sign back in before changing your email or password.', type: 'error' });
192
+ } else {
193
+ setMsg({ text: err.message || 'Failed to update profile.', type: 'error' });
194
+ }
195
+ } finally {
196
+ setLoading(false);
197
+ }
198
+ };
199
+
200
+ return (
201
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col">
202
+
203
+ {/* Header */}
204
+ <motion.div
205
+ initial={{ opacity: 0, y: 20 }}
206
+ animate={{ opacity: 1, y: 0 }}
207
+ className="mb-8"
208
+ >
209
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">
210
+ Profile Settings
211
+ </h1>
212
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
213
+ <span className="w-8 h-[1px] bg-foreground/10" />
214
+ /profile
215
+ </div>
216
+ </motion.div>
217
+
218
+ <div className="flex flex-col gap-6 animate-in fade-in duration-500 pb-20">
219
+ <div className="flex justify-start">
220
+ <DashboardNav />
221
+ </div>
222
+
223
+ <AnimatePresence>
224
+ {msg && (
225
+ <motion.div
226
+ initial={{ opacity: 0, scale: 0.98, y: -10 }}
227
+ animate={{ opacity: 1, scale: 1, y: 0 }}
228
+ exit={{ opacity: 0, scale: 0.98, y: -10 }}
229
+ className={`p-4 mb-6 rounded-2xl border flex flex-col sm:flex-row sm:items-center gap-3 ${msg.type === 'error' ? 'bg-red-500/10 border-red-500/20 text-red-400' : 'bg-accent/10 border-accent/20 text-accent'}`}
230
+ >
231
+ {msg.type === 'success' ? <CheckCircle2 className="w-5 h-5 shrink-0" /> : <AlertCircle className="w-5 h-5 shrink-0" />}
232
+ <span className="text-[14px] font-semibold tracking-wide leading-relaxed">{msg.text}</span>
233
+ </motion.div>
234
+ )}
235
+ </AnimatePresence>
236
+
237
+ <form onSubmit={handleSave} className="w-full flex flex-col">
238
+ <div className="glass-panel w-full p-8 md:p-12 rounded-3xl border border-[var(--panel-border)] shadow-2xl space-y-8 min-h-[500px]">
239
+
240
+ {/* Avatar section */}
241
+ <div className="flex flex-col sm:flex-row items-center gap-6 pb-6 border-b border-[var(--panel-border)]/50">
242
+ <div className="w-24 h-24 md:w-28 md:h-28 rounded-full glass-panel border-4 border-[var(--panel-border)] shadow-xl bg-accent/10 flex items-center justify-center relative overflow-hidden shrink-0 group transition-transform duration-300 hover:scale-[1.02]">
243
+ {(photoLoading || (photoURL && imageFetching && !photoError)) && (
244
+ <div className="absolute inset-0 flex items-center justify-center bg-accent/5 backdrop-blur-sm z-10">
245
+ <Loader2 className="w-8 h-8 text-accent animate-spin" />
246
+ </div>
247
+ )}
248
+ {photoURL && !photoError ? (
249
+ <img
250
+ src={photoURL}
251
+ alt="Avatar"
252
+ className={`w-full h-full object-cover transition-opacity duration-300 ${imageFetching ? 'opacity-0' : 'opacity-100'}`}
253
+ onLoad={() => setImageFetching(false)}
254
+ onError={() => { setPhotoError(true); setImageFetching(false); }}
255
+ />
256
+ ) : (
257
+ <User className="w-10 h-10 md:w-12 md:h-12 text-accent" />
258
+ )}
259
+
260
+ {!photoLoading && (
261
+ <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2 z-10 backdrop-blur-sm">
262
+ <label className="flex flex-col items-center justify-center cursor-pointer p-2 rounded-full hover:bg-white/20 transition-colors text-white" title="Upload Photo">
263
+ <Camera className="w-5 h-5" />
264
+ <input type="file" accept="image/*" onChange={handlePhotoUpload} className="hidden" />
265
+ </label>
266
+ {photoURL && (
267
+ <button type="button" onClick={handlePhotoDelete} className="p-2 rounded-full hover:bg-red-500/80 text-white transition-colors" title="Delete Photo">
268
+ <Trash2 className="w-5 h-5" />
269
+ </button>
270
+ )}
271
+ </div>
272
+ )}
273
+ </div>
274
+ <div className="flex-1 text-center sm:text-left">
275
+ <h3 className="font-extrabold text-2xl text-foreground mb-1">{name || 'Firebase OS User'}</h3>
276
+ <div className="text-[12px] font-mono uppercase tracking-widest text-accent bg-accent/10 px-2.5 py-1 rounded-md border border-accent/20 inline-block">
277
+ {user?.emailVerified ? 'Verified Account' : 'Unverified'}
278
+ </div>
279
+ <p className="text-[13px] text-foreground/40 mt-2 font-medium">{user?.email}</p>
280
+ </div>
281
+ </div>
282
+
283
+ {/* Profile fields */}
284
+ <div className="space-y-6">
285
+ <h3 className="text-[16px] font-extrabold text-foreground flex items-center gap-2">
286
+ Account Details
287
+ </h3>
288
+
289
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-6">
290
+ <Input name="name" value={name} onChange={(e) => setName(e.target.value)} label="Display Name" placeholder="John Doe" />
291
+ <div className="flex flex-col">
292
+ <Input name="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} label="Email Address" placeholder="name@example.com" required />
293
+ <p className="text-[11px] text-foreground/40 mt-1.5 px-2 font-medium">Changing email sends a verification link to the new address.</p>
294
+ </div>
295
+ </div>
296
+ </div>
297
+
298
+ {/* Password */}
299
+ <div className="pt-6 border-t border-[var(--panel-border)]/50">
300
+ <h3 className="text-[15px] font-bold text-foreground mb-4 flex items-center gap-2">
301
+ Change Password
302
+ </h3>
303
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-6">
304
+ <Input name="password" type="password" label="New Password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Minimum 6 characters" />
305
+ <Input name="confirmPassword" type="password" label="Confirm Password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} placeholder="Repeat password" />
306
+ </div>
307
+ </div>
308
+
309
+ <div className="pt-8 border-t border-[var(--panel-border)]/50 flex justify-start w-full">
310
+ <Button type="submit" isLoading={loading} className="px-12 py-3 md:py-2.5 text-[14px] md:text-[15px] font-bold tracking-wide rounded-xl hover:-translate-y-0.5">
311
+ Save Changes
312
+ </Button>
313
+ </div>
314
+ </div>
315
+ </form>
316
+ </div>
317
+ </main>
318
+ );
319
+ }
@@ -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 { Register } from './Register';
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('Register 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
+ <Register />
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,116 @@
1
+ import { useState } from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { motion } from 'framer-motion';
4
+ import { ArrowRight } from 'lucide-react';
5
+ import { Input } from '../components/Input';
6
+ import { Button } from '../components/Button';
7
+ import { auth } from '../lib/firebase';
8
+ import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
9
+ import { useAuth } from '../lib/AuthContext';
10
+
11
+ export function Register() {
12
+ const [loading, setLoading] = useState(false);
13
+ const [error, setError] = useState<React.ReactNode | null>(null);
14
+ const navigate = useNavigate();
15
+ const { registerUser } = useAuth();
16
+
17
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
18
+ e.preventDefault();
19
+ setLoading(true);
20
+ setError(null);
21
+
22
+ const formData = new FormData(e.currentTarget);
23
+ const email = formData.get('email') as string;
24
+ const password = formData.get('password') as string;
25
+ const firstName = formData.get('firstName') as string;
26
+ const lastName = formData.get('lastName') as string;
27
+
28
+ if (password.length < 6) {
29
+ setError('Password must be at least 6 characters.');
30
+ setLoading(false);
31
+ return;
32
+ }
33
+
34
+ try {
35
+ await registerUser(email, password, firstName, lastName);
36
+ navigate('/verify');
37
+ } catch (err: any) {
38
+ if (err?.code === 'auth/email-already-in-use') {
39
+ setError(
40
+ <span>
41
+ An account with this email already exists.{' '}
42
+ <Link to={`/login?email=${encodeURIComponent(email)}`} className="font-bold underline hover:text-accent transition-colors">Sign in?</Link>
43
+ </span>
44
+ );
45
+ } else if (err?.code === 'auth/weak-password') {
46
+ setError('Password must be at least 6 characters.');
47
+ } else {
48
+ setError(err?.message || 'An error occurred during registration.');
49
+ }
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ };
54
+
55
+ const handleGoogle = async () => {
56
+ setLoading(true);
57
+ try {
58
+ const provider = new GoogleAuthProvider();
59
+ await signInWithPopup(auth, provider);
60
+ // Auth listener will detect login → redirect to dashboard
61
+ } catch (err: unknown) {
62
+ if (err instanceof Error) {
63
+ setError(err.message);
64
+ }
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ };
69
+
70
+ return (
71
+ <div className="flex-1 flex items-center justify-center w-full z-10 px-4 mt-6 md:mt-8 mb-8">
72
+ <motion.div
73
+ initial={{ opacity: 0, y: 10 }}
74
+ animate={{ opacity: 1, y: 0 }}
75
+ transition={{ duration: 0.5 }}
76
+ className="w-full max-w-[500px] p-6 sm:p-8 lg:p-10 rounded-3xl md:rounded-3xl glass-panel relative glow-hover shadow-[0_8px_32px_rgba(0,0,0,0.08)]"
77
+ >
78
+ <div className="absolute -inset-[1px] bg-gradient-to-br from-[var(--primary-glow)] to-transparent opacity-30 blur-[20px] rounded-3xl md:rounded-3xl -z-10" />
79
+
80
+ <div className="mb-6 text-center">
81
+ <h3 className="text-2xl sm:text-3xl font-extrabold mb-2 text-foreground tracking-tight">Create an account</h3>
82
+ <p className="text-[14px] md:text-[15px] font-medium text-foreground/60 tracking-wide">
83
+ Join Firebase OS and start building today.
84
+ </p>
85
+ </div>
86
+
87
+ {error && <div className="p-3 mb-6 text-[14px] font-medium text-red-400 bg-red-500/10 rounded-xl border border-red-500/20">{error}</div>}
88
+
89
+ <form className="space-y-4" onSubmit={handleSubmit}>
90
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
91
+ <Input name="firstName" label="First Name" type="text" placeholder="John" required />
92
+ <Input name="lastName" label="Last Name" type="text" placeholder="Doe" required />
93
+ </div>
94
+ <Input name="email" label="Email" type="email" placeholder="name@example.com" required />
95
+ <Input name="password" label="Password" type="password" placeholder="••••••••" required />
96
+
97
+ <div className="pt-2">
98
+ <Button type="submit" className="w-full py-3 text-[15px] font-bold tracking-wide rounded-xl transition-all hover:-translate-y-0.5" isLoading={loading}>
99
+ Create Account <ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform shrink-0" />
100
+ </Button>
101
+ </div>
102
+ </form>
103
+
104
+ <div className="mt-6 pt-5 border-t border-[var(--panel-border)] space-y-4 text-center">
105
+ <Button variant="secondary" onClick={handleGoogle} className="w-full py-3 text-[15px] font-bold tracking-wide rounded-xl transition-all hover:-translate-y-0.5" disabled={loading}>
106
+ <svg className="w-5 h-5 mr-3" viewBox="0 0 24 24"><path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" /><path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" /><path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" /><path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" /></svg>
107
+ Sign up with Google
108
+ </Button>
109
+ <p className="text-[14px] text-foreground/70 font-medium pt-2 tracking-wide">
110
+ Already have an account? <Link to="/login" className="text-accent hover:underline font-bold ml-1">Sign in</Link>
111
+ </p>
112
+ </div>
113
+ </motion.div>
114
+ </div>
115
+ );
116
+ }