Add subtitles to Genres and Specials

This commit is contained in:
Hördle Bot
2025-11-23 15:20:12 +01:00
parent b8321cef56
commit 80e6066c17
9 changed files with 237 additions and 25 deletions

View File

@@ -6,6 +6,7 @@ import { useState, useEffect } from 'react';
interface Special {
id: number;
name: string;
subtitle?: string;
maxAttempts: number;
unlockSteps: string;
launchDate?: string;
@@ -19,6 +20,7 @@ interface Special {
interface Genre {
id: number;
name: string;
subtitle?: string;
_count?: {
songs: number;
};
@@ -61,10 +63,15 @@ export default function AdminPage() {
const [songs, setSongs] = useState<Song[]>([]);
const [genres, setGenres] = useState<Genre[]>([]);
const [newGenreName, setNewGenreName] = useState('');
const [newGenreSubtitle, setNewGenreSubtitle] = useState('');
const [editingGenreId, setEditingGenreId] = useState<number | null>(null);
const [editGenreName, setEditGenreName] = useState('');
const [editGenreSubtitle, setEditGenreSubtitle] = useState('');
// Specials state
const [specials, setSpecials] = useState<Special[]>([]);
const [newSpecialName, setNewSpecialName] = useState('');
const [newSpecialSubtitle, setNewSpecialSubtitle] = useState('');
const [newSpecialMaxAttempts, setNewSpecialMaxAttempts] = useState(7);
const [newSpecialUnlockSteps, setNewSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]');
const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState('');
@@ -73,6 +80,7 @@ export default function AdminPage() {
const [editingSpecialId, setEditingSpecialId] = useState<number | null>(null);
const [editSpecialName, setEditSpecialName] = useState('');
const [editSpecialSubtitle, setEditSpecialSubtitle] = useState('');
const [editSpecialMaxAttempts, setEditSpecialMaxAttempts] = useState(7);
const [editSpecialUnlockSteps, setEditSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]');
const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState('');
@@ -161,16 +169,42 @@ export default function AdminPage() {
if (!newGenreName.trim()) return;
const res = await fetch('/api/genres', {
method: 'POST',
body: JSON.stringify({ name: newGenreName }),
body: JSON.stringify({ name: newGenreName, subtitle: newGenreSubtitle }),
});
if (res.ok) {
setNewGenreName('');
setNewGenreSubtitle('');
fetchGenres();
} else {
alert('Failed to create genre');
}
};
const startEditGenre = (genre: Genre) => {
setEditingGenreId(genre.id);
setEditGenreName(genre.name);
setEditGenreSubtitle(genre.subtitle || '');
};
const saveEditedGenre = async () => {
if (editingGenreId === null) return;
const res = await fetch('/api/genres', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: editingGenreId,
name: editGenreName,
subtitle: editGenreSubtitle
}),
});
if (res.ok) {
setEditingGenreId(null);
fetchGenres();
} else {
alert('Failed to update genre');
}
};
// Specials functions
const fetchSpecials = async () => {
const res = await fetch('/api/specials');
@@ -187,6 +221,7 @@ export default function AdminPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: newSpecialName,
subtitle: newSpecialSubtitle,
maxAttempts: newSpecialMaxAttempts,
unlockSteps: newSpecialUnlockSteps,
launchDate: newSpecialLaunchDate || null,
@@ -196,6 +231,7 @@ export default function AdminPage() {
});
if (res.ok) {
setNewSpecialName('');
setNewSpecialSubtitle('');
setNewSpecialMaxAttempts(7);
setNewSpecialUnlockSteps('[2,4,7,11,16,30,60]');
setNewSpecialLaunchDate('');
@@ -278,6 +314,7 @@ export default function AdminPage() {
const startEditSpecial = (special: Special) => {
setEditingSpecialId(special.id);
setEditSpecialName(special.name);
setEditSpecialSubtitle(special.subtitle || '');
setEditSpecialMaxAttempts(special.maxAttempts);
setEditSpecialUnlockSteps(special.unlockSteps);
setEditSpecialLaunchDate(special.launchDate ? new Date(special.launchDate).toISOString().split('T')[0] : '');
@@ -293,6 +330,7 @@ export default function AdminPage() {
body: JSON.stringify({
id: editingSpecialId,
name: editSpecialName,
subtitle: editSpecialSubtitle,
maxAttempts: editSpecialMaxAttempts,
unlockSteps: editSpecialUnlockSteps,
launchDate: editSpecialLaunchDate || null,
@@ -700,6 +738,10 @@ export default function AdminPage() {
<label style={{ fontSize: '0.75rem', color: '#666' }}>Name</label>
<input type="text" placeholder="Special name" value={newSpecialName} onChange={e => setNewSpecialName(e.target.value)} className="form-input" required />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Subtitle</label>
<input type="text" placeholder="Subtitle" value={newSpecialSubtitle} onChange={e => setNewSpecialSubtitle(e.target.value)} className="form-input" />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Max Attempts</label>
<input type="number" placeholder="Max attempts" value={newSpecialMaxAttempts} onChange={e => setNewSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} />
@@ -735,6 +777,7 @@ export default function AdminPage() {
fontSize: '0.875rem'
}}>
<span>{special.name} ({special._count?.songs || 0})</span>
{special.subtitle && <span style={{ fontSize: '0.75rem', color: '#666', marginLeft: '0.25rem' }}>- {special.subtitle}</span>}
<a href={`/admin/specials/${special.id}`} className="btn-primary" style={{ marginRight: '0.5rem', textDecoration: 'none' }}>Curate</a>
<button onClick={() => startEditSpecial(special)} className="btn-secondary" style={{ marginRight: '0.5rem' }}>Edit</button>
<button onClick={() => handleDeleteSpecial(special.id)} className="btn-danger">Delete</button>
@@ -749,6 +792,10 @@ export default function AdminPage() {
<label style={{ fontSize: '0.75rem', color: '#666' }}>Name</label>
<input type="text" value={editSpecialName} onChange={e => setEditSpecialName(e.target.value)} className="form-input" />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Subtitle</label>
<input type="text" value={editSpecialSubtitle} onChange={e => setEditSpecialSubtitle(e.target.value)} className="form-input" />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Max Attempts</label>
<input type="number" value={editSpecialMaxAttempts} onChange={e => setEditSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} />
@@ -788,6 +835,14 @@ export default function AdminPage() {
className="form-input"
style={{ maxWidth: '200px' }}
/>
<input
type="text"
value={newGenreSubtitle}
onChange={e => setNewGenreSubtitle(e.target.value)}
placeholder="Subtitle"
className="form-input"
style={{ maxWidth: '300px' }}
/>
<button onClick={createGenre} className="btn-primary">Add Genre</button>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
@@ -802,15 +857,29 @@ export default function AdminPage() {
fontSize: '0.875rem'
}}>
<span>{genre.name} ({genre._count?.songs || 0})</span>
<button
onClick={() => deleteGenre(genre.id)}
style={{ border: 'none', background: 'none', cursor: 'pointer', color: '#ef4444', fontWeight: 'bold' }}
>
×
</button>
{genre.subtitle && <span style={{ fontSize: '0.75rem', color: '#666' }}>- {genre.subtitle}</span>}
<button onClick={() => startEditGenre(genre)} className="btn-secondary" style={{ padding: '0.1rem 0.5rem', fontSize: '0.75rem' }}>Edit</button>
<button onClick={() => deleteGenre(genre.id)} className="btn-danger" style={{ padding: '0.1rem 0.5rem', fontSize: '0.75rem' }}>×</button>
</div>
))}
</div>
{editingGenreId !== null && (
<div style={{ marginTop: '1rem', padding: '1rem', background: '#f9fafb', borderRadius: '0.5rem' }}>
<h3>Edit Genre</h3>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'flex-end' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Name</label>
<input type="text" value={editGenreName} onChange={e => setEditGenreName(e.target.value)} className="form-input" />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Subtitle</label>
<input type="text" value={editGenreSubtitle} onChange={e => setEditGenreSubtitle(e.target.value)} className="form-input" style={{ width: '300px' }} />
</div>
<button onClick={saveEditedGenre} className="btn-primary">Save</button>
<button onClick={() => setEditingGenreId(null)} className="btn-secondary">Cancel</button>
</div>
</div>
)}
{/* AI Categorization */}
<div style={{ marginTop: '1.5rem', paddingTop: '1rem', borderTop: '1px solid #e5e7eb' }}>