mirror of
https://gitlab.w-hs.de/an14051/backerup-website.git
synced 2026-07-27 17:35:29 +00:00
feat: implement file deletion functionality and enhance deployment script
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import type { FileRecord } from "@/lib/store";
|
||||
|
||||
@@ -54,20 +54,52 @@ function SortIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
function TrashIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BrowseClient({
|
||||
records: allRecords,
|
||||
groups,
|
||||
isAdmin = false,
|
||||
}: {
|
||||
records: FileRecord[];
|
||||
groups: string[];
|
||||
isAdmin?: boolean;
|
||||
}) {
|
||||
const [records, setRecords] = useState<FileRecord[]>(allRecords);
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
|
||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all");
|
||||
const [sort, setSort] = useState<SortOrder>("newest");
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
|
||||
const handleDelete = useCallback(async (token: string, name: string, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Delete "${name}"?\n\nThis permanently removes the file and cannot be undone.`)) return;
|
||||
setDeleting(token);
|
||||
try {
|
||||
const res = await fetch(`/api/files/${token}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
setRecords((prev) => prev.filter((r) => r.token !== token));
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({})) as { error?: string };
|
||||
alert(data.error ?? `Delete failed (${res.status})`);
|
||||
}
|
||||
} catch {
|
||||
alert("Network error – could not delete file");
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let r = allRecords;
|
||||
let r = records;
|
||||
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase();
|
||||
@@ -107,7 +139,7 @@ export default function BrowseClient({
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}, [allRecords, search, selectedGroup, typeFilter, sort]);
|
||||
}, [records, search, selectedGroup, typeFilter, sort]);
|
||||
|
||||
const activeFilters =
|
||||
(search.trim() ? 1 : 0) + (selectedGroup ? 1 : 0) + (typeFilter !== "all" ? 1 : 0);
|
||||
@@ -218,9 +250,9 @@ export default function BrowseClient({
|
||||
|
||||
{/* ── Results count ── */}
|
||||
<p className="mb-5 text-xs text-slate-600">
|
||||
{filtered.length === allRecords.length
|
||||
? `${allRecords.length} file${allRecords.length !== 1 ? "s" : ""} shared by the community`
|
||||
: `${filtered.length} of ${allRecords.length} files`}
|
||||
{filtered.length === records.length
|
||||
? `${records.length} file${records.length !== 1 ? "s" : ""} shared by the community`
|
||||
: `${filtered.length} of ${records.length} files`}
|
||||
</p>
|
||||
|
||||
{/* ── Cards ── */}
|
||||
@@ -241,11 +273,11 @@ export default function BrowseClient({
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{filtered.map((r) => (
|
||||
<Link
|
||||
key={r.token}
|
||||
href={`/files/${r.token}`}
|
||||
className="group rounded-2xl border border-white/5 glass p-5 card-hover block"
|
||||
>
|
||||
<div key={r.token} className="group/card rounded-2xl border border-white/5 glass card-hover overflow-hidden">
|
||||
<Link
|
||||
href={`/files/${r.token}`}
|
||||
className="group block px-5 pt-5 pb-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* Badges */}
|
||||
@@ -295,20 +327,34 @@ export default function BrowseClient({
|
||||
<ArrowIcon />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Meta row */}
|
||||
<div className="mt-4 flex items-center gap-3 text-xs text-slate-600">
|
||||
<span>{formatBytes(r.size)}</span>
|
||||
<span className="text-slate-800">·</span>
|
||||
<span>{formatDate(r.uploadedAt)}</span>
|
||||
{r.containerName && r.displayName && (
|
||||
<>
|
||||
<span className="text-slate-800">·</span>
|
||||
<span className="font-mono truncate">{r.containerName}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Card footer: meta row + admin delete inline */}
|
||||
<div className="flex items-center gap-3 px-5 pb-4 text-xs text-slate-600">
|
||||
<span>{formatBytes(r.size)}</span>
|
||||
<span className="text-slate-800">·</span>
|
||||
<span>{formatDate(r.uploadedAt)}</span>
|
||||
{r.containerName && r.displayName && (
|
||||
<>
|
||||
<span className="text-slate-800">·</span>
|
||||
<span className="font-mono truncate">{r.containerName}</span>
|
||||
</>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={(e) => handleDelete(r.token, r.displayName ?? r.containerName ?? r.originalName, e)}
|
||||
disabled={deleting === r.token}
|
||||
className="ml-auto flex items-center gap-1 rounded-md px-1.5 py-1 opacity-0 group-hover/card:opacity-100 transition-opacity hover:text-rose-400 hover:bg-rose-500/10 active:scale-95 disabled:opacity-40 text-slate-600"
|
||||
title="Delete file"
|
||||
aria-label="Delete file"
|
||||
>
|
||||
{deleting === r.token
|
||||
? <span className="w-3 h-3 rounded-full border border-rose-400 border-t-transparent animate-spin" />
|
||||
: <TrashIcon />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user