mirror of
https://gitlab.w-hs.de/an14051/backerup-website.git
synced 2026-07-27 17:35:29 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function BrowseClient({
|
||||
groups,
|
||||
selectedGroup,
|
||||
}: {
|
||||
groups: string[];
|
||||
selectedGroup: string | null;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
function select(g: string | null) {
|
||||
if (g === null) {
|
||||
router.push("/browse");
|
||||
} else {
|
||||
router.push(`/browse?group=${encodeURIComponent(g)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-8 flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => select(null)}
|
||||
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition-colors ${selectedGroup === null
|
||||
? "bg-cyan-500 text-slate-900"
|
||||
: "border border-slate-700 bg-slate-800/50 text-slate-400 hover:border-slate-500 hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{groups.map((g) => (
|
||||
<button
|
||||
key={g}
|
||||
onClick={() => select(g)}
|
||||
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition-colors ${selectedGroup === g
|
||||
? "bg-cyan-500 text-slate-900"
|
||||
: "border border-slate-700 bg-slate-800/50 text-slate-400 hover:border-slate-500 hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
{g}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import Link from "next/link";
|
||||
import { readMeta, listGroups } from "@/lib/store";
|
||||
import type { FileRecord } from "@/lib/store";
|
||||
import BrowseClient from "./BrowseClient";
|
||||
|
||||
function formatBytes(n: number) {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
function TypeBadge({ type }: { type: FileRecord["type"] }) {
|
||||
return type === "backerup" ? (
|
||||
<span className="rounded-full bg-cyan-500/15 px-2.5 py-0.5 text-[11px] font-semibold text-cyan-400">
|
||||
.backerup
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-full bg-violet-500/15 px-2.5 py-0.5 text-[11px] font-semibold text-violet-400">
|
||||
.json config
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function BrowsePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ group?: string }>;
|
||||
}) {
|
||||
const { group: selectedGroup } = await searchParams;
|
||||
const [allRecords, groups] = await Promise.all([readMeta(), listGroups()]);
|
||||
|
||||
const records = selectedGroup
|
||||
? allRecords.filter((r) => r.group === selectedGroup)
|
||||
: allRecords;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl px-6 py-16">
|
||||
<div className="mb-10 flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Browse shared files</h1>
|
||||
<p className="mt-2 text-slate-400">
|
||||
{records.length} file{records.length !== 1 ? "s" : ""}
|
||||
{selectedGroup ? ` in "${selectedGroup}"` : " shared by the community"}.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/share"
|
||||
className="rounded-xl bg-cyan-500 px-6 py-2.5 text-sm font-bold text-slate-900 hover:bg-cyan-400 transition-colors"
|
||||
>
|
||||
+ Share a file
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Group filter bar */}
|
||||
{groups.length > 0 && (
|
||||
<BrowseClient groups={groups} selectedGroup={selectedGroup ?? null} />
|
||||
)}
|
||||
|
||||
{records.length === 0 ? (
|
||||
<div className="rounded-2xl border border-slate-800 bg-slate-900/60 p-16 text-center">
|
||||
<p className="text-4xl">📭</p>
|
||||
<p className="mt-4 text-slate-400">
|
||||
{selectedGroup ? `No files in group "${selectedGroup}".` : "No files shared yet."}
|
||||
</p>
|
||||
<Link href="/share" className="mt-4 inline-block text-sm text-cyan-400 hover:underline">
|
||||
Be the first to share one →
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{records.map((r) => (
|
||||
<Link
|
||||
key={r.token}
|
||||
href={`/files/${r.token}`}
|
||||
className="group rounded-2xl border border-slate-800 bg-slate-900/60 p-5 transition hover:border-slate-600 hover:bg-slate-900"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<TypeBadge type={r.type} />
|
||||
{r.group && (
|
||||
<span className="rounded-full bg-slate-700/60 px-2.5 py-0.5 text-[11px] font-medium text-slate-400">
|
||||
{r.group}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate font-mono text-xs text-slate-500">{r.originalName}</span>
|
||||
</div>
|
||||
{(r.displayName ?? r.containerName) && (
|
||||
<p className="mt-2 font-semibold text-white truncate">
|
||||
{r.displayName ?? r.containerName}
|
||||
</p>
|
||||
)}
|
||||
{r.image && (
|
||||
<p className="mt-0.5 text-xs text-slate-400 truncate">{r.image}</p>
|
||||
)}
|
||||
{r.description && (
|
||||
<p className="mt-2 text-sm text-slate-400 line-clamp-2">{r.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="shrink-0 text-slate-600 group-hover:text-slate-400 transition-colors text-lg">→</span>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-4 text-xs text-slate-600">
|
||||
<span>{formatBytes(r.size)}</span>
|
||||
<span>{new Date(r.uploadedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user