| import { useState } from "react"; | |
| import { useResearchObjectRepository } from "@/hooks/useResearchObjectRepository"; | |
| import { ObjectUploader } from "./ObjectUploader"; | |
| import { Search, Plus, Grid3X3, List, Download, Eye, Trash2, ExternalLink, Database } from "lucide-react"; | |
| // โ Single definition and export | |
| export const TYPE_ICONS: Record<string, string> = { | |
| dataset: "๐", | |
| code: "๐ป", | |
| software: "๐งฉ", | |
| presentation: "๐ฝ๏ธ", | |
| poster: "๐ผ๏ธ", | |
| video: "๐ฌ", | |
| image: "๐ท", | |
| document: "๐", | |
| paper: "๐", | |
| other: "๐", | |
| }; | |
| export function RepositoryBrowser() { | |
| const { objects, loading, filter, setFilter, search, setSearch, uploading, uploadProgress, uploadObject, deleteObject } = useResearchObjectRepository(); | |
| const [showUpload, setShowUpload] = useState(false); | |
| const [view, setView] = useState<"grid" | "list">("grid"); | |
| if (loading) return <div className="text-center py-8">Loading research objects...</div>; | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <h2 className="text-xl font-bold flex items-center gap-2"> | |
| <Database className="w-5 h-5 text-primary" /> Research Objects ({objects.length}) | |
| </h2> | |
| <div className="flex gap-2"> | |
| <button onClick={() => setView("grid")} className={`p-1.5 rounded ${view === "grid" ? "bg-primary/10 text-primary" : "text-gray-400"}`}> | |
| <Grid3X3 className="w-4 h-4" /> | |
| </button> | |
| <button onClick={() => setView("list")} className={`p-1.5 rounded ${view === "list" ? "bg-primary/10 text-primary" : "text-gray-400"}`}> | |
| <List className="w-4 h-4" /> | |
| </button> | |
| <button onClick={() => setShowUpload(true)} className="btn btn-primary btn-sm gap-1.5"> | |
| <Plus className="w-4 h-4" /> Upload | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex gap-2"> | |
| <div className="relative flex-1 max-w-xs"> | |
| <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" /> | |
| <input | |
| type="text" | |
| value={search} | |
| onChange={e => setSearch(e.target.value)} | |
| placeholder="Search..." | |
| className="w-full pl-8 pr-3 py-1.5 text-sm border rounded-lg bg-white/10" | |
| /> | |
| </div> | |
| <select aria-label="Select option" value={filter} onChange={e => setFilter(e.target.value as any)} className="text-sm border rounded-lg px-3 py-1.5 bg-white/10"> | |
| <option value="all">All Types</option> | |
| {Object.keys(TYPE_ICONS).map(k => <option key={k} value={k}>{k}</option>)} | |
| </select> | |
| </div> | |
| {view === "grid" ? ( | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| {objects.map(obj => ( | |
| <div key={obj.id} className="group relative bg-white/5 rounded-xl border border-white/10 overflow-hidden hover:shadow-md transition"> | |
| <div className="aspect-video bg-white/10 flex items-center justify-center text-4xl">{TYPE_ICONS[obj.type] || "๐"}</div> | |
| <div className="p-3"> | |
| <p className="text-sm font-semibold truncate">{obj.title}</p> | |
| <p className="text-xs text-gray-500 mt-1">{(obj.authors || []).slice(0, 2).join(", ")}</p> | |
| <div className="flex items-center gap-2 mt-2 text-xs text-gray-400"> | |
| <Eye className="w-3 h-3" /> {obj.viewCount || 0} ยท <Download className="w-3 h-3" /> {obj.downloadCount || 0} | |
| </div> | |
| {obj.doi && ( | |
| <a href={`https://doi.org/${obj.doi}`} target="_blank" rel="noopener noreferrer" className="text-[10px] text-primary hover:underline"> | |
| <ExternalLink className="w-3 h-3 inline" /> DOI | |
| </a> | |
| )} | |
| </div> | |
| <button onClick={() => deleteObject(obj.id)} className="absolute top-2 right-2 p-1 bg-red-500/80 text-white rounded-full opacity-0 group-hover:opacity-100 transition"> | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {objects.map(obj => ( | |
| <div key={obj.id} className="flex items-center justify-between p-3 rounded-lg bg-white/5 hover:bg-white/10 transition"> | |
| <div className="flex items-center gap-3"> | |
| <span className="text-2xl">{TYPE_ICONS[obj.type] || "๐"}</span> | |
| <div> | |
| <p className="font-semibold text-sm">{obj.title}</p> | |
| <p className="text-xs text-gray-500">{(obj.authors || []).join(", ")} ยท v{obj.version || 1}</p> | |
| </div> | |
| </div> | |
| <div className="flex gap-1"> | |
| <a href={obj.fileUrl} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-xs"> | |
| <Download className="w-3.5 h-3.5" /> | |
| </a> | |
| <button onClick={() => deleteObject(obj.id)} className="btn btn-ghost btn-xs text-red-500"> | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {showUpload && ( | |
| <ObjectUploader | |
| onUpload={uploadObject} | |
| onClose={() => setShowUpload(false)} | |
| uploading={uploading} | |
| progress={uploadProgress} | |
| /> | |
| )} | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 5.49 kB
- Xet hash:
- 2fb628192b08e31d00d7816c4aa6167a5e033c802713f2b34f63ec9f4a5a0d09
ยท
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.