P42 Dashboard: erster Stand

FastAPI-Backend und React-Frontend mit Konten, Sendungsverfolgung,
Projekt-Artikeln, Vorrat & Einkauf mit Rezeptabgleich, Reise-Weltkarte,
Kanban-Projekten mit Priorität, Server-Monitoring und den Designs
Matrix und Blackwall. Betrieb per Docker hinter nginx (siehe DEPLOY.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-14 14:08:11 +02:00
commit dd58b3d1f8
75 files changed
+11418

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { api, fmtDate, imgUrl, personName } from '../api.js';
import StatusBadge from '../components/StatusBadge.jsx';
export default function Projects() {
const [projects, setProjects] = useState(null);
const [error, setError] = useState(null);
const dialog = useRef(null);
useEffect(() => {
api('GET', '/api/projects').then(setProjects).catch((e) => setError(e.message));
}, []);
return (
<>
<div className="page-head">
<div>
<h1>P42 Projekte</h1>
<p className="lead">Übersicht aller laufenden und abgeschlossenen Projekte.</p>
</div>
<button className="btn" type="button" onClick={() => dialog.current.showModal()}>+ Neues Projekt</button>
</div>
{error && <p className="error">{error}</p>}
{!projects && !error && <p className="hint">Lade Projekte …</p>}
{projects?.length === 0 && (
<div className="notice">Noch keine Projekte. Leg mit „+ Neues Projekt“ das erste an.</div>
)}
{projects?.length > 0 && (
<div className="grid projects-grid">
{projects.map((p) => (
<Link key={p.id} className="card project-card" to={`/projekte/${p.id}`}>
<div className="project-cover-wrap">
{p.cover
? <img className="project-cover" src={imgUrl(p.cover)} alt="" loading="lazy" />
: <div className="project-cover empty" />}
<StatusBadge status={p.status} />
</div>
<div className="project-body">
<h3>{p.title}</h3>
{p.description && <p className="project-teaser">{p.description}</p>}
<div className="meta project-meta">
<span>Von {personName(p.createdBy)}</span>
<span title="Zuletzt bearbeitet">✎ {personName(p.updatedBy)} · {fmtDate(p.updatedAt)}</span>
</div>
</div>
</Link>
))}
</div>
)}
<NewProjectDialog ref={dialog} />
</>
);
}
function NewProjectDialog({ ref }) {
const navigate = useNavigate();
const [title, setTitle] = useState('');
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
async function submit(e) {
e.preventDefault();
setBusy(true);
setError(null);
try {
const project = await api('POST', '/api/projects', { title });
navigate(`/projekte/${project.id}?bearbeiten`);
} catch (err) {
setError(err.message);
setBusy(false);
}
}
return (
<dialog className="modal" ref={ref} onClose={() => { setTitle(''); setError(null); }}>
<form className="form" onSubmit={submit}>
<h2>Neues Projekt</h2>
<label>
Titel
<input value={title} onChange={(e) => setTitle(e.target.value)} required maxLength={120} autoFocus />
</label>
<p className="hint">Beschreibung, Bilder und Text schreibst du danach direkt im Artikel.</p>
{error && <p className="error">{error}</p>}
<div className="form-actions">
<button className="btn ghost" type="button" onClick={() => ref.current.close()}>Abbrechen</button>
<button className="btn" type="submit" disabled={busy}>Projekt anlegen</button>
</div>
</form>
</dialog>
);
}