your-ai-workflow-firebase-os 1.2.20 → 1.2.21

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.
@@ -1,13 +1,14 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import { motion, AnimatePresence } from 'framer-motion';
3
- import { Bot, Plus, X, Loader2, Trash2, FileText, Download, Check, Edit2, Info } from 'lucide-react';
4
+ import { Bot, Plus, X, Loader2, Trash2, FileText, Download, Check, Edit2, Info, Link , Image , Video , Archive , Code , FileIcon } from 'lucide-react';
4
5
  import * as LucideIcons from 'lucide-react';
5
6
  import { DashboardNav } from '../components/DashboardNav';
6
7
  import { Button } from '../components/Button';
7
8
  import { useAuth } from '../lib/AuthContext';
8
9
  import { useTheme } from '../lib/ThemeContext';
9
10
  import { db, storage } from '../lib/firebase';
10
- import { collection, addDoc, getDocs, query, where, serverTimestamp, deleteDoc, updateDoc, doc, limit } from 'firebase/firestore';
11
+ import { collection, addDoc, getDocs, query, where, serverTimestamp, deleteDoc, updateDoc, doc, limit, deleteField } from 'firebase/firestore';
11
12
  import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage';
12
13
  import { ConfirmModal } from '../components/ConfirmModal';
13
14
 
@@ -38,10 +39,22 @@ export function PrivatePageTemplate({ config }: { config: any }) {
38
39
  const [copied, setCopied] = useState(false);
39
40
  const [uploading, setUploading] = useState(false);
40
41
  const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
42
+ const [_previewFile, _setPreviewFile] = useState<any | null>(null);
43
+ const setPreviewFile = (f: any) => {
44
+ _setPreviewFile(f);
45
+ if (f) {
46
+ const t = f.fileType || '';
47
+ const canPreview = t.startsWith('image/') || t.startsWith('video/') || t === 'application/pdf';
48
+ setPreviewLoading(canPreview);
49
+ }
50
+ };
51
+ const previewFile = _previewFile;
52
+ const [previewLoading, setPreviewLoading] = useState(true);
41
53
  const [isDeleting, setIsDeleting] = useState(false);
42
54
  const [editingId, setEditingId] = useState<string | null>(null);
43
55
  const [editName, setEditName] = useState('');
44
56
  const [editNote, setEditNote] = useState('');
57
+ const [editFields, setEditFields] = useState<{key: string; value: string}[]>([]);
45
58
  const fileInputRef = useRef<HTMLInputElement>(null);
46
59
  const [availableForms, setAvailableForms] = useState<string[]>([]);
47
60
 
@@ -235,31 +248,152 @@ export function PrivatePageTemplate({ config }: { config: any }) {
235
248
  };
236
249
 
237
250
  // ── Edit record (inline) ──────────────────────────────────────────────────
251
+ // Inline rename state for file records (Drive-style)
252
+ const [renamingFileId, setRenamingFileId] = useState<string | null>(null);
253
+ const [renameFileName, setRenameFileName] = useState('');
254
+ const [isRenamingFile, setIsRenamingFile] = useState(false);
255
+ const [copiedLink, setCopiedLink] = useState<string | null>(null);
256
+ const originalFieldKeysRef = useRef<string[]>([]);
257
+
238
258
  const startEdit = (record: SavedRecord) => {
259
+ // For file records, use inline rename (Drive-style)
260
+ if (record.recordType === 'file') {
261
+ setRenamingFileId(record.id);
262
+ setRenameFileName(record.name);
263
+ return;
264
+ }
239
265
  setEditingId(record.id);
240
266
  setEditName(record.name);
241
267
  setEditNote(record.note || '');
268
+ // Load any extra custom fields (anything beyond the reserved keys)
269
+ const reserved = ['name','note','uid','recordType','fileName','fileType','fileSize','downloadURL','createdAt'];
270
+ const extras = Object.entries(record as any)
271
+ .filter(([k]) => !reserved.includes(k) && k !== 'id')
272
+ .map(([key, value]) => ({ key, value: String(value ?? '') }));
273
+ setEditFields(extras);
274
+ originalFieldKeysRef.current = extras.map(f => f.key);
242
275
  };
243
276
 
244
- const handleSaveEdit = async () => {
245
- if (!editingId || !editName.trim()) { setEditingId(null); return; }
246
- const original = records.find(r => r.id === editingId);
247
- if (original && original.name === editName.trim() && (original.note || '') === editNote.trim()) {
248
- setEditingId(null); return; // no changes
277
+ const handleRenameFile = async () => {
278
+ if (!renamingFileId || !renameFileName.trim()) { setRenamingFileId(null); return; }
279
+ const original = records.find(r => r.id === renamingFileId);
280
+ if (original && original.name === renameFileName.trim()) { setRenamingFileId(null); return; }
281
+ setIsRenamingFile(true);
282
+ try {
283
+ await updateDoc(doc(db, recordsCollection, renamingFileId), {
284
+ name: renameFileName.trim(),
285
+ });
286
+ setRecords(prev => prev.map(r =>
287
+ r.id === renamingFileId ? { ...r, name: renameFileName.trim() } : r
288
+ ));
289
+ } catch (e) {
290
+ console.error('Error renaming file:', e);
249
291
  }
292
+ setIsRenamingFile(false);
293
+ setRenamingFileId(null);
294
+ };
295
+
296
+ const handleSaveEdit = async () => {
297
+ await handleSilentSave();
298
+ setEditingId(null);
299
+ };
300
+
301
+ // Silent save: persist to Firestore without closing modal, show tiny loader
302
+ const handleSilentSave = async () => {
303
+ if (!editingId || !editName.trim()) return;
304
+ setSaving(true);
305
+ const extraData: Record<string, any> = {};
306
+ editFields.forEach(({ key, value }) => {
307
+ if (key.trim()) extraData[key.trim()] = value;
308
+ });
309
+ // Detect removed fields and mark them for deletion
310
+ const currentKeys = editFields.map(f => f.key.trim()).filter(Boolean);
311
+ const removedKeys = originalFieldKeysRef.current.filter(k => !currentKeys.includes(k));
312
+ removedKeys.forEach(k => { extraData[k] = deleteField(); });
250
313
  try {
251
314
  await updateDoc(doc(db, recordsCollection, editingId), {
252
315
  name: editName.trim(),
253
316
  note: editNote.trim(),
317
+ ...extraData,
254
318
  });
255
- setRecords(prev => prev.map(r => r.id === editingId ? { ...r, name: editName.trim(), note: editNote.trim() } : r));
319
+ // Update local records: remove deleted keys, add new ones
320
+ setRecords(prev => prev.map(r => {
321
+ if (r.id !== editingId) return r;
322
+ const updated = { ...r, name: editName.trim(), note: editNote.trim() };
323
+ removedKeys.forEach(k => { delete (updated as any)[k]; });
324
+ editFields.forEach(({ key, value }) => { if (key.trim()) (updated as any)[key.trim()] = value; });
325
+ return updated;
326
+ }));
327
+ originalFieldKeysRef.current = currentKeys;
256
328
  } catch (e) {
257
- console.error('Error updating record:', e);
329
+ console.error('Error saving record:', e);
258
330
  }
259
- setEditingId(null);
331
+ setSaving(false);
260
332
  };
261
333
 
262
334
  // ── Utils ─────────────────────────────────────────────────────────────────
335
+
336
+ // ── File type icon renderer ───────────────────────────────────────────────
337
+ const renderFileIcon = (type?: string, url?: string) => {
338
+ if (!type) return <FileText className="w-4 h-4" />;
339
+ if (type.startsWith('image/')) {
340
+ if (url) return <div className="w-9 h-9 rounded-xl bg-cover bg-center shrink-0" style={{ backgroundImage: `url(${url})` }} />;
341
+ return <Image className="w-4 h-4" />;
342
+ }
343
+ if (type.startsWith('video/') || type.startsWith('audio/')) return <Video className="w-4 h-4" />;
344
+ if (type.includes('pdf') || type.includes('word') || type.includes('text')) return <FileText className="w-4 h-4" />;
345
+ if (type.includes('zip') || type.includes('rar') || type.includes('archive')) return <Archive className="w-4 h-4" />;
346
+ if (type.includes('html') || type.includes('csv') || type.includes('json')) return <Code className="w-4 h-4" />;
347
+ return <FileIcon className="w-4 h-4" />;
348
+ };
349
+
350
+ const renderFileIconBg = (type?: string) => {
351
+ if (!type) return 'bg-foreground/5 text-foreground/40';
352
+ if (type.startsWith('image/')) return 'bg-emerald-500/10 text-emerald-500';
353
+ if (type.startsWith('video/') || type.startsWith('audio/')) return 'bg-purple-500/10 text-purple-500';
354
+ if (type.includes('pdf') || type.includes('word') || type.includes('text')) return 'bg-blue-500/10 text-blue-500';
355
+ if (type.includes('zip') || type.includes('rar') || type.includes('archive')) return 'bg-yellow-500/10 text-yellow-500';
356
+ if (type.includes('html') || type.includes('csv') || type.includes('json')) return 'bg-emerald-500/10 text-emerald-500';
357
+ return 'bg-foreground/5 text-foreground/40';
358
+ };
359
+
360
+ const formatExtension = (type?: string) => {
361
+ if (!type) return 'FILE';
362
+ const mimeMap: Record<string, string> = {
363
+ 'application/pdf': 'PDF',
364
+ 'application/zip': 'ZIP',
365
+ 'application/x-zip-compressed': 'ZIP',
366
+ 'application/x-rar-compressed': 'RAR',
367
+ 'application/x-7z-compressed': '7Z',
368
+ 'application/json': 'JSON',
369
+ 'application/xml': 'XML',
370
+ 'application/javascript': 'JS',
371
+ 'application/typescript': 'TS',
372
+ 'application/msword': 'DOC',
373
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
374
+ 'application/vnd.ms-excel': 'XLS',
375
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
376
+ 'application/vnd.ms-powerpoint': 'PPT',
377
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPTX',
378
+ 'application/rtf': 'RTF',
379
+ 'application/x-tar': 'TAR',
380
+ 'application/gzip': 'GZ',
381
+ 'text/plain': 'TXT',
382
+ 'text/html': 'HTML',
383
+ 'text/css': 'CSS',
384
+ 'text/csv': 'CSV',
385
+ 'text/markdown': 'MD',
386
+ };
387
+ const lower = type.toLowerCase();
388
+ if (mimeMap[lower]) return mimeMap[lower];
389
+ // image/png → PNG, video/mp4 → MP4, audio/mpeg → MPEG
390
+ const parts = lower.split('/');
391
+ const sub = parts[parts.length - 1];
392
+ // Strip x- prefix and vnd. noise
393
+ const clean = sub.replace(/^x-/, '').replace(/^vnd\..*\./, '');
394
+ return clean.toUpperCase().slice(0, 10);
395
+ };
396
+
263
397
  const formatSize = (bytes: number) => {
264
398
  if (!bytes) return '0 B';
265
399
  const k = 1024;
@@ -443,75 +577,79 @@ export function PrivatePageTemplate({ config }: { config: any }) {
443
577
  animate={{ opacity: 1, y: 0 }}
444
578
  exit={{ opacity: 0, scale: 0.95 }}
445
579
  transition={{ delay: i * 0.03, duration: 0.25 }}
446
- className={`group flex flex-col bg-background border rounded-2xl p-5 transition-all relative ${isEditing ? 'border-accent/40 shadow-lg ring-1 ring-accent/20' : 'border-[var(--panel-border)] hover:border-accent/30 hover:shadow-lg'
580
+ onClick={() => startEdit(record)}
581
+ className={`group flex flex-col bg-background border rounded-2xl p-5 transition-all relative cursor-pointer ${isEditing ? 'border-accent/40 shadow-lg ring-1 ring-accent/20' : 'border-[var(--panel-border)] hover:border-accent/30 hover:shadow-lg'
447
582
  }`}
448
583
  >
449
584
  {/* Top-right action icons — download + edit + delete */}
450
- {!isEditing && (
451
- <div className="absolute top-3 right-3 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-all">
585
+ {true && (
586
+ <div className="absolute top-3 right-3 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-all z-10">
452
587
  {record.recordType === 'file' && record.downloadURL && (
453
- <a href={record.downloadURL} target="_blank" rel="noreferrer" className="p-1.5 rounded-lg text-foreground/25 hover:text-blue-500 hover:bg-blue-500/10 transition-all" title="Download">
454
- <Download className="w-3.5 h-3.5" />
455
- </a>
588
+ <>
589
+ <button onClick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(record.downloadURL!); setCopiedLink(record.id); setTimeout(() => setCopiedLink(null), 2000); }} className="p-1.5 rounded-lg text-foreground/25 hover:text-emerald-500 hover:bg-emerald-500/10 transition-all cursor-pointer" title="Copy Link">
590
+ {copiedLink === record.id ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Link className="w-3.5 h-3.5" />}
591
+ </button>
592
+ <a href={record.downloadURL} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="p-1.5 rounded-lg text-foreground/25 hover:text-blue-500 hover:bg-blue-500/10 transition-all" title="Download">
593
+ <Download className="w-3.5 h-3.5" />
594
+ </a>
595
+ </>
456
596
  )}
457
- <button onClick={() => startEdit(record)} className="p-1.5 rounded-lg text-foreground/25 hover:text-accent hover:bg-accent/10 transition-all cursor-pointer" title="Edit">
597
+ <button onClick={(e) => { e.stopPropagation(); startEdit(record); }} className="p-1.5 rounded-lg text-foreground/25 hover:text-accent hover:bg-accent/10 transition-all cursor-pointer" title="Edit">
458
598
  <Edit2 className="w-3.5 h-3.5" />
459
599
  </button>
460
- <button onClick={() => setDeleteConfirm(record.id)} className="p-1.5 rounded-lg text-foreground/25 hover:text-red-500 hover:bg-red-500/10 transition-all cursor-pointer" title="Delete">
600
+ <button onClick={(e) => { e.stopPropagation(); setDeleteConfirm(record.id); }} className="p-1.5 rounded-lg text-foreground/25 hover:text-red-500 hover:bg-red-500/10 transition-all cursor-pointer" title="Delete">
461
601
  <Trash2 className="w-3.5 h-3.5" />
462
602
  </button>
463
603
  </div>
464
604
  )}
465
605
 
466
606
  <div className="flex items-start gap-3 mb-3">
467
- <div className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${record.recordType === 'file'
468
- ? 'bg-blue-500/10 text-blue-500'
469
- : 'bg-accent/10 text-accent'
470
- }`}>
471
- {record.recordType === 'file' ? (
472
- <FileText className="w-4 h-4" />
473
- ) : (
474
- <div className="w-2 h-2 rounded-full bg-current" />
475
- )}
607
+ <div
608
+ onClick={(e) => { e.stopPropagation(); if (record.recordType === 'file') setPreviewFile(record); }}
609
+ className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${record.recordType === 'file' ? `cursor-pointer hover:scale-105 transition-transform ${renderFileIconBg(record.fileType)}` : 'bg-accent/10 text-accent'}`}
610
+ >
611
+ {record.recordType === 'file' ? renderFileIcon(record.fileType, record.downloadURL) : <div className="w-2 h-2 rounded-full bg-current" />}
476
612
  </div>
477
- <div className="flex flex-col min-w-0 flex-1">
478
- {isEditing ? (
479
- <input
480
- autoFocus
481
- value={editName}
482
- onChange={e => setEditName(e.target.value)}
483
- onBlur={handleSaveEdit}
484
- onKeyDown={e => { if (e.key === 'Enter') handleSaveEdit(); if (e.key === 'Escape') setEditingId(null); }}
485
- className="text-[14px] font-bold text-foreground bg-transparent border-b border-accent/30 outline-none pb-0.5 w-full"
486
- />
487
- ) : (
488
- <span className="text-[14px] font-bold text-foreground truncate pr-12">
489
- {record.name}
490
- </span>
491
- )}
613
+ <div className="flex flex-col min-w-0 flex-1 relative">
614
+ {renamingFileId === record.id ? (
615
+ <input
616
+ autoFocus
617
+ type="text"
618
+ value={renameFileName}
619
+ onChange={e => setRenameFileName(e.target.value)}
620
+ onBlur={() => handleRenameFile()}
621
+ onKeyDown={e => { if (e.key === 'Enter') handleRenameFile(); if (e.key === 'Escape') setRenamingFileId(null); }}
622
+ onClick={e => e.stopPropagation()}
623
+ className="no-glow text-[14px] font-bold text-foreground bg-transparent border-b border-accent/30 outline-none pb-0.5 w-full pr-12"
624
+ />
625
+ ) : (
626
+ <span className="text-[14px] font-bold text-foreground truncate pr-12">
627
+ {record.name}
628
+ </span>
629
+ )}
630
+ {record.recordType === 'file' && <div className="absolute inset-0 z-0 cursor-pointer" onClick={() => setPreviewFile(record)} />}
492
631
  <span className="text-[11px] text-foreground/35 font-bold uppercase tracking-wider mt-0.5">
493
632
  {formatDate(record.createdAt)}
494
633
  </span>
495
634
  </div>
496
635
  </div>
497
636
 
498
- {isEditing ? (
499
- <textarea
500
- value={editNote}
501
- onChange={e => setEditNote(e.target.value)}
502
- onBlur={handleSaveEdit}
503
- onKeyDown={e => { if (e.key === 'Escape') setEditingId(null); }}
504
- placeholder="Note (optional)"
505
- rows={2}
506
- className="text-[13px] text-foreground/70 font-medium bg-transparent border-b border-accent/20 outline-none resize-none w-full mb-1 placeholder:text-foreground/30"
507
- />
508
- ) : (
509
- record.note && (
510
- <p className="text-[13px] text-foreground/55 font-medium leading-relaxed line-clamp-3 mb-1">
511
- {record.note}
512
- </p>
513
- )
637
+ {record.note && (
638
+ <p className="text-[13px] text-foreground/55 font-medium leading-relaxed line-clamp-3 mb-1">
639
+ {record.note}
640
+ </p>
514
641
  )}
642
+ {/* Extra custom fields display */}
643
+ {Object.entries(record as any)
644
+ .filter(([k]) => !['name','note','uid','recordType','fileName','fileType','fileSize','downloadURL','createdAt','creatorName','creatorEmail','creatorAvatar'].includes(k) && k !== 'id')
645
+ .slice(0, 3)
646
+ .map(([k, v]) => (
647
+ <div key={k} className="flex items-center gap-1.5 mt-1">
648
+ <span className="text-[10px] font-bold text-foreground/30 uppercase tracking-wider shrink-0">{k}:</span>
649
+ <span className="text-[11px] font-semibold text-foreground/60 truncate">{String(v)}</span>
650
+ </div>
651
+ ))
652
+ }
515
653
 
516
654
 
517
655
  </motion.div>
@@ -600,13 +738,256 @@ export function PrivatePageTemplate({ config }: { config: any }) {
600
738
  )}
601
739
  </AnimatePresence>
602
740
 
741
+ {/* ── File Preview Modal (Drive-style) ───────────────────────────────────── */}
742
+ {typeof window !== 'undefined' && createPortal(
743
+ <AnimatePresence>
744
+ {previewFile && (
745
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 md:p-12">
746
+ <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" />
747
+ <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 outline-none">
748
+ <X className="w-5 h-5" />
749
+ </button>
750
+ <motion.div
751
+ initial={{ opacity: 0, scale: 0.95, y: 10 }}
752
+ animate={{ opacity: 1, scale: 1, y: 0 }}
753
+ exit={{ opacity: 0, scale: 0.95, y: 10 }}
754
+ 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)]"
755
+ >
756
+ {/* File Header */}
757
+ <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">
758
+ <div className="flex flex-col min-w-0 pr-4">
759
+ <span className="text-xl font-extrabold text-foreground truncate">{previewFile.fileName || previewFile.name}</span>
760
+ <span className="text-[12px] text-foreground/70 font-bold uppercase tracking-wider mt-1">
761
+ {formatExtension(previewFile.fileType)} • {formatSize(previewFile.fileSize || 0)}
762
+ </span>
763
+ </div>
764
+ <div className="flex items-center gap-2 shrink-0">
765
+ <button
766
+ onClick={() => { navigator.clipboard.writeText(previewFile.downloadURL!); setCopiedLink('preview'); setTimeout(() => setCopiedLink(null), 2000); }}
767
+ className="p-2.5 bg-foreground/5 hover:bg-foreground/10 text-foreground/70 transition-colors rounded-xl shadow-sm hover:text-emerald-500 cursor-pointer"
768
+ title="Copy Link"
769
+ >
770
+ {copiedLink === 'preview' ? <Check className="w-4 h-4 text-emerald-500" /> : <Link className="w-4 h-4" />}
771
+ </button>
772
+ <button
773
+ onClick={() => { setPreviewFile(null); setRenamingFileId(previewFile.id); setRenameFileName(previewFile.name); }}
774
+ className="p-2.5 bg-foreground/5 hover:bg-foreground/10 text-foreground/70 hover:text-accent transition-colors rounded-xl shadow-sm cursor-pointer"
775
+ title="Rename"
776
+ >
777
+ <Edit2 className="w-4 h-4" />
778
+ </button>
779
+ <button
780
+ onClick={() => { setPreviewFile(null); setDeleteConfirm(previewFile.id); }}
781
+ className="p-2.5 bg-red-500/10 hover:bg-red-500/20 text-red-500 transition-colors rounded-xl shadow-sm cursor-pointer"
782
+ title="Delete"
783
+ >
784
+ <Trash2 className="w-4 h-4" />
785
+ </button>
786
+ <a
787
+ href={previewFile.downloadURL}
788
+ download
789
+ target="_blank"
790
+ rel="noreferrer"
791
+ className="p-2.5 btn-primary transition-colors rounded-xl shadow-lg ml-1"
792
+ title="Download"
793
+ >
794
+ <Download className="w-4 h-4" />
795
+ </a>
796
+ </div>
797
+ </div>
798
+
799
+ {/* Preview Content */}
800
+ <div className="flex-1 w-full relative bg-foreground/[0.02] min-h-[50vh] md:min-h-[600px]" onClick={() => setPreviewFile(null)}>
801
+ <div className="absolute inset-x-4 inset-y-8 md:inset-x-12 md:inset-y-12 flex items-center justify-center">
802
+ {previewLoading && (
803
+ <div className="absolute inset-0 flex items-center justify-center rounded-lg z-50 pointer-events-none">
804
+ <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">
805
+ <Loader2 className="w-5 h-5 text-accent animate-spin" />
806
+ </div>
807
+ </div>
808
+ )}
809
+ {previewFile.fileType?.startsWith('image/') ? (
810
+ <img
811
+ onLoad={() => setPreviewLoading(false)}
812
+ onError={() => setPreviewLoading(false)}
813
+ src={previewFile.downloadURL}
814
+ alt={previewFile.fileName || previewFile.name}
815
+ 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'}`}
816
+ onClick={e => e.stopPropagation()}
817
+ />
818
+ ) : previewFile.fileType?.startsWith('video/') ? (
819
+ <video
820
+ onLoadedData={() => setPreviewLoading(false)}
821
+ onError={() => setPreviewLoading(false)}
822
+ src={previewFile.downloadURL}
823
+ controls
824
+ autoPlay
825
+ 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'}`}
826
+ onClick={e => e.stopPropagation()}
827
+ />
828
+ ) : previewFile.fileType === 'application/pdf' ? (
829
+ <iframe
830
+ onLoad={() => setPreviewLoading(false)}
831
+ src={previewFile.downloadURL}
832
+ className={`w-full h-full border-none rounded-lg relative z-10 transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`}
833
+ style={{ backgroundColor: 'white' }}
834
+ title="PDF Preview"
835
+ onClick={e => e.stopPropagation()}
836
+ />
837
+ ) : (
838
+ <div
839
+ className="w-full h-full flex flex-col items-center justify-center text-foreground/40 text-center relative z-10"
840
+ onClick={e => { e.stopPropagation(); setPreviewLoading(false); }}
841
+ >
842
+ <FileText className="w-24 h-24 mb-6 opacity-40 drop-shadow-sm" />
843
+ <span className="text-[20px] font-extrabold text-foreground">Preview Unavailable</span>
844
+ <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>
845
+ </div>
846
+ )}
847
+ </div>
848
+ </div>
849
+ </motion.div>
850
+ </div>
851
+ )}
852
+ </AnimatePresence>,
853
+ document.body
854
+ )}
855
+
856
+ {/* ── Edit Record Modal ──────────────────────────────────────────────── */}
857
+ <AnimatePresence>
858
+ {editingId && (
859
+ <div className="fixed inset-0 z-[120] flex items-center justify-center p-4">
860
+ <motion.div
861
+ onClick={() => handleSaveEdit()}
862
+ initial={{ opacity: 0 }}
863
+ animate={{ opacity: 1 }}
864
+ exit={{ opacity: 0 }}
865
+ className="absolute inset-0 bg-background/60 backdrop-blur-sm cursor-pointer"
866
+ />
867
+ <motion.div
868
+ initial={{ opacity: 0, scale: 0.97, y: 6 }}
869
+ animate={{ opacity: 1, scale: 1, y: 0 }}
870
+ exit={{ opacity: 0, scale: 0.97, y: 6 }}
871
+ transition={{ duration: 0.2 }}
872
+ className="w-full max-w-[440px] border-[var(--panel-border)] border rounded-2xl relative z-10 glass-panel shadow-2xl bg-background overflow-hidden"
873
+ >
874
+ <div className="p-5 flex flex-col gap-3 max-h-[80vh] overflow-y-auto">
875
+ {/* Header */}
876
+ <div className="flex items-center justify-between mb-2 mt-2 px-1">
877
+ <div className="flex items-center gap-2">
878
+ <span className="text-[14px] font-bold text-foreground">Edit Record</span>
879
+ {saving && <Loader2 className="w-3 h-3 animate-spin text-foreground/40" />}
880
+ </div>
881
+ <button
882
+ type="button"
883
+ onClick={() => handleSaveEdit()}
884
+ className="text-foreground/30 hover:text-foreground/60 transition-colors cursor-pointer"
885
+ >
886
+ <X className="w-4 h-4" />
887
+ </button>
888
+ </div>
889
+
890
+ <div className="flex flex-col gap-0 max-h-[70vh] overflow-y-auto no-scrollbar px-1">
891
+ {/* Name field */}
892
+ <div className="flex flex-col relative group/field">
893
+ <input
894
+ readOnly
895
+ value="NAME"
896
+ className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none pt-3 pb-0.5"
897
+ />
898
+ <input
899
+ autoFocus
900
+ required
901
+ value={editName}
902
+ onChange={e => setEditName(e.target.value)}
903
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
904
+ onBlur={() => handleSilentSave()}
905
+ placeholder="Name *"
906
+ className="no-glow bg-transparent text-[13px] font-medium text-foreground outline-none placeholder:text-foreground/15 pb-1 caret-foreground/50"
907
+ />
908
+ <div className="border-b border-dashed border-[var(--panel-border)]/60" />
909
+ </div>
910
+
911
+ {/* Note field */}
912
+ <div className="flex flex-col relative group/field">
913
+ <input
914
+ readOnly
915
+ value="NOTE"
916
+ className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none pt-3 pb-0.5"
917
+ />
918
+ <textarea
919
+ ref={el => { if (el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; } }}
920
+ value={editNote}
921
+ onChange={e => { setEditNote(e.target.value); e.target.style.height = 'auto'; e.target.style.height = e.target.scrollHeight + 'px'; }}
922
+ onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); (e.target as HTMLTextAreaElement).blur(); } }}
923
+ onBlur={() => handleSilentSave()}
924
+ placeholder="Add a note..."
925
+ rows={1}
926
+ className="no-glow bg-transparent text-[13px] font-medium text-foreground outline-none placeholder:text-foreground/15 pb-1 caret-foreground/50 resize-none overflow-hidden"
927
+ />
928
+ <div className="border-b border-dashed border-[var(--panel-border)]/60" />
929
+ </div>
930
+
931
+ {/* Custom Fields */}
932
+ {editFields.map((field, idx) => (
933
+ <div key={idx} className="flex flex-col relative group/field">
934
+ <input
935
+ placeholder="FIELD NAME"
936
+ value={field.key}
937
+ onChange={e => {
938
+ const next = [...editFields];
939
+ next[idx].key = e.target.value;
940
+ setEditFields(next);
941
+ }}
942
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
943
+ onBlur={() => handleSilentSave()}
944
+ className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none placeholder:text-foreground/15 pt-3 pb-0.5 caret-foreground/40"
945
+ />
946
+ <input
947
+ placeholder="Field value..."
948
+ value={field.value}
949
+ onChange={e => {
950
+ const next = [...editFields];
951
+ next[idx].value = e.target.value;
952
+ setEditFields(next);
953
+ }}
954
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
955
+ onBlur={() => handleSilentSave()}
956
+ className="no-glow bg-transparent text-[13px] font-medium text-foreground outline-none placeholder:text-foreground/15 pt-1.5 pb-1 caret-foreground/50"
957
+ />
958
+ <div className="border-b border-dashed border-[var(--panel-border)]/60" />
959
+ <button
960
+ type="button"
961
+ onClick={() => setEditFields(editFields.filter((_, i) => i !== idx))}
962
+ className="absolute top-3 right-0 p-1 text-foreground/0 group-hover/field:text-foreground/20 hover:!text-red-400 transition-colors cursor-pointer"
963
+ >
964
+ <X className="w-3 h-3" />
965
+ </button>
966
+ </div>
967
+ ))}
968
+
969
+ {/* Add Field */}
970
+ <button
971
+ type="button"
972
+ onClick={() => setEditFields([...editFields, { key: '', value: '' }])}
973
+ className="flex items-center gap-1.5 text-[11px] font-medium text-foreground/25 hover:text-foreground/40 transition-colors cursor-pointer mt-4 mb-4"
974
+ >
975
+ <Plus className="w-3 h-3" /> Add Field
976
+ </button>
977
+ </div>
978
+ </div>
979
+ </motion.div>
980
+ </div>
981
+ )}
982
+ </AnimatePresence>
983
+
603
984
  {/* ── Delete Confirmation ────────────────────────────────────────────── */}
604
985
  <ConfirmModal
605
986
  isOpen={!!deleteConfirm}
606
987
  onCancel={() => setDeleteConfirm(null)}
607
988
  onConfirm={handleDelete}
608
989
  title="Delete Record"
609
- message="Are you sure you want to delete this record? This action cannot be undone."
990
+ message={<>Are you sure you want to delete <strong className="text-foreground block my-0.5 text-left overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{records.find(r => r.id === deleteConfirm)?.name}</strong> This action cannot be undone.</>}
610
991
  confirmText="Delete"
611
992
  isProcessing={isDeleting}
612
993
  />