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.
- package/dist/{ContactPopup-B666UTn5.cjs → ContactPopup-5mXFBqTs.cjs} +1 -1
- package/dist/{ContactPopup-QbqZ8VJs.js → ContactPopup-D0Fb80uS.js} +1 -1
- package/dist/renderer.cjs.js +1 -1
- package/dist/renderer.es.js +1 -1
- package/dist/your-ai-workflow-firebase-os.cjs.js +35 -35
- package/dist/your-ai-workflow-firebase-os.css +1 -1
- package/dist/your-ai-workflow-firebase-os.es.js +4029 -2588
- package/package.json +1 -1
- package/src/pages/PagesAdmin.tsx +143 -25
- package/src/pages/StorageAdmin.tsx +8 -8
- package/src/pages/ThemeAdmin.tsx +15 -0
- package/src/templates/AdminPageTemplate.tsx +500 -111
- package/src/templates/PrivatePageTemplate.tsx +440 -59
- package/src/templates/SharedPageTemplate.tsx +432 -38
- package/src/templates/TemplateBoard.tsx +17 -8
- package/src/templates/TemplateCalendar.tsx +9 -4
- package/src/templates/TemplateTable.tsx +10 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, useCallback } 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, getDoc, query, orderBy, where, serverTimestamp, deleteDoc, updateDoc, doc, onSnapshot, limit } from 'firebase/firestore';
|
|
11
|
+
import { collection, addDoc, getDocs, getDoc, query, orderBy, where, serverTimestamp, deleteDoc, updateDoc, doc, onSnapshot, limit, deleteField } from 'firebase/firestore';
|
|
11
12
|
import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage';
|
|
12
13
|
import { ConfirmModal } from '../components/ConfirmModal';
|
|
13
14
|
|
|
@@ -42,11 +43,25 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
42
43
|
const [copied, setCopied] = useState(false);
|
|
43
44
|
const [uploading, setUploading] = useState(false);
|
|
44
45
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
|
46
|
+
const [_previewFile, _setPreviewFile] = useState<any | null>(null);
|
|
47
|
+
const setPreviewFile = (f: any) => {
|
|
48
|
+
_setPreviewFile(f);
|
|
49
|
+
if (f) {
|
|
50
|
+
const t = f.fileType || '';
|
|
51
|
+
const canPreview = t.startsWith('image/') || t.startsWith('video/') || t === 'application/pdf';
|
|
52
|
+
setPreviewLoading(canPreview);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const previewFile = _previewFile;
|
|
56
|
+
const [previewLoading, setPreviewLoading] = useState(true);
|
|
45
57
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
46
58
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
47
59
|
const [editName, setEditName] = useState('');
|
|
48
60
|
const [editNote, setEditNote] = useState('');
|
|
61
|
+
const [editFields, setEditFields] = useState<{ key: string; value: string }[]>([]);
|
|
49
62
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
63
|
+
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
|
64
|
+
const originalFieldKeysRef = useRef<string[]>([]);
|
|
50
65
|
|
|
51
66
|
|
|
52
67
|
const parsedTabName = (config.tabName || config.pageId || 'admin_page')
|
|
@@ -69,7 +84,7 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
69
84
|
// ── Real-time listener for admin records (shared between admins) ─────────
|
|
70
85
|
useEffect(() => {
|
|
71
86
|
if (!user) return;
|
|
72
|
-
|
|
87
|
+
|
|
73
88
|
// 1. Instant Cache Hydration
|
|
74
89
|
try {
|
|
75
90
|
const cached = localStorage.getItem(`fbos_records_${recordsCollection}`);
|
|
@@ -79,7 +94,7 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
79
94
|
} else {
|
|
80
95
|
setLoading(true);
|
|
81
96
|
}
|
|
82
|
-
} catch(e) {}
|
|
97
|
+
} catch (e) { }
|
|
83
98
|
|
|
84
99
|
// Helper to serialize records for cache
|
|
85
100
|
const processItems = (snapDocs: any[]) => {
|
|
@@ -212,7 +227,7 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
212
227
|
const fileName = record.downloadURL.split('admin_files%2F')[1]?.split('?')[0];
|
|
213
228
|
if (fileName) {
|
|
214
229
|
const fileRef = ref(storage, `admin_files/${decodeURIComponent(fileName)}`);
|
|
215
|
-
await deleteObject(fileRef).catch(() => {});
|
|
230
|
+
await deleteObject(fileRef).catch(() => { });
|
|
216
231
|
}
|
|
217
232
|
// Delete from admin_files Firestore collection
|
|
218
233
|
const filesSnap = await getDocs(query(
|
|
@@ -236,30 +251,169 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
236
251
|
};
|
|
237
252
|
|
|
238
253
|
// ── Edit record (inline) ──────────────────────────────────────────────────
|
|
254
|
+
// Inline rename state for file records (Drive-style)
|
|
255
|
+
const [renamingFileId, setRenamingFileId] = useState<string | null>(null);
|
|
256
|
+
const [renameFileName, setRenameFileName] = useState('');
|
|
257
|
+
const [isRenamingFile, setIsRenamingFile] = useState(false);
|
|
258
|
+
|
|
239
259
|
const startEdit = (record: SavedRecord) => {
|
|
260
|
+
// For file records, use inline rename (Drive-style)
|
|
261
|
+
if (record.recordType === 'file') {
|
|
262
|
+
setRenamingFileId(record.id);
|
|
263
|
+
setRenameFileName(record.name);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
240
266
|
setEditingId(record.id);
|
|
241
267
|
setEditName(record.name);
|
|
242
268
|
setEditNote(record.note || '');
|
|
269
|
+
// Load any extra custom fields (anything beyond the reserved keys)
|
|
270
|
+
const reserved = ['name', 'note', 'uid', 'recordType', 'fileName', 'fileType', 'fileSize', 'downloadURL', 'createdAt', 'creatorName', 'creatorEmail', 'creatorAvatar'];
|
|
271
|
+
const extras = Object.entries(record as any)
|
|
272
|
+
.filter(([k]) => !reserved.includes(k) && k !== 'id')
|
|
273
|
+
.map(([key, value]) => ({ key, value: String(value ?? '') }));
|
|
274
|
+
setEditFields(extras);
|
|
275
|
+
originalFieldKeysRef.current = extras.map(f => f.key);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const handleRenameFile = async () => {
|
|
279
|
+
if (!renamingFileId || !renameFileName.trim()) { setRenamingFileId(null); return; }
|
|
280
|
+
const original = records.find(r => r.id === renamingFileId);
|
|
281
|
+
if (original && original.name === renameFileName.trim()) { setRenamingFileId(null); return; }
|
|
282
|
+
setIsRenamingFile(true);
|
|
283
|
+
try {
|
|
284
|
+
await updateDoc(doc(db, recordsCollection, renamingFileId), {
|
|
285
|
+
name: renameFileName.trim(),
|
|
286
|
+
});
|
|
287
|
+
setRecords(prev => prev.map(r =>
|
|
288
|
+
r.id === renamingFileId ? { ...r, name: renameFileName.trim() } : r
|
|
289
|
+
));
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.error('Error renaming file:', e);
|
|
292
|
+
}
|
|
293
|
+
setIsRenamingFile(false);
|
|
294
|
+
setRenamingFileId(null);
|
|
243
295
|
};
|
|
244
296
|
|
|
245
297
|
const handleSaveEdit = async () => {
|
|
246
298
|
if (!editingId || !editName.trim()) { setEditingId(null); return; }
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
299
|
+
setSaving(true);
|
|
300
|
+
const extraData: Record<string, any> = {};
|
|
301
|
+
editFields.forEach(({ key, value }) => {
|
|
302
|
+
if (key.trim()) extraData[key.trim()] = value;
|
|
303
|
+
});
|
|
304
|
+
const currentKeys = editFields.map(f => f.key.trim()).filter(Boolean);
|
|
305
|
+
const removedKeys = originalFieldKeysRef.current.filter(k => !currentKeys.includes(k));
|
|
306
|
+
removedKeys.forEach(k => { extraData[k] = deleteField(); });
|
|
251
307
|
try {
|
|
252
308
|
await updateDoc(doc(db, recordsCollection, editingId), {
|
|
253
309
|
name: editName.trim(),
|
|
254
310
|
note: editNote.trim(),
|
|
311
|
+
...extraData,
|
|
255
312
|
});
|
|
256
|
-
|
|
313
|
+
setRecords(prev => prev.map(r => {
|
|
314
|
+
if (r.id !== editingId) return r;
|
|
315
|
+
const updated = { ...r, name: editName.trim(), note: editNote.trim() };
|
|
316
|
+
removedKeys.forEach(k => { delete (updated as any)[k]; });
|
|
317
|
+
editFields.forEach(({ key, value }) => { if (key.trim()) (updated as any)[key.trim()] = value; });
|
|
318
|
+
return updated;
|
|
319
|
+
}));
|
|
320
|
+
originalFieldKeysRef.current = currentKeys;
|
|
257
321
|
} catch (e) {
|
|
258
|
-
console.error('Error updating
|
|
322
|
+
console.error('Error updating record:', e);
|
|
259
323
|
}
|
|
324
|
+
setSaving(false);
|
|
260
325
|
setEditingId(null);
|
|
261
326
|
};
|
|
262
327
|
|
|
328
|
+
// Silent save: persist to Firestore without closing modal or showing loader
|
|
329
|
+
const handleSilentSave = async () => {
|
|
330
|
+
if (!editingId || !editName.trim()) return;
|
|
331
|
+
const extraData: Record<string, any> = {};
|
|
332
|
+
editFields.forEach(({ key, value }) => {
|
|
333
|
+
if (key.trim()) extraData[key.trim()] = value;
|
|
334
|
+
});
|
|
335
|
+
const currentKeys = editFields.map(f => f.key.trim()).filter(Boolean);
|
|
336
|
+
const removedKeys = originalFieldKeysRef.current.filter(k => !currentKeys.includes(k));
|
|
337
|
+
removedKeys.forEach(k => { extraData[k] = deleteField(); });
|
|
338
|
+
try {
|
|
339
|
+
await updateDoc(doc(db, recordsCollection, editingId), {
|
|
340
|
+
name: editName.trim(),
|
|
341
|
+
note: editNote.trim(),
|
|
342
|
+
...extraData,
|
|
343
|
+
});
|
|
344
|
+
setRecords(prev => prev.map(r => {
|
|
345
|
+
if (r.id !== editingId) return r;
|
|
346
|
+
const updated = { ...r, name: editName.trim(), note: editNote.trim() };
|
|
347
|
+
removedKeys.forEach(k => { delete (updated as any)[k]; });
|
|
348
|
+
editFields.forEach(({ key, value }) => { if (key.trim()) (updated as any)[key.trim()] = value; });
|
|
349
|
+
return updated;
|
|
350
|
+
}));
|
|
351
|
+
originalFieldKeysRef.current = currentKeys;
|
|
352
|
+
} catch (e) {
|
|
353
|
+
console.error('Error silently saving record:', e);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
// ── File type icon renderer ───────────────────────────────────────────────
|
|
359
|
+
const renderFileIcon = (type?: string, url?: string) => {
|
|
360
|
+
if (!type) return <FileText className="w-4 h-4" />;
|
|
361
|
+
if (type.startsWith('image/')) {
|
|
362
|
+
if (url) return <div className="w-9 h-9 rounded-xl bg-cover bg-center shrink-0" style={{ backgroundImage: `url(${url})` }} />;
|
|
363
|
+
return <Image className="w-4 h-4" />;
|
|
364
|
+
}
|
|
365
|
+
if (type.startsWith('video/') || type.startsWith('audio/')) return <Video className="w-4 h-4" />;
|
|
366
|
+
if (type.includes('pdf') || type.includes('word') || type.includes('text')) return <FileText className="w-4 h-4" />;
|
|
367
|
+
if (type.includes('zip') || type.includes('rar') || type.includes('archive')) return <Archive className="w-4 h-4" />;
|
|
368
|
+
if (type.includes('html') || type.includes('csv') || type.includes('json')) return <Code className="w-4 h-4" />;
|
|
369
|
+
return <FileIcon className="w-4 h-4" />;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const renderFileIconBg = (type?: string) => {
|
|
373
|
+
if (!type) return 'bg-foreground/5 text-foreground/40';
|
|
374
|
+
if (type.startsWith('image/')) return 'bg-emerald-500/10 text-emerald-500';
|
|
375
|
+
if (type.startsWith('video/') || type.startsWith('audio/')) return 'bg-purple-500/10 text-purple-500';
|
|
376
|
+
if (type.includes('pdf') || type.includes('word') || type.includes('text')) return 'bg-blue-500/10 text-blue-500';
|
|
377
|
+
if (type.includes('zip') || type.includes('rar') || type.includes('archive')) return 'bg-yellow-500/10 text-yellow-500';
|
|
378
|
+
if (type.includes('html') || type.includes('csv') || type.includes('json')) return 'bg-emerald-500/10 text-emerald-500';
|
|
379
|
+
return 'bg-foreground/5 text-foreground/40';
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const formatExtension = (type?: string) => {
|
|
383
|
+
if (!type) return 'FILE';
|
|
384
|
+
const mimeMap: Record<string, string> = {
|
|
385
|
+
'application/pdf': 'PDF',
|
|
386
|
+
'application/zip': 'ZIP',
|
|
387
|
+
'application/x-zip-compressed': 'ZIP',
|
|
388
|
+
'application/x-rar-compressed': 'RAR',
|
|
389
|
+
'application/x-7z-compressed': '7Z',
|
|
390
|
+
'application/json': 'JSON',
|
|
391
|
+
'application/xml': 'XML',
|
|
392
|
+
'application/javascript': 'JS',
|
|
393
|
+
'application/typescript': 'TS',
|
|
394
|
+
'application/msword': 'DOC',
|
|
395
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
|
|
396
|
+
'application/vnd.ms-excel': 'XLS',
|
|
397
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
|
|
398
|
+
'application/vnd.ms-powerpoint': 'PPT',
|
|
399
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPTX',
|
|
400
|
+
'application/rtf': 'RTF',
|
|
401
|
+
'application/x-tar': 'TAR',
|
|
402
|
+
'application/gzip': 'GZ',
|
|
403
|
+
'text/plain': 'TXT',
|
|
404
|
+
'text/html': 'HTML',
|
|
405
|
+
'text/css': 'CSS',
|
|
406
|
+
'text/csv': 'CSV',
|
|
407
|
+
'text/markdown': 'MD',
|
|
408
|
+
};
|
|
409
|
+
const lower = type.toLowerCase();
|
|
410
|
+
if (mimeMap[lower]) return mimeMap[lower];
|
|
411
|
+
const parts = lower.split('/');
|
|
412
|
+
const sub = parts[parts.length - 1];
|
|
413
|
+
const clean = sub.replace(/^x-/, '').replace(/^vnd\..*\./, '');
|
|
414
|
+
return clean.toUpperCase().slice(0, 10);
|
|
415
|
+
};
|
|
416
|
+
|
|
263
417
|
const formatSize = (bytes: number) => {
|
|
264
418
|
if (!bytes) return '0 B';
|
|
265
419
|
const k = 1024;
|
|
@@ -357,7 +511,7 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
357
511
|
{config.tabTitle || config.tabName || 'Dashboard'}
|
|
358
512
|
</h2>
|
|
359
513
|
<p className="text-[13px] font-medium text-foreground/50 mt-1">
|
|
360
|
-
{loading ? 'Loading…' : `${records.length} ${records.length === 1 ? 'item' : 'items'}
|
|
514
|
+
{loading ? 'Loading…' : `${records.length} ${records.length === 1 ? 'item' : 'items'}`}
|
|
361
515
|
</p>
|
|
362
516
|
</div>
|
|
363
517
|
|
|
@@ -369,11 +523,10 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
369
523
|
whileTap={{ scale: 0.95 }}
|
|
370
524
|
onClick={handleCopyPrompt}
|
|
371
525
|
title={copied ? 'Copied!' : 'Copy Developer Prompt'}
|
|
372
|
-
className={`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer ${
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}`}
|
|
526
|
+
className={`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer ${copied
|
|
527
|
+
? 'bg-emerald-500/10 text-emerald-500'
|
|
528
|
+
: 'btn-secondary'
|
|
529
|
+
}`}
|
|
377
530
|
>
|
|
378
531
|
{copied ? <Check className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
|
|
379
532
|
</motion.button>
|
|
@@ -394,9 +547,8 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
394
547
|
onClick={() => fileInputRef.current?.click()}
|
|
395
548
|
disabled={uploading}
|
|
396
549
|
title={config.buttonStorageText || 'Upload File'}
|
|
397
|
-
className={`h-10 flex items-center justify-center gap-2 rounded-xl transition-all duration-300 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
398
|
-
|
|
399
|
-
} ${isPrimaryStorage ? 'btn-primary' : 'btn-secondary'}`}
|
|
550
|
+
className={`h-10 flex items-center justify-center gap-2 rounded-xl transition-all duration-300 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed ${config.buttonStorageText ? 'px-4' : 'w-10'
|
|
551
|
+
} ${isPrimaryStorage ? 'btn-primary' : 'btn-secondary'}`}
|
|
400
552
|
>
|
|
401
553
|
{uploading ? (
|
|
402
554
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
@@ -417,9 +569,8 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
417
569
|
whileTap={{ scale: 0.95 }}
|
|
418
570
|
onClick={() => setShowAddForm(true)}
|
|
419
571
|
title="Add New"
|
|
420
|
-
className={`h-10 flex items-center justify-center gap-2 rounded-xl transition-all duration-300 cursor-pointer ${
|
|
421
|
-
|
|
422
|
-
} ${isPrimaryBtn ? 'btn-primary' : 'btn-secondary'}`}
|
|
572
|
+
className={`h-10 flex items-center justify-center gap-2 rounded-xl transition-all duration-300 cursor-pointer ${config.buttonText && config.buttonText !== '+' ? 'px-5' : 'w-10'
|
|
573
|
+
} ${isPrimaryBtn ? 'btn-primary' : 'btn-secondary'}`}
|
|
423
574
|
>
|
|
424
575
|
{config.buttonText === '+' || !config.buttonText ? (
|
|
425
576
|
<Plus className="w-4 h-4" />
|
|
@@ -465,99 +616,94 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
465
616
|
const isEditing = editingId === record.id;
|
|
466
617
|
const owned = isOwner(record);
|
|
467
618
|
return (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
isEditing ? 'border-accent/40 shadow-lg ring-1 ring-accent/20' : 'border-[var(--panel-border)] hover:border-accent/30 hover:shadow-lg'
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
<
|
|
528
|
-
|
|
619
|
+
<motion.div
|
|
620
|
+
key={record.id}
|
|
621
|
+
initial={false}
|
|
622
|
+
animate={{ opacity: 1, y: 0 }}
|
|
623
|
+
exit={{ opacity: 0, scale: 0.95 }}
|
|
624
|
+
onClick={() => startEdit(record)}
|
|
625
|
+
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'
|
|
626
|
+
}`}
|
|
627
|
+
>
|
|
628
|
+
{/* Top-right action icons — download + edit + delete */}
|
|
629
|
+
{true && (
|
|
630
|
+
<div className="absolute top-3 right-3 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-all z-10">
|
|
631
|
+
{record.recordType === 'file' && record.downloadURL && (
|
|
632
|
+
<>
|
|
633
|
+
<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">
|
|
634
|
+
{copiedLink === record.id ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Link className="w-3.5 h-3.5" />}
|
|
635
|
+
</button>
|
|
636
|
+
<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">
|
|
637
|
+
<Download className="w-3.5 h-3.5" />
|
|
638
|
+
</a>
|
|
639
|
+
</>
|
|
640
|
+
)}
|
|
641
|
+
{owned && (
|
|
642
|
+
<>
|
|
643
|
+
<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">
|
|
644
|
+
<Edit2 className="w-3.5 h-3.5" />
|
|
645
|
+
</button>
|
|
646
|
+
<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">
|
|
647
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
648
|
+
</button>
|
|
649
|
+
</>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
|
|
654
|
+
<div className="flex items-start gap-3 mb-2">
|
|
655
|
+
<div
|
|
656
|
+
onClick={(e) => { e.stopPropagation(); if (record.recordType === 'file') setPreviewFile(record); }}
|
|
657
|
+
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'}`}
|
|
658
|
+
>
|
|
659
|
+
{record.recordType === 'file' ? renderFileIcon(record.fileType, record.downloadURL) : <div className="w-2 h-2 rounded-full bg-current" />}
|
|
660
|
+
</div>
|
|
661
|
+
<div className="flex flex-col min-w-0 flex-1 relative">
|
|
662
|
+
{renamingFileId === record.id ? (
|
|
663
|
+
<input
|
|
664
|
+
autoFocus
|
|
665
|
+
type="text"
|
|
666
|
+
value={renameFileName}
|
|
667
|
+
onChange={e => setRenameFileName(e.target.value)}
|
|
668
|
+
onBlur={() => handleRenameFile()}
|
|
669
|
+
onKeyDown={e => { if (e.key === 'Enter') handleRenameFile(); if (e.key === 'Escape') setRenamingFileId(null); }}
|
|
670
|
+
onClick={e => e.stopPropagation()}
|
|
671
|
+
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"
|
|
672
|
+
/>
|
|
673
|
+
) : (
|
|
674
|
+
<span className="text-[14px] font-bold text-foreground truncate pr-12">
|
|
675
|
+
{record.name}
|
|
676
|
+
</span>
|
|
677
|
+
)}
|
|
678
|
+
{record.recordType === 'file' && <div className="absolute inset-0 z-0 cursor-pointer" onClick={() => setPreviewFile(record)} />}
|
|
679
|
+
<span className="text-[11px] text-foreground/35 font-bold uppercase tracking-wider mt-0.5">
|
|
680
|
+
{formatDate(record.createdAt)}
|
|
529
681
|
</span>
|
|
530
|
-
|
|
531
|
-
<span className="text-[11px] text-foreground/35 font-bold uppercase tracking-wider mt-0.5">
|
|
532
|
-
{formatDate(record.createdAt)}
|
|
533
|
-
</span>
|
|
682
|
+
</div>
|
|
534
683
|
</div>
|
|
535
|
-
</div>
|
|
536
684
|
|
|
537
|
-
|
|
538
|
-
<textarea
|
|
539
|
-
value={editNote}
|
|
540
|
-
onChange={e => setEditNote(e.target.value)}
|
|
541
|
-
onBlur={handleSaveEdit}
|
|
542
|
-
onKeyDown={e => { if (e.key === 'Escape') setEditingId(null); }}
|
|
543
|
-
placeholder="Note (optional)"
|
|
544
|
-
rows={2}
|
|
545
|
-
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"
|
|
546
|
-
/>
|
|
547
|
-
) : (
|
|
548
|
-
record.note && (
|
|
685
|
+
{record.note && (
|
|
549
686
|
<p className="text-[13px] text-foreground/55 font-medium leading-relaxed line-clamp-3 mb-1">
|
|
550
687
|
{record.note}
|
|
551
688
|
</p>
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
|
|
689
|
+
)}
|
|
690
|
+
{/* Extra custom fields display */}
|
|
691
|
+
{Object.entries(record as any)
|
|
692
|
+
.filter(([k]) => !['name','note','uid','recordType','fileName','fileType','fileSize','downloadURL','createdAt','creatorName','creatorEmail','creatorAvatar'].includes(k) && k !== 'id')
|
|
693
|
+
.slice(0, 3)
|
|
694
|
+
.map(([k, v]) => (
|
|
695
|
+
<div key={k} className="flex items-center gap-1.5 mt-1">
|
|
696
|
+
<span className="text-[10px] font-bold text-foreground/30 uppercase tracking-wider shrink-0">{k}:</span>
|
|
697
|
+
<span className="text-[11px] font-semibold text-foreground/60 truncate">{String(v)}</span>
|
|
698
|
+
</div>
|
|
699
|
+
))
|
|
700
|
+
}
|
|
555
701
|
|
|
556
702
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
703
|
+
{/* Creator profile badge */}
|
|
704
|
+
<CreatorBadge record={record} />
|
|
705
|
+
</motion.div>
|
|
706
|
+
);
|
|
561
707
|
})}
|
|
562
708
|
</AnimatePresence>
|
|
563
709
|
</div>
|
|
@@ -569,8 +715,8 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
569
715
|
<div className="flex items-start gap-2 text-[11px] text-foreground/30 font-medium leading-relaxed">
|
|
570
716
|
<Info className="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
|
571
717
|
<span>
|
|
572
|
-
Shared between all admins · Edit and delete available on your own items only ·
|
|
573
|
-
Records: <code className="text-foreground/40 font-mono text-[10px]">{recordsCollection}</code> ·
|
|
718
|
+
Shared between all admins · Edit and delete available on your own items only ·
|
|
719
|
+
Records: <code className="text-foreground/40 font-mono text-[10px]">{recordsCollection}</code> ·
|
|
574
720
|
Files: <code className="text-foreground/40 font-mono text-[10px]">admin_files</code> → Drive / Admin
|
|
575
721
|
</span>
|
|
576
722
|
</div>
|
|
@@ -642,13 +788,256 @@ export function AdminPageTemplate({ config }: { config: any }) {
|
|
|
642
788
|
)}
|
|
643
789
|
</AnimatePresence>
|
|
644
790
|
|
|
791
|
+
{/* ── File Preview Modal (Drive-style) ───────────────────────────────────── */}
|
|
792
|
+
{typeof window !== 'undefined' && createPortal(
|
|
793
|
+
<AnimatePresence>
|
|
794
|
+
{previewFile && (
|
|
795
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 md:p-12">
|
|
796
|
+
<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" />
|
|
797
|
+
<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">
|
|
798
|
+
<X className="w-5 h-5" />
|
|
799
|
+
</button>
|
|
800
|
+
<motion.div
|
|
801
|
+
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
|
802
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
803
|
+
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
|
804
|
+
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)]"
|
|
805
|
+
>
|
|
806
|
+
{/* File Header */}
|
|
807
|
+
<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">
|
|
808
|
+
<div className="flex flex-col min-w-0 pr-4">
|
|
809
|
+
<span className="text-xl font-extrabold text-foreground truncate">{previewFile.fileName || previewFile.name}</span>
|
|
810
|
+
<span className="text-[12px] text-foreground/70 font-bold uppercase tracking-wider mt-1">
|
|
811
|
+
{formatExtension(previewFile.fileType)} • {formatSize(previewFile.fileSize || 0)}
|
|
812
|
+
</span>
|
|
813
|
+
</div>
|
|
814
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
815
|
+
<button
|
|
816
|
+
onClick={() => { navigator.clipboard.writeText(previewFile.downloadURL!); setCopiedLink('preview'); setTimeout(() => setCopiedLink(null), 2000); }}
|
|
817
|
+
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"
|
|
818
|
+
title="Copy Link"
|
|
819
|
+
>
|
|
820
|
+
{copiedLink === 'preview' ? <Check className="w-4 h-4 text-emerald-500" /> : <Link className="w-4 h-4" />}
|
|
821
|
+
</button>
|
|
822
|
+
<button
|
|
823
|
+
onClick={() => { setPreviewFile(null); setRenamingFileId(previewFile.id); setRenameFileName(previewFile.name); }}
|
|
824
|
+
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"
|
|
825
|
+
title="Rename"
|
|
826
|
+
>
|
|
827
|
+
<Edit2 className="w-4 h-4" />
|
|
828
|
+
</button>
|
|
829
|
+
<button
|
|
830
|
+
onClick={() => { setPreviewFile(null); setDeleteConfirm(previewFile.id); }}
|
|
831
|
+
className="p-2.5 bg-red-500/10 hover:bg-red-500/20 text-red-500 transition-colors rounded-xl shadow-sm cursor-pointer"
|
|
832
|
+
title="Delete"
|
|
833
|
+
>
|
|
834
|
+
<Trash2 className="w-4 h-4" />
|
|
835
|
+
</button>
|
|
836
|
+
<a
|
|
837
|
+
href={previewFile.downloadURL}
|
|
838
|
+
download
|
|
839
|
+
target="_blank"
|
|
840
|
+
rel="noreferrer"
|
|
841
|
+
className="p-2.5 btn-primary transition-colors rounded-xl shadow-lg ml-1"
|
|
842
|
+
title="Download"
|
|
843
|
+
>
|
|
844
|
+
<Download className="w-4 h-4" />
|
|
845
|
+
</a>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
{/* Preview Content */}
|
|
850
|
+
<div className="flex-1 w-full relative bg-foreground/[0.02] min-h-[50vh] md:min-h-[600px]" onClick={() => setPreviewFile(null)}>
|
|
851
|
+
<div className="absolute inset-x-4 inset-y-8 md:inset-x-12 md:inset-y-12 flex items-center justify-center">
|
|
852
|
+
{previewLoading && (
|
|
853
|
+
<div className="absolute inset-0 flex items-center justify-center rounded-lg z-50 pointer-events-none">
|
|
854
|
+
<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">
|
|
855
|
+
<Loader2 className="w-5 h-5 text-accent animate-spin" />
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
)}
|
|
859
|
+
{previewFile.fileType?.startsWith('image/') ? (
|
|
860
|
+
<img
|
|
861
|
+
onLoad={() => setPreviewLoading(false)}
|
|
862
|
+
onError={() => setPreviewLoading(false)}
|
|
863
|
+
src={previewFile.downloadURL}
|
|
864
|
+
alt={previewFile.fileName || previewFile.name}
|
|
865
|
+
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'}`}
|
|
866
|
+
onClick={e => e.stopPropagation()}
|
|
867
|
+
/>
|
|
868
|
+
) : previewFile.fileType?.startsWith('video/') ? (
|
|
869
|
+
<video
|
|
870
|
+
onLoadedData={() => setPreviewLoading(false)}
|
|
871
|
+
onError={() => setPreviewLoading(false)}
|
|
872
|
+
src={previewFile.downloadURL}
|
|
873
|
+
controls
|
|
874
|
+
autoPlay
|
|
875
|
+
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'}`}
|
|
876
|
+
onClick={e => e.stopPropagation()}
|
|
877
|
+
/>
|
|
878
|
+
) : previewFile.fileType === 'application/pdf' ? (
|
|
879
|
+
<iframe
|
|
880
|
+
onLoad={() => setPreviewLoading(false)}
|
|
881
|
+
src={previewFile.downloadURL}
|
|
882
|
+
className={`w-full h-full border-none rounded-lg relative z-10 transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`}
|
|
883
|
+
style={{ backgroundColor: 'white' }}
|
|
884
|
+
title="PDF Preview"
|
|
885
|
+
onClick={e => e.stopPropagation()}
|
|
886
|
+
/>
|
|
887
|
+
) : (
|
|
888
|
+
<div
|
|
889
|
+
className="w-full h-full flex flex-col items-center justify-center text-foreground/40 text-center relative z-10"
|
|
890
|
+
onClick={e => { e.stopPropagation(); setPreviewLoading(false); }}
|
|
891
|
+
>
|
|
892
|
+
<FileText className="w-24 h-24 mb-6 opacity-40 drop-shadow-sm" />
|
|
893
|
+
<span className="text-[20px] font-extrabold text-foreground">Preview Unavailable</span>
|
|
894
|
+
<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>
|
|
895
|
+
</div>
|
|
896
|
+
)}
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
</motion.div>
|
|
900
|
+
</div>
|
|
901
|
+
)}
|
|
902
|
+
</AnimatePresence>,
|
|
903
|
+
document.body
|
|
904
|
+
)}
|
|
905
|
+
|
|
906
|
+
{/* ── Edit Record Modal ──────────────────────────────────────────────── */}
|
|
907
|
+
<AnimatePresence>
|
|
908
|
+
{editingId && (
|
|
909
|
+
<div className="fixed inset-0 z-[120] flex items-center justify-center p-4">
|
|
910
|
+
<motion.div
|
|
911
|
+
onClick={() => handleSaveEdit()}
|
|
912
|
+
initial={{ opacity: 0 }}
|
|
913
|
+
animate={{ opacity: 1 }}
|
|
914
|
+
exit={{ opacity: 0 }}
|
|
915
|
+
className="absolute inset-0 bg-background/60 backdrop-blur-sm cursor-pointer"
|
|
916
|
+
/>
|
|
917
|
+
<motion.div
|
|
918
|
+
initial={{ opacity: 0, scale: 0.97, y: 6 }}
|
|
919
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
920
|
+
exit={{ opacity: 0, scale: 0.97, y: 6 }}
|
|
921
|
+
transition={{ duration: 0.2 }}
|
|
922
|
+
className="w-full max-w-[440px] border-[var(--panel-border)] border rounded-2xl relative z-10 glass-panel shadow-2xl bg-background overflow-hidden"
|
|
923
|
+
>
|
|
924
|
+
<div className="p-5 flex flex-col gap-3 max-h-[80vh] overflow-y-auto">
|
|
925
|
+
{/* Header */}
|
|
926
|
+
<div className="flex items-center justify-between mb-2 mt-2 px-1">
|
|
927
|
+
<div className="flex items-center gap-2">
|
|
928
|
+
<span className="text-[14px] font-bold text-foreground">Edit Record</span>
|
|
929
|
+
{saving && <Loader2 className="w-3 h-3 animate-spin text-foreground/40" />}
|
|
930
|
+
</div>
|
|
931
|
+
<button
|
|
932
|
+
type="button"
|
|
933
|
+
onClick={() => handleSaveEdit()}
|
|
934
|
+
className="text-foreground/30 hover:text-foreground/60 transition-colors cursor-pointer"
|
|
935
|
+
>
|
|
936
|
+
<X className="w-4 h-4" />
|
|
937
|
+
</button>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<div className="flex flex-col gap-0 max-h-[70vh] overflow-y-auto no-scrollbar px-1">
|
|
941
|
+
{/* Name field */}
|
|
942
|
+
<div className="flex flex-col relative group/field">
|
|
943
|
+
<input
|
|
944
|
+
readOnly
|
|
945
|
+
value="NAME"
|
|
946
|
+
className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none pt-3 pb-0.5"
|
|
947
|
+
/>
|
|
948
|
+
<input
|
|
949
|
+
autoFocus
|
|
950
|
+
required
|
|
951
|
+
value={editName}
|
|
952
|
+
onChange={e => setEditName(e.target.value)}
|
|
953
|
+
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
|
|
954
|
+
onBlur={() => handleSilentSave()}
|
|
955
|
+
placeholder="Name *"
|
|
956
|
+
className="no-glow bg-transparent text-[13px] font-medium text-foreground outline-none placeholder:text-foreground/15 pb-1 caret-foreground/50"
|
|
957
|
+
/>
|
|
958
|
+
<div className="border-b border-dashed border-[var(--panel-border)]/60" />
|
|
959
|
+
</div>
|
|
960
|
+
|
|
961
|
+
{/* Note field */}
|
|
962
|
+
<div className="flex flex-col relative group/field">
|
|
963
|
+
<input
|
|
964
|
+
readOnly
|
|
965
|
+
value="NOTE"
|
|
966
|
+
className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none pt-3 pb-0.5"
|
|
967
|
+
/>
|
|
968
|
+
<textarea
|
|
969
|
+
ref={el => { if (el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; } }}
|
|
970
|
+
value={editNote}
|
|
971
|
+
onChange={e => { setEditNote(e.target.value); e.target.style.height = 'auto'; e.target.style.height = e.target.scrollHeight + 'px'; }}
|
|
972
|
+
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); (e.target as HTMLTextAreaElement).blur(); } }}
|
|
973
|
+
onBlur={() => handleSilentSave()}
|
|
974
|
+
placeholder="Add a note..."
|
|
975
|
+
rows={1}
|
|
976
|
+
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"
|
|
977
|
+
/>
|
|
978
|
+
<div className="border-b border-dashed border-[var(--panel-border)]/60" />
|
|
979
|
+
</div>
|
|
980
|
+
|
|
981
|
+
{/* Custom Fields */}
|
|
982
|
+
{editFields.map((field, idx) => (
|
|
983
|
+
<div key={idx} className="flex flex-col relative group/field">
|
|
984
|
+
<input
|
|
985
|
+
placeholder="FIELD NAME"
|
|
986
|
+
value={field.key}
|
|
987
|
+
onChange={e => {
|
|
988
|
+
const next = [...editFields];
|
|
989
|
+
next[idx].key = e.target.value;
|
|
990
|
+
setEditFields(next);
|
|
991
|
+
}}
|
|
992
|
+
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
|
|
993
|
+
onBlur={() => handleSilentSave()}
|
|
994
|
+
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"
|
|
995
|
+
/>
|
|
996
|
+
<input
|
|
997
|
+
placeholder="Field value..."
|
|
998
|
+
value={field.value}
|
|
999
|
+
onChange={e => {
|
|
1000
|
+
const next = [...editFields];
|
|
1001
|
+
next[idx].value = e.target.value;
|
|
1002
|
+
setEditFields(next);
|
|
1003
|
+
}}
|
|
1004
|
+
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
|
|
1005
|
+
onBlur={() => handleSilentSave()}
|
|
1006
|
+
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"
|
|
1007
|
+
/>
|
|
1008
|
+
<div className="border-b border-dashed border-[var(--panel-border)]/60" />
|
|
1009
|
+
<button
|
|
1010
|
+
type="button"
|
|
1011
|
+
onClick={() => setEditFields(editFields.filter((_, i) => i !== idx))}
|
|
1012
|
+
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"
|
|
1013
|
+
>
|
|
1014
|
+
<X className="w-3 h-3" />
|
|
1015
|
+
</button>
|
|
1016
|
+
</div>
|
|
1017
|
+
))}
|
|
1018
|
+
|
|
1019
|
+
{/* Add Field */}
|
|
1020
|
+
<button
|
|
1021
|
+
type="button"
|
|
1022
|
+
onClick={() => setEditFields([...editFields, { key: '', value: '' }])}
|
|
1023
|
+
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"
|
|
1024
|
+
>
|
|
1025
|
+
<Plus className="w-3 h-3" /> Add Field
|
|
1026
|
+
</button>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
</motion.div>
|
|
1030
|
+
</div>
|
|
1031
|
+
)}
|
|
1032
|
+
</AnimatePresence>
|
|
1033
|
+
|
|
645
1034
|
{/* ── Delete Confirmation ────────────────────────────────────────────── */}
|
|
646
1035
|
<ConfirmModal
|
|
647
1036
|
isOpen={!!deleteConfirm}
|
|
648
1037
|
onCancel={() => setDeleteConfirm(null)}
|
|
649
1038
|
onConfirm={handleDelete}
|
|
650
1039
|
title="Delete Record"
|
|
651
|
-
message=
|
|
1040
|
+
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.</>}
|
|
652
1041
|
confirmText="Delete"
|
|
653
1042
|
isProcessing={isDeleting}
|
|
654
1043
|
/>
|