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>
96 lines
3.4 KiB
React
96 lines
3.4 KiB
React
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>
|
|
);
|
|
}
|