feat: Curator-Hilfe-System implementiert

- Hilfe-Seite /curator/help mit vollständiger Dokumentation (de/en)
- HelpTooltip-Komponente mit Hover- und Click-Modi
- Tooltips bei allen wichtigen Dashboard-Bereichen:
  * Dashboard-Übersicht
  * Upload-Bereich & Genre-Zuweisung
  * Track-Liste (Suche, Filter, Batch-Edit)
  * Kommentar-Verwaltung
- Prominenter Hilfe-Button im Header
- Umfassende Übersetzungen für alle Hilfe-Texte
- Fix: TypeScript-Fehler in batch route behoben
- Fix: Doppelter Browser-Tooltip entfernt (nur noch custom Tooltip)
This commit is contained in:
Hördle Bot
2025-12-04 01:07:45 +01:00
parent a61caa2d13
commit 65425ac15c
8 changed files with 694 additions and 94 deletions

View File

@@ -2,6 +2,8 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslations, useLocale } from 'next-intl';
import { Link } from '@/lib/navigation';
import HelpTooltip from '@/components/HelpTooltip';
interface Genre {
id: number;
@@ -83,6 +85,8 @@ function getCuratorUploadHeaders() {
export default function CuratorPageClient() {
const t = useTranslations('Curator');
const tNav = useTranslations('Navigation');
const tHelp = useTranslations('CuratorHelp');
const locale = useLocale();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isAuthenticated, setIsAuthenticated] = useState(false);
@@ -787,7 +791,14 @@ export default function CuratorPageClient() {
}}
>
<div>
<h1 style={{ fontSize: '1.75rem', marginBottom: '0.25rem' }}>Kuratoren-Dashboard</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<h1 style={{ fontSize: '1.75rem', marginBottom: '0.25rem' }}>Kuratoren-Dashboard</h1>
<HelpTooltip
shortText={tHelp('tooltipDashboardShort')}
longText={tHelp('tooltipDashboardLong')}
position="bottom"
/>
</div>
{curatorInfo && (
<p style={{ color: '#4b5563', fontSize: '0.9rem' }}>
{t('loggedInAs', { username: curatorInfo.username })}
@@ -795,20 +806,38 @@ export default function CuratorPageClient() {
</p>
)}
</div>
<button
type="button"
onClick={handleLogout}
style={{
padding: '0.5rem 1rem',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer',
}}
>
{t('logout')}
</button>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<Link
href={`/${locale}/curator/help`}
style={{
padding: '0.5rem 1rem',
background: '#3b82f6',
color: 'white',
textDecoration: 'none',
borderRadius: '0.375rem',
fontSize: '0.9rem',
display: 'inline-flex',
alignItems: 'center',
gap: '0.25rem',
}}
>
{tHelp('helpButton')}
</Link>
<button
type="button"
onClick={handleLogout}
style={{
padding: '0.5rem 1rem',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer',
}}
>
{t('logout')}
</button>
</div>
</header>
{loading && <p>{t('loadingData')}</p>}
@@ -825,9 +854,16 @@ export default function CuratorPageClient() {
<section style={{ marginBottom: '2rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.75rem' }}>
<h2 style={{ fontSize: '1.25rem', marginBottom: 0 }}>
{t('commentsTitle')} ({comments.length})
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<h2 style={{ fontSize: '1.25rem', marginBottom: 0 }}>
{t('commentsTitle')} ({comments.length})
</h2>
<HelpTooltip
shortText={tHelp('tooltipCommentsShort')}
longText={tHelp('tooltipCommentsLong')}
position="right"
/>
</div>
{hasUnread && (
<span style={{
padding: '0.25rem 0.75rem',
@@ -978,7 +1014,14 @@ export default function CuratorPageClient() {
})()}
<section style={{ marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>{t('uploadSectionTitle')}</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.75rem' }}>
<h2 style={{ fontSize: '1.25rem', margin: 0 }}>{t('uploadSectionTitle')}</h2>
<HelpTooltip
shortText={tHelp('tooltipUploadShort')}
longText={tHelp('tooltipUploadLong')}
position="right"
/>
</div>
<p style={{ marginBottom: '0.75rem', color: '#4b5563', fontSize: '0.9rem' }}>
{t('uploadSectionDescription')}
</p>
@@ -1078,7 +1121,14 @@ export default function CuratorPageClient() {
)}
<div>
<div style={{ fontWeight: 500, marginBottom: '0.25rem' }}>{t('assignGenresLabel')}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<div style={{ fontWeight: 500 }}>{t('assignGenresLabel')}</div>
<HelpTooltip
shortText={tHelp('tooltipGenreAssignmentShort')}
longText={tHelp('tooltipGenreAssignmentLong')}
position="right"
/>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
{genres
.filter(g => curatorInfo?.genreIds?.includes(g.id))
@@ -1154,70 +1204,91 @@ export default function CuratorPageClient() {
</section>
<section style={{ marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>
{t('tracklistTitle', { count: filteredSongs.length })}
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.75rem' }}>
<h2 style={{ fontSize: '1.25rem', margin: 0 }}>
{t('tracklistTitle', { count: filteredSongs.length })}
</h2>
<HelpTooltip
shortText={tHelp('tooltipTracklistShort')}
longText={tHelp('tooltipTracklistLong')}
position="right"
/>
</div>
<p style={{ marginBottom: '0.75rem', color: '#4b5563', fontSize: '0.9rem' }}>
{t('tracklistDescription')}
</p>
{/* Suche & Filter */}
<div style={{ marginBottom: '0.75rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<input
type="text"
placeholder={t('searchPlaceholder')}
value={searchQuery}
onChange={e => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
style={{
flex: '1',
minWidth: '200px',
padding: '0.4rem 0.6rem',
borderRadius: '0.25rem',
border: '1px solid #d1d5db',
}}
/>
<select
value={selectedFilter}
onChange={e => {
setSelectedFilter(e.target.value);
setCurrentPage(1);
}}
style={{
minWidth: '180px',
padding: '0.4rem 0.6rem',
borderRadius: '0.25rem',
border: '1px solid #d1d5db',
}}
>
<option value="">{t('filterAll')}</option>
<option value="no-global">{t('filterNoGlobal')}</option>
<optgroup label="Genres">
{genres
.filter(g => curatorInfo?.genreIds?.includes(g.id))
.map(genre => (
<option key={genre.id} value={`genre:${genre.id}`}>
{typeof genre.name === 'string'
? genre.name
: genre.name?.de ?? genre.name?.en}
</option>
))}
</optgroup>
<optgroup label="Specials">
{specials
.filter(s => curatorInfo?.specialIds?.includes(s.id))
.map(special => (
<option key={special.id} value={`special:${special.id}`}>
{' '}
{typeof special.name === 'string'
? special.name
: special.name?.de ?? special.name?.en}
</option>
))}
</optgroup>
</select>
<div style={{ marginBottom: '0.75rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
<div style={{ flex: '1', minWidth: '200px', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<input
type="text"
placeholder={t('searchPlaceholder')}
value={searchQuery}
onChange={e => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
style={{
flex: '1',
minWidth: '200px',
padding: '0.4rem 0.6rem',
borderRadius: '0.25rem',
border: '1px solid #d1d5db',
}}
/>
<HelpTooltip
shortText={tHelp('tooltipSearchShort')}
longText={tHelp('tooltipSearchLong')}
position="top"
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<select
value={selectedFilter}
onChange={e => {
setSelectedFilter(e.target.value);
setCurrentPage(1);
}}
style={{
minWidth: '180px',
padding: '0.4rem 0.6rem',
borderRadius: '0.25rem',
border: '1px solid #d1d5db',
}}
>
<option value="">{t('filterAll')}</option>
<option value="no-global">{t('filterNoGlobal')}</option>
<optgroup label="Genres">
{genres
.filter(g => curatorInfo?.genreIds?.includes(g.id))
.map(genre => (
<option key={genre.id} value={`genre:${genre.id}`}>
{typeof genre.name === 'string'
? genre.name
: genre.name?.de ?? genre.name?.en}
</option>
))}
</optgroup>
<optgroup label="Specials">
{specials
.filter(s => curatorInfo?.specialIds?.includes(s.id))
.map(special => (
<option key={special.id} value={`special:${special.id}`}>
{' '}
{typeof special.name === 'string'
? special.name
: special.name?.de ?? special.name?.en}
</option>
))}
</optgroup>
</select>
<HelpTooltip
shortText={tHelp('tooltipFilterShort')}
longText={tHelp('tooltipFilterLong')}
position="top"
/>
</div>
{(searchQuery || selectedFilter) && (
<button
type="button"
@@ -1256,9 +1327,16 @@ export default function CuratorPageClient() {
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
<strong style={{ fontSize: '1rem' }}>
{t('batchEditTitle') || `Batch Edit: ${selectedSongIds.size} ${selectedSongIds.size === 1 ? 'song' : 'songs'} selected`}
</strong>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<strong style={{ fontSize: '1rem' }}>
{t('batchEditTitle') || `Batch Edit: ${selectedSongIds.size} ${selectedSongIds.size === 1 ? 'song' : 'songs'} selected`}
</strong>
<HelpTooltip
shortText={tHelp('tooltipBatchEditShort')}
longText={tHelp('tooltipBatchEditLong')}
position="right"
/>
</div>
<button
type="button"
onClick={clearSelection}
@@ -1278,9 +1356,16 @@ export default function CuratorPageClient() {
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{/* Genre Toggle */}
<div>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '0.25rem', fontSize: '0.9rem' }}>
{t('batchToggleGenres') || 'Toggle Genres'}
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<label style={{ display: 'block', fontWeight: 500, fontSize: '0.9rem', margin: 0 }}>
{t('batchToggleGenres') || 'Toggle Genres'}
</label>
<HelpTooltip
shortText={tHelp('tooltipBatchGenreToggleShort')}
longText={tHelp('tooltipBatchGenreToggleLong')}
position="right"
/>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
{genres
.filter(g => curatorInfo?.genreIds?.includes(g.id))
@@ -1319,9 +1404,16 @@ export default function CuratorPageClient() {
{/* Special Toggle */}
<div>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '0.25rem', fontSize: '0.9rem' }}>
{t('batchToggleSpecials') || 'Toggle Specials'}
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<label style={{ display: 'block', fontWeight: 500, fontSize: '0.9rem', margin: 0 }}>
{t('batchToggleSpecials') || 'Toggle Specials'}
</label>
<HelpTooltip
shortText={tHelp('tooltipBatchSpecialToggleShort')}
longText={tHelp('tooltipBatchSpecialToggleLong')}
position="right"
/>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
{specials
.filter(s => curatorInfo?.specialIds?.includes(s.id))
@@ -1361,9 +1453,16 @@ export default function CuratorPageClient() {
{/* Artist Change */}
<div>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '0.25rem', fontSize: '0.9rem' }}>
{t('batchChangeArtist') || 'Change Artist'}
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<label style={{ display: 'block', fontWeight: 500, fontSize: '0.9rem', margin: 0 }}>
{t('batchChangeArtist') || 'Change Artist'}
</label>
<HelpTooltip
shortText={tHelp('tooltipBatchArtistShort')}
longText={tHelp('tooltipBatchArtistLong')}
position="right"
/>
</div>
<input
type="text"
value={batchArtist}