Compare commits
6 Commits
e9526918e1
...
8262a96213
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8262a96213 | ||
|
|
b294a3a8e6 | ||
|
|
2a38bce02c | ||
|
|
6fd5f8ed0c | ||
|
|
c05ead4493 | ||
|
|
5fb450d37e |
@@ -10,6 +10,7 @@ interface Special {
|
||||
unlockSteps: string;
|
||||
launchDate?: string;
|
||||
endDate?: string;
|
||||
curator?: string;
|
||||
_count?: {
|
||||
songs: number;
|
||||
};
|
||||
@@ -66,6 +67,7 @@ export default function AdminPage() {
|
||||
const [newSpecialUnlockSteps, setNewSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]');
|
||||
const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState('');
|
||||
const [newSpecialEndDate, setNewSpecialEndDate] = useState('');
|
||||
const [newSpecialCurator, setNewSpecialCurator] = useState('');
|
||||
|
||||
const [editingSpecialId, setEditingSpecialId] = useState<number | null>(null);
|
||||
const [editSpecialName, setEditSpecialName] = useState('');
|
||||
@@ -73,6 +75,7 @@ export default function AdminPage() {
|
||||
const [editSpecialUnlockSteps, setEditSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]');
|
||||
const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState('');
|
||||
const [editSpecialEndDate, setEditSpecialEndDate] = useState('');
|
||||
const [editSpecialCurator, setEditSpecialCurator] = useState('');
|
||||
|
||||
// Edit state
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
@@ -186,6 +189,7 @@ export default function AdminPage() {
|
||||
unlockSteps: newSpecialUnlockSteps,
|
||||
launchDate: newSpecialLaunchDate || null,
|
||||
endDate: newSpecialEndDate || null,
|
||||
curator: newSpecialCurator || null,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
@@ -194,6 +198,7 @@ export default function AdminPage() {
|
||||
setNewSpecialUnlockSteps('[2,4,7,11,16,30,60]');
|
||||
setNewSpecialLaunchDate('');
|
||||
setNewSpecialEndDate('');
|
||||
setNewSpecialCurator('');
|
||||
fetchSpecials();
|
||||
} else {
|
||||
alert('Failed to create special');
|
||||
@@ -275,6 +280,7 @@ export default function AdminPage() {
|
||||
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] : '');
|
||||
setEditSpecialCurator(special.curator || '');
|
||||
};
|
||||
|
||||
const saveEditedSpecial = async () => {
|
||||
@@ -289,6 +295,7 @@ export default function AdminPage() {
|
||||
unlockSteps: editSpecialUnlockSteps,
|
||||
launchDate: editSpecialLaunchDate || null,
|
||||
endDate: editSpecialEndDate || null,
|
||||
curator: editSpecialCurator || null,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
@@ -707,6 +714,10 @@ export default function AdminPage() {
|
||||
<label style={{ fontSize: '0.75rem', color: '#666' }}>End Date</label>
|
||||
<input type="date" value={newSpecialEndDate} onChange={e => setNewSpecialEndDate(e.target.value)} className="form-input" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<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" />
|
||||
</div>
|
||||
<button type="submit" className="btn-primary" style={{ height: '38px' }}>Add Special</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -752,6 +763,10 @@ export default function AdminPage() {
|
||||
<label style={{ fontSize: '0.75rem', color: '#666' }}>End Date</label>
|
||||
<input type="date" value={editSpecialEndDate} onChange={e => setEditSpecialEndDate(e.target.value)} className="form-input" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<label style={{ fontSize: '0.75rem', color: '#666' }}>Curator</label>
|
||||
<input type="text" value={editSpecialCurator} onChange={e => setEditSpecialCurator(e.target.value)} className="form-input" />
|
||||
</div>
|
||||
<button onClick={saveEditedSpecial} className="btn-primary" style={{ height: '38px' }}>Save</button>
|
||||
<button onClick={() => setEditingSpecialId(null)} className="btn-secondary" style={{ height: '38px' }}>Cancel</button>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function PUT(
|
||||
try {
|
||||
const { id } = await params;
|
||||
const specialId = parseInt(id);
|
||||
const { name, maxAttempts, unlockSteps, launchDate, endDate } = await request.json();
|
||||
const { name, maxAttempts, unlockSteps, launchDate, endDate, curator } = await request.json();
|
||||
|
||||
const special = await prisma.special.update({
|
||||
where: { id: specialId },
|
||||
@@ -53,6 +53,7 @@ export async function PUT(
|
||||
unlockSteps: typeof unlockSteps === 'string' ? unlockSteps : JSON.stringify(unlockSteps),
|
||||
launchDate: launchDate ? new Date(launchDate) : null,
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
curator: curator || null,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,12 +6,17 @@ const prisma = new PrismaClient();
|
||||
export async function GET() {
|
||||
const specials = await prisma.special.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: { songs: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
return NextResponse.json(specials);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { name, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]', launchDate, endDate } = await request.json();
|
||||
const { name, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]', launchDate, endDate, curator } = await request.json();
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||
}
|
||||
@@ -22,6 +27,7 @@ export async function POST(request: Request) {
|
||||
unlockSteps,
|
||||
launchDate: launchDate ? new Date(launchDate) : null,
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
curator: curator || null,
|
||||
},
|
||||
});
|
||||
return NextResponse.json(special);
|
||||
@@ -37,7 +43,7 @@ export async function DELETE(request: Request) {
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const { id, name, maxAttempts, unlockSteps, launchDate, endDate } = await request.json();
|
||||
const { id, name, maxAttempts, unlockSteps, launchDate, endDate, curator } = await request.json();
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
}
|
||||
@@ -49,6 +55,7 @@ export async function PUT(request: Request) {
|
||||
...(unlockSteps && { unlockSteps }),
|
||||
launchDate: launchDate ? new Date(launchDate) : null,
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
curator: curator || null,
|
||||
},
|
||||
});
|
||||
return NextResponse.json(updated);
|
||||
|
||||
@@ -278,12 +278,59 @@ body {
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #4b5563;
|
||||
color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.app-footer {
|
||||
margin-top: auto;
|
||||
|
||||
16
app/page.tsx
16
app/page.tsx
@@ -44,8 +44,8 @@ export default async function Home() {
|
||||
|
||||
{/* Active Specials */}
|
||||
{activeSpecials.map(s => (
|
||||
<div key={s.id} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Link
|
||||
key={s.id}
|
||||
href={`/special/${s.name}`}
|
||||
style={{
|
||||
color: '#be185d', // Pink-700
|
||||
@@ -55,6 +55,12 @@ export default async function Home() {
|
||||
>
|
||||
★ {s.name}
|
||||
</Link>
|
||||
{s.curator && (
|
||||
<span style={{ fontSize: '0.75rem', color: '#666' }}>
|
||||
Curated by {s.curator}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -63,7 +69,13 @@ export default async function Home() {
|
||||
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#666' }}>
|
||||
Coming soon: {upcomingSpecials.map(s => (
|
||||
<span key={s.id} style={{ marginLeft: '0.5rem' }}>
|
||||
★ {s.name} ({s.launchDate?.toLocaleDateString()})
|
||||
★ {s.name} ({s.launchDate ? new Date(s.launchDate).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
timeZone: process.env.TZ
|
||||
}) : ''})
|
||||
{s.curator && <span style={{ fontStyle: 'italic', marginLeft: '0.25rem' }}>Curated by {s.curator}</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Special" ADD COLUMN "curator" TEXT;
|
||||
@@ -37,6 +37,7 @@ model Special {
|
||||
createdAt DateTime @default(now())
|
||||
launchDate DateTime?
|
||||
endDate DateTime?
|
||||
curator String?
|
||||
songs SpecialSong[]
|
||||
puzzles DailyPuzzle[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user