Files
snakkimo-cmt/src/pages/Dashboard.jsx
admin 9eecee9ace feat: Veröffentlichen-Seite, Einstellungen (TTS-Stimmen), klarere Navigation
- 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>
2026-06-02 22:02:18 +02:00

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>
);
}