feat: add hidden flag to specials

This commit is contained in:
Hördle Bot
2025-12-06 01:35:01 +01:00
parent bc2c0bad59
commit 71b8e98f23
8 changed files with 98 additions and 14 deletions

View File

@@ -15,6 +15,7 @@ interface Special {
launchDate?: string; launchDate?: string;
endDate?: string; endDate?: string;
curator?: string; curator?: string;
hidden?: boolean;
_count?: { _count?: {
songs: number; songs: number;
}; };
@@ -119,6 +120,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState(''); const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState('');
const [newSpecialEndDate, setNewSpecialEndDate] = useState(''); const [newSpecialEndDate, setNewSpecialEndDate] = useState('');
const [newSpecialCurator, setNewSpecialCurator] = useState(''); const [newSpecialCurator, setNewSpecialCurator] = useState('');
const [newSpecialHidden, setNewSpecialHidden] = useState(false);
const [editingSpecialId, setEditingSpecialId] = useState<number | null>(null); const [editingSpecialId, setEditingSpecialId] = useState<number | null>(null);
const [editSpecialName, setEditSpecialName] = useState({ de: '', en: '' }); const [editSpecialName, setEditSpecialName] = useState({ de: '', en: '' });
@@ -129,6 +131,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState(''); const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState('');
const [editSpecialEndDate, setEditSpecialEndDate] = useState(''); const [editSpecialEndDate, setEditSpecialEndDate] = useState('');
const [editSpecialCurator, setEditSpecialCurator] = useState(''); const [editSpecialCurator, setEditSpecialCurator] = useState('');
const [editSpecialHidden, setEditSpecialHidden] = useState(false);
// News state // News state
const [news, setNews] = useState<News[]>([]); const [news, setNews] = useState<News[]>([]);
@@ -393,6 +396,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
launchDate: newSpecialLaunchDate || null, launchDate: newSpecialLaunchDate || null,
endDate: newSpecialEndDate || null, endDate: newSpecialEndDate || null,
curator: newSpecialCurator || null, curator: newSpecialCurator || null,
hidden: newSpecialHidden,
}), }),
}); });
if (res.ok) { if (res.ok) {
@@ -404,6 +408,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
setNewSpecialLaunchDate(''); setNewSpecialLaunchDate('');
setNewSpecialEndDate(''); setNewSpecialEndDate('');
setNewSpecialCurator(''); setNewSpecialCurator('');
setNewSpecialHidden(false);
fetchSpecials(); fetchSpecials();
} else { } else {
const errorData = await res.json().catch(() => ({})); const errorData = await res.json().catch(() => ({}));
@@ -491,6 +496,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
setEditSpecialLaunchDate(special.launchDate ? new Date(special.launchDate).toISOString().split('T')[0] : ''); setEditSpecialLaunchDate(special.launchDate ? new Date(special.launchDate).toISOString().split('T')[0] : '');
setEditSpecialEndDate(special.endDate ? new Date(special.endDate).toISOString().split('T')[0] : ''); setEditSpecialEndDate(special.endDate ? new Date(special.endDate).toISOString().split('T')[0] : '');
setEditSpecialCurator(special.curator || ''); setEditSpecialCurator(special.curator || '');
setEditSpecialHidden(special.hidden || false);
}; };
const saveEditedSpecial = async () => { const saveEditedSpecial = async () => {
@@ -516,6 +522,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
launchDate: editSpecialLaunchDate || null, launchDate: editSpecialLaunchDate || null,
endDate: editSpecialEndDate || null, endDate: editSpecialEndDate || null,
curator: editSpecialCurator || null, curator: editSpecialCurator || null,
hidden: editSpecialHidden,
}), }),
}); });
if (res.ok) { if (res.ok) {
@@ -1389,6 +1396,18 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
<label style={{ fontSize: '0.75rem', color: '#666' }}>{t('curator')}</label> <label style={{ fontSize: '0.75rem', color: '#666' }}>{t('curator')}</label>
<input type="text" placeholder={t('curator')} value={newSpecialCurator} onChange={e => setNewSpecialCurator(e.target.value)} className="form-input" /> <input type="text" placeholder={t('curator')} value={newSpecialCurator} onChange={e => setNewSpecialCurator(e.target.value)} className="form-input" />
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666', visibility: 'hidden' }}>Hidden</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', height: '38px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={newSpecialHidden}
onChange={e => setNewSpecialHidden(e.target.checked)}
style={{ width: '1rem', height: '1rem' }}
/>
Hidden
</label>
</div>
<button <button
type="submit" type="submit"
className="btn-primary" className="btn-primary"
@@ -1418,7 +1437,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
}} }}
> >
<span> <span>
{getLocalizedValue(special.name, activeTab)} ({special._count?.songs || 0}) {special.hidden && <span title="Hidden from navigation">👁🗨</span>} {getLocalizedValue(special.name, activeTab)} ({special._count?.songs || 0})
</span> </span>
{special.subtitle && ( {special.subtitle && (
<span <span
@@ -1508,6 +1527,18 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
<label style={{ fontSize: '0.75rem', color: '#666' }}>{t('curator')}</label> <label style={{ fontSize: '0.75rem', color: '#666' }}>{t('curator')}</label>
<input type="text" value={editSpecialCurator} onChange={e => setEditSpecialCurator(e.target.value)} className="form-input" /> <input type="text" value={editSpecialCurator} onChange={e => setEditSpecialCurator(e.target.value)} className="form-input" />
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666', visibility: 'hidden' }}>Hidden</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', height: '38px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={editSpecialHidden}
onChange={e => setEditSpecialHidden(e.target.checked)}
style={{ width: '1rem', height: '1rem' }}
/>
Hidden
</label>
</div>
<button <button
onClick={saveEditedSpecial} onClick={saveEditedSpecial}
className="btn-primary" className="btn-primary"

View File

@@ -43,7 +43,9 @@ export default async function Home({
const genres = await prisma.genre.findMany({ const genres = await prisma.genre.findMany({
where: { active: true }, where: { active: true },
}); });
const specials = await prisma.special.findMany(); const specials = await prisma.special.findMany({
where: { hidden: false },
});
// Sort in memory // Sort in memory
genres.sort((a, b) => getLocalizedValue(a.name, locale).localeCompare(getLocalizedValue(b.name, locale))); genres.sort((a, b) => getLocalizedValue(a.name, locale).localeCompare(getLocalizedValue(b.name, locale)));

View File

@@ -86,6 +86,7 @@ export default async function SpecialPage({ params }: PageProps) {
specials.sort((a, b) => getLocalizedValue(a.name, locale).localeCompare(getLocalizedValue(b.name, locale))); specials.sort((a, b) => getLocalizedValue(a.name, locale).localeCompare(getLocalizedValue(b.name, locale)));
const activeSpecials = specials.filter(s => { const activeSpecials = specials.filter(s => {
if (s.hidden) return false;
const sStarted = !s.launchDate || s.launchDate <= now; const sStarted = !s.launchDate || s.launchDate <= now;
const sEnded = s.endDate && s.endDate < now; const sEnded = s.endDate && s.endDate < now;
return sStarted && !sEnded; return sStarted && !sEnded;

View File

@@ -12,6 +12,7 @@ interface Special {
launchDate?: string; launchDate?: string;
endDate?: string; endDate?: string;
curator?: string; curator?: string;
hidden?: boolean;
_count?: { _count?: {
songs: number; songs: number;
}; };
@@ -88,6 +89,7 @@ export default function AdminPage() {
const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState(''); const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState('');
const [newSpecialEndDate, setNewSpecialEndDate] = useState(''); const [newSpecialEndDate, setNewSpecialEndDate] = useState('');
const [newSpecialCurator, setNewSpecialCurator] = useState(''); const [newSpecialCurator, setNewSpecialCurator] = useState('');
const [newSpecialHidden, setNewSpecialHidden] = useState(false);
const [editingSpecialId, setEditingSpecialId] = useState<number | null>(null); const [editingSpecialId, setEditingSpecialId] = useState<number | null>(null);
const [editSpecialName, setEditSpecialName] = useState(''); const [editSpecialName, setEditSpecialName] = useState('');
@@ -97,6 +99,7 @@ export default function AdminPage() {
const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState(''); const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState('');
const [editSpecialEndDate, setEditSpecialEndDate] = useState(''); const [editSpecialEndDate, setEditSpecialEndDate] = useState('');
const [editSpecialCurator, setEditSpecialCurator] = useState(''); const [editSpecialCurator, setEditSpecialCurator] = useState('');
const [editSpecialHidden, setEditSpecialHidden] = useState(false);
// News state // News state
const [news, setNews] = useState<News[]>([]); const [news, setNews] = useState<News[]>([]);
@@ -268,6 +271,7 @@ export default function AdminPage() {
launchDate: newSpecialLaunchDate || null, launchDate: newSpecialLaunchDate || null,
endDate: newSpecialEndDate || null, endDate: newSpecialEndDate || null,
curator: newSpecialCurator || null, curator: newSpecialCurator || null,
hidden: newSpecialHidden,
}), }),
}); });
if (res.ok) { if (res.ok) {
@@ -278,6 +282,7 @@ export default function AdminPage() {
setNewSpecialLaunchDate(''); setNewSpecialLaunchDate('');
setNewSpecialEndDate(''); setNewSpecialEndDate('');
setNewSpecialCurator(''); setNewSpecialCurator('');
setNewSpecialHidden(false);
fetchSpecials(); fetchSpecials();
} else { } else {
alert('Failed to create special'); alert('Failed to create special');
@@ -363,6 +368,7 @@ export default function AdminPage() {
setEditSpecialLaunchDate(special.launchDate ? new Date(special.launchDate).toISOString().split('T')[0] : ''); setEditSpecialLaunchDate(special.launchDate ? new Date(special.launchDate).toISOString().split('T')[0] : '');
setEditSpecialEndDate(special.endDate ? new Date(special.endDate).toISOString().split('T')[0] : ''); setEditSpecialEndDate(special.endDate ? new Date(special.endDate).toISOString().split('T')[0] : '');
setEditSpecialCurator(special.curator || ''); setEditSpecialCurator(special.curator || '');
setEditSpecialHidden(special.hidden || false);
}; };
const saveEditedSpecial = async () => { const saveEditedSpecial = async () => {
@@ -379,6 +385,7 @@ export default function AdminPage() {
launchDate: editSpecialLaunchDate || null, launchDate: editSpecialLaunchDate || null,
endDate: editSpecialEndDate || null, endDate: editSpecialEndDate || null,
curator: editSpecialCurator || null, curator: editSpecialCurator || null,
hidden: editSpecialHidden,
}), }),
}); });
if (res.ok) { if (res.ok) {
@@ -632,6 +639,15 @@ export default function AdminPage() {
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Curator</label> <label style={{ fontSize: '0.75rem', color: '#666' }}>Curator</label>
<input type="text" placeholder="Curator name" value={newSpecialCurator} onChange={e => setNewSpecialCurator(e.target.value)} className="form-input" /> <input type="text" placeholder="Curator name" value={newSpecialCurator} onChange={e => setNewSpecialCurator(e.target.value)} className="form-input" />
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.875rem', cursor: 'pointer', marginTop: '0.5rem' }}>
<input
type="checkbox"
checked={newSpecialHidden}
onChange={e => setNewSpecialHidden(e.target.checked)}
style={{ width: '1rem', height: '1rem' }}
/>
Hidden Special (not in navigation)
</label>
</div> </div>
<button type="submit" className="btn-primary" style={{ height: '38px' }}>Add Special</button> <button type="submit" className="btn-primary" style={{ height: '38px' }}>Add Special</button>
</div> </div>
@@ -651,7 +667,7 @@ export default function AdminPage() {
}} }}
> >
<span> <span>
{special.name} ({special._count?.songs || 0}) {special.hidden && <span title="Hidden from navigation">👁🗨</span>} {special.name} ({special._count?.songs || 0})
</span> </span>
{special.subtitle && ( {special.subtitle && (
<span <span
@@ -711,6 +727,15 @@ export default function AdminPage() {
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<label style={{ fontSize: '0.75rem', color: '#666' }}>Curator</label> <label style={{ fontSize: '0.75rem', color: '#666' }}>Curator</label>
<input type="text" value={editSpecialCurator} onChange={e => setEditSpecialCurator(e.target.value)} className="form-input" /> <input type="text" value={editSpecialCurator} onChange={e => setEditSpecialCurator(e.target.value)} className="form-input" />
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.875rem', cursor: 'pointer', marginTop: '0.5rem' }}>
<input
type="checkbox"
checked={editSpecialHidden}
onChange={e => setEditSpecialHidden(e.target.checked)}
style={{ width: '1rem', height: '1rem' }}
/>
Hidden Special
</label>
</div> </div>
<button onClick={saveEditedSpecial} className="btn-primary" style={{ height: '38px' }}>Save</button> <button onClick={saveEditedSpecial} className="btn-primary" style={{ height: '38px' }}>Save</button>
<button onClick={() => setEditingSpecialId(null)} className="btn-secondary" style={{ height: '38px' }}>Cancel</button> <button onClick={() => setEditingSpecialId(null)} className="btn-secondary" style={{ height: '38px' }}>Cancel</button>

View File

@@ -43,18 +43,20 @@ export async function PUT(
try { try {
const { id } = await params; const { id } = await params;
const specialId = parseInt(id); const specialId = parseInt(id);
const { name, maxAttempts, unlockSteps, launchDate, endDate, curator } = await request.json(); const { name, maxAttempts, unlockSteps, launchDate, endDate, curator, hidden } = await request.json();
const updateData: any = {};
if (name !== undefined) updateData.name = name;
if (maxAttempts !== undefined) updateData.maxAttempts = maxAttempts;
if (unlockSteps !== undefined) updateData.unlockSteps = typeof unlockSteps === 'string' ? unlockSteps : JSON.stringify(unlockSteps);
if (launchDate !== undefined) updateData.launchDate = launchDate ? new Date(launchDate) : null;
if (endDate !== undefined) updateData.endDate = endDate ? new Date(endDate) : null;
if (curator !== undefined) updateData.curator = curator || null;
if (hidden !== undefined) updateData.hidden = Boolean(hidden);
const special = await prisma.special.update({ const special = await prisma.special.update({
where: { id: specialId }, where: { id: specialId },
data: { data: updateData
name,
maxAttempts,
unlockSteps: typeof unlockSteps === 'string' ? unlockSteps : JSON.stringify(unlockSteps),
launchDate: launchDate ? new Date(launchDate) : null,
endDate: endDate ? new Date(endDate) : null,
curator: curator || null,
}
}); });
return NextResponse.json(special); return NextResponse.json(special);

View File

@@ -35,7 +35,7 @@ export async function POST(request: Request) {
const authError = await requireAdminAuth(request as any); const authError = await requireAdminAuth(request as any);
if (authError) return authError; if (authError) return authError;
const { name, subtitle, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]', launchDate, endDate, curator } = await request.json(); const { name, subtitle, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]', launchDate, endDate, curator, hidden = false } = await request.json();
if (!name) { if (!name) {
return NextResponse.json({ error: 'Name is required' }, { status: 400 }); return NextResponse.json({ error: 'Name is required' }, { status: 400 });
} }
@@ -68,6 +68,7 @@ export async function POST(request: Request) {
launchDate: launchDate ? new Date(launchDate) : null, launchDate: launchDate ? new Date(launchDate) : null,
endDate: endDate ? new Date(endDate) : null, endDate: endDate ? new Date(endDate) : null,
curator: curator || null, curator: curator || null,
hidden: Boolean(hidden),
}, },
}); });
return NextResponse.json(special); return NextResponse.json(special);
@@ -91,7 +92,7 @@ export async function PUT(request: Request) {
const authError = await requireAdminAuth(request as any); const authError = await requireAdminAuth(request as any);
if (authError) return authError; if (authError) return authError;
const { id, name, subtitle, maxAttempts, unlockSteps, launchDate, endDate, curator } = await request.json(); const { id, name, subtitle, maxAttempts, unlockSteps, launchDate, endDate, curator, hidden } = await request.json();
if (!id) { if (!id) {
return NextResponse.json({ error: 'ID required' }, { status: 400 }); return NextResponse.json({ error: 'ID required' }, { status: 400 });
} }
@@ -119,6 +120,7 @@ export async function PUT(request: Request) {
if (launchDate !== undefined) updateData.launchDate = launchDate ? new Date(launchDate) : null; if (launchDate !== undefined) updateData.launchDate = launchDate ? new Date(launchDate) : null;
if (endDate !== undefined) updateData.endDate = endDate ? new Date(endDate) : null; if (endDate !== undefined) updateData.endDate = endDate ? new Date(endDate) : null;
if (curator !== undefined) updateData.curator = curator || null; if (curator !== undefined) updateData.curator = curator || null;
if (hidden !== undefined) updateData.hidden = Boolean(hidden);
const updated = await prisma.special.update({ const updated = await prisma.special.update({
where: { id: Number(id) }, where: { id: Number(id) },

View File

@@ -0,0 +1,20 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Special" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" JSONB NOT NULL,
"subtitle" JSONB,
"maxAttempts" INTEGER NOT NULL DEFAULT 7,
"unlockSteps" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"launchDate" DATETIME,
"endDate" DATETIME,
"curator" TEXT,
"hidden" BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO "new_Special" ("createdAt", "curator", "endDate", "id", "launchDate", "maxAttempts", "name", "subtitle", "unlockSteps") SELECT "createdAt", "curator", "endDate", "id", "launchDate", "maxAttempts", "name", "subtitle", "unlockSteps" FROM "Special";
DROP TABLE "Special";
ALTER TABLE "new_Special" RENAME TO "Special";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -47,6 +47,7 @@ model Special {
launchDate DateTime? launchDate DateTime?
endDate DateTime? endDate DateTime?
curator String? curator String?
hidden Boolean @default(false)
songs SpecialSong[] songs SpecialSong[]
puzzles DailyPuzzle[] puzzles DailyPuzzle[]
news News[] news News[]