Compare commits
3 Commits
v0.1.6.25
...
83e1281079
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e1281079 | ||
|
|
2e1f1e599b | ||
|
|
71c4e2509f |
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Hördle Admin Dashboard",
|
||||
description: "Admin dashboard for managing songs and daily puzzles",
|
||||
};
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
1185
app/admin/page.tsx
1185
app/admin/page.tsx
File diff suppressed because it is too large
Load Diff
@@ -1,89 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter, usePathname } from 'next/navigation';
|
||||
import CurateSpecialEditor, { CurateSpecial } from '@/components/CurateSpecialEditor';
|
||||
|
||||
export default function SpecialEditorPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const specialId = params.id as string;
|
||||
|
||||
// Locale aus dem Pfad ableiten (/en/..., /de/...)
|
||||
const localeFromPath = pathname?.split('/')[1] as 'de' | 'en' | undefined;
|
||||
const locale: 'de' | 'en' = localeFromPath === 'de' || localeFromPath === 'en' ? localeFromPath : 'en';
|
||||
|
||||
const [special, setSpecial] = useState<CurateSpecial | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchSpecial = async (showLoading = true) => {
|
||||
try {
|
||||
if (showLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
const res = await fetch(`/api/specials/${specialId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSpecial(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching special:', error);
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSpecial(true);
|
||||
}, [specialId]);
|
||||
|
||||
const handleSaveStartTime = async (songId: number, startTime: number) => {
|
||||
const res = await fetch(`/api/specials/${specialId}/songs`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ songId, startTime }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => res.statusText || 'Unknown error');
|
||||
console.error('Error updating special song (admin):', res.status, errorText);
|
||||
throw new Error(`Failed to save start time: ${errorText}`);
|
||||
} else {
|
||||
// Reload special data to update the start time in the song list
|
||||
await fetchSpecial(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!special) {
|
||||
return (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<p>Special not found</p>
|
||||
<button onClick={() => router.push('/admin')}>Back to Admin</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CurateSpecialEditor
|
||||
special={special}
|
||||
locale={locale}
|
||||
onBack={() => router.push('/admin')}
|
||||
onSaveStartTime={handleSaveStartTime}
|
||||
backLabel="← Back to Admin"
|
||||
headerPrefix="Edit Special:"
|
||||
noSongsSubHint="Go back to the admin dashboard to add songs to this special."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
23
app/api/admin/reset-activations/route.ts
Normal file
23
app/api/admin/reset-activations/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Delete all daily puzzles (activations)
|
||||
const result = await prisma.dailyPuzzle.deleteMany({});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully deleted ${result.count} daily puzzles (activations)`,
|
||||
count: result.count,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error resetting activations:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to reset activations' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
28
app/api/admin/reset-ratings/route.ts
Normal file
28
app/api/admin/reset-ratings/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Reset all song ratings to 0
|
||||
const result = await prisma.song.updateMany({
|
||||
data: {
|
||||
averageRating: 0,
|
||||
ratingCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully reset ratings for ${result.count} songs`,
|
||||
count: result.count,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error resetting ratings:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to reset ratings' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
.page {
|
||||
--background: #fafafa;
|
||||
--foreground: #fff;
|
||||
|
||||
--text-primary: #000;
|
||||
--text-secondary: #666;
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
--button-secondary-border: #ebebeb;
|
||||
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-geist-sans);
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
background-color: var(--foreground);
|
||||
padding: 120px 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
max-width: 320px;
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
line-height: 48px;
|
||||
letter-spacing: -2.4px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.intro p {
|
||||
max-width: 440px;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.intro a {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 128px;
|
||||
border: 1px solid transparent;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--text-primary);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--button-secondary-border);
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.main {
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
letter-spacing: -1.92px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.page {
|
||||
--background: #000;
|
||||
--foreground: #000;
|
||||
|
||||
--text-primary: #ededed;
|
||||
--text-secondary: #999;
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
--button-secondary-border: #1a1a1a;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoerdle",
|
||||
"version": "0.1.6.25",
|
||||
"version": "0.1.6.26",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -32,4 +32,4 @@
|
||||
"eslint-config-next": "^16.0.7",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user