| import { useNavigate } from "react-router-dom"; | |
| import { Folder, Trash2, Lock, Globe, Loader2 } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| interface Collection { | |
| id: string; | |
| name: string; | |
| description?: string; | |
| articleCount?: number; | |
| paperCount?: number; | |
| isPublic: boolean; | |
| createdAt: string; | |
| updatedAt: string; | |
| ownerUsername: string; | |
| } | |
| interface Props { | |
| collection: Collection; | |
| onDelete: (id: string) => Promise<void> | void; | |
| isDeleting?: boolean; | |
| } | |
| export function CollectionCard({ collection, onDelete, isDeleting }: Props) { | |
| const navigate = useNavigate(); | |
| const handleCardClick = () => { | |
| navigate(`/collections/${collection.id}`); | |
| }; | |
| const handleDelete = async (e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| if (!confirm(`Are you sure you want to delete "${collection.name}"?`)) { | |
| return; | |
| } | |
| try { | |
| await onDelete(collection.id); | |
| toast.success("Collection deleted successfully"); | |
| } catch (error) { | |
| console.error("Delete error:", error); | |
| toast.error("Failed to delete collection"); | |
| } | |
| }; | |
| return ( | |
| <div | |
| onClick={handleCardClick} | |
| className="group relative cursor-pointer rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 transition-all duration-200 hover:shadow-lg hover:border-primary/50 hover:-translate-y-1" | |
| > | |
| <div className="flex items-start justify-between"> | |
| {/* Icon */} | |
| <div className="p-2 rounded-lg bg-primary/10"> | |
| <Folder className="w-6 h-6 text-primary" /> | |
| </div> | |
| {/* Delete Button */} | |
| <button | |
| onClick={handleDelete} | |
| disabled={isDeleting} | |
| className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-lg text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50" | |
| title="Delete collection" | |
| > | |
| {isDeleting ? ( | |
| <Loader2 className="w-4 h-4 animate-spin" /> | |
| ) : ( | |
| <Trash2 className="w-4 h-4" /> | |
| )} | |
| </button> | |
| </div> | |
| {/* Title */} | |
| <h3 className="mt-3 line-clamp-1 text-lg font-semibold text-gray-900 dark:text-gray-100"> | |
| {collection.name || "Untitled Collection"} | |
| </h3> | |
| {/* Description */} | |
| {collection.description && ( | |
| <p className="mt-1 line-clamp-2 text-sm text-gray-500 dark:text-gray-400"> | |
| {collection.description} | |
| </p> | |
| )} | |
| {/* Stats */} | |
| <div className="mt-4 flex items-center justify-between text-xs"> | |
| <span className="text-gray-500 dark:text-gray-400"> | |
| {(collection.paperCount ?? collection.articleCount ?? 0)} paper{(collection.paperCount ?? collection.articleCount ?? 0) !== 1 ? "s" : ""} | |
| </span> | |
| {/* Privacy Badge */} | |
| <div className="flex items-center gap-1.5"> | |
| {collection.isPublic ? ( | |
| <> | |
| <Globe className="w-3 h-3 text-green-500" /> | |
| <span className="text-green-600 dark:text-green-400">Public</span> | |
| </> | |
| ) : ( | |
| <> | |
| <Lock className="w-3 h-3 text-amber-500" /> | |
| <span className="text-amber-600 dark:text-amber-400">Private</span> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| {/* Created Date */} | |
| <div className="mt-2 text-xs text-gray-400 dark:text-gray-500"> | |
| Created {new Date(collection.createdAt).toLocaleDateString()} | |
| </div> | |
| {/* Hover Effect Border */} | |
| <div className="absolute inset-0 rounded-xl border-2 border-transparent group-hover:border-primary/20 pointer-events-none transition-colors" /> | |
| </div> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 3.73 kB
- Xet hash:
- cb19f26dfe29b0035d20c1cfd9504df958ee097c6ef659956baee756f60b8bb3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.