Notes / src /components /NotesView.tsx
harma-boutique-2026's picture
Upload 26 files
30bc573 verified
Raw History Blame Contribute Delete
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>
);
};