Files
files/nextjs/app/manage/dashboard/page.js
2026-03-28 08:43:04 +01:00

231 lines
8.6 KiB
JavaScript

import {
createUploadRequestAction,
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 uploadRequests = await all(
`SELECT id, note, created_at, expires_at, completed_at, uploaded_original_name
FROM upload_requests
WHERE owner = ?
ORDER BY created_at DESC
LIMIT 200`,
[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);
const nowTs = Date.now();
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>
<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>
</aside>
</div>
<section className="panel">
<h2>Upload-Anfragen</h2>
<p className="muted">Benachrichtigung erfolgt an deinen Benutzernamen (E-Mail-Adresse).</p>
<form className="form-grid" action={createUploadRequestAction}>
<input type="hidden" name="csrfToken" value={csrfToken} />
<label className="field">
Notiz (optional)
<input className="input" name="note" placeholder="z. B. Bitte die Rechnung als PDF senden" />
</label>
<label className="field">
Gültig in Stunden
<input className="input" name="expiresHours" placeholder="72" />
</label>
<button className="btn" type="submit">
Anfrage erstellen
</button>
</form>
{uploadRequests.length === 0 ? (
<p className="muted">Noch keine Upload-Anfragen.</p>
) : (
<div className="table-wrap">
<table>
<thead>
<tr>
<th>Anfrage</th>
<th>Status</th>
<th>Erstellt</th>
<th>Gültig bis</th>
<th>Ergebnis</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{uploadRequests.map((entry) => {
const requestPath = `/_request/${encodeURIComponent(entry.id)}`;
const isCompleted = Number(entry.completed_at || 0) > 0;
const isExpired = !isCompleted && Number(entry.expires_at || 0) <= nowTs;
return (
<tr key={entry.id}>
<td>
<strong>{entry.id}</strong>
{entry.note ? <div className="muted">{entry.note}</div> : null}
</td>
<td>
<span
className={`badge ${
isCompleted ? 'success' : isExpired ? 'danger' : 'primary'
}`}
>
{isCompleted ? 'Abgeschlossen' : isExpired ? 'Abgelaufen' : 'Offen'}
</span>
</td>
<td>{formatTimestamp(entry.created_at)}</td>
<td>{formatTimestamp(entry.expires_at)}</td>
<td>{entry.uploaded_original_name || '-'}</td>
<td>
<div className="row-actions">
<a className="btn secondary" href={requestPath}>
Öffnen
</a>
<CopyLinkButton path={requestPath} label="Upload-Anfrage" />
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
<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-form-row">
<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" formAction={extendOwnUploadAction}>
Verlängern
</button>
<button className="btn danger" type="submit" formAction={deleteOwnUploadAction}>
Löschen
</button>
</form>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</main>
);
}