diff --git a/app/admin/page.tsx b/app/admin/page.tsx index b8c6dc5..6c46ccb 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -8,6 +8,8 @@ interface Special { name: string; maxAttempts: number; unlockSteps: string; + launchDate?: string; + endDate?: string; _count?: { songs: number; }; @@ -62,10 +64,15 @@ export default function AdminPage() { const [newSpecialName, setNewSpecialName] = useState(''); const [newSpecialMaxAttempts, setNewSpecialMaxAttempts] = useState(7); const [newSpecialUnlockSteps, setNewSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]'); + const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState(''); + const [newSpecialEndDate, setNewSpecialEndDate] = useState(''); + const [editingSpecialId, setEditingSpecialId] = useState(null); const [editSpecialName, setEditSpecialName] = useState(''); const [editSpecialMaxAttempts, setEditSpecialMaxAttempts] = useState(7); const [editSpecialUnlockSteps, setEditSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]'); + const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState(''); + const [editSpecialEndDate, setEditSpecialEndDate] = useState(''); // Edit state const [editingId, setEditingId] = useState(null); @@ -177,12 +184,16 @@ export default function AdminPage() { name: newSpecialName, maxAttempts: newSpecialMaxAttempts, unlockSteps: newSpecialUnlockSteps, + launchDate: newSpecialLaunchDate || null, + endDate: newSpecialEndDate || null, }), }); if (res.ok) { setNewSpecialName(''); setNewSpecialMaxAttempts(7); setNewSpecialUnlockSteps('[2,4,7,11,16,30,60]'); + setNewSpecialLaunchDate(''); + setNewSpecialEndDate(''); fetchSpecials(); } else { alert('Failed to create special'); @@ -262,6 +273,8 @@ export default function AdminPage() { setEditSpecialName(special.name); setEditSpecialMaxAttempts(special.maxAttempts); setEditSpecialUnlockSteps(special.unlockSteps); + setEditSpecialLaunchDate(special.launchDate ? new Date(special.launchDate).toISOString().split('T')[0] : ''); + setEditSpecialEndDate(special.endDate ? new Date(special.endDate).toISOString().split('T')[0] : ''); }; const saveEditedSpecial = async () => { @@ -274,6 +287,8 @@ export default function AdminPage() { name: editSpecialName, maxAttempts: editSpecialMaxAttempts, unlockSteps: editSpecialUnlockSteps, + launchDate: editSpecialLaunchDate || null, + endDate: editSpecialEndDate || null, }), }); if (res.ok) { @@ -671,11 +686,28 @@ export default function AdminPage() {

Manage Specials

-
- setNewSpecialName(e.target.value)} className="form-input" required /> - setNewSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} /> - setNewSpecialUnlockSteps(e.target.value)} className="form-input" /> - +
+
+ + setNewSpecialName(e.target.value)} className="form-input" required /> +
+
+ + setNewSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} /> +
+
+ + setNewSpecialUnlockSteps(e.target.value)} className="form-input" style={{ width: '200px' }} /> +
+
+ + setNewSpecialLaunchDate(e.target.value)} className="form-input" /> +
+
+ + setNewSpecialEndDate(e.target.value)} className="form-input" /> +
+
@@ -691,6 +723,7 @@ export default function AdminPage() { }}> {special.name} ({special._count?.songs || 0}) Curate +
))} @@ -698,12 +731,29 @@ export default function AdminPage() { {editingSpecialId !== null && (

Edit Special

-
- setEditSpecialName(e.target.value)} className="form-input" /> - setEditSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} /> - setEditSpecialUnlockSteps(e.target.value)} className="form-input" /> - - +
+
+ + setEditSpecialName(e.target.value)} className="form-input" /> +
+
+ + setEditSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} /> +
+
+ + setEditSpecialUnlockSteps(e.target.value)} className="form-input" style={{ width: '200px' }} /> +
+
+ + setEditSpecialLaunchDate(e.target.value)} className="form-input" /> +
+
+ + setEditSpecialEndDate(e.target.value)} className="form-input" /> +
+ +
)} diff --git a/app/api/specials/[id]/route.ts b/app/api/specials/[id]/route.ts index 41383fd..e2f6c49 100644 --- a/app/api/specials/[id]/route.ts +++ b/app/api/specials/[id]/route.ts @@ -43,14 +43,16 @@ export async function PUT( try { const { id } = await params; const specialId = parseInt(id); - const { name, maxAttempts, unlockSteps } = await request.json(); + const { name, maxAttempts, unlockSteps, launchDate, endDate } = await request.json(); const special = await prisma.special.update({ where: { id: specialId }, data: { name, maxAttempts, - unlockSteps: JSON.stringify(unlockSteps) + unlockSteps: typeof unlockSteps === 'string' ? unlockSteps : JSON.stringify(unlockSteps), + launchDate: launchDate ? new Date(launchDate) : null, + endDate: endDate ? new Date(endDate) : null, } }); diff --git a/app/api/specials/route.ts b/app/api/specials/route.ts index e546a8a..148928e 100644 --- a/app/api/specials/route.ts +++ b/app/api/specials/route.ts @@ -11,7 +11,7 @@ export async function GET() { } export async function POST(request: Request) { - const { name, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]' } = await request.json(); + const { name, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]', launchDate, endDate } = await request.json(); if (!name) { return NextResponse.json({ error: 'Name is required' }, { status: 400 }); } @@ -20,6 +20,8 @@ export async function POST(request: Request) { name, maxAttempts: Number(maxAttempts), unlockSteps, + launchDate: launchDate ? new Date(launchDate) : null, + endDate: endDate ? new Date(endDate) : null, }, }); return NextResponse.json(special); @@ -35,7 +37,7 @@ export async function DELETE(request: Request) { } export async function PUT(request: Request) { - const { id, name, maxAttempts, unlockSteps } = await request.json(); + const { id, name, maxAttempts, unlockSteps, launchDate, endDate } = await request.json(); if (!id) { return NextResponse.json({ error: 'ID required' }, { status: 400 }); } @@ -45,6 +47,8 @@ export async function PUT(request: Request) { ...(name && { name }), ...(maxAttempts && { maxAttempts: Number(maxAttempts) }), ...(unlockSteps && { unlockSteps }), + launchDate: launchDate ? new Date(launchDate) : null, + endDate: endDate ? new Date(endDate) : null, }, }); return NextResponse.json(updated); diff --git a/app/page.tsx b/app/page.tsx index e0b68a9..3a716ad 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -12,6 +12,18 @@ export default async function Home() { const genres = await prisma.genre.findMany({ orderBy: { name: 'asc' } }); const specials = await prisma.special.findMany({ orderBy: { name: 'asc' } }); + const now = new Date(); + + const activeSpecials = specials.filter(s => { + const isStarted = !s.launchDate || s.launchDate <= now; + const isEnded = s.endDate && s.endDate < now; + return isStarted && !isEnded; + }); + + const upcomingSpecials = specials.filter(s => { + return s.launchDate && s.launchDate > now; + }); + return ( <>
@@ -26,12 +38,12 @@ export default async function Home() { ))} {/* Separator if both exist */} - {genres.length > 0 && specials.length > 0 && ( + {genres.length > 0 && activeSpecials.length > 0 && ( | )} - {/* Specials */} - {specials.map(s => ( + {/* Active Specials */} + {activeSpecials.map(s => ( ))}
+ + {/* Upcoming Specials */} + {upcomingSpecials.length > 0 && ( +
+ Coming soon: {upcomingSpecials.map(s => ( + + ★ {s.name} ({s.launchDate?.toLocaleDateString()}) + + ))} +
+ )}
diff --git a/app/special/[name]/page.tsx b/app/special/[name]/page.tsx index 3a62938..c256c5f 100644 --- a/app/special/[name]/page.tsx +++ b/app/special/[name]/page.tsx @@ -14,10 +14,45 @@ interface PageProps { export default async function SpecialPage({ params }: PageProps) { const { name } = await params; const decodedName = decodeURIComponent(name); + + const currentSpecial = await prisma.special.findUnique({ + where: { name: decodedName } + }); + + const now = new Date(); + const isStarted = currentSpecial && (!currentSpecial.launchDate || currentSpecial.launchDate <= now); + const isEnded = currentSpecial && (currentSpecial.endDate && currentSpecial.endDate < now); + + if (!currentSpecial || !isStarted) { + return ( +
+

Special Not Available

+

This special has not launched yet or does not exist.

+ Go Home +
+ ); + } + + if (isEnded) { + return ( +
+

Special Ended

+

This special event has ended.

+ Go Home +
+ ); + } + const dailyPuzzle = await getOrCreateSpecialPuzzle(decodedName); const genres = await prisma.genre.findMany({ orderBy: { name: 'asc' } }); const specials = await prisma.special.findMany({ orderBy: { name: 'asc' } }); + const activeSpecials = specials.filter(s => { + const sStarted = !s.launchDate || s.launchDate <= now; + const sEnded = s.endDate && s.endDate < now; + return sStarted && !sEnded; + }); + return ( <>
@@ -39,12 +74,12 @@ export default async function SpecialPage({ params }: PageProps) { ))} {/* Separator if both exist */} - {genres.length > 0 && specials.length > 0 && ( + {genres.length > 0 && activeSpecials.length > 0 && ( | )} {/* Specials */} - {specials.map(s => ( + {activeSpecials.map(s => (