feat: Implement genre activation/deactivation with UI controls and main page filtering.
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 })
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user