Files
1bis8/app/src/pages/Cards.tsx
T
Kimi Work 8cf4986b48
Build & Deploy / deploy (push) Successful in 19s
Red Mode: Nacht-Navigator-Logo mit 'RED RALF MODE'-Bogen
- Logo generiert: unterer Textbogen ersetzt, Goldringe rekonstruiert
- Logo in Karten-Seite + Spielraum-Lobby eingebunden
- Favicon-Swap bei aktivem Red Mode
- ?tab= URL-Param fuer Tab-Auswahl (Screenshots/Deep-Links)
2026-10-10 18:35:58 +02:00

217 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react'
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
function KartenGesicht({ karte, zeigen }: { karte: Spielkarte | null; zeigen: boolean }) {
const kat = karte ? kategorieById(karte.kategorie) : null
return (
<div className={`perspektive h-72 w-full select-none`}>
<div className={`flip-innen ${zeigen && karte ? 'ist-gedreht' : ''}`}>
{/* Rückseite = Nachziehstapel */}
<div className="flip-seite flex flex-col items-center justify-center gap-3 border-2 border-white/10 bg-gradient-to-br from-[#2a1550] to-[#1b0f38] shadow-[0_16px_48px_rgba(0,0,0,0.5)]">
<span className="font-display text-6xl font-bold text-white/10">?</span>
<span className="flex items-center gap-2 text-sm font-bold uppercase tracking-[0.2em] text-white/35">
<Shuffle size={15} /> Nachziehstapel
</span>
<span className="text-xs text-white/25">Antippen, um eine Karte zu ziehen</span>
</div>
{/* Vorderseite = Statement */}
{karte && kat && (
<div
className={`flip-seite flip-rueckseite flex flex-col border-2 p-5 shadow-[0_16px_48px_rgba(0,0,0,0.5)] bg-gradient-to-br ${kat.farbe} border-white/25`}
>
<div className="flex items-center justify-between">
<span className="rounded-full bg-black/25 px-3 py-1 text-[11px] font-extrabold uppercase tracking-widest text-white">
{kat.emoji} {kat.titel}
</span>
<span className="font-display text-4xl font-bold text-white/30">1–8</span>
</div>
<div className="flex flex-1 items-center justify-center">
<p className="text-center font-display text-[22px] font-semibold leading-snug text-white drop-shadow-sm">
{karte.text}
</p>
</div>
<p className="text-center text-[11px] font-bold uppercase tracking-[0.2em] text-white/60">
Karte {String(karte.id).padStart(2, '0')} / 20
</p>
</div>
)}
</div>
</div>
)
}
export default function Cards() {
const [filter, setFilter] = useState<Filter>('alle')
const [gezogen, setGezogen] = useState<Spielkarte | null>(null)
const [zaehler, setZaehler] = useState(0) // triggert Re-Flip-Animation
const [redMode, setRedMode] = useState(
() =>
localStorage.getItem(REDMODE_KEY) === '1' ||
new URLSearchParams(window.location.search).get('redmode') === '1'
)
useRedMode(redMode)
const toggleRedMode = () => {
setRedMode((alt) => {
localStorage.setItem(REDMODE_KEY, alt ? '0' : '1')
return !alt
})
}
const stapel = useMemo(
() =>
filter === 'alle'
? redMode
? KARTEN
: KARTEN_OHNE_ROT
: KARTEN.filter((k) => k.kategorie === filter),
[filter, redMode]
)
const ziehen = () => {
const rest = stapel.filter((k) => k.id !== gezogen?.id)
const pool = rest.length > 0 ? rest : stapel
setGezogen(pool[Math.floor(Math.random() * pool.length)])
setZaehler((z) => z + 1)
}
return (
<div className="px-5 pt-8">
{redMode && (
<img
src="/redmode-logo.png"
alt="Red Ralf Mode Logo"
className="mx-auto mb-4 w-32 rounded-full shadow-[0_0_36px_rgba(239,68,68,0.4)] ring-2 ring-red-500/40"
/>
)}
<p className="mb-1 text-xs font-extrabold uppercase tracking-[0.25em] text-violet-300">
20 Statements · 5 Kategorien
</p>
<h2 className="font-display text-3xl font-bold text-white">Die Karten</h2>
<p className="mt-2 text-sm leading-relaxed text-white/60">
Farblich nach Kategorien sortiert – entscheidet vorab, welche Vibe die
Runde haben soll! 🎨
</p>
{/* Red Mode Toggle */}
<button
onClick={toggleRedMode}
className={`mt-4 flex w-full items-center justify-between rounded-2xl border p-3.5 transition-all active:scale-[0.98] ${
redMode
? 'border-red-400/50 bg-red-500/10 shadow-[0_0_20px_rgba(239,68,68,0.25)]'
: 'border-white/10 bg-white/[0.05]'
}`}
>
<span className="flex items-center gap-2.5">
<Flame size={19} className={redMode ? 'text-red-400' : 'text-white/40'} />
<span className="text-left">
<span className={`block text-sm font-bold ${redMode ? 'text-red-300' : 'text-white/75'}`}>
🔞 Red Mode <span className="font-extrabold">(FSK 18)</span>
</span>
<span className="block text-xs text-white/45">
{redMode ? 'Aktiviert – 10 extra freche Karten im Stapel' : 'Schaltet 10 freche Karten frei'}
</span>
</span>
</span>
<span
className={`relative h-7 w-12 shrink-0 rounded-full transition-colors ${
redMode ? 'bg-gradient-to-r from-red-500 to-rose-600' : 'bg-white/15'
}`}
>
<span
className={`absolute top-1 h-5 w-5 rounded-full bg-white shadow transition-all ${
redMode ? 'left-6' : 'left-1'
}`}
/>
</span>
</button>
{/* Kategorie-Filter */}
<div className="scrollbar-none -mx-5 mt-4 flex gap-2 overflow-x-auto px-5 pb-1">
<FilterChip aktiv={filter === 'alle'} onClick={() => setFilter('alle')} label="✨ Alle" />
{KATEGORIEN.filter((k) => k.id !== 'rotlicht' || redMode).map((k) => (
<FilterChip
key={k.id}
aktiv={filter === k.id}
onClick={() => setFilter(k.id)}
label={k.id === 'rotlicht' ? `🌶️ Rotlicht` : `${k.emoji} ${k.titel.split(' ')[0]}`}
chipKlasse={k.chip}
/>
))}
</div>
{/* Zieh-Stapel */}
<div className="mt-5" onClick={ziehen} role="button" aria-label="Karte ziehen">
<div key={zaehler} className={gezogen && zaehler > 0 ? 'rein' : ''}>
<KartenGesicht karte={gezogen} zeigen={!!gezogen} />
</div>
</div>
<button
onClick={ziehen}
className="glanz mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-gradient-to-r from-violet-500 to-fuchsia-500 py-3.5 font-display text-base font-semibold text-white shadow-[0_10px_30px_rgba(139,92,246,0.4)] transition-transform active:scale-[0.97]"
>
<RefreshCw size={18} /> {gezogen ? 'Nächste Karte ziehen' : 'Karte ziehen'}
</button>
{/* Alle Karten im Überblick */}
<h3 className="mt-8 font-display text-xl font-semibold text-white">
Alle Karten <span className="text-white/40">({stapel.length})</span>
</h3>
<div className="mt-3 grid grid-cols-1 gap-3 pb-2">
{stapel.map((karte) => {
const kat = kategorieById(karte.kategorie)
return (
<div
key={karte.id}
className={`glass-card relative overflow-hidden p-4 ring-1 ${kat.ring}`}
>
<div className={`absolute inset-y-0 left-0 w-1.5 bg-gradient-to-b ${kat.farbe}`} />
<div className="flex items-start gap-3 pl-2">
<span className="text-xl">{kat.emoji}</span>
<div>
<p className="text-[15px] font-bold leading-snug text-white/90">{karte.text}</p>
<p className={`mt-1.5 text-[11px] font-extrabold uppercase tracking-widest ${kat.textfarbe}`}>
{kat.titel}
</p>
</div>
</div>
</div>
)
})}
</div>
</div>
)
}
function FilterChip({
aktiv,
onClick,
label,
chipKlasse,
}: {
aktiv: boolean
onClick: () => void
label: string
chipKlasse?: string
}) {
return (
<button
onClick={onClick}
className={`shrink-0 rounded-full border px-3.5 py-1.5 text-xs font-bold transition-all active:scale-95 ${
aktiv
? 'border-pink-400/60 bg-pink-400/20 text-pink-200 shadow-[0_0_14px_rgba(244,114,182,0.3)]'
: `border-white/10 bg-white/[0.05] text-white/55 ${chipKlasse ?? ''}`
}`}
>
{label}
</button>
)
}