1 bis 8 Party: mobile Web-App (Regeln, Karten, Spielleiter) + Docker-Deploy

This commit is contained in:
Kimi Work
2026-10-10 15:59:26 +02:00
commit 6ac0282851
92 changed files with 17107 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
import { useMemo, useState } from 'react'
import { RefreshCw, Shuffle } from 'lucide-react'
import { KARTEN, KATEGORIEN, kategorieById, type KategorieId, type Spielkarte } from '@/data/game'
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 stapel = useMemo(
() => (filter === 'alle' ? KARTEN : KARTEN.filter((k) => k.kategorie === filter)),
[filter]
)
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">
<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>
{/* Kategorie-Filter */}
<div className="scrollbar-none -mx-5 mt-5 flex gap-2 overflow-x-auto px-5 pb-1">
<FilterChip aktiv={filter === 'alle'} onClick={() => setFilter('alle')} label="✨ Alle" />
{KATEGORIEN.map((k) => (
<FilterChip
key={k.id}
aktiv={filter === k.id}
onClick={() => setFilter(k.id)}
label={`${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>
)
}