- Navigation: Dashboard/Inhalte/Audio/Veröffentlichen/Datenbank/Einstellungen mit Active-State - Veröffentlichen (/publish): Pairs sortiert nach 'am wenigsten fehlt', 1-Klick-Publish je Sprache - Einstellungen (/settings): TTS-Stimme + Parameter pro Sprache bearbeiten - tts-settings in DB-Admin; Dashboard-Kacheln ergänzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
5.1 KiB
JavaScript
135 lines
5.1 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import Layout from '../components/Layout';
|
|
import { fetchAll } from '../lib/api';
|
|
import { STATUS_COLORS } from '../lib/tables';
|
|
|
|
const TILES = [
|
|
{
|
|
title: 'Content erstellen',
|
|
icon: '✏️',
|
|
description: 'Objekte markieren, Pairs & Sätze erstellen und prüfen.',
|
|
path: '/content',
|
|
color: 'border-amber-200 hover:border-amber-400 hover:bg-amber-50',
|
|
},
|
|
{
|
|
title: 'Audio / TTS',
|
|
icon: '🔊',
|
|
description: 'Sehen was noch kein Audio hat und Sprachausgabe generieren.',
|
|
path: '/audio',
|
|
color: 'border-purple-200 hover:border-purple-400 hover:bg-purple-50',
|
|
},
|
|
{
|
|
title: 'Wörter generieren',
|
|
icon: '🪄',
|
|
description: 'Neue Wörter zu einem Thema per KI erzeugen lassen.',
|
|
path: '/content/words',
|
|
color: 'border-emerald-200 hover:border-emerald-400 hover:bg-emerald-50',
|
|
},
|
|
{
|
|
title: 'Veröffentlichen',
|
|
icon: '🚀',
|
|
description: 'Fast fertige Inhalte sehen und mit einem Klick live schalten.',
|
|
path: '/publish',
|
|
color: 'border-violet-200 hover:border-violet-400 hover:bg-violet-50',
|
|
},
|
|
{
|
|
title: 'Datenbankverwaltung',
|
|
icon: '🗄️',
|
|
description: 'Alle Tabellen, Datensätze, Filter und verknüpfte Felder.',
|
|
path: '/db',
|
|
color: 'border-indigo-200 hover:border-indigo-400 hover:bg-indigo-50',
|
|
},
|
|
{
|
|
title: 'Einstellungen',
|
|
icon: '⚙️',
|
|
description: 'TTS-Stimmen pro Sprache und weitere Konfiguration.',
|
|
path: '/settings',
|
|
color: 'border-slate-200 hover:border-slate-400 hover:bg-slate-100',
|
|
},
|
|
];
|
|
|
|
// Pipeline-Stufen, die den Lebenszyklus eines Inhalts abbilden.
|
|
const PIPELINES = [
|
|
{ key: 'pictures', label: 'Bilder', endpoint: '/pictures', stages: ['uploaded', 'published', 'blocked'] },
|
|
{ key: 'objects', label: 'Objekte', endpoint: '/objects', stages: ['draft', 'reviewed', 'published', 'blocked'] },
|
|
{ key: 'pairs', label: 'Pairs', endpoint: '/pairs', stages: ['draft', 'reviewed', 'published', 'blocked'] },
|
|
{ key: 'words', label: 'Wörter', endpoint: '/words', stages: ['requested', 'translated', 'generated', 'published', 'blocked'] },
|
|
{ key: 'audios', label: 'Audios', endpoint: '/audios', stages: ['generated', 'published', 'blocked'] },
|
|
];
|
|
|
|
function countByStatus(rows) {
|
|
const out = {};
|
|
for (const r of rows || []) out[r.status] = (out[r.status] || 0) + 1;
|
|
return out;
|
|
}
|
|
|
|
export default function Dashboard() {
|
|
const navigate = useNavigate();
|
|
const [counts, setCounts] = useState(null);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
(async () => {
|
|
const entries = await Promise.all(
|
|
PIPELINES.map(async p => {
|
|
try { return [p.key, countByStatus(await fetchAll(p.endpoint))]; }
|
|
catch { return [p.key, {}]; }
|
|
})
|
|
);
|
|
if (active) setCounts(Object.fromEntries(entries));
|
|
})();
|
|
return () => { active = false; };
|
|
}, []);
|
|
|
|
return (
|
|
<Layout>
|
|
<h2 className="text-xl font-semibold text-slate-700 mb-2">Dashboard</h2>
|
|
<p className="text-sm text-slate-500 mb-6 max-w-2xl">
|
|
Der Inhalts-Lebenszyklus: <b>draft</b> (erstellt) → <b>reviewed</b> (im Tool geprüft) →{' '}
|
|
<b>published</b> (fertig inkl. Audio, in der App sichtbar). Nur veröffentlichte Inhalte mit
|
|
Bild und Audio erscheinen für Lernende.
|
|
</p>
|
|
|
|
{/* Pipeline-Übersicht */}
|
|
<div className="bg-white rounded-2xl border border-slate-200 p-5 mb-8 max-w-4xl">
|
|
<h3 className="text-sm font-semibold text-slate-600 uppercase tracking-wide mb-4">Pipeline-Übersicht</h3>
|
|
<div className="space-y-3">
|
|
{PIPELINES.map(p => (
|
|
<div key={p.key} className="flex items-center gap-3">
|
|
<div className="w-20 shrink-0 text-sm font-medium text-slate-700">{p.label}</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{p.stages.map(stage => {
|
|
const n = counts?.[p.key]?.[stage] ?? null;
|
|
const cls = STATUS_COLORS[stage] || 'bg-gray-100 text-gray-600';
|
|
return (
|
|
<span key={stage} className={`${cls} rounded-full px-2.5 py-0.5 text-xs font-medium`}>
|
|
{stage}: {counts ? (n ?? 0) : '…'}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="text-[11px] text-slate-400 mt-3">Zählung bis max. 500 pro Tabelle.</p>
|
|
</div>
|
|
|
|
{/* Werkzeuge */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 max-w-4xl">
|
|
{TILES.map(tile => (
|
|
<div
|
|
key={tile.title}
|
|
onClick={() => navigate(tile.path)}
|
|
className={`bg-white rounded-2xl border-2 p-6 transition-all cursor-pointer ${tile.color}`}
|
|
>
|
|
<div className="text-4xl mb-3">{tile.icon}</div>
|
|
<h3 className="font-semibold text-slate-800 text-lg mb-1">{tile.title}</h3>
|
|
<p className="text-slate-500 text-sm">{tile.description}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|