Spaces:
Running
Running
Download src/components/NotesView.tsx from harma-boutique-2026/Notes: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/spaces/harma-boutique-2026/Notes/resolve/main/src/components/NotesView.tsx
- Command line
-
hf download hf://spaces/harma-boutique-2026/Notes/src/components/NotesView.tsx
-
curl -L -o NotesView.tsx https://huggingface.co/spaces/harma-boutique-2026/Notes/resolve/main/src/components/NotesView.tsx
10.9 kB
| import React, { useState } from 'react'; | |
| import { | |
| FileText, | |
| Image as ImageIcon, | |
| Download, | |
| Trash2, | |
| Eye, | |
| User as UserIcon, | |
| Calendar, | |
| X, | |
| FileSpreadsheet, | |
| AlertCircle, | |
| ZoomIn, | |
| } from 'lucide-react'; | |
| import { NoteItem, User } from '../types'; | |
| interface NotesViewProps { | |
| notes: NoteItem[]; | |
| currentUser: User; | |
| onDeleteNote: (noteId: string) => Promise<void>; | |
| searchQuery: string; | |
| } | |
| export const NotesView: React.FC<NotesViewProps> = ({ | |
| notes, | |
| currentUser, | |
| onDeleteNote, | |
| searchQuery, | |
| }) => { | |
| const [selectedImage, setSelectedImage] = useState<NoteItem | null>(null); | |
| const [deletingId, setDeletingId] = useState<string | null>(null); | |
| const canDelete = (note: NoteItem) => { | |
| const isOwner = note.uploadedBy.toLowerCase() === currentUser.username.toLowerCase(); | |
| const isAdmin = currentUser.role === 'admin'; | |
| const hasSpecialPerm = | |
| currentUser.permissions?.canEditDeleteOthers && | |
| (currentUser.permissions.allowedGroups?.includes('ALL') || | |
| currentUser.permissions.allowedGroups | |
| ?.map((g) => g.toLowerCase()) | |
| .includes(note.subject.toLowerCase())); | |
| return isOwner || isAdmin || hasSpecialPerm; | |
| }; | |
| const handleDelete = async (noteId: string) => { | |
| if (!window.confirm('Are you sure you want to delete this archived note?')) return; | |
| setDeletingId(noteId); | |
| try { | |
| await onDeleteNote(noteId); | |
| } finally { | |
| setDeletingId(null); | |
| } | |
| }; | |
| const formatSize = (bytes: number) => { | |
| if (bytes < 1024) return `${bytes} B`; | |
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | |
| return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | |
| }; | |
| return ( | |
| <div className="space-y-4"> | |
| {/* Header Info */} | |
| <div className="flex items-center justify-between"> | |
| <h3 className="font-bold text-sm text-slate-800 dark:text-slate-200"> | |
| Archived Documents {searchQuery ? `(Search: "${searchQuery}")` : ''} ({notes.length}) | |
| </h3> | |
| </div> | |
| {/* Grid of Notes */} | |
| {notes.length === 0 ? ( | |
| <div className="p-12 text-center bg-white dark:bg-slate-800/60 rounded-2xl border border-dashed border-slate-300 dark:border-slate-700"> | |
| <FileText className="w-12 h-12 text-slate-300 dark:text-slate-600 mx-auto mb-3" /> | |
| <h4 className="font-bold text-slate-700 dark:text-slate-300 text-sm">No notes found</h4> | |
| <p className="text-xs text-slate-500 dark:text-slate-400 mt-1 max-w-sm mx-auto"> | |
| {searchQuery | |
| ? `No archived materials matched your search query "${searchQuery}".` | |
| : 'There are no notes uploaded in this subject folder yet. Click "Upload Notes" to add materials.'} | |
| </p> | |
| </div> | |
| ) : ( | |
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {notes.map((note) => { | |
| const isImg = note.fileType === 'image'; | |
| const userCanDelete = canDelete(note); | |
| return ( | |
| <div | |
| key={note.id} | |
| className="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700/80 shadow-sm hover:shadow-md transition-all flex flex-col overflow-hidden" | |
| > | |
| {/* File Thumbnail / Preview Header */} | |
| <div className="relative h-40 bg-slate-100 dark:bg-slate-900 overflow-hidden flex items-center justify-center border-b border-slate-100 dark:border-slate-700/60"> | |
| {isImg ? ( | |
| <div | |
| className="w-full h-full cursor-pointer relative group" | |
| onClick={() => setSelectedImage(note)} | |
| > | |
| <img | |
| src={note.fileUrl} | |
| alt={note.originalName} | |
| className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" | |
| referrerPolicy="no-referrer" | |
| /> | |
| <div className="absolute inset-0 bg-slate-900/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white gap-1.5 font-medium text-xs"> | |
| <ZoomIn className="w-4 h-4" /> Preview Image | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="p-6 text-center flex flex-col items-center"> | |
| <div className="p-3.5 rounded-2xl bg-blue-100 dark:bg-blue-950/80 text-blue-600 dark:text-blue-400 mb-2"> | |
| <FileText className="w-8 h-8" /> | |
| </div> | |
| <span className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"> | |
| DOCX Document | |
| </span> | |
| <span className="text-[10px] text-slate-400 mt-0.5">{formatSize(note.size)}</span> | |
| </div> | |
| )} | |
| {/* Subject Tag Badge */} | |
| <div className="absolute top-2.5 left-2.5 flex items-center gap-1"> | |
| <span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-900/80 text-white backdrop-blur-md"> | |
| {note.subject} | |
| </span> | |
| {note.subSubject && ( | |
| <span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-600/90 text-white backdrop-blur-md"> | |
| {note.subSubject} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| {/* Body Details */} | |
| <div className="p-4 flex-1 flex flex-col justify-between space-y-3"> | |
| <div> | |
| <h4 | |
| className="font-bold text-sm text-slate-900 dark:text-white line-clamp-1" | |
| title={note.originalName} | |
| > | |
| {note.originalName} | |
| </h4> | |
| {note.description && ( | |
| <p className="text-xs text-slate-500 dark:text-slate-400 mt-1 line-clamp-2"> | |
| {note.description} | |
| </p> | |
| )} | |
| </div> | |
| {/* Upload Metadata: Uploader + Timestamp */} | |
| <div className="pt-2 border-t border-slate-100 dark:border-slate-700/60 text-[11px] text-slate-500 dark:text-slate-400 space-y-1"> | |
| <div className="flex items-center gap-1.5 font-medium text-slate-700 dark:text-slate-300"> | |
| <UserIcon className="w-3.5 h-3.5 text-indigo-500" /> | |
| <span>Uploaded by:</span> | |
| <span className="font-bold text-indigo-600 dark:text-indigo-400"> | |
| {note.uploadedBy} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-1.5"> | |
| <Calendar className="w-3.5 h-3.5 text-slate-400" /> | |
| <span> | |
| {new Date(note.uploadedAt).toLocaleDateString(undefined, { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| })} | |
| </span> | |
| </div> | |
| </div> | |
| {/* Action Buttons */} | |
| <div className="flex items-center justify-between pt-1"> | |
| <a | |
| href={note.fileUrl} | |
| download={note.originalName} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-950/60 dark:hover:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 font-semibold text-xs transition-colors" | |
| > | |
| <Download className="w-3.5 h-3.5" /> | |
| <span>Download</span> | |
| </a> | |
| {userCanDelete && ( | |
| <button | |
| onClick={() => handleDelete(note.id)} | |
| disabled={deletingId === note.id} | |
| className="p-1.5 rounded-lg text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/50 transition-colors" | |
| title="Delete this note" | |
| > | |
| {deletingId === note.id ? ( | |
| <div className="w-4 h-4 border-2 border-rose-500 border-t-transparent rounded-full animate-spin" /> | |
| ) : ( | |
| <Trash2 className="w-4 h-4" /> | |
| )} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| {/* Image Lightbox Modal */} | |
| {selectedImage && ( | |
| <div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4"> | |
| <div className="relative max-w-4xl w-full bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden shadow-2xl p-4"> | |
| <div className="flex items-center justify-between mb-3 pb-2 border-b border-slate-800"> | |
| <div> | |
| <h3 className="font-bold text-slate-100 text-sm">{selectedImage.originalName}</h3> | |
| <p className="text-xs text-slate-400"> | |
| Uploaded by {selectedImage.uploadedBy} in {selectedImage.subject} | |
| </p> | |
| </div> | |
| <button | |
| onClick={() => setSelectedImage(null)} | |
| className="p-1.5 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition-colors" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="max-h-[75vh] overflow-auto flex items-center justify-center bg-black/60 rounded-xl p-2"> | |
| <img | |
| src={selectedImage.fileUrl} | |
| alt={selectedImage.originalName} | |
| className="max-h-[70vh] object-contain rounded-lg" | |
| /> | |
| </div> | |
| <div className="mt-3 flex justify-end gap-2"> | |
| <a | |
| href={selectedImage.fileUrl} | |
| download={selectedImage.originalName} | |
| className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-xs rounded-xl flex items-center gap-1.5 shadow" | |
| > | |
| <Download className="w-4 h-4" /> Download Image | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |