firebase-os 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/FirebaseOS.d.ts +15 -0
  2. package/dist/firebase-os.cjs.js +2 -17
  3. package/dist/firebase-os.es.js +63 -72
  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 +89 -10
  8. package/src/App.css +184 -0
  9. package/src/App.tsx +214 -0
  10. package/src/FirebaseOS.tsx +80 -0
  11. package/src/assets/hero.png +0 -0
  12. package/src/assets/react.svg +1 -0
  13. package/src/assets/vite.svg +1 -0
  14. package/src/components/AdminNotifications.test.tsx +98 -0
  15. package/src/components/AdminNotifications.tsx +194 -0
  16. package/src/components/Button.test.tsx +22 -0
  17. package/src/components/Button.tsx +53 -0
  18. package/src/components/ConfirmModal.test.tsx +98 -0
  19. package/src/components/ConfirmModal.tsx +73 -0
  20. package/src/components/ContactPopup.test.tsx +98 -0
  21. package/src/components/ContactPopup.tsx +437 -0
  22. package/src/components/CustomSelect.test.tsx +47 -0
  23. package/src/components/CustomSelect.tsx +89 -0
  24. package/src/components/DashboardNav.test.tsx +98 -0
  25. package/src/components/DashboardNav.tsx +281 -0
  26. package/src/components/Input.test.tsx +33 -0
  27. package/src/components/Input.tsx +61 -0
  28. package/src/components/JsonEditor.tsx +579 -0
  29. package/src/components/Navbar.test.tsx +98 -0
  30. package/src/components/Navbar.tsx +563 -0
  31. package/src/configs/forms/contactForm.config.ts +15 -0
  32. package/src/configs/forms/index.ts +29 -0
  33. package/src/configs/forms/pubForm.config.ts +11 -0
  34. package/src/configs/forms/supportForm.config.ts +14 -0
  35. package/src/configs/forms/userForm.config.ts +11 -0
  36. package/src/configs/pages/admin.config.ts +29 -0
  37. package/src/configs/pages/contact.config.ts +6 -0
  38. package/src/configs/pages/home.config.ts +18 -0
  39. package/src/configs/pages/mem.config.ts +2 -0
  40. package/src/configs/pages/menuOrders.config.ts +11 -0
  41. package/src/configs/pages/pub.config.ts +11 -0
  42. package/src/configs/pages/shared.config.ts +29 -0
  43. package/src/configs/pages/support.config.ts +7 -0
  44. package/src/configs/pages/tabOrders.config.ts +33 -0
  45. package/src/configs/pages/user.config.ts +29 -0
  46. package/src/configs/theme.config.ts +93 -0
  47. package/src/index.css +403 -0
  48. package/src/index.ts +22 -0
  49. package/src/lib/AuthContext.test.tsx +88 -0
  50. package/src/lib/AuthContext.tsx +191 -0
  51. package/src/lib/ConfigContext.tsx +45 -0
  52. package/src/lib/ThemeContext.tsx +227 -0
  53. package/src/lib/firebase.ts +91 -0
  54. package/src/main.tsx +22 -0
  55. package/src/microcomponents/AdminExampleContent.tsx +44 -0
  56. package/src/microcomponents/PrivateExampleContent.tsx +39 -0
  57. package/src/microcomponents/Public.tsx +126 -0
  58. package/src/microcomponents/SharedExampleContent.tsx +53 -0
  59. package/src/pages/Dashboard.test.tsx +98 -0
  60. package/src/pages/Dashboard.tsx +60 -0
  61. package/src/pages/DynamicPage.tsx +237 -0
  62. package/src/pages/FormsAdmin.test.tsx +98 -0
  63. package/src/pages/FormsAdmin.tsx +459 -0
  64. package/src/pages/Home.test.tsx +98 -0
  65. package/src/pages/Home.tsx +144 -0
  66. package/src/pages/Login.test.tsx +98 -0
  67. package/src/pages/Login.tsx +108 -0
  68. package/src/pages/PagesAdmin.test.tsx +98 -0
  69. package/src/pages/PagesAdmin.tsx +1022 -0
  70. package/src/pages/Profile.test.tsx +98 -0
  71. package/src/pages/Profile.tsx +319 -0
  72. package/src/pages/Register.test.tsx +98 -0
  73. package/src/pages/Register.tsx +116 -0
  74. package/src/pages/Requests.test.tsx +95 -0
  75. package/src/pages/Requests.tsx +422 -0
  76. package/src/pages/ResetPassword.test.tsx +98 -0
  77. package/src/pages/ResetPassword.tsx +92 -0
  78. package/src/pages/Settings.test.tsx +98 -0
  79. package/src/pages/Settings.tsx +393 -0
  80. package/src/pages/Setup.tsx +401 -0
  81. package/src/pages/StorageAdmin.test.tsx +150 -0
  82. package/src/pages/StorageAdmin.tsx +769 -0
  83. package/src/pages/Submissions.test.tsx +95 -0
  84. package/src/pages/Submissions.tsx +372 -0
  85. package/src/pages/Templates.test.tsx +98 -0
  86. package/src/pages/Templates.tsx +103 -0
  87. package/src/pages/ThemeAdmin.test.tsx +144 -0
  88. package/src/pages/ThemeAdmin.tsx +1000 -0
  89. package/src/pages/Users.test.tsx +95 -0
  90. package/src/pages/Users.tsx +334 -0
  91. package/src/pages/Verify.test.tsx +98 -0
  92. package/src/pages/Verify.tsx +95 -0
  93. package/src/prompts/index.ts +13 -0
  94. package/src/prompts/pages/publicPage.ts +44 -0
  95. package/src/prompts/sharedConstants.ts +12 -0
  96. package/src/prompts/tabs/board/adminboard.ts +32 -0
  97. package/src/prompts/tabs/board/privateboard.ts +36 -0
  98. package/src/prompts/tabs/board/publicboard.ts +36 -0
  99. package/src/prompts/tabs/calendar/admincalendar.ts +32 -0
  100. package/src/prompts/tabs/calendar/privatecalendar.ts +36 -0
  101. package/src/prompts/tabs/calendar/publiccalendar.ts +36 -0
  102. package/src/prompts/tabs/crud/admin.ts +54 -0
  103. package/src/prompts/tabs/crud/private.ts +55 -0
  104. package/src/prompts/tabs/crud/shared.ts +53 -0
  105. package/src/prompts/tabs/table/admintable.ts +32 -0
  106. package/src/prompts/tabs/table/privatetable.ts +36 -0
  107. package/src/prompts/tabs/table/publictable.ts +36 -0
  108. package/src/setupTests.ts +1 -0
  109. package/src/templates/AdminPageTemplate.tsx +678 -0
  110. package/src/templates/PrivatePageTemplate.tsx +594 -0
  111. package/src/templates/PublicPageTemplate.tsx +92 -0
  112. package/src/templates/SharedPageTemplate.tsx +551 -0
  113. package/src/templates/TemplateBoard.test.tsx +106 -0
  114. package/src/templates/TemplateBoard.tsx +642 -0
  115. package/src/templates/TemplateCalendar.test.tsx +106 -0
  116. package/src/templates/TemplateCalendar.tsx +848 -0
  117. package/src/templates/TemplateConfirmation.test.tsx +106 -0
  118. package/src/templates/TemplateConfirmation.tsx +145 -0
  119. package/src/templates/TemplateInlineForm.test.tsx +106 -0
  120. package/src/templates/TemplateInlineForm.tsx +129 -0
  121. package/src/templates/TemplatePopupForm.test.tsx +106 -0
  122. package/src/templates/TemplatePopupForm.tsx +174 -0
  123. package/src/templates/TemplateTable.test.tsx +106 -0
  124. package/src/templates/TemplateTable.tsx +675 -0
@@ -0,0 +1,769 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useParams, useNavigate } from 'react-router-dom';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { db, storage } from '../lib/firebase';
6
+ import { collection, addDoc, getDocs, query, serverTimestamp, deleteDoc, doc, orderBy, updateDoc, where, onSnapshot } from 'firebase/firestore';
7
+ import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage';
8
+ import { useAuth } from '../lib/AuthContext';
9
+ import { Button } from '../components/Button';
10
+ import { ConfirmModal } from '../components/ConfirmModal';
11
+ import { DashboardNav } from '../components/DashboardNav';
12
+ import { UploadCloud, Trash2, X, File as FileIcon, Image, FileText, Video, Archive, Code, Check, Eye, Download, Link, Edit2, FolderOpen, Plus, Globe, Users as UsersIcon, Lock, Shield, Loader2, User as UserIcon, Database } from 'lucide-react';
13
+
14
+ interface StorageRecord {
15
+ _id: string;
16
+ fileName: string;
17
+ fileType: string;
18
+ fileSize: number;
19
+ downloadURL: string;
20
+ uid: string;
21
+ uploaderEmail?: string;
22
+ uploaderRole?: string;
23
+ accessPrefix: 'pub_' | 'mem_' | 'user_' | 'admin_' | 'users_';
24
+ createdAt: any;
25
+ }
26
+
27
+ export function StorageAdmin() {
28
+ const { user, userRole } = useAuth();
29
+ const [records, setRecords] = useState<StorageRecord[]>([]);
30
+ const [loading, setLoading] = useState(false);
31
+
32
+ const navigate = useNavigate();
33
+ const { '*': subPath } = useParams();
34
+
35
+ const pathParts = subPath ? subPath.split('/').filter(Boolean) : [];
36
+ const folderSlug = pathParts[0] || 'private';
37
+
38
+ let targetUserId = null;
39
+ let fileIdFromUrl = null;
40
+
41
+ if (folderSlug === 'users') {
42
+ targetUserId = pathParts[1] || null;
43
+ fileIdFromUrl = pathParts[2] || null;
44
+ } else {
45
+ fileIdFromUrl = pathParts[1] || null;
46
+ }
47
+
48
+ const folderNameToPrefix: Record<string, string> = {
49
+ 'public': 'pub_',
50
+ 'shared': 'mem_',
51
+ 'private': 'user_',
52
+ 'admin': 'admin_',
53
+ 'users': 'users_'
54
+ };
55
+
56
+ const prefixToFolderName: Record<string, string> = {
57
+ 'pub_': 'public',
58
+ 'mem_': 'shared',
59
+ 'user_': 'private',
60
+ 'admin_': 'admin',
61
+ 'users_': 'users'
62
+ };
63
+
64
+ const activeFolder = (folderNameToPrefix[folderSlug as string] || 'user_') as 'pub_' | 'mem_' | 'user_' | 'admin_' | 'users_';
65
+
66
+ const setActiveFolder = (newPrefix: any) => {
67
+ navigate(`/drive/${prefixToFolderName[newPrefix]}`);
68
+ };
69
+
70
+ const [allUsers, setAllUsers] = useState<any[]>([]);
71
+
72
+ useEffect(() => {
73
+ if (activeFolder === 'users_' && userRole === 'admin' && allUsers.length === 0) {
74
+ getDocs(collection(db, 'user_profiles')).then(snap => {
75
+ setAllUsers(snap.docs.map(d => ({ id: d.id, ...d.data() })));
76
+ });
77
+ }
78
+ }, [activeFolder, userRole, allUsers.length]);
79
+
80
+ // Staged Upload State
81
+ const [isUploading, setIsUploading] = useState(false);
82
+ const [stagedFile, setStagedFile] = useState<File | null>(null);
83
+ const [stagedName, setStagedName] = useState('');
84
+ const [stagedAccess, setStagedAccess] = useState<'pub_' | 'mem_' | 'user_' | 'admin_' | 'users_'>('user_');
85
+ const [uploadModal, setUploadModal] = useState(false);
86
+ const [showLocationDropdown, setShowLocationDropdown] = useState(false);
87
+ const [isDragging, setIsDragging] = useState(false);
88
+ const fileInputRef = useRef<HTMLInputElement>(null);
89
+
90
+ const handleDragOver = (e: React.DragEvent) => {
91
+ e.preventDefault();
92
+ e.stopPropagation();
93
+ setIsDragging(true);
94
+ };
95
+
96
+ const handleDragLeave = (e: React.DragEvent) => {
97
+ e.preventDefault();
98
+ e.stopPropagation();
99
+ setIsDragging(false);
100
+ };
101
+
102
+ const onGlobalDrop = (e: React.DragEvent) => {
103
+ e.preventDefault();
104
+ e.stopPropagation();
105
+ setIsDragging(false);
106
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
107
+ const file = e.dataTransfer.files[0];
108
+ setStagedFile(file);
109
+ setStagedName(file.name);
110
+ setStagedAccess(activeFolder);
111
+ setUploadModal(true);
112
+ }
113
+ };
114
+
115
+ // File Preview & Actions
116
+ const previewFile = fileIdFromUrl ? records.find(r => r._id === fileIdFromUrl) || null : null;
117
+
118
+ const setPreviewFile = (file: StorageRecord | null) => {
119
+ setPreviewLoading(true);
120
+ if (file) {
121
+ if (activeFolder === 'users_' && targetUserId) {
122
+ navigate(`/drive/users/${targetUserId}/${file._id}`);
123
+ } else {
124
+ navigate(`/drive/${prefixToFolderName[activeFolder]}/${file._id}`);
125
+ }
126
+ } else {
127
+ if (activeFolder === 'users_' && targetUserId) {
128
+ navigate(`/drive/users/${targetUserId}`);
129
+ } else {
130
+ navigate(`/drive/${prefixToFolderName[activeFolder]}`);
131
+ }
132
+ }
133
+ };
134
+
135
+ const [renameFile, setRenameFile] = useState<{ id: string, name: string, prefix: string } | null>(null);
136
+ const [isRenaming, setIsRenaming] = useState(false);
137
+ const [confirmDelete, setConfirmDelete] = useState<{ id: string, url: string, name: string, prefix: string } | null>(null);
138
+ const [isDeleting, setIsDeleting] = useState(false);
139
+ const [copiedLink, setCopiedLink] = useState<string | null>(null);
140
+ const [previewLoading, setPreviewLoading] = useState(true);
141
+ const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>({});
142
+ const [showNotFound, setShowNotFound] = useState(false);
143
+
144
+ useEffect(() => {
145
+ if (previewFile || confirmDelete || renameFile || uploadModal || showNotFound) {
146
+ document.body.style.overflow = 'hidden';
147
+ } else {
148
+ document.body.style.overflow = '';
149
+ }
150
+ return () => { document.body.style.overflow = ''; };
151
+ }, [previewFile, confirmDelete, renameFile, uploadModal, showNotFound]);
152
+
153
+ useEffect(() => {
154
+ if (!loading && fileIdFromUrl && !records.some(r => r._id === fileIdFromUrl)) {
155
+ setShowNotFound(true);
156
+ const timer = setTimeout(() => {
157
+ setShowNotFound(false);
158
+ if (activeFolder === 'users_' && targetUserId) {
159
+ navigate(`/drive/users/${targetUserId}`);
160
+ } else {
161
+ navigate(`/drive/${prefixToFolderName[activeFolder]}`);
162
+ }
163
+ }, 2500);
164
+ return () => clearTimeout(timer);
165
+ } else {
166
+ setShowNotFound(false);
167
+ }
168
+ }, [loading, fileIdFromUrl, records, activeFolder, navigate, targetUserId]);
169
+
170
+ useEffect(() => {
171
+ if (!user) return;
172
+ setLoading(true);
173
+
174
+ const prefixes = userRole === 'admin'
175
+ ? ['pub_', 'mem_', 'user_', 'admin_']
176
+ : ['pub_', 'mem_', 'user_'];
177
+
178
+ const unsubs = prefixes.map(pfx => {
179
+ let q;
180
+ if (pfx === 'user_' && userRole !== 'admin') {
181
+ q = query(collection(db, `${pfx}files`), where('uid', '==', user.uid));
182
+ } else {
183
+ q = query(collection(db, `${pfx}files`), orderBy('createdAt', 'desc'));
184
+ }
185
+
186
+ return onSnapshot(q, (snap) => {
187
+ const newData = (snap.docs || []).map(d => ({ _id: d.id, accessPrefix: pfx as any, ...d.data() }) as StorageRecord);
188
+
189
+ setRecords(prev => {
190
+ // Filter out existing records for this prefix and add new ones
191
+ const otherPrefixes = prev.filter(r => r.accessPrefix !== pfx);
192
+ const combined = [...otherPrefixes, ...newData];
193
+ return combined.sort((a, b) => {
194
+ const d1 = a.createdAt?.toMillis ? a.createdAt.toMillis() : 0;
195
+ const d2 = b.createdAt?.toMillis ? b.createdAt.toMillis() : 0;
196
+ return d2 - d1;
197
+ });
198
+ });
199
+ setLoading(false);
200
+ }, (err) => {
201
+ console.warn(`Error listening to ${pfx}files:`, err);
202
+ });
203
+ });
204
+
205
+ return () => unsubs.forEach(unsub => unsub());
206
+ }, [user?.uid, userRole]);
207
+
208
+ const handleSelectFile = (e: React.ChangeEvent<HTMLInputElement>) => {
209
+ if (e.target.files && e.target.files[0]) {
210
+ const file = e.target.files[0];
211
+ setStagedFile(file);
212
+ setStagedName(file.name);
213
+ }
214
+ };
215
+
216
+ const handleDrop = (e: React.DragEvent) => {
217
+ e.preventDefault();
218
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
219
+ const file = e.dataTransfer.files[0];
220
+ setStagedFile(file);
221
+ setStagedName(file.name);
222
+ }
223
+ };
224
+
225
+ const handleSaveUpload = async (e: React.FormEvent) => {
226
+ e.preventDefault();
227
+ if (!stagedFile || !stagedName) return;
228
+
229
+ setIsUploading(true);
230
+ try {
231
+ const uniqueName = `${Date.now()}_${stagedFile.name.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
232
+ const storagePath = `${stagedAccess}files/${uniqueName}`;
233
+
234
+ const fileRef = ref(storage, storagePath);
235
+ await uploadBytes(fileRef, stagedFile, {
236
+ customMetadata: { ownerId: user!.uid }
237
+ });
238
+
239
+ const downloadURL = await getDownloadURL(fileRef);
240
+
241
+ await addDoc(collection(db, `${stagedAccess}files`), {
242
+ fileName: stagedName,
243
+ fileType: stagedFile.type || 'unknown',
244
+ fileSize: stagedFile.size,
245
+ downloadURL,
246
+ uid: user!.uid,
247
+ uploaderEmail: user!.email || 'user',
248
+ uploaderRole: userRole || 'user',
249
+ createdAt: serverTimestamp()
250
+ });
251
+
252
+ setUploadModal(false);
253
+ setStagedFile(null);
254
+ setStagedName('');
255
+ setActiveFolder(stagedAccess);
256
+ } catch (err: any) {
257
+ console.error(err);
258
+ alert(`Upload failed: ${err.message}`);
259
+ }
260
+ setIsUploading(false);
261
+ };
262
+
263
+ const executeDelete = async () => {
264
+ if (!confirmDelete) return;
265
+ setIsDeleting(true);
266
+ try {
267
+ try {
268
+ const fileRef = ref(storage, confirmDelete.url);
269
+ await deleteObject(fileRef);
270
+ } catch (err) {
271
+ console.warn("Storage item missing:", err);
272
+ }
273
+ await deleteDoc(doc(db, `${confirmDelete.prefix}files`, confirmDelete.id));
274
+ setRecords(prev => prev.filter(r => r._id !== confirmDelete.id));
275
+ setConfirmDelete(null);
276
+ } catch (e: any) {
277
+ alert(e.message);
278
+ }
279
+ setIsDeleting(false);
280
+ };
281
+
282
+ const executeRename = async () => {
283
+ if (!renameFile || !renameFile.name.trim()) { setRenameFile(null); return; }
284
+ const original = records.find(r => r._id === renameFile.id);
285
+ if (original && original.fileName === renameFile.name.trim()) { setRenameFile(null); return; }
286
+ setIsRenaming(true);
287
+ try {
288
+ await updateDoc(doc(db, `${renameFile.prefix}files`, renameFile.id), {
289
+ fileName: renameFile.name.trim()
290
+ });
291
+ setRecords(prev => prev.map(r => r._id === renameFile.id ? { ...r, fileName: renameFile.name.trim() } : r));
292
+ setRenameFile(null);
293
+ } catch (err: any) {
294
+ console.error('Rename failed:', err);
295
+ }
296
+ setIsRenaming(false);
297
+ };
298
+
299
+ const handleCopyLink = (url: string, id: string) => {
300
+ navigator.clipboard.writeText(url);
301
+ setCopiedLink(id);
302
+ setTimeout(() => setCopiedLink(null), 2000);
303
+ };
304
+
305
+ const formatSize = (bytes: number) => {
306
+ if (bytes === 0) return '0 B';
307
+ const k = 1024;
308
+ const sizes = ['B', 'KB', 'MB', 'GB'];
309
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
310
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
311
+ };
312
+
313
+ const canEditFile = (rec: StorageRecord) => {
314
+ if (!user) return false;
315
+
316
+ // Shared folder (mem_): owner or admin (but admins can't delete other admins)
317
+ if (rec.accessPrefix === 'mem_') {
318
+ if (rec.uid === user.uid) return true;
319
+ if (userRole === 'admin') return rec.uploaderRole !== 'admin';
320
+ return false;
321
+ }
322
+
323
+ // Admin folder (admin_): strictly creator only
324
+ if (rec.accessPrefix === 'admin_') return rec.uid === user.uid;
325
+
326
+ // Public folder (pub_): strictly admins only
327
+ if (rec.accessPrefix === 'pub_') return userRole === 'admin';
328
+
329
+ // Private (user_): owner or admin (if viewing others)
330
+ return rec.uid === user.uid || userRole === 'admin';
331
+ };
332
+
333
+ const renderIcon = (type: string, url: string, big = false) => {
334
+ const sizeClasses = big ? "w-16 h-16" : "w-10 h-10";
335
+ const iconSize = big ? "w-8 h-8" : "w-5 h-5";
336
+
337
+ if (type.startsWith('image/')) {
338
+ if (big) return <img src={url} alt="preview" className="w-full h-full object-contain rounded-lg" />;
339
+ return <div className={`${sizeClasses} bg-cover bg-center rounded-lg`} style={{ backgroundImage: `url(${url})` }} />;
340
+ }
341
+ if (type.includes('video') || type.includes('audio')) return <div className={`${sizeClasses} flex items-center justify-center bg-purple-500/10 text-purple-500 rounded-lg`}><Video className={iconSize} /></div>;
342
+ if (type.includes('pdf') || type.includes('word') || type.includes('text')) return <div className={`${sizeClasses} flex items-center justify-center bg-blue-500/10 text-blue-500 rounded-lg`}><FileText className={iconSize} /></div>;
343
+ if (type.includes('zip') || type.includes('rar') || type.includes('archive')) return <div className={`${sizeClasses} flex items-center justify-center bg-yellow-500/10 text-yellow-500 rounded-lg`}><Archive className={iconSize} /></div>;
344
+ if (type.includes('html') || type.includes('csv') || type.includes('json')) return <div className={`${sizeClasses} flex items-center justify-center bg-emerald-500/10 text-emerald-500 rounded-lg`}><Code className={iconSize} /></div>;
345
+ return <div className={`${sizeClasses} flex items-center justify-center bg-foreground/5 text-foreground/40 rounded-lg`}><FileIcon className={iconSize} /></div>;
346
+ };
347
+
348
+ const folders = [
349
+ { id: 'user_', label: 'Private', icon: Lock, description: "This is a private folder availbale to all users in your app. Each user can see their own files. Admins can see all everyone's files here." },
350
+ { id: 'mem_', label: 'Shared', icon: UsersIcon, description: "This is a shared folder that all signed-in users can see. Every user can upload files here and view other member's files. However, editing and deletion are strictly restricted to the original creator of the file." },
351
+ { id: 'pub_', label: 'Public', icon: Globe, description: "This admin-only folder is for assets like logos, banners, or favicons. Files here are view-only for everyone; only admins can upload or delete them." },
352
+ { id: 'admin_', label: 'Admin Shared', icon: Shield, description: "This is a shared folder for app administrators. Admins can view everything here, but they can only edit or delete files that they themselves uploaded." },
353
+ { id: 'users_', label: 'User Uploads', icon: Database, description: "View all underlying private user uploads securely. Select a user to explore their files." }
354
+ ];
355
+
356
+ const visibleFolders = folders.filter(f => {
357
+ if (userRole === 'admin') return true;
358
+ return f.id === 'user_' || f.id === 'mem_';
359
+ });
360
+
361
+ let activeRecords = [];
362
+ if (activeFolder === 'users_') {
363
+ activeRecords = records.filter(r => r.accessPrefix === 'user_' && r.uid === targetUserId);
364
+ } else if (activeFolder === 'user_') {
365
+ activeRecords = records.filter(r => r.accessPrefix === 'user_' && r.uid === user?.uid);
366
+ } else {
367
+ activeRecords = records.filter(r => r.accessPrefix === activeFolder);
368
+ }
369
+
370
+ const currentFolderObj = visibleFolders.find(f => f.id === activeFolder);
371
+
372
+ return (
373
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 flex flex-col pt-12 min-h-screen relative overflow-visible">
374
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-6 flex items-center justify-between">
375
+ <div>
376
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">Drive</h1>
377
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
378
+ <span className="w-8 h-[1px] bg-foreground/10" />
379
+ /drive
380
+ </div>
381
+ </div>
382
+ </motion.div>
383
+
384
+ <div
385
+ className="flex flex-col gap-6 flex-1 pb-16"
386
+ onDragOver={handleDragOver}
387
+ onDragLeave={handleDragLeave}
388
+ onDrop={onGlobalDrop}
389
+ >
390
+ <DashboardNav />
391
+
392
+ <div className="flex flex-col md:flex-row glass-panel border border-[var(--panel-border)] rounded-3xl overflow-hidden flex-1 shadow-2xl min-h-[600px] bg-background">
393
+
394
+ {/* Sidebar Navigation */}
395
+ <div className="md:w-[280px] border-b md:border-b-0 md:border-r border-[var(--panel-border)] bg-foreground/[0.01] p-4 flex flex-col shrink-0 overflow-y-auto hide-scrollbar">
396
+ <div className="text-[11px] font-extrabold text-foreground/40 uppercase tracking-widest pl-4 mb-4 mt-2">Locations</div>
397
+ <div className="flex flex-col gap-2">
398
+ {visibleFolders.map(folder => (
399
+ <button
400
+ key={folder.id}
401
+ onClick={() => setActiveFolder(folder.id as any)}
402
+ className={`w-full flex items-center gap-3 px-4 py-3.5 rounded-2xl transition-all font-bold text-[14px] text-left ${activeFolder === folder.id
403
+ ? 'bg-accent/15 text-accent shadow-sm border border-accent/20'
404
+ : 'text-foreground/70 hover:bg-foreground/5 hover:text-foreground border border-transparent'
405
+ }`}
406
+ >
407
+ <folder.icon className={`w-5 h-5 ${activeFolder === folder.id ? 'text-accent' : 'text-foreground/40'}`} />
408
+ {folder.label}
409
+ </button>
410
+ ))}
411
+ </div>
412
+ </div>
413
+
414
+ {/* Active Folder Viewer */}
415
+ <div className="flex-1 flex flex-col relative w-full h-full min-w-0">
416
+
417
+ {/* Folder Header & Upload Action */}
418
+ <div className="p-6 md:p-8 flex items-start justify-between gap-6 shrink-0 border-b border-[var(--panel-border)]/50 bg-background/50">
419
+ <div className="flex-1 max-w-xl">
420
+ <h2 className="text-2xl font-extrabold text-foreground tracking-tight flex items-center gap-3">
421
+ {currentFolderObj?.label}
422
+ </h2>
423
+ <p className="text-[13px] mt-3 text-foreground/60 leading-relaxed font-medium">
424
+ {currentFolderObj?.description}
425
+ </p>
426
+ </div>
427
+
428
+ {activeFolder !== 'users_' && (
429
+ <Button onClick={() => { setStagedAccess(activeFolder); setUploadModal(true); }} className="flex items-center gap-2 px-5 py-2.5 rounded-xl text-[13px] font-bold shadow-md hover:shadow-lg transition-all glow-hover border-transparent !border-0 outline-none ring-0">
430
+ <Plus className="w-4 h-4" /> Add File Here
431
+ </Button>
432
+ )}
433
+ </div>
434
+
435
+ {/* Grid content */}
436
+ <div className="p-6 md:p-8 flex-1 overflow-y-auto relative min-h-[300px]">
437
+ {loading && (
438
+ <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/50 backdrop-blur-sm rounded-b-[2rem]">
439
+ <Loader2 className="w-8 h-8 text-accent animate-spin" />
440
+ </div>
441
+ )}
442
+
443
+ {activeFolder === 'users_' && !targetUserId ? (
444
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-20">
445
+ {allUsers.map((u: any) => (
446
+ <div
447
+ key={u.id}
448
+ onClick={() => navigate(`/drive/users/${u.id}`)}
449
+ className="flex items-center gap-4 bg-background border border-[var(--panel-border)] p-5 rounded-2xl cursor-pointer hover:border-accent/50 hover:shadow-md transition-all group"
450
+ >
451
+ <div className="w-12 h-12 rounded-full bg-foreground/5 flex items-center justify-center shrink-0 border border-[var(--panel-border)]">
452
+ {u.photoURL ? <img src={u.photoURL} alt="avatar" className="w-full h-full rounded-full object-cover" /> : <UserIcon className="w-5 h-5 text-foreground/40" />}
453
+ </div>
454
+ <div className="flex flex-col min-w-0">
455
+ <span className="font-bold text-foreground truncate group-hover:text-accent transition-colors text-[14px]">{u.firstName} {u.lastName}</span>
456
+ <span className="text-[12px] font-medium text-foreground/50 truncate mt-0.5">{u.email}</span>
457
+ </div>
458
+ </div>
459
+ ))}
460
+ {allUsers.length === 0 && (
461
+ <div className="col-span-full py-12 flex flex-col items-center justify-center text-center">
462
+ <UserIcon className="w-12 h-12 text-foreground/10 mb-4" />
463
+ <p className="text-[15px] font-bold text-foreground/50">No users found</p>
464
+ </div>
465
+ )}
466
+ </div>
467
+ ) : activeRecords.length > 0 ? (
468
+ <div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pb-20">
469
+ {activeRecords.map(rec => (
470
+ <div key={rec._id} className="flex flex-col bg-background border border-[var(--panel-border)] rounded-2xl hover:border-accent/40 transition-all group overflow-hidden shadow-sm hover:shadow-md h-[190px] relative">
471
+ {/* Context Actions */}
472
+ <div className="absolute top-2 right-2 flex flex-col gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10 bg-background/90 backdrop-blur-md rounded-lg p-1 border border-[var(--panel-border)] shadow-xl">
473
+ <a href={rec.downloadURL} download target="_blank" rel="noreferrer" className="p-1.5 hover:bg-foreground/5 rounded-md text-foreground/60 hover:text-blue-500 transition-colors" title="Download"><Download className="w-4 h-4" /></a>
474
+ <button onClick={(e) => { e.stopPropagation(); handleCopyLink(rec.accessPrefix === 'pub_' ? rec.downloadURL : `${window.location.origin}/drive/${prefixToFolderName[rec.accessPrefix]}/${rec._id}`, rec._id); }} className="p-1.5 hover:bg-foreground/5 rounded-md text-foreground/60 hover:text-emerald-500 transition-colors" title="Copy Link">
475
+ {copiedLink === rec._id ? <Check className="w-4 h-4 text-emerald-500" /> : <Link className="w-4 h-4" />}
476
+ </button>
477
+ {canEditFile(rec) && (
478
+ <>
479
+ <button onClick={(e) => { e.stopPropagation(); setRenameFile({ id: rec._id, name: rec.fileName, prefix: rec.accessPrefix }); }} className="p-1.5 hover:bg-foreground/5 rounded-md text-foreground/60 hover:text-purple-500 transition-colors" title="Rename"><Edit2 className="w-4 h-4" /></button>
480
+ <button onClick={(e) => { e.stopPropagation(); setConfirmDelete({ id: rec._id, url: rec.downloadURL, name: rec.fileName, prefix: rec.accessPrefix }); }} className="p-1.5 hover:bg-red-500/10 rounded-md text-foreground/60 hover:text-red-500 transition-colors" title="Delete"><Trash2 className="w-4 h-4" /></button>
481
+ </>
482
+ )}
483
+ </div>
484
+
485
+ <div
486
+ className="h-[120px] w-full border-b border-[var(--panel-border)]/50 bg-foreground/[0.02] flex items-center justify-center overflow-hidden relative cursor-pointer rounded-t-[calc(1rem-1px)]"
487
+ onClick={() => setPreviewFile(rec)}
488
+ >
489
+ {rec.fileType.startsWith('image/') ? (
490
+ <>
491
+ {loadingImages[rec._id] !== false && (
492
+ <div className="absolute inset-0 flex items-center justify-center bg-accent/5 backdrop-blur-sm z-0">
493
+ <Loader2 className="w-5 h-5 text-accent animate-spin" />
494
+ </div>
495
+ )}
496
+ <img
497
+ src={rec.downloadURL}
498
+ alt=""
499
+ className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 group-hover:scale-105 ${loadingImages[rec._id] === false ? 'opacity-100' : 'opacity-0'}`}
500
+ onLoad={() => setLoadingImages(prev => ({ ...prev, [rec._id]: false }))}
501
+ onError={() => setLoadingImages(prev => ({ ...prev, [rec._id]: false }))}
502
+ />
503
+ </>
504
+ ) : (
505
+ renderIcon(rec.fileType, rec.downloadURL, true)
506
+ )}
507
+ </div>
508
+ <div className="p-3 flex flex-col justify-center flex-1 min-w-0" onClick={() => !renameFile && setPreviewFile(rec)}>
509
+ {renameFile?.id === rec._id ? (
510
+ <input
511
+ autoFocus
512
+ type="text"
513
+ value={renameFile.name}
514
+ onChange={e => setRenameFile({ ...renameFile, name: e.target.value })}
515
+ onBlur={() => executeRename()}
516
+ onKeyDown={e => { if (e.key === 'Enter') executeRename(); if (e.key === 'Escape') setRenameFile(null); }}
517
+ onClick={e => e.stopPropagation()}
518
+ className="text-[12.5px] font-bold text-foreground bg-transparent border-b border-accent/30 outline-none pb-0.5 w-full"
519
+ />
520
+ ) : (
521
+ <span className="text-[12.5px] font-bold text-foreground truncate select-none group-hover:text-accent transition-colors" title={rec.fileName}>{rec.fileName}</span>
522
+ )}
523
+ <span className="text-[11px] text-foreground/40 font-bold uppercase tracking-wider mt-1 select-none">{formatSize(rec.fileSize)}</span>
524
+ </div>
525
+ </div>
526
+ ))}
527
+ </div>
528
+ ) : (
529
+ !loading && (
530
+ <div
531
+ onDragOver={e => e.preventDefault()}
532
+ onDrop={(e) => {
533
+ e.preventDefault();
534
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
535
+ const file = e.dataTransfer.files[0];
536
+ setStagedFile(file);
537
+ setStagedName(file.name);
538
+ setStagedAccess(activeFolder);
539
+ setUploadModal(true);
540
+ }
541
+ }}
542
+ onClick={() => { setStagedAccess(activeFolder); setUploadModal(true); }}
543
+ className="absolute inset-0 m-6 flex flex-col items-center justify-center text-foreground/40 transition-colors cursor-pointer group"
544
+ >
545
+ <p className="text-[20px] font-bold text-foreground/50 group-hover:text-foreground/70 transition-colors">Folder is empty</p>
546
+ <p className="text-[13px] font-medium mt-1 text-foreground/40 group-hover:text-foreground/60 transition-colors">Click here or drag and drop a file to upload</p>
547
+ </div>
548
+ )
549
+ )}
550
+ </div>
551
+ </div>
552
+ </div>
553
+ </div>
554
+
555
+ <AnimatePresence>
556
+ {uploadModal && (
557
+ <div className="fixed inset-0 z-[120] flex items-center justify-center p-4">
558
+ <motion.div onClick={() => !isUploading && setUploadModal(false)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 bg-background/80 backdrop-blur-xl cursor-pointer" />
559
+ <motion.div initial={{ opacity: 0, scale: 0.95, y: 10 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 10 }} className="w-full max-w-[480px] border-[var(--panel-border)] border rounded-3xl relative z-10 glass-panel shadow-2xl flex flex-col bg-background">
560
+
561
+ <div className="flex items-center justify-between p-6 border-b border-[var(--panel-border)]/50 relative z-20">
562
+ <div className="relative">
563
+ <button onClick={() => setShowLocationDropdown(!showLocationDropdown)} className="flex items-center gap-2 group outline-none">
564
+ <span className="text-[16px] font-extrabold text-foreground tracking-tight">Upload to</span>
565
+ <span className="bg-accent/10 group-hover:bg-accent/20 text-accent px-2 py-1 rounded-md text-[13px] font-bold transition-colors flex items-center gap-1 shadow-sm">
566
+ {visibleFolders.find(f => f.id === stagedAccess)?.label}
567
+ </span>
568
+ </button>
569
+
570
+ <AnimatePresence>
571
+ {showLocationDropdown && (
572
+ <React.Fragment>
573
+ <div className="fixed inset-0 z-[90]" onClick={() => setShowLocationDropdown(false)} />
574
+ <motion.div initial={{ opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -5 }} className="absolute top-full left-0 mt-3 w-48 bg-background border border-[var(--panel-border)] rounded-xl shadow-2xl py-1 overflow-hidden z-[100]">
575
+ {visibleFolders.map(f => (
576
+ <button
577
+ key={f.id}
578
+ type="button"
579
+ onClick={() => { setStagedAccess(f.id as any); setShowLocationDropdown(false); }}
580
+ className="w-full text-left px-4 py-3 hover:bg-foreground/5 text-[13px] font-bold text-foreground flex items-center gap-3 transition-colors outline-none"
581
+ >
582
+ <f.icon className="w-4 h-4 text-foreground/50" />
583
+ {f.label}
584
+ </button>
585
+ ))}
586
+ </motion.div>
587
+ </React.Fragment>
588
+ )}
589
+ </AnimatePresence>
590
+ </div>
591
+ <button onClick={() => !isUploading && setUploadModal(false)} className="text-foreground/40 hover:text-foreground transition-colors"><X className="w-5 h-5" /></button>
592
+ </div>
593
+
594
+ {!stagedFile ? (
595
+ <div
596
+ onDragOver={e => e.preventDefault()}
597
+ onDrop={handleDrop}
598
+ className="p-12 flex flex-col items-center justify-center rounded-b-[2rem]"
599
+ >
600
+ <div className="w-20 h-20 rounded-full bg-foreground/5 flex items-center justify-center mb-5 border-2 border-dashed border-[var(--panel-border)]">
601
+ <UploadCloud className="w-8 h-8 text-foreground/40" />
602
+ </div>
603
+ <p className="text-[15px] font-bold text-foreground mb-1">Drag and drop file here</p>
604
+ <p className="text-[13px] text-foreground/50 font-medium mb-8">or click to browse local files</p>
605
+ <input type="file" ref={fileInputRef} onChange={handleSelectFile} className="hidden" />
606
+ <Button onClick={() => fileInputRef.current?.click()} className="px-8 py-3 rounded-full font-bold shadow-md bg-accent text-white hover:bg-accent/90 transition-colors">Browse Files</Button>
607
+ </div>
608
+ ) : (
609
+ <form onSubmit={handleSaveUpload} className="p-6 flex flex-col gap-6 relative z-10 rounded-b-[2rem]">
610
+ <div className="flex items-center gap-4 bg-foreground/[0.03] p-4 rounded-2xl border border-[var(--panel-border)] shadow-sm">
611
+ {renderIcon(stagedFile.type, URL.createObjectURL(stagedFile))}
612
+ <div className="flex flex-col flex-1 min-w-0 pr-4">
613
+ <span className="text-[13px] font-bold text-foreground truncate">{stagedFile.name}</span>
614
+ <span className="text-[11px] text-foreground/50 font-bold uppercase tracking-wider mt-1">{formatSize(stagedFile.size)}</span>
615
+ </div>
616
+ <button type="button" onClick={() => !isUploading && setStagedFile(null)} className="w-8 h-8 flex items-center justify-center text-foreground/40 hover:text-red-500 rounded-xl hover:bg-red-500/10 transition-colors shrink-0"><Trash2 className="w-4 h-4" /></button>
617
+ </div>
618
+
619
+ <div className="flex flex-col gap-2">
620
+ <label className="text-[11px] font-bold text-foreground/50 uppercase tracking-widest pl-1">Edit File Name</label>
621
+ <input
622
+ type="text"
623
+ required
624
+ value={stagedName}
625
+ onChange={e => setStagedName(e.target.value)}
626
+ disabled={isUploading}
627
+ className="w-full bg-foreground/[0.02] border border-[var(--panel-border)] rounded-xl px-4 py-3.5 text-[14px] font-bold text-foreground outline-none focus:border-accent transition-colors shadow-sm"
628
+ />
629
+ </div>
630
+
631
+ <div className="flex justify-end pt-4 mt-2 border-t border-[var(--panel-border)]/50">
632
+ <Button type="submit" isLoading={isUploading} className="w-full py-4 rounded-xl text-[14px] font-bold shadow-lg">
633
+ {isUploading ? 'Uploading to Drive...' : `Save to ${visibleFolders.find(f => f.id === stagedAccess)?.label}`}
634
+ </Button>
635
+ </div>
636
+ </form>
637
+ )}
638
+ </motion.div>
639
+ </div>
640
+ )}
641
+ </AnimatePresence>
642
+
643
+ {typeof window !== 'undefined' && createPortal(
644
+ <AnimatePresence>
645
+ {previewFile && (
646
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 md:p-12">
647
+ <motion.div onClick={() => setPreviewFile(null)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 bg-background/95 backdrop-blur-xl cursor-pointer" />
648
+
649
+ <button onClick={() => setPreviewFile(null)} className="absolute top-6 right-6 z-[160] w-12 h-12 bg-foreground/10 hover:bg-foreground/20 text-foreground rounded-full flex items-center justify-center backdrop-blur-md transition-all cursor-pointer">
650
+ <X className="w-5 h-5" />
651
+ </button>
652
+
653
+ <motion.div initial={{ opacity: 0, scale: 0.95, y: 10 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 10 }} className="relative z-10 w-full max-w-6xl max-h-[85vh] flex flex-col pointer-events-auto shadow-2xl rounded-3xl overflow-hidden glass-panel border border-[var(--panel-border)]">
654
+ <div className="flex items-center justify-between p-6 bg-background/80 backdrop-blur-md border-b border-[var(--panel-border)] shrink-0 shadow-sm z-20">
655
+ <div className="flex flex-col min-w-0 pr-4">
656
+ <span className="text-xl font-extrabold text-foreground truncate">{previewFile.fileName}</span>
657
+ <span className="text-[12px] text-foreground/70 font-bold uppercase tracking-wider mt-1">
658
+ {previewFile.fileType} • {formatSize(previewFile.fileSize)}
659
+ {previewFile.accessPrefix === 'mem_' && (previewFile.uploaderEmail || previewFile.uid) && ` • From: ${previewFile.uploaderEmail || previewFile.uid}`}
660
+ </span>
661
+ </div>
662
+ <div className="flex items-center gap-2 shrink-0">
663
+ <button onClick={() => handleCopyLink(previewFile.accessPrefix === 'pub_' ? previewFile.downloadURL : `${window.location.origin}/drive/${prefixToFolderName[previewFile.accessPrefix]}/${previewFile._id}`, previewFile._id)} className="p-2.5 bg-foreground/5 hover:bg-foreground/10 text-foreground/70 transition-colors rounded-xl shadow-sm hover:text-emerald-500" title="Copy Link">
664
+ {copiedLink === previewFile._id ? <Check className="w-4 h-4 text-emerald-500" /> : <Link className="w-4 h-4" />}
665
+ </button>
666
+ {canEditFile(previewFile) && (
667
+ <>
668
+ <button onClick={() => { setPreviewFile(null); setRenameFile({ id: previewFile._id, name: previewFile.fileName, prefix: previewFile.accessPrefix }); }} className="p-2.5 bg-foreground/5 hover:bg-foreground/10 text-foreground/70 hover:text-purple-500 transition-colors rounded-xl shadow-sm" title="Rename">
669
+ <Edit2 className="w-4 h-4" />
670
+ </button>
671
+ <button onClick={() => { setPreviewFile(null); setConfirmDelete({ id: previewFile._id, url: previewFile.downloadURL, name: previewFile.fileName, prefix: previewFile.accessPrefix }); }} className="p-2.5 bg-red-500/10 hover:bg-red-500/20 text-red-500 transition-colors rounded-xl shadow-sm" title="Delete">
672
+ <Trash2 className="w-4 h-4" />
673
+ </button>
674
+ </>
675
+ )}
676
+ <a href={previewFile.downloadURL} download target="_blank" rel="noreferrer" className="p-2.5 bg-accent text-white hover:bg-accent/90 transition-colors rounded-xl shadow-lg ml-1" title="Download">
677
+ <Download className="w-4 h-4" />
678
+ </a>
679
+ </div>
680
+ </div>
681
+
682
+ <div className="flex-1 w-full relative bg-foreground/[0.02] min-h-[50vh] md:min-h-[600px]" onClick={() => setPreviewFile(null)}>
683
+ <div className="absolute inset-x-4 inset-y-8 md:inset-x-12 md:inset-y-12 flex items-center justify-center">
684
+ {previewLoading && (
685
+ <div className="absolute inset-0 flex items-center justify-center rounded-lg z-50 pointer-events-none fade-in">
686
+ <div className="w-12 h-12 rounded-full bg-background/80 backdrop-blur-md shadow-xl flex items-center justify-center border border-[var(--panel-border)]/50">
687
+ <Loader2 className="w-5 h-5 text-accent animate-spin" />
688
+ </div>
689
+ </div>
690
+ )}
691
+ {previewFile.fileType.startsWith('image/') ? (
692
+ <img onLoad={() => setPreviewLoading(false)} src={previewFile.downloadURL} alt={previewFile.fileName} className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-auto h-auto max-w-full max-h-full object-contain drop-shadow-[0_0_40px_rgba(0,0,0,0.5)] rounded-lg pointer-events-auto transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`} onClick={e => e.stopPropagation()} />
693
+ ) : previewFile.fileType.startsWith('video/') ? (
694
+ <video onLoadedData={() => setPreviewLoading(false)} src={previewFile.downloadURL} controls autoPlay className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-auto h-auto max-w-full max-h-full outline-none bg-black/40 rounded-lg pointer-events-auto drop-shadow-[0_0_40px_rgba(0,0,0,0.5)] transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`} onClick={e => e.stopPropagation()} />
695
+ ) : previewFile.fileType === 'application/pdf' ? (
696
+ <iframe onLoad={() => setPreviewLoading(false)} src={previewFile.downloadURL} className={`w-full h-full border-none bg-white rounded-lg relative z-10 transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`} title="PDF Preview" />
697
+ ) : (
698
+ <div className="w-full h-full flex flex-col items-center justify-center text-foreground/40 text-center relative z-10" onClick={e => { e.stopPropagation(); setPreviewLoading(false); }}>
699
+ <FileIcon className="w-24 h-24 mb-6 opacity-40 drop-shadow-sm" />
700
+ <span className="text-[20px] font-extrabold text-foreground">Preview Unavailable</span>
701
+ <span className="text-[14px] mt-2 text-foreground/60 font-medium max-w-xs">This file format cannot be previewed. Please download it instead.</span>
702
+ </div>
703
+ )}
704
+ </div>
705
+ </div>
706
+ </motion.div>
707
+ </div>
708
+ )}
709
+ </AnimatePresence>,
710
+ document.body
711
+ )}
712
+
713
+ <AnimatePresence>
714
+ {showNotFound && (
715
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
716
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 bg-background/90 backdrop-blur-xl" />
717
+ <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} className="relative z-10 w-full max-w-sm flex flex-col items-center pointer-events-auto shadow-2xl rounded-3xl overflow-hidden glass-panel border border-red-500/20 bg-background/80 p-8 text-center">
718
+ <div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center mb-6">
719
+ <X className="w-8 h-8 text-red-500" />
720
+ </div>
721
+ <h3 className="text-xl font-bold text-foreground mb-2">Access Denied</h3>
722
+ <p className="text-[14px] text-foreground/60 font-medium leading-relaxed">You don't have access to this file, or it may have been deleted.</p>
723
+ <div className="w-5 h-5 border-2 border-accent/20 border-t-accent rounded-full animate-spin mt-6" />
724
+ </motion.div>
725
+ </div>
726
+ )}
727
+ </AnimatePresence>
728
+
729
+
730
+
731
+ <ConfirmModal
732
+ isOpen={!!confirmDelete}
733
+ title="Confirm Deletion"
734
+ message={<>Are you entirely sure you wish to delete <strong className="text-foreground truncate block my-0.5">{confirmDelete?.name}</strong> This action is permanent.</>}
735
+ onConfirm={executeDelete}
736
+ onCancel={() => setConfirmDelete(null)}
737
+ isProcessing={isDeleting}
738
+ confirmText="Yes, Delete"
739
+ />
740
+
741
+ <AnimatePresence>
742
+ {isDragging && (
743
+ <motion.div
744
+ initial={{ opacity: 0 }}
745
+ animate={{ opacity: 1 }}
746
+ exit={{ opacity: 0 }}
747
+ className="fixed inset-0 z-[1000] bg-background/60 backdrop-blur-md pointer-events-none flex items-center justify-center p-6"
748
+ >
749
+ <motion.div
750
+ initial={{ scale: 0.9, opacity: 0 }}
751
+ animate={{ scale: 1, opacity: 1 }}
752
+ exit={{ scale: 0.9, opacity: 0 }}
753
+ className="w-full max-w-lg aspect-video bg-background/80 border-2 border-dashed border-accent p-12 rounded-[40px] shadow-2xl flex flex-col items-center justify-center gap-6 relative"
754
+ >
755
+ <div className="absolute inset-0 bg-accent/5 rounded-[38px] animate-pulse" />
756
+ <div className="w-20 h-20 rounded-full bg-accent/10 flex items-center justify-center text-accent shadow-[0_0_40px_rgba(var(--accent-rgb),0.2)]">
757
+ <UploadCloud className="w-10 h-10 animate-bounce" />
758
+ </div>
759
+ <div className="flex flex-col items-center text-center gap-2 relative z-10">
760
+ <span className="text-[24px] font-extrabold text-foreground tracking-tight">Drop files to upload</span>
761
+ <span className="text-[14px] font-medium text-foreground/50">Your files will be staged for review</span>
762
+ </div>
763
+ </motion.div>
764
+ </motion.div>
765
+ )}
766
+ </AnimatePresence>
767
+ </main>
768
+ );
769
+ }