Compare commits
3 Commits
3f544bf77c
...
e931787752
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e931787752 | ||
|
|
3f47fac276 | ||
|
|
75a8a63b21 |
@@ -62,7 +62,9 @@ Das Projekt ist für den Betrieb mit Docker optimiert.
|
|||||||
```bash
|
```bash
|
||||||
cp docker-compose.example.yml docker-compose.yml
|
cp docker-compose.example.yml docker-compose.yml
|
||||||
```
|
```
|
||||||
Passe ggf. das `ADMIN_PASSWORD` in der `docker-compose.yml` an.
|
Passe die Umgebungsvariablen in der `docker-compose.yml` an:
|
||||||
|
- `ADMIN_PASSWORD`: Admin-Passwort (Standard: `admin123`)
|
||||||
|
- `TZ`: Zeitzone für täglichen Puzzle-Wechsel (Standard: `Europe/Berlin`)
|
||||||
|
|
||||||
2. **Starten:**
|
2. **Starten:**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface Song {
|
|||||||
activations: number;
|
activations: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortField = 'title' | 'artist' | 'createdAt';
|
type SortField = 'id' | 'title' | 'artist' | 'createdAt';
|
||||||
type SortDirection = 'asc' | 'desc';
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
@@ -210,8 +210,14 @@ export default function AdminPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sortedSongs = [...filteredSongs].sort((a, b) => {
|
const sortedSongs = [...filteredSongs].sort((a, b) => {
|
||||||
const valA = a[sortField].toLowerCase();
|
// Handle numeric sorting for ID
|
||||||
const valB = b[sortField].toLowerCase();
|
if (sortField === 'id') {
|
||||||
|
return sortDirection === 'asc' ? a.id - b.id : b.id - a.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String sorting for other fields
|
||||||
|
const valA = String(a[sortField]).toLowerCase();
|
||||||
|
const valB = String(b[sortField]).toLowerCase();
|
||||||
|
|
||||||
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
|
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
|
||||||
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
|
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
|
||||||
@@ -302,7 +308,12 @@ export default function AdminPage() {
|
|||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: '2px solid #e5e7eb', textAlign: 'left' }}>
|
<tr style={{ borderBottom: '2px solid #e5e7eb', textAlign: 'left' }}>
|
||||||
<th style={{ padding: '0.75rem' }}>ID</th>
|
<th
|
||||||
|
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
||||||
|
onClick={() => handleSort('id')}
|
||||||
|
>
|
||||||
|
ID {sortField === 'id' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
||||||
onClick={() => handleSort('title')}
|
onClick={() => handleSort('title')}
|
||||||
|
|||||||
@@ -5,13 +5,22 @@ const prisma = new PrismaClient();
|
|||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
// Use timezone from environment variable (default: Europe/Berlin)
|
||||||
|
const timezone = process.env.TZ || 'Europe/Berlin';
|
||||||
|
const today = new Date().toLocaleDateString('en-CA', {
|
||||||
|
timeZone: timezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}); // Format: "2025-11-21"
|
||||||
|
|
||||||
let dailyPuzzle = await prisma.dailyPuzzle.findUnique({
|
let dailyPuzzle = await prisma.dailyPuzzle.findUnique({
|
||||||
where: { date: today },
|
where: { date: today },
|
||||||
include: { song: true },
|
include: { song: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[Daily Puzzle] Date: ${today}, Found existing: ${!!dailyPuzzle}`);
|
||||||
|
|
||||||
if (!dailyPuzzle) {
|
if (!dailyPuzzle) {
|
||||||
// Get all songs with their usage count
|
// Get all songs with their usage count
|
||||||
const allSongs = await prisma.song.findMany({
|
const allSongs = await prisma.song.findMany({
|
||||||
@@ -54,6 +63,8 @@ export async function GET() {
|
|||||||
},
|
},
|
||||||
include: { song: true },
|
include: { song: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[Daily Puzzle] Created new puzzle for ${today} with song: ${selectedSong.title} (ID: ${selectedSong.id})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dailyPuzzle) {
|
if (!dailyPuzzle) {
|
||||||
@@ -63,7 +74,10 @@ export async function GET() {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: dailyPuzzle.id,
|
id: dailyPuzzle.id,
|
||||||
audioUrl: `/uploads/${dailyPuzzle.song.filename}`,
|
audioUrl: `/uploads/${dailyPuzzle.song.filename}`,
|
||||||
songId: dailyPuzzle.songId
|
songId: dailyPuzzle.songId,
|
||||||
|
title: dailyPuzzle.song.title,
|
||||||
|
artist: dailyPuzzle.song.artist,
|
||||||
|
coverImage: dailyPuzzle.song.coverImage ? `/uploads/covers/${dailyPuzzle.song.coverImage}` : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -103,7 +103,19 @@ export async function POST(request: Request) {
|
|||||||
if (!title) title = 'Unknown Title';
|
if (!title) title = 'Unknown Title';
|
||||||
if (!artist) artist = 'Unknown Artist';
|
if (!artist) artist = 'Unknown Artist';
|
||||||
|
|
||||||
const filename = `${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.]/g, '_')}`;
|
// Create URL-safe filename
|
||||||
|
const originalName = file.name.replace(/\.mp3$/i, '');
|
||||||
|
const sanitizedName = originalName
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, '-') // Replace special chars with dash
|
||||||
|
.replace(/-+/g, '-') // Replace multiple dashes with single dash
|
||||||
|
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
||||||
|
|
||||||
|
// Warn if filename was changed
|
||||||
|
if (originalName !== sanitizedName) {
|
||||||
|
validationInfo.warnings.push(`Filename sanitized: "${originalName}" → "${sanitizedName}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `${Date.now()}-${sanitizedName}.mp3`;
|
||||||
const uploadDir = path.join(process.cwd(), 'public/uploads');
|
const uploadDir = path.join(process.cwd(), 'public/uploads');
|
||||||
|
|
||||||
await writeFile(path.join(uploadDir, filename), buffer);
|
await writeFile(path.join(uploadDir, filename), buffer);
|
||||||
|
|||||||
@@ -194,17 +194,30 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.skip-button {
|
.skip-button {
|
||||||
background: none;
|
width: 100%;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
color: #666;
|
border-radius: 0.5rem;
|
||||||
text-decoration: underline;
|
font-size: 1rem;
|
||||||
font-size: 0.875rem;
|
font-weight: 600;
|
||||||
margin-top: 0.5rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skip-button:hover {
|
.skip-button:hover {
|
||||||
color: #000;
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Messages */
|
/* Messages */
|
||||||
|
|||||||
@@ -136,12 +136,28 @@ export default function Game({ dailyPuzzle }: GameProps) {
|
|||||||
{!hasWon && !hasLost && (
|
{!hasWon && !hasLost && (
|
||||||
<>
|
<>
|
||||||
<GuessInput onGuess={handleGuess} disabled={false} />
|
<GuessInput onGuess={handleGuess} disabled={false} />
|
||||||
<button
|
{gameState.guesses.length < 6 ? (
|
||||||
onClick={() => addGuess("SKIPPED", false)}
|
<button
|
||||||
className="skip-button"
|
onClick={() => addGuess("SKIPPED", false)}
|
||||||
>
|
className="skip-button"
|
||||||
Skip (+{UNLOCK_STEPS[Math.min(gameState.guesses.length + 1, 6)] - unlockedSeconds}s)
|
>
|
||||||
</button>
|
Skip (+{UNLOCK_STEPS[Math.min(gameState.guesses.length + 1, 6)] - unlockedSeconds}s)
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
addGuess("SKIPPED", false);
|
||||||
|
setHasLost(true);
|
||||||
|
}}
|
||||||
|
className="skip-button"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||||
|
boxShadow: '0 4px 15px rgba(245, 87, 108, 0.4)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Solve (Give Up)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=file:/app/data/prod.db
|
- DATABASE_URL=file:/app/data/prod.db
|
||||||
- ADMIN_PASSWORD=admin123 # Change this!
|
- ADMIN_PASSWORD=admin123 # Change this!
|
||||||
|
- TZ=Europe/Berlin # Timezone for daily puzzle rotation
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./public/uploads:/app/public/uploads
|
- ./public/uploads:/app/public/uploads
|
||||||
|
|||||||
@@ -4,6 +4,32 @@ const nextConfig: NextConfig = {
|
|||||||
/* config options here */
|
/* config options here */
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
bodySizeLimit: '50mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/uploads/:path*.mp3',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'audio/mpeg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Accept-Ranges',
|
||||||
|
value: 'bytes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Cache-Control',
|
||||||
|
value: 'public, max-age=31536000, immutable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user