diff --git a/app/[locale]/admin/page.tsx b/app/[locale]/admin/page.tsx index 4570dcc..c20ab6f 100644 --- a/app/[locale]/admin/page.tsx +++ b/app/[locale]/admin/page.tsx @@ -115,6 +115,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) { const [newSpecialSubtitle, setNewSpecialSubtitle] = useState({ de: '', en: '' }); const [newSpecialMaxAttempts, setNewSpecialMaxAttempts] = useState(7); const [newSpecialUnlockSteps, setNewSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]'); + const [newSpecialUnlockStepsError, setNewSpecialUnlockStepsError] = useState(null); const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState(''); const [newSpecialEndDate, setNewSpecialEndDate] = useState(''); const [newSpecialCurator, setNewSpecialCurator] = useState(''); @@ -124,6 +125,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) { const [editSpecialSubtitle, setEditSpecialSubtitle] = useState({ de: '', en: '' }); const [editSpecialMaxAttempts, setEditSpecialMaxAttempts] = useState(7); const [editSpecialUnlockSteps, setEditSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]'); + const [editSpecialUnlockStepsError, setEditSpecialUnlockStepsError] = useState(null); const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState(''); const [editSpecialEndDate, setEditSpecialEndDate] = useState(''); const [editSpecialCurator, setEditSpecialCurator] = useState(''); @@ -239,6 +241,25 @@ export default function AdminPage({ params }: { params: { locale: string } }) { } }; + // Validate JSON for unlock steps + const validateUnlockSteps = (value: string): string | null => { + if (!value.trim()) { + return t('unlockStepsRequired'); + } + try { + const parsed = JSON.parse(value); + if (!Array.isArray(parsed)) { + return t('unlockStepsMustBeArray'); + } + if (parsed.some((item: any) => typeof item !== 'number' || item < 1)) { + return t('unlockStepsMustBePositiveNumbers'); + } + return null; + } catch (e) { + return t('unlockStepsInvalidJson'); + } + }; + const handleLogout = () => { localStorage.removeItem('hoerdle_admin_auth'); setIsAuthenticated(false); @@ -352,6 +373,15 @@ export default function AdminPage({ params }: { params: { locale: string } }) { const handleCreateSpecial = async (e: React.FormEvent) => { e.preventDefault(); if (!newSpecialName.de.trim() && !newSpecialName.en.trim()) return; + + // Validate unlock steps + const unlockStepsError = validateUnlockSteps(newSpecialUnlockSteps); + if (unlockStepsError) { + setNewSpecialUnlockStepsError(unlockStepsError); + return; + } + setNewSpecialUnlockStepsError(null); + const res = await fetch('/api/specials', { method: 'POST', headers: getAuthHeaders(), @@ -370,12 +400,14 @@ export default function AdminPage({ params }: { params: { locale: string } }) { setNewSpecialSubtitle({ de: '', en: '' }); setNewSpecialMaxAttempts(7); setNewSpecialUnlockSteps('[2,4,7,11,16,30,60]'); + setNewSpecialUnlockStepsError(null); setNewSpecialLaunchDate(''); setNewSpecialEndDate(''); setNewSpecialCurator(''); fetchSpecials(); } else { - alert('Failed to create special'); + const errorData = await res.json().catch(() => ({})); + alert(errorData.error || 'Failed to create special'); } }; @@ -455,6 +487,7 @@ export default function AdminPage({ params }: { params: { locale: string } }) { setEditSpecialSubtitle(special.subtitle ? (typeof special.subtitle === 'string' ? { de: special.subtitle, en: special.subtitle } : special.subtitle) : { de: '', en: '' }); setEditSpecialMaxAttempts(special.maxAttempts); setEditSpecialUnlockSteps(special.unlockSteps); + setEditSpecialUnlockStepsError(null); 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 || ''); @@ -462,6 +495,15 @@ export default function AdminPage({ params }: { params: { locale: string } }) { const saveEditedSpecial = async () => { if (editingSpecialId === null) return; + + // Validate unlock steps + const unlockStepsError = validateUnlockSteps(editSpecialUnlockSteps); + if (unlockStepsError) { + setEditSpecialUnlockStepsError(unlockStepsError); + return; + } + setEditSpecialUnlockStepsError(null); + const res = await fetch('/api/specials', { method: 'PUT', headers: getAuthHeaders(), @@ -478,9 +520,11 @@ export default function AdminPage({ params }: { params: { locale: string } }) { }); if (res.ok) { setEditingSpecialId(null); + setEditSpecialUnlockStepsError(null); fetchSpecials(); } else { - alert('Failed to update special'); + const errorData = await res.json().catch(() => ({})); + alert(errorData.error || 'Failed to update special'); } }; @@ -1300,8 +1344,38 @@ export default function AdminPage({ params }: { params: { locale: string } }) { setNewSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} />
- - setNewSpecialUnlockSteps(e.target.value)} className="form-input" style={{ width: '200px' }} /> + + { + const value = e.target.value; + setNewSpecialUnlockSteps(value); + const error = validateUnlockSteps(value); + setNewSpecialUnlockStepsError(error); + }} + className="form-input" + title={newSpecialUnlockStepsError || undefined} + style={{ + width: '200px', + borderColor: newSpecialUnlockStepsError ? '#ef4444' : undefined + }} + />
@@ -1315,7 +1389,18 @@ export default function AdminPage({ params }: { params: { locale: string } }) { setNewSpecialCurator(e.target.value)} className="form-input" />
- +
@@ -1379,8 +1464,37 @@ export default function AdminPage({ params }: { params: { locale: string } }) { setEditSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} />
- - setEditSpecialUnlockSteps(e.target.value)} className="form-input" style={{ width: '200px' }} /> + + { + const value = e.target.value; + setEditSpecialUnlockSteps(value); + const error = validateUnlockSteps(value); + setEditSpecialUnlockStepsError(error); + }} + className="form-input" + title={editSpecialUnlockStepsError || undefined} + style={{ + width: '200px', + borderColor: editSpecialUnlockStepsError ? '#ef4444' : undefined + }} + />
@@ -1394,7 +1508,18 @@ export default function AdminPage({ params }: { params: { locale: string } }) { setEditSpecialCurator(e.target.value)} className="form-input" />
- + diff --git a/app/api/specials/route.ts b/app/api/specials/route.ts index bc5a3ab..0c58d82 100644 --- a/app/api/specials/route.ts +++ b/app/api/specials/route.ts @@ -40,6 +40,21 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Name is required' }, { status: 400 }); } + // Validate unlockSteps JSON + if (unlockSteps) { + try { + const parsed = JSON.parse(unlockSteps); + if (!Array.isArray(parsed)) { + return NextResponse.json({ error: 'Unlock steps must be a JSON array' }, { status: 400 }); + } + if (parsed.some((item: any) => typeof item !== 'number' || item < 1)) { + return NextResponse.json({ error: 'All unlock step values must be positive numbers' }, { status: 400 }); + } + } catch (e) { + return NextResponse.json({ error: 'Invalid JSON format for unlock steps. Please use an array of numbers, e.g. [2,4,7,11,16,30,60]' }, { status: 400 }); + } + } + // Ensure name is stored as JSON const nameData = typeof name === 'string' ? { de: name, en: name } : name; const subtitleData = subtitle ? (typeof subtitle === 'string' ? { de: subtitle, en: subtitle } : subtitle) : null; @@ -81,6 +96,21 @@ export async function PUT(request: Request) { return NextResponse.json({ error: 'ID required' }, { status: 400 }); } + // Validate unlockSteps JSON if provided + if (unlockSteps !== undefined) { + try { + const parsed = JSON.parse(unlockSteps); + if (!Array.isArray(parsed)) { + return NextResponse.json({ error: 'Unlock steps must be a JSON array' }, { status: 400 }); + } + if (parsed.some((item: any) => typeof item !== 'number' || item < 1)) { + return NextResponse.json({ error: 'All unlock step values must be positive numbers' }, { status: 400 }); + } + } catch (e) { + return NextResponse.json({ error: 'Invalid JSON format for unlock steps. Please use an array of numbers, e.g. [2,4,7,11,16,30,60]' }, { status: 400 }); + } + } + const updateData: any = {}; if (name) updateData.name = typeof name === 'string' ? { de: name, en: name } : name; if (subtitle !== undefined) updateData.subtitle = subtitle ? (typeof subtitle === 'string' ? { de: subtitle, en: subtitle } : subtitle) : null; diff --git a/messages/de.json b/messages/de.json index 1ddb34a..d9973bf 100644 --- a/messages/de.json +++ b/messages/de.json @@ -149,6 +149,10 @@ "subtitle": "Untertitel", "maxAttempts": "Max. Versuche", "unlockSteps": "Freischalt-Schritte", + "unlockStepsRequired": "Freischalt-Schritte sind erforderlich", + "unlockStepsInvalidJson": "Ungültiges JSON-Format. Bitte verwende ein Array von Zahlen, z.B. [2,4,7,11,16,30,60]", + "unlockStepsMustBeArray": "Freischalt-Schritte müssen ein Array sein", + "unlockStepsMustBePositiveNumbers": "Alle Werte müssen positive Zahlen sein", "launchDate": "Startdatum", "endDate": "Enddatum", "curator": "Kurator", diff --git a/messages/en.json b/messages/en.json index 38ff8bf..0c625eb 100644 --- a/messages/en.json +++ b/messages/en.json @@ -149,6 +149,10 @@ "subtitle": "Subtitle", "maxAttempts": "Max Attempts", "unlockSteps": "Unlock Steps", + "unlockStepsRequired": "Unlock steps are required", + "unlockStepsInvalidJson": "Invalid JSON format. Please use an array of numbers, e.g. [2,4,7,11,16,30,60]", + "unlockStepsMustBeArray": "Unlock steps must be an array", + "unlockStepsMustBePositiveNumbers": "All values must be positive numbers", "launchDate": "Launch Date", "endDate": "End Date", "curator": "Curator",