Build & Deploy / deploy (push) Successful in 19s
- 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)
217 lines
8.3 KiB
TypeScript
217 lines
8.3 KiB
TypeScript
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>
|
||
)
|
||
}
|