Compare commits
3 Commits
v0.1.6.0
...
50ca51b143
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50ca51b143 | ||
|
|
afe6e12afc | ||
|
|
91b12ad859 |
28
README.md
28
README.md
@@ -15,6 +15,7 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k
|
||||
- Bearbeitung von Metadaten.
|
||||
- Sortierbare Song-Bibliothek (Titel, Interpret, Hinzugefügt am, Erscheinungsjahr, Aktivierungen, Rating).
|
||||
- Play/Pause-Funktion zum Vorhören in der Bibliothek.
|
||||
- **Kuratoren-Verwaltung:** Erstellen und Verwalten von Kurator-Accounts mit Zuweisung zu Genres und Specials.
|
||||
- **Cover Art:**
|
||||
- Automatische Extraktion von Cover-Bildern aus MP3-Dateien.
|
||||
- Anzeige des Covers nach Spielende (Sieg/Niederlage).
|
||||
@@ -42,7 +43,6 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k
|
||||
- Live-Vorschau beim Hovern über die Waveform.
|
||||
- Playback-Cursor zeigt aktuelle Abspielposition.
|
||||
- Einzelne Segmente zum Testen abspielen.
|
||||
- Einzelne Segmente zum Testen abspielen.
|
||||
- Manuelle Speicherung mit visueller Bestätigung.
|
||||
- **News & Announcements:**
|
||||
- Integriertes News-System für Ankündigungen (z.B. neue Specials, Features).
|
||||
@@ -51,6 +51,24 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k
|
||||
- **Featured News:** Hervorhebung wichtiger Ankündigungen.
|
||||
- Special-Verknüpfung: Direkte Links zu Specials in News-Beiträgen.
|
||||
- Verwaltung über das Admin-Dashboard.
|
||||
- **Kurator-System:**
|
||||
- **Kurator-Accounts:** Separate Login-Accounts für Kuratoren (nicht Admins).
|
||||
- **Genre- & Special-Zuweisung:** Kuratoren können einzelnen Genres oder Specials zugewiesen werden.
|
||||
- **Global-Kuratoren:** Optionale globale Kuratoren, die für alle Rätsel zuständig sind.
|
||||
- **Kurator-Dashboard:** Eigene Dashboard-Seite (`/curator` oder `/de/curator`, `/en/curator`) für Kuratoren.
|
||||
- **Song-Verwaltung:** Kuratoren können Songs hochladen, bearbeiten und Genres/Specials zuweisen.
|
||||
- **Kommentar-Verwaltung:** Kuratoren können Spieler-Kommentare zu ihren Rätseln einsehen, als gelesen markieren und archivieren.
|
||||
- **Spieler-Kommentare:**
|
||||
- **Feedback an Kuratoren:** Spieler können nach Abschluss eines Rätsels optional eine Nachricht an die Kuratoren senden.
|
||||
- **Automatische Zuordnung:** Kommentare werden automatisch an relevante Kuratoren verteilt (Genre-Kuratoren, Special-Kuratoren, Global-Kuratoren).
|
||||
- **Rate-Limiting:** Pro Spieler nur ein Kommentar pro Puzzle möglich.
|
||||
- **Kontext-Informationen:** Kommentare enthalten vollständigen Rätsel-Kontext (Hördle #, Genre/Special, Titel/Artist).
|
||||
- **Kommentar-Verwaltung:** Kuratoren sehen Kommentare in ihrem Dashboard mit Badge für neue/ungelesene Nachrichten.
|
||||
- **Analytics:**
|
||||
- **Plausible Analytics:** Integration mit Plausible Analytics für anonyme Nutzungsstatistiken.
|
||||
- **Automatisches Domain-Tracking:** Unterstützt mehrere Domains mit automatischer Erkennung.
|
||||
- **Privacy-First:** Keine Cookies, kein Cross-Site-Tracking.
|
||||
- 👉 **[Plausible Setup-Dokumentation](docs/PLAUSIBLE_SETUP.md)**
|
||||
|
||||
## Internationalisierung (i18n)
|
||||
|
||||
@@ -139,6 +157,7 @@ Das Projekt ist für den Betrieb mit Docker optimiert.
|
||||
- `GOTIFY_URL`: URL deines Gotify Servers (z.B. `https://gotify.example.com`)
|
||||
- `GOTIFY_APP_TOKEN`: App Token für Gotify (z.B. `A...`)
|
||||
- `OPENROUTER_API_KEY`: API-Key für OpenRouter (für KI-Kategorisierung, optional)
|
||||
- `NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC`: URL zum Plausible Analytics Script (z.B. `https://plausible.example.com/js/script.js`, optional)
|
||||
|
||||
2. **Starten:**
|
||||
```bash
|
||||
@@ -156,7 +175,12 @@ Das Projekt ist für den Betrieb mit Docker optimiert.
|
||||
- URL: `/de/admin` oder `/en/admin`
|
||||
- Standard-Passwort: `admin123` (Bitte in `docker-compose.yml` ändern! Muss als Hash hinterlegt werden.)
|
||||
|
||||
5. **Special Curation & Scheduling verwenden:**
|
||||
5. **Kurator-Zugang:**
|
||||
- URL: `/de/curator` oder `/en/curator`
|
||||
- Kurator-Accounts werden vom Admin erstellt und verwaltet.
|
||||
- Kuratoren können Songs hochladen und verwalten, sowie Kommentare von Spielern einsehen.
|
||||
|
||||
6. **Special Curation & Scheduling verwenden:**
|
||||
- Erstelle ein Special im Admin-Dashboard:
|
||||
- Gib Name, Max Attempts und Unlock Steps ein.
|
||||
- **Optional:** Setze ein Startdatum (Launch Date) und Enddatum.
|
||||
|
||||
@@ -35,11 +35,15 @@ function curatorCanEditSong(context: StaffContext, song: any, assignments: { gen
|
||||
// - `SpecialSong` (mit `specialId`)
|
||||
// - `SpecialSong` (mit Relation `special.id`)
|
||||
// sein. Wir normalisieren hier auf reine Zahlen-IDs.
|
||||
// WICHTIG: Bei SpecialSong-Objekten ist s.id die SpecialSong-ID, nicht die Special-ID!
|
||||
// Daher zuerst specialId oder special.id prüfen.
|
||||
const songSpecialIds = (song.specials || [])
|
||||
.map((s: any) => {
|
||||
if (s?.id != null) return s.id;
|
||||
// Priorität: specialId oder special.id (die tatsächliche Special-ID)
|
||||
if (s?.specialId != null) return s.specialId;
|
||||
if (s?.special?.id != null) return s.special.id;
|
||||
// Nur wenn es direkt ein Special-Objekt ist (nicht SpecialSong), verwende s.id
|
||||
if (s?.id != null && s?.specialId == null && s?.special == null) return s.id;
|
||||
return undefined;
|
||||
})
|
||||
.filter((id: any): id is number => typeof id === 'number');
|
||||
@@ -59,11 +63,15 @@ function curatorCanDeleteSong(context: StaffContext, song: any, assignments: { g
|
||||
if (context.role === 'admin') return true;
|
||||
|
||||
const songGenreIds = (song.genres || []).map((g: any) => g.id);
|
||||
// WICHTIG: Bei SpecialSong-Objekten ist s.id die SpecialSong-ID, nicht die Special-ID!
|
||||
// Daher zuerst specialId oder special.id prüfen.
|
||||
const songSpecialIds = (song.specials || [])
|
||||
.map((s: any) => {
|
||||
if (s?.id != null) return s.id;
|
||||
// Priorität: specialId oder special.id (die tatsächliche Special-ID)
|
||||
if (s?.specialId != null) return s.specialId;
|
||||
if (s?.special?.id != null) return s.special.id;
|
||||
// Nur wenn es direkt ein Special-Objekt ist (nicht SpecialSong), verwende s.id
|
||||
if (s?.id != null && s?.specialId == null && s?.special == null) return s.id;
|
||||
return undefined;
|
||||
})
|
||||
.filter((id: any): id is number => typeof id === 'number');
|
||||
@@ -382,7 +390,11 @@ export async function PUT(request: Request) {
|
||||
where: { id: Number(id) },
|
||||
include: {
|
||||
genres: true,
|
||||
specials: true,
|
||||
specials: {
|
||||
include: {
|
||||
special: true
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1316,6 +1316,41 @@ export default function CuratorPageClient() {
|
||||
: genre.name?.de ?? genre.name?.en}
|
||||
</label>
|
||||
))}
|
||||
{specials
|
||||
.filter(s => curatorInfo?.specialIds?.includes(s.id))
|
||||
.map(special => (
|
||||
<label
|
||||
key={special.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
padding: '0.15rem 0.4rem',
|
||||
borderRadius: '999px',
|
||||
background: editSpecialIds.includes(special.id)
|
||||
? '#fee2e2'
|
||||
: '#f3f4f6',
|
||||
fontSize: '0.8rem',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editSpecialIds.includes(special.id)}
|
||||
onChange={() =>
|
||||
setEditSpecialIds(prev =>
|
||||
prev.includes(special.id)
|
||||
? prev.filter(id => id !== special.id)
|
||||
: [...prev, special.id]
|
||||
)
|
||||
}
|
||||
/>
|
||||
★{' '}
|
||||
{typeof special.name === 'string'
|
||||
? special.name
|
||||
: special.name?.de ?? special.name?.en}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||
{song.genres
|
||||
@@ -1337,21 +1372,26 @@ export default function CuratorPageClient() {
|
||||
: g.name?.de ?? g.name?.en}
|
||||
</span>
|
||||
))}
|
||||
{song.specials.map(s => (
|
||||
<span
|
||||
key={`s-${s.id}`}
|
||||
style={{
|
||||
padding: '0.1rem 0.4rem',
|
||||
borderRadius: '999px',
|
||||
background: '#fee2e2',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
{typeof s.name === 'string'
|
||||
? s.name
|
||||
: s.name?.de ?? s.name?.en}
|
||||
</span>
|
||||
))}
|
||||
{song.specials
|
||||
.filter(
|
||||
s => !curatorInfo?.specialIds?.includes(s.id)
|
||||
)
|
||||
.map(s => (
|
||||
<span
|
||||
key={`fixed-s-${s.id}`}
|
||||
style={{
|
||||
padding: '0.1rem 0.4rem',
|
||||
borderRadius: '999px',
|
||||
background: '#fee2e2',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
★{' '}
|
||||
{typeof s.name === 'string'
|
||||
? s.name
|
||||
: s.name?.de ?? s.name?.en}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user