feat: Implement genre activation/deactivation with UI controls and main page filtering.

This commit is contained in:
Hördle Bot
2025-11-25 00:20:29 +01:00
parent a744393335
commit 41ce6c12ce
5 changed files with 59 additions and 9 deletions

View File

@@ -21,6 +21,7 @@ interface Genre {
id: number; id: number;
name: string; name: string;
subtitle?: string; subtitle?: string;
active: boolean;
_count?: { _count?: {
songs: number; songs: number;
}; };
@@ -66,9 +67,11 @@ export default function AdminPage() {
const [genres, setGenres] = useState<Genre[]>([]); const [genres, setGenres] = useState<Genre[]>([]);
const [newGenreName, setNewGenreName] = useState(''); const [newGenreName, setNewGenreName] = useState('');
const [newGenreSubtitle, setNewGenreSubtitle] = useState(''); const [newGenreSubtitle, setNewGenreSubtitle] = useState('');
const [newGenreActive, setNewGenreActive] = useState(true);
const [editingGenreId, setEditingGenreId] = useState<number | null>(null); const [editingGenreId, setEditingGenreId] = useState<number | null>(null);
const [editGenreName, setEditGenreName] = useState(''); const [editGenreName, setEditGenreName] = useState('');
const [editGenreSubtitle, setEditGenreSubtitle] = useState(''); const [editGenreSubtitle, setEditGenreSubtitle] = useState('');
const [editGenreActive, setEditGenreActive] = useState(true);
// Specials state // Specials state
const [specials, setSpecials] = useState<Special[]>([]); const [specials, setSpecials] = useState<Special[]>([]);
@@ -200,11 +203,16 @@ export default function AdminPage() {
const res = await fetch('/api/genres', { const res = await fetch('/api/genres', {
method: 'POST', method: 'POST',
headers: getAuthHeaders(), headers: getAuthHeaders(),
body: JSON.stringify({ name: newGenreName, subtitle: newGenreSubtitle }), body: JSON.stringify({
name: newGenreName,
subtitle: newGenreSubtitle,
active: newGenreActive
}),
}); });
if (res.ok) { if (res.ok) {
setNewGenreName(''); setNewGenreName('');
setNewGenreSubtitle(''); setNewGenreSubtitle('');
setNewGenreActive(true);
fetchGenres(); fetchGenres();
} else { } else {
alert('Failed to create genre'); alert('Failed to create genre');
@@ -215,6 +223,7 @@ export default function AdminPage() {
setEditingGenreId(genre.id); setEditingGenreId(genre.id);
setEditGenreName(genre.name); setEditGenreName(genre.name);
setEditGenreSubtitle(genre.subtitle || ''); setEditGenreSubtitle(genre.subtitle || '');
setEditGenreActive(genre.active !== undefined ? genre.active : true);
}; };
const saveEditedGenre = async () => { const saveEditedGenre = async () => {
@@ -225,7 +234,8 @@ export default function AdminPage() {
body: JSON.stringify({ body: JSON.stringify({
id: editingGenreId, id: editingGenreId,
name: editGenreName, name: editGenreName,
subtitle: editGenreSubtitle subtitle: editGenreSubtitle,
active: editGenreActive
}), }),
}); });
if (res.ok) { if (res.ok) {
@@ -978,7 +988,7 @@ export default function AdminPage() {
{/* Genre Management */} {/* Genre Management */}
<div className="admin-card" style={{ marginBottom: '2rem' }}> <div className="admin-card" style={{ marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>Manage Genres</h2> <h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>Manage Genres</h2>
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}> <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem', alignItems: 'center' }}>
<input <input
type="text" type="text"
value={newGenreName} value={newGenreName}
@@ -995,12 +1005,21 @@ export default function AdminPage() {
className="form-input" className="form-input"
style={{ maxWidth: '300px' }} style={{ maxWidth: '300px' }}
/> />
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.875rem', cursor: 'pointer' }}>
<input
type="checkbox"
checked={newGenreActive}
onChange={e => setNewGenreActive(e.target.checked)}
/>
Active
</label>
<button onClick={createGenre} className="btn-primary">Add Genre</button> <button onClick={createGenre} className="btn-primary">Add Genre</button>
</div> </div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
{genres.map(genre => ( {genres.map(genre => (
<div key={genre.id} style={{ <div key={genre.id} style={{
background: '#f3f4f6', background: genre.active ? '#f3f4f6' : '#fee2e2',
opacity: genre.active ? 1 : 0.8,
padding: '0.25rem 0.75rem', padding: '0.25rem 0.75rem',
borderRadius: '999px', borderRadius: '999px',
display: 'flex', display: 'flex',
@@ -1027,6 +1046,16 @@ export default function AdminPage() {
<label style={{ fontSize: '0.75rem', color: '#666' }}>Subtitle</label> <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' }} /> <input type="text" value={editGenreSubtitle} onChange={e => setEditGenreSubtitle(e.target.value)} className="form-input" style={{ width: '300px' }} />
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', paddingBottom: '0.5rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.875rem', cursor: 'pointer' }}>
<input
type="checkbox"
checked={editGenreActive}
onChange={e => setEditGenreActive(e.target.checked)}
/>
Active
</label>
</div>
<button onClick={saveEditedGenre} className="btn-primary">Save</button> <button onClick={saveEditedGenre} className="btn-primary">Save</button>
<button onClick={() => setEditingGenreId(null)} className="btn-secondary">Cancel</button> <button onClick={() => setEditingGenreId(null)} className="btn-secondary">Cancel</button>
</div> </div>

View File

@@ -27,7 +27,7 @@ export async function POST(request: Request) {
if (authError) return authError; if (authError) return authError;
try { try {
const { name, subtitle } = await request.json(); const { name, subtitle, active } = await request.json();
if (!name || typeof name !== 'string') { if (!name || typeof name !== 'string') {
return NextResponse.json({ error: 'Invalid name' }, { status: 400 }); return NextResponse.json({ error: 'Invalid name' }, { status: 400 });
@@ -36,7 +36,8 @@ export async function POST(request: Request) {
const genre = await prisma.genre.create({ const genre = await prisma.genre.create({
data: { data: {
name: name.trim(), name: name.trim(),
subtitle: subtitle ? subtitle.trim() : null subtitle: subtitle ? subtitle.trim() : null,
active: active !== undefined ? active : true
}, },
}); });
@@ -76,7 +77,7 @@ export async function PUT(request: Request) {
if (authError) return authError; if (authError) return authError;
try { try {
const { id, name, subtitle } = await request.json(); const { id, name, subtitle, active } = await request.json();
if (!id) { if (!id) {
return NextResponse.json({ error: 'Missing id' }, { status: 400 }); return NextResponse.json({ error: 'Missing id' }, { status: 400 });
@@ -86,7 +87,8 @@ export async function PUT(request: Request) {
where: { id: Number(id) }, where: { id: Number(id) },
data: { data: {
...(name && { name: name.trim() }), ...(name && { name: name.trim() }),
subtitle: subtitle ? subtitle.trim() : null // Allow clearing subtitle if empty string passed? Or just update if provided? Let's assume null/empty string clears it. subtitle: subtitle ? subtitle.trim() : null,
...(active !== undefined && { active })
}, },
}); });

View File

@@ -9,7 +9,10 @@ const prisma = new PrismaClient();
export default async function Home() { export default async function Home() {
const dailyPuzzle = await getOrCreateDailyPuzzle(null); // Global puzzle const dailyPuzzle = await getOrCreateDailyPuzzle(null); // Global puzzle
const genres = await prisma.genre.findMany({ orderBy: { name: 'asc' } }); const genres = await prisma.genre.findMany({
where: { active: true },
orderBy: { name: 'asc' }
});
const specials = await prisma.special.findMany({ orderBy: { name: 'asc' } }); const specials = await prisma.special.findMany({ orderBy: { name: 'asc' } });
const now = new Date(); const now = new Date();

View File

@@ -0,0 +1,15 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Genre" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"subtitle" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true
);
INSERT INTO "new_Genre" ("id", "name", "subtitle") SELECT "id", "name", "subtitle" FROM "Genre";
DROP TABLE "Genre";
ALTER TABLE "new_Genre" RENAME TO "Genre";
CREATE UNIQUE INDEX "Genre_name_key" ON "Genre"("name");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -30,6 +30,7 @@ model Genre {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
subtitle String? subtitle String?
active Boolean @default(true)
songs Song[] songs Song[]
dailyPuzzles DailyPuzzle[] dailyPuzzles DailyPuzzle[]
} }