Add subtitles to Genres and Specials
This commit is contained in:
@@ -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' }}>
|
||||
|
||||
Reference in New Issue
Block a user