diff --git a/app/src/data/game.ts b/app/src/data/game.ts index 67ac2b6..dfc4e90 100644 --- a/app/src/data/game.ts +++ b/app/src/data/game.ts @@ -1,6 +1,6 @@ // ── Kategorien & Statement-Karten (aus dem offiziellen Spielregeln-PDF) ── -export type KategorieId = 'abenteuer' | 'persoenlichkeit' | 'alltag' | 'party' | 'erfolg' +export type KategorieId = 'abenteuer' | 'persoenlichkeit' | 'alltag' | 'party' | 'erfolg' | 'rotlicht' export interface Kategorie { id: KategorieId @@ -59,6 +59,15 @@ export const KATEGORIEN: Kategorie[] = [ ring: 'ring-emerald-400/40', chip: 'bg-emerald-400/15 text-emerald-300 border-emerald-400/30', }, + { + id: 'rotlicht', + titel: 'Rotlicht & Versuchung 🔞', + emoji: '🌶️', + farbe: 'from-red-500 to-rose-700', + textfarbe: 'text-red-300', + ring: 'ring-red-400/40', + chip: 'bg-red-400/15 text-red-300 border-red-400/30', + }, ] export const kategorieById = (id: KategorieId): Kategorie => @@ -96,8 +105,23 @@ export const KARTEN: Spielkarte[] = [ { id: 18, kategorie: 'erfolg', text: 'Wer wäre der beste Kandidat für eine Reality-TV-Show?' }, { id: 19, kategorie: 'erfolg', text: 'Wer könnte am ehesten eine 30-minütige Präsentation über ein Thema halten, von dem er eigentlich gar keine Ahnung hat?' }, { id: 20, kategorie: 'erfolg', text: 'Wer hat das Potenzial, ein berühmter Geheimagent zu sein, weil er am besten Geheimnisse bewahren kann?' }, + // Rotlicht & Versuchung (Red Mode / FSK 18) + { id: 21, kategorie: 'rotlicht', text: 'Wer aus der Gruppe würde am ehesten einen One-Night-Stand über eine Dating-App haben?' }, + { id: 22, kategorie: 'rotlicht', text: 'Wer würde bei „Wahrheit oder Pflicht“ drei Mal hintereinander „Pflicht“ wählen – egal, was kommt?' }, + { id: 23, kategorie: 'rotlicht', text: 'Wer würde am ehesten heimlich in der hintersten Reihe des Kinos rummachen?' }, + { id: 24, kategorie: 'rotlicht', text: 'Wer würde nach einer durchzechten Nacht als Erster den Namen seines Dates vergessen?' }, + { id: 25, kategorie: 'rotlicht', text: 'Wer würde am ehesten einen Strip-Poker-Abend anzetteln – und zwar gewinnen?' }, + { id: 26, kategorie: 'rotlicht', text: 'Wer würde am ehesten mit dem Ex eines guten Freunds anbändeln und es vertuschen?' }, + { id: 27, kategorie: 'rotlicht', text: 'Wer hat die Stimme, mit der man am Telefon am ehesten flirten könnte?' }, + { id: 28, kategorie: 'rotlicht', text: 'Wer würde in einem Club am ehesten auf der Theke tanzen – und es auch noch gut finden?' }, + { id: 29, kategorie: 'rotlicht', text: 'Wer würde am ehesten den ganzen Abend mit einem Fremden flirten, nur um einen Drink abzugreifen?' }, + { id: 30, kategorie: 'rotlicht', text: 'Wer sagt „Ich melde mich noch“ – und meint damit die nächsten drei Jahre?' }, ] +/** FSK-18-Karten – nur im Red Mode verfügbar */ +export const ROT_KARTEN: Spielkarte[] = KARTEN.filter((k) => k.kategorie === 'rotlicht') +export const KARTEN_OHNE_ROT: Spielkarte[] = KARTEN.filter((k) => k.kategorie !== 'rotlicht') + // ── Spielregeln (Inhalt aus dem offiziellen PDF) ────────── export interface RegelSchritt { diff --git a/app/src/hooks/useRedMode.ts b/app/src/hooks/useRedMode.ts new file mode 100644 index 0000000..9ff7126 --- /dev/null +++ b/app/src/hooks/useRedMode.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react' + +/** Hängt die Design-Klasse red-mode an und passt theme-color an. */ +export function useRedMode(aktiv: boolean) { + useEffect(() => { + document.body.classList.toggle('red-mode', aktiv) + const meta = document.querySelector('meta[name="theme-color"]') + const vorher = meta?.getAttribute('content') + if (meta) meta.setAttribute('content', aktiv ? '#1f060b' : '#170a2e') + return () => { + document.body.classList.remove('red-mode') + if (meta && vorher) meta.setAttribute('content', vorher) + } + }, [aktiv]) +} diff --git a/app/src/index.css b/app/src/index.css index e88c60d..8b66e30 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -136,6 +136,28 @@ animation: glanzen 3.5s linear infinite; } +/* ── Red Mode (FSK 18): rote Design-Welt ────────────────── */ +body.red-mode { + background-image: + radial-gradient(ellipse 80% 50% at 50% -10%, rgba(239, 68, 68, 0.32), transparent), + radial-gradient(ellipse 60% 40% at 100% 0%, rgba(244, 63, 94, 0.22), transparent), + radial-gradient(ellipse 60% 40% at 0% 100%, rgba(190, 18, 60, 0.2), transparent), + linear-gradient(180deg, #1f060b 0%, #2a0710 55%, #1a0409 100%); +} +body.red-mode .text-gradient { + background-image: linear-gradient(to right, #f87171, #fb7185, #f472b6); +} +body.red-mode nav.fixed { + background-color: rgb(38 8 14 / 0.88); + border-color: rgba(248, 113, 113, 0.18); +} +body.red-mode .glanz { + background-image: linear-gradient(90deg, #ef4444, #e11d48, #be123c, #e11d48, #ef4444) !important; +} +body.red-mode .glass-card { + border-color: rgba(248, 113, 113, 0.14); +} + /* Zahlen-Chips der Hero-Sektion */ .zahlen-chip { @apply flex h-9 w-9 items-center justify-center rounded-xl font-display text-lg font-semibold shadow-lg; diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts index 6100b50..d93a0d1 100644 --- a/app/src/lib/api.ts +++ b/app/src/lib/api.ts @@ -32,6 +32,7 @@ export interface RaumZustand { spieler: SpielerInfo[] minSpieler: number maxSpieler: number + redMode: boolean alleReady: boolean rankingsFertig: number punkte: Record @@ -79,11 +80,11 @@ async function anfrage(methode: 'GET' | 'POST', pfad: string, body?: object, tok } /** Raum eröffnen; liefert Session direkt mit */ -export async function raumErstellen(name: string): Promise<{ session: RaumSession; zustand: RaumZustand }> { +export async function raumErstellen(name: string, redMode = false): Promise<{ session: RaumSession; zustand: RaumZustand }> { const res = await fetch('/api/game/room', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), + body: JSON.stringify({ name, redMode }), }) const daten = await res.json().catch(() => ({})) if (!res.ok || !daten.token) throw new ApiFehler(daten.fehler || 'Raum konnte nicht erstellt werden') @@ -108,6 +109,7 @@ export async function raumBeitreten(code: string, name: string): Promise<{ sessi export const zustandLaden = (s: RaumSession) => anfrage('GET', `/${s.code}`, undefined, s.token) export const readySetzen = (s: RaumSession, ready: boolean) => anfrage('POST', `/${s.code}/ready`, { ready }, s.token) +export const redModeSetzen = (s: RaumSession, redMode: boolean) => anfrage('POST', `/${s.code}/redmode`, { redMode }, s.token) export const frageStellen = (s: RaumSession, frage: string) => anfrage('POST', `/${s.code}/question`, { frage }, s.token) export const rankingAbgeben = (s: RaumSession, reihenfolge: string[]) => anfrage('POST', `/${s.code}/ranking`, { reihenfolge }, s.token) diff --git a/app/src/pages/Cards.tsx b/app/src/pages/Cards.tsx index 76a2c82..9cfaa69 100644 --- a/app/src/pages/Cards.tsx +++ b/app/src/pages/Cards.tsx @@ -1,6 +1,9 @@ import { useMemo, useState } from 'react' -import { RefreshCw, Shuffle } from 'lucide-react' -import { KARTEN, KATEGORIEN, kategorieById, type KategorieId, type Spielkarte } from '@/data/game' +import { Flame, RefreshCw, Shuffle } from 'lucide-react' +import { KARTEN, KATEGORIEN, KARTEN_OHNE_ROT, kategorieById, type KategorieId, type Spielkarte } from '@/data/game' +import { useRedMode } from '@/hooks/useRedMode' + +const REDMODE_KEY = 'einsbisacht-redmode' type Filter = 'alle' | KategorieId @@ -47,10 +50,24 @@ export default function Cards() { const [filter, setFilter] = useState('alle') const [gezogen, setGezogen] = useState(null) const [zaehler, setZaehler] = useState(0) // triggert Re-Flip-Animation + const [redMode, setRedMode] = useState(() => localStorage.getItem(REDMODE_KEY) === '1') + useRedMode(redMode) + + const toggleRedMode = () => { + setRedMode((alt) => { + localStorage.setItem(REDMODE_KEY, alt ? '0' : '1') + return !alt + }) + } const stapel = useMemo( - () => (filter === 'alle' ? KARTEN : KARTEN.filter((k) => k.kategorie === filter)), - [filter] + () => + filter === 'alle' + ? redMode + ? KARTEN + : KARTEN_OHNE_ROT + : KARTEN.filter((k) => k.kategorie === filter), + [filter, redMode] ) const ziehen = () => { @@ -71,15 +88,48 @@ export default function Cards() { Runde haben soll! 🎨

+ {/* Red Mode Toggle */} + + {/* Kategorie-Filter */} -
+
setFilter('alle')} label="✨ Alle" /> - {KATEGORIEN.map((k) => ( + {KATEGORIEN.filter((k) => k.id !== 'rotlicht' || redMode).map((k) => ( setFilter(k.id)} - label={`${k.emoji} ${k.titel.split(' ')[0]}`} + label={k.id === 'rotlicht' ? `🌶️ Rotlicht` : `${k.emoji} ${k.titel.split(' ')[0]}`} chipKlasse={k.chip} /> ))} diff --git a/app/src/pages/Game.tsx b/app/src/pages/Game.tsx index 06ca687..930e07d 100644 --- a/app/src/pages/Game.tsx +++ b/app/src/pages/Game.tsx @@ -14,7 +14,8 @@ import { Trophy, Users, } from 'lucide-react' -import { KARTEN } from '@/data/game' +import { KARTEN, KARTEN_OHNE_ROT } from '@/data/game' +import { useRedMode } from '@/hooks/useRedMode' import { ladeSession, speichereSession, @@ -22,6 +23,7 @@ import { raumBeitreten, zustandLaden, readySetzen, + redModeSetzen, frageStellen, rankingAbgeben, naechstePhase, @@ -34,6 +36,8 @@ export default function Game() { const [session, setSession] = useState(ladeSession) const [zustand, setZustand] = useState(null) const [fehler, setFehler] = useState('') + // Red Mode des Raums verdunkelt das Design (nur während des Spiels) + useRedMode(!!zustand?.redMode) const refresh = useCallback(async () => { if (!session) return @@ -66,8 +70,8 @@ export default function Game() { setSession(s) }, []) - const erstellen = useCallback(async (name: string) => { - const { session: s } = await raumErstellen(name) + const erstellen = useCallback(async (name: string, redMode: boolean) => { + const { session: s } = await raumErstellen(name, redMode) setSession(s) }, []) @@ -142,12 +146,13 @@ function Einstieg({ onErstellen, onBeitreten, }: { - onErstellen: (name: string) => Promise + onErstellen: (name: string, redMode: boolean) => Promise onBeitreten: (code: string, name: string) => Promise }) { const [modus, setModus] = useState<'start' | 'beitreten'>('start') const [name, setName] = useState('') const [code, setCode] = useState(() => new URLSearchParams(window.location.search).get('raum') ?? '') + const [redMode, setRedMode] = useState(false) const [lade, setLade] = useState(false) const [fehler, setFehler] = useState('') @@ -157,7 +162,7 @@ function Einstieg({ setLade(true) setFehler('') try { - if (modus === 'start') await onErstellen(name.trim()) + if (modus === 'start') await onErstellen(name.trim(), redMode) else await onBeitreten(code.trim(), name.trim()) } catch (e) { setFehler(e instanceof Error ? e.message : 'Verbindung fehlgeschlagen') @@ -207,6 +212,30 @@ function Einstieg({ className="w-full rounded-xl border border-white/10 bg-white/[0.06] px-3.5 py-3 text-[15px] font-bold text-white placeholder:font-normal placeholder:text-white/30 focus:border-pink-400/60 focus:outline-none focus:ring-2 focus:ring-pink-400/20" /> + {modus === 'start' && ( + + )} {fehler &&

{fehler}

}
+ + {/* Red Mode: Host schaltet frei, alle sehen den Status */} + + {!ich?.istHost && ( +

Nur der Host kann den Red Mode ändern.

+ )}
{z.spieler.map((s) => (
@@ -333,7 +392,8 @@ function FragePhase({ z, onAct }: { z: RaumZustand; onAct: (fn: (s: RaumSession) const istFragesteller = z.ich === z.fragesteller const ziehen = () => { - const karte = KARTEN[Math.floor(Math.random() * KARTEN.length)] + const stapel = z.redMode ? KARTEN : KARTEN_OHNE_ROT + const karte = stapel[Math.floor(Math.random() * stapel.length)] setFrage(karte.text) } @@ -354,6 +414,11 @@ function FragePhase({ z, onAct }: { z: RaumZustand; onAct: (fn: (s: RaumSession)

Du bist dran!

Ziehe eine Karte und lies sie laut vor – oder erfinde eigene Frage.

+ {z.redMode && ( +

+ 🌶️ Red Mode – freche Karten sind im Stapel +

+ )}