Files
entscheidomat/components/tools/ja-nein-generator.tsx
2026-07-23 21:59:15 +02:00

189 lines
6.8 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.
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle2, HelpCircle, XCircle } from "lucide-react";
import { gsap } from "@/lib/gsap";
import { popConfetti } from "@/lib/confetti";
import { useSound } from "@/components/providers/sound-provider";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
type Answer = "Ja" | "Nein";
export default function JaNeinGenerator() {
const [question, setQuestion] = useState("");
const [answer, setAnswer] = useState<Answer | null>(null);
const [asking, setAsking] = useState(false);
const [history, setHistory] = useState<Answer[]>([]);
const cardRef = useRef<HTMLDivElement>(null);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
const ask = () => {
if (asking) return;
const pick: Answer = Math.random() < 0.5 ? "Ja" : "Nein";
setAsking(true);
setAnswer(null);
play("whoosh");
const reveal = () => {
setAsking(false);
setAnswer(pick);
setHistory((h) => [pick, ...h].slice(0, 10));
play(pick === "Ja" ? "win" : "pop");
if (pick === "Ja") popConfetti();
};
if (reduced) {
gsap.delayedCall(0.25, reveal);
return;
}
const tl = gsap.timeline({ onComplete: reveal });
tl.to(cardRef.current, {
rotate: -6,
x: -10,
duration: 0.09,
ease: "power1.inOut",
})
.to(cardRef.current, {
rotate: 6,
x: 10,
duration: 0.09,
ease: "power1.inOut",
})
.repeat(3)
.to(cardRef.current, {
rotate: 0,
x: 0,
duration: 0.2,
ease: "power2.out",
});
};
return (
<div className="grid items-center gap-10 lg:grid-cols-[1.15fr_0.85fr]">
{/* Bühne */}
<div className="flex flex-col items-center">
<input
value={question}
onChange={(e) => setQuestion(e.target.value)}
maxLength={100}
placeholder="Deine Ja/Nein-Frage (optional) …"
aria-label="Deine Frage an den Ja-Nein-Generator"
className="w-full max-w-sm rounded-full border border-line bg-surface px-5 py-3 text-center text-sm font-medium shadow-soft outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
/>
<div ref={cardRef} className="mt-8">
<button
type="button"
onClick={ask}
aria-label="Ja-Nein-Generator fragen"
className="flex h-52 w-52 cursor-pointer items-center justify-center rounded-[2rem] border border-line bg-surface shadow-lift transition-transform duration-200 hover:scale-[1.03] active:scale-95 sm:h-60 sm:w-60"
>
<div
className="relative flex h-full w-full items-center justify-center px-6"
aria-live="polite"
>
<AnimatePresence mode="wait">
{answer ? (
<motion.div
key={answer + history.length}
initial={{ opacity: 0, scale: 0.7 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }}
className={`flex flex-col items-center gap-2 ${
answer === "Ja" ? "text-[#0d7a57]" : "text-clay"
}`}
>
{answer === "Ja" ? (
<CheckCircle2 className="h-14 w-14" />
) : (
<XCircle className="h-14 w-14" />
)}
<span className="font-display text-3xl font-semibold tracking-tight">
{answer}
</span>
</motion.div>
) : (
<motion.div
key="idle"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex flex-col items-center gap-2 text-ink-soft"
>
<HelpCircle className="h-14 w-14" />
<span className="text-sm font-medium">Tippen zum Fragen</span>
</motion.div>
)}
</AnimatePresence>
</div>
</button>
</div>
<motion.button
type="button"
onClick={ask}
disabled={asking}
whileTap={{ scale: 0.95 }}
className="mt-6 inline-flex cursor-pointer items-center gap-2 rounded-full bg-primary px-9 py-3.5 text-base font-semibold text-white shadow-cta transition-colors duration-200 hover:bg-primary-strong disabled:cursor-not-allowed disabled:opacity-60"
>
<HelpCircle className="h-5 w-5" />
{asking ? "Überlegt …" : "Ja oder Nein?"}
</motion.button>
<p className="mt-3 text-xs text-ink-soft">
{question.trim()
? `${question.trim()}`
: "Stell eine Frage oder lass dich einfach überraschen."}
</p>
</div>
{/* Info & Verlauf */}
<div className="space-y-5">
<div className="rounded-3xl border border-line bg-cream/70 p-5">
<h3 className="text-sm font-semibold uppercase tracking-[0.16em] text-ink-soft">
So funktioniert&apos;s
</h3>
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
Frage eingeben (optional), auf die Karte oder den Button tippen
der Generator entscheidet zufällig zwischen Ja und Nein. Perfekt
für schnelle Entscheidungen ohne langes Abwägen.
</p>
</div>
{history.length > 0 && (
<div className="rounded-3xl border border-line bg-cream/70 p-5">
<h3 className="text-sm font-semibold uppercase tracking-[0.16em] text-ink-soft">
Verlauf
</h3>
<div className="mt-3 flex flex-wrap gap-1.5">
<AnimatePresence initial={false}>
{history.map((entry, i) => (
<motion.span
key={`${entry}-${history.length - i}`}
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
className={`rounded-full px-3 py-1 text-xs font-semibold ${
i === 0
? entry === "Ja"
? "bg-[#0d7a57] text-white"
: "bg-clay text-white"
: "bg-surface text-ink-soft ring-1 ring-line"
}`}
>
{entry}
</motion.span>
))}
</AnimatePresence>
</div>
</div>
)}
</div>
</div>
);
}