Compare commits

...

4 Commits

Author SHA1 Message Date
Hördle Bot
702f47b7e5 Bump version to v0.1.6.7 2025-12-04 13:40:38 +01:00
Hördle Bot
86f3349f80 Fix duplicate toggleUploadSpecial definition in curator client 2025-12-04 13:40:23 +01:00
Hördle Bot
bdb74fb462 Bump version to v0.1.6.6 2025-12-04 13:36:40 +01:00
Hördle Bot
66c0071257 Allow curators to assign specials on upload and update help text 2025-12-04 13:36:24 +01:00
4 changed files with 96 additions and 48 deletions

View File

@@ -107,6 +107,7 @@ export default function CuratorPageClient() {
// Upload state (analog zum Admin-Upload, aber vereinfacht) // Upload state (analog zum Admin-Upload, aber vereinfacht)
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [uploadGenreIds, setUploadGenreIds] = useState<number[]>([]); const [uploadGenreIds, setUploadGenreIds] = useState<number[]>([]);
const [uploadSpecialIds, setUploadSpecialIds] = useState<number[]>([]);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [uploadProgress, setUploadProgress] = useState<{ current: number; total: number }>({ const [uploadProgress, setUploadProgress] = useState<{ current: number; total: number }>({
@@ -534,6 +535,12 @@ export default function CuratorPageClient() {
); );
}; };
const toggleUploadSpecial = (specialId: number) => {
setUploadSpecialIds(prev =>
prev.includes(specialId) ? prev.filter(id => id !== specialId) : [...prev, specialId]
);
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = Array.from(e.target.files || []); const selected = Array.from(e.target.files || []);
if (selected.length === 0) return; if (selected.length === 0) return;
@@ -636,8 +643,8 @@ export default function CuratorPageClient() {
setFiles([]); setFiles([]);
setIsUploading(false); setIsUploading(false);
// Genres den erfolgreich hochgeladenen Songs zuweisen // Genres/Specials den erfolgreich hochgeladenen Songs zuweisen
if (uploadGenreIds.length > 0) { if (uploadGenreIds.length > 0 || uploadSpecialIds.length > 0) {
const successfulUploads = results.filter(r => r.success && r.song); const successfulUploads = results.filter(r => r.success && r.song);
for (const result of successfulUploads) { for (const result of successfulUploads) {
try { try {
@@ -649,12 +656,13 @@ export default function CuratorPageClient() {
title: result.song.title, title: result.song.title,
artist: result.song.artist, artist: result.song.artist,
releaseYear: result.song.releaseYear, releaseYear: result.song.releaseYear,
genreIds: uploadGenreIds, genreIds: uploadGenreIds.length > 0 ? uploadGenreIds : undefined,
specialIds: uploadSpecialIds.length > 0 ? uploadSpecialIds : undefined,
}), }),
}); });
} catch { } catch {
// Fehler beim Genre-Assigning werden nur geloggt, nicht abgebrochen // Fehler beim Zuweisen werden nur geloggt, nicht abgebrochen
console.error(`Failed to assign genres to ${result.song.title}`); console.error(`Failed to assign genres/specials to ${result.song.title}`);
} }
} }
} }
@@ -1149,44 +1157,82 @@ export default function CuratorPageClient() {
)} )}
<div> <div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<div style={{ fontWeight: 500 }}>{t('assignGenresLabel')}</div> <div>
<HelpTooltip <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
shortText={tHelp('tooltipGenreAssignmentShort')} <div style={{ fontWeight: 500 }}>{t('assignGenresLabel')}</div>
longText={tHelp('tooltipGenreAssignmentLong')} <HelpTooltip
position="right" shortText={tHelp('tooltipGenreAssignmentShort')}
/> longText={tHelp('tooltipGenreAssignmentLong')}
</div> position="right"
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}> />
{genres </div>
.filter(g => curatorInfo?.genreIds?.includes(g.id)) <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
.map(genre => ( {genres
<label .filter(g => curatorInfo?.genreIds?.includes(g.id))
key={genre.id} .map(genre => (
style={{ <label
display: 'flex', key={genre.id}
alignItems: 'center', style={{
gap: '0.25rem', display: 'flex',
padding: '0.25rem 0.5rem', alignItems: 'center',
borderRadius: '999px', gap: '0.25rem',
background: uploadGenreIds.includes(genre.id) ? '#e5f3ff' : '#f3f4f6', padding: '0.25rem 0.5rem',
fontSize: '0.8rem', borderRadius: '999px',
cursor: 'pointer', background: uploadGenreIds.includes(genre.id) ? '#e5f3ff' : '#f3f4f6',
}} fontSize: '0.8rem',
> cursor: 'pointer',
<input }}
type="checkbox" >
checked={uploadGenreIds.includes(genre.id)} <input
onChange={() => toggleUploadGenre(genre.id)} type="checkbox"
/> checked={uploadGenreIds.includes(genre.id)}
{typeof genre.name === 'string' ? genre.name : genre.name?.de ?? genre.name?.en} onChange={() => toggleUploadGenre(genre.id)}
</label> />
))} {typeof genre.name === 'string' ? genre.name : genre.name?.de ?? genre.name?.en}
{curatorInfo && curatorInfo.genreIds.length === 0 && ( </label>
<span style={{ fontSize: '0.8rem', color: '#9ca3af' }}> ))}
{t('noAssignedGenres')} {curatorInfo && curatorInfo.genreIds.length === 0 && (
</span> <span style={{ fontSize: '0.8rem', color: '#9ca3af' }}>
)} {t('noAssignedGenres')}
</span>
)}
</div>
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem', marginTop: '0.5rem' }}>
<div style={{ fontWeight: 500 }}>{t('assignSpecialsLabel')}</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
{specials
.filter(s => curatorInfo?.specialIds?.includes(s.id))
.map(special => (
<label
key={special.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
padding: '0.25rem 0.5rem',
borderRadius: '999px',
background: uploadSpecialIds.includes(special.id) ? '#fef3c7' : '#f3f4f6',
fontSize: '0.8rem',
cursor: 'pointer',
}}
>
<input
type="checkbox"
checked={uploadSpecialIds.includes(special.id)}
onChange={() => toggleUploadSpecial(special.id)}
/>
{typeof special.name === 'string'
? special.name
: special.name?.de ?? special.name?.en}
</label>
))}
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -203,6 +203,7 @@
"selectedFilesTitle": "Ausgewählte Dateien:", "selectedFilesTitle": "Ausgewählte Dateien:",
"uploadProgress": "Upload: {current} / {total}", "uploadProgress": "Upload: {current} / {total}",
"assignGenresLabel": "Genres zuordnen", "assignGenresLabel": "Genres zuordnen",
"assignSpecialsLabel": "Specials zuordnen",
"noAssignedGenres": "Dir sind noch keine Genres zugeordnet. Bitte wende dich an den Admin.", "noAssignedGenres": "Dir sind noch keine Genres zugeordnet. Bitte wende dich an den Admin.",
"uploadButtonIdle": "Upload starten", "uploadButtonIdle": "Upload starten",
"uploadButtonUploading": "Lade hoch...", "uploadButtonUploading": "Lade hoch...",
@@ -311,12 +312,12 @@
"uploadTitle": "Songs hochladen", "uploadTitle": "Songs hochladen",
"uploadStepsTitle": "Schritt-für-Schritt-Anleitung", "uploadStepsTitle": "Schritt-für-Schritt-Anleitung",
"uploadStep1": "MP3-Dateien in den Upload-Bereich ziehen oder klicken, um Dateien auszuwählen", "uploadStep1": "MP3-Dateien in den Upload-Bereich ziehen oder klicken, um Dateien auszuwählen",
"uploadStep2": "Ein oder mehrere Genres auswählen, um sie den hochgeladenen Songs zuzuordnen", "uploadStep2": "Ein oder mehrere Genres und falls passend Specials auswählen, um sie den hochgeladenen Songs zuzuordnen",
"uploadStep3": "Auf 'Upload starten' klicken, um den Upload-Prozess zu beginnen", "uploadStep3": "Auf 'Upload starten' klicken, um den Upload-Prozess zu beginnen",
"uploadStep4": "Das System extrahiert automatisch Metadaten (Titel, Artist, Erscheinungsjahr) aus den Dateien", "uploadStep4": "Das System extrahiert automatisch Metadaten (Titel, Artist, Erscheinungsjahr) aus den Dateien",
"uploadBestPracticesTitle": "Best Practices", "uploadBestPracticesTitle": "Best Practices",
"uploadBestPractice1": "Stelle sicher, dass MP3-Dateien korrekte ID3-Tags (Titel, Artist) für die automatische Metadaten-Extraktion haben", "uploadBestPractice1": "Stelle sicher, dass MP3-Dateien korrekte ID3-Tags (Titel, Artist) für die automatische Metadaten-Extraktion haben",
"uploadBestPractice2": "Passende Genres vor dem Upload auswählen, um spätere manuelle Zuordnung zu vermeiden", "uploadBestPractice2": "Passende Genres (und Specials) vor dem Upload auswählen, um spätere manuelle Zuordnung zu vermeiden",
"uploadBestPractice3": "Vor dem Upload auf Duplikate prüfen - das System warnt dich, wenn ein Song bereits existiert", "uploadBestPractice3": "Vor dem Upload auf Duplikate prüfen - das System warnt dich, wenn ein Song bereits existiert",
"tip": "Tipp", "tip": "Tipp",
"uploadTip": "Alle von Kuratoren hochgeladenen Songs werden automatisch von der globalen Playlist ausgeschlossen. Nur Admins können diese Einstellung ändern.", "uploadTip": "Alle von Kuratoren hochgeladenen Songs werden automatisch von der globalen Playlist ausgeschlossen. Nur Admins können diese Einstellung ändern.",

View File

@@ -203,6 +203,7 @@
"selectedFilesTitle": "Selected files:", "selectedFilesTitle": "Selected files:",
"uploadProgress": "Upload: {current} / {total}", "uploadProgress": "Upload: {current} / {total}",
"assignGenresLabel": "Assign genres", "assignGenresLabel": "Assign genres",
"assignSpecialsLabel": "Assign specials",
"noAssignedGenres": "No genres are assigned to you yet. Please contact the admin.", "noAssignedGenres": "No genres are assigned to you yet. Please contact the admin.",
"uploadButtonIdle": "Start upload", "uploadButtonIdle": "Start upload",
"uploadButtonUploading": "Uploading...", "uploadButtonUploading": "Uploading...",
@@ -311,12 +312,12 @@
"uploadTitle": "Uploading Songs", "uploadTitle": "Uploading Songs",
"uploadStepsTitle": "Step-by-Step Guide", "uploadStepsTitle": "Step-by-Step Guide",
"uploadStep1": "Drag MP3 files into the upload area or click to select files", "uploadStep1": "Drag MP3 files into the upload area or click to select files",
"uploadStep2": "Select one or more genres to assign to the uploaded songs", "uploadStep2": "Select one or more genres and, if applicable, specials to assign to the uploaded songs",
"uploadStep3": "Click 'Start upload' to begin the upload process", "uploadStep3": "Click 'Start upload' to begin the upload process",
"uploadStep4": "The system will automatically extract metadata (title, artist, release year) from the files", "uploadStep4": "The system will automatically extract metadata (title, artist, release year) from the files",
"uploadBestPracticesTitle": "Best Practices", "uploadBestPracticesTitle": "Best Practices",
"uploadBestPractice1": "Ensure MP3 files have correct ID3 tags (title, artist) for automatic metadata extraction", "uploadBestPractice1": "Ensure MP3 files have correct ID3 tags (title, artist) for automatic metadata extraction",
"uploadBestPractice2": "Select appropriate genres before uploading to avoid manual assignment later", "uploadBestPractice2": "Select appropriate genres (and specials) before uploading to avoid manual assignment later",
"uploadBestPractice3": "Check for duplicates before uploading - the system will warn you if a song already exists", "uploadBestPractice3": "Check for duplicates before uploading - the system will warn you if a song already exists",
"tip": "Tip", "tip": "Tip",
"uploadTip": "All songs uploaded by curators are automatically excluded from the global playlist. Only admins can change this setting.", "uploadTip": "All songs uploaded by curators are automatically excluded from the global playlist. Only admins can change this setting.",

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoerdle", "name": "hoerdle",
"version": "0.1.6.5", "version": "0.1.6.7",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",