Files
hejyou_content_creation/frontend/src/pages/ContentManage.tsx
admin d02788bd0e feat: CRM-Dashboard, Content-Verwaltung und Wort-Autocomplete
- Home-Seite nach Login mit Begrüßung und 3 Kacheln (Content erstellen, Content verwalten, User verwalten)
- AuthContext speichert User-Profil + Rolle; AdminRoute blockt Nicht-Admins
- Content verwalten (admin-only): Status-Dashboard pro Collection, Liste/Kachel-View, generisches Edit-Formular
- Nur aktive db_-Collections im Dashboard (alte pictures/objects/words/questions entfernt)
- Wort-Autocomplete in DrawIt: ab dem ersten Buchstaben Vorschläge aus db_words, Tastatur-Navigation, Duplikat-Filter
- Backend: /users/me Proxy, db-words/search Endpoint, generische Collection-Endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 23:37:48 +02:00

107 lines
4.1 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import Topbar from '../components/Topbar'
import { getDashboardSummary, type DashboardEntry } from '../api'
const groupLabel = (g: 'alt' | 'neu') =>
g === 'alt' ? 'Alt (vor DB-Refactor)' : 'Neu (DB-basiert)'
function StatusPill({ status, count, onClick }: { status: string | null; count: number; onClick?: () => void }) {
const label = status ?? '—'
return (
<button type="button" className="cm-pill" onClick={onClick} title={`Status: ${label}`}>
<span className={`cm-pill-dot cm-pill-dot-${(status || 'unknown').toLowerCase().replace(/[^a-z]/g, '')}`} />
<span className="cm-pill-label">{label}</span>
<span className="cm-pill-count">{count}</span>
</button>
)
}
export default function ContentManage() {
const navigate = useNavigate()
const { token } = useAuth()
const [entries, setEntries] = useState<DashboardEntry[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!token) return
setLoading(true)
setError(null)
getDashboardSummary(token)
.then(setEntries)
.catch((e: Error) => setError(e.message))
.finally(() => setLoading(false))
}, [token])
const groups: ('neu' | 'alt')[] = ['neu', 'alt']
return (
<div className="app-shell">
<Topbar page="home" />
<div className="cm-page">
<div className="cm-header">
<div>
<h1 className="cm-title">Content verwalten</h1>
<p className="cm-subtitle">Übersicht aller Collections nach Status. Klick auf eine Karte oder einen Status zum Drill-Down.</p>
</div>
</div>
{loading && <div className="cm-empty">Wird geladen</div>}
{error && <div className="cm-error">{error}</div>}
{!loading && !error && groups.map(group => {
const items = entries.filter(e => e.group === group)
if (items.length === 0) return null
return (
<section key={group} className="cm-section">
<h2 className="cm-section-title">{groupLabel(group)}</h2>
<div className="cm-grid">
{items.map(entry => (
<article key={entry.name} className="cm-card">
<button
type="button"
className="cm-card-head"
onClick={() => navigate(`/content/${entry.name}`)}
>
<div className="cm-card-titlewrap">
<span className={`cm-kind cm-kind-${entry.kind}`}>{entry.kind === 'image' ? 'Bilder' : 'Text'}</span>
<span className="cm-card-label">{entry.label}</span>
</div>
<div className="cm-card-total">{entry.total.toLocaleString('de-DE')}</div>
</button>
{entry.error && <div className="cm-card-error">{entry.error}</div>}
{entry.has_status && entry.by_status.length > 0 && (
<div className="cm-pills">
{entry.by_status.map(g => (
<StatusPill
key={String(g.status)}
status={g.status}
count={g.count}
onClick={() => navigate(`/content/${entry.name}?status=${encodeURIComponent(g.status || '')}`)}
/>
))}
</div>
)}
{!entry.has_status && !entry.error && (
<div className="cm-pills cm-pills-muted">
<span className="cm-pill cm-pill-static">
<span className="cm-pill-label">Kein Status-Feld</span>
</span>
</div>
)}
</article>
))}
</div>
</section>
)
})}
</div>
</div>
)
}