Files
files/nextjs/app/manage/dashboard/page.js
2026-03-27 20:08:40 +01:00

158 lines
6.1 KiB
JavaScript

import {
deleteOwnUploadAction,
extendOwnUploadAction,
userLogoutAction,
} from '@/src/lib/actions.js';
import { all, runCleanupIfNeeded } from '@/src/lib/db.js';
import { sharedLinkName } from '@/src/lib/files.js';
import { formatBytes, formatCountdown, formatTimestamp, readSearchParam } from '@/src/lib/format.js';
import { ensureCsrfToken, requireAuthenticatedUser } from '@/src/lib/security.js';
import { CopyLinkButton } from '../_components/copy-link-button.js';
import { StatusMessage } from '../_components/status-message.js';
import { UploadProgressForm } from '../_components/upload-progress-form.js';
export const dynamic = 'force-dynamic';
export default async function DashboardPage({ searchParams }) {
await runCleanupIfNeeded();
const user = await requireAuthenticatedUser();
const csrfToken = await ensureCsrfToken();
const uploads = await all(
'SELECT id, original_name, stored_name, size_bytes, uploaded_at, expires_at FROM uploads WHERE owner = ? ORDER BY uploaded_at DESC',
[user.username]
);
const resolvedSearchParams = await searchParams;
const error = readSearchParam(resolvedSearchParams, 'error');
const success = readSearchParam(resolvedSearchParams, 'success');
const totalBytes = uploads.reduce((total, item) => total + (Number(item.size_bytes) || 0), 0);
return (
<main className="page-shell">
<header className="page-header">
<div className="header-main">
<h1>Dateiverwaltung</h1>
<p className="muted">Angemeldet als {user.username}</p>
</div>
<div className="toolbar">
{user.admin ? (
<a className="chip primary" href="/manage/admin/dashboard">
Adminbereich
</a>
) : null}
<form className="inline-form" action={userLogoutAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<button className="btn secondary" type="submit">
Abmelden
</button>
</form>
</div>
</header>
<StatusMessage error={error} success={success} />
<div className="dashboard-top-grid">
<section className="panel panel-spotlight">
<h2>Neue Datei hochladen</h2>
<p className="muted">Der Fortschritt wird während des Uploads live angezeigt.</p>
<UploadProgressForm csrfToken={csrfToken} />
</section>
<aside className="panel panel-soft">
<h2>Schnellüberblick</h2>
<div className="info-stack">
<div className="info-card">
<strong>{uploads.length}</strong>
<span className="muted">aktive Uploads</span>
</div>
<div className="info-card">
<strong>{totalBytes > 0 ? formatBytes(totalBytes) : '0 B'}</strong>
<span className="muted">genutzter Speicher</span>
</div>
<div className="info-card">
<strong>/_share/&lt;id&gt;</strong>
<span className="muted">Kurzlinks ohne Dateiendung</span>
</div>
</div>
</aside>
</div>
<section className="panel">
<h2>Aktuelle Uploads</h2>
{uploads.length === 0 ? (
<p className="muted">Noch keine Uploads.</p>
) : (
<div className="table-wrap">
<table>
<thead>
<tr>
<th>Datei</th>
<th>Größe</th>
<th>Hochgeladen</th>
<th>Ablauf</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{uploads.map((item) => {
const shareName = sharedLinkName(item.stored_name);
const sharePath = `/_share/${encodeURIComponent(shareName)}`;
return (
<tr key={item.id}>
<td>
<strong>{item.original_name}</strong>
<div className="muted mono">{item.stored_name}</div>
</td>
<td>{formatBytes(item.size_bytes)}</td>
<td>{formatTimestamp(item.uploaded_at)}</td>
<td>
<div>{formatTimestamp(item.expires_at)}</div>
<div className="muted">Noch {formatCountdown(item.expires_at)}</div>
</td>
<td>
<div className="stack-actions">
<div className="row-actions">
<a className="btn secondary" href={sharePath}>
Download
</a>
<CopyLinkButton path={sharePath} label={item.original_name} />
</div>
<form className="inline-form" action={extendOwnUploadAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<input type="hidden" name="uploadId" value={item.id} />
<input
className="input small"
name="extendHours"
placeholder="Stunden"
/>
<button className="btn" type="submit">
Verlängern
</button>
</form>
<form className="inline-form" action={deleteOwnUploadAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<input type="hidden" name="uploadId" value={item.id} />
<button className="btn danger" type="submit">
Löschen
</button>
</form>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</main>
);
}