Bug fixes

This commit is contained in:
2026-07-24 16:33:44 +02:00
parent 6b71a98a45
commit b6512a318d
156 changed files with 69800 additions and 51 deletions

View File

@@ -315,7 +315,7 @@ export default function Hero() {
aria-label="Zu den Tools scrollen"
className="absolute bottom-6 left-1/2 z-10 hidden -translate-x-1/2 flex-col items-center gap-1 text-ink-soft transition-colors hover:text-ink md:flex"
>
<span className="text-[11px] font-semibold uppercase tracking-[0.2em]">
<span className="text-xs font-semibold uppercase tracking-[0.15em]">
Scroll
</span>
<ArrowDown className="animate-bob h-4 w-4" />

View File

@@ -27,7 +27,7 @@ export default function LegalLayout({
ladungsfähige Geschäftsanschrift vollständig ergänzt werden.
</div>
<div className="mt-10 space-y-8 text-[15px] leading-relaxed text-ink-soft [&_h2]:font-display [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:text-ink [&_a]:text-[#0d7a57] [&_a]:underline [&_a]:underline-offset-2 [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5">
<div className="mt-10 space-y-8 text-base leading-relaxed text-ink-soft [&_h2]:font-display [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:text-ink [&_a]:text-[#0d7a57] [&_a]:underline [&_a]:underline-offset-2 [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5">
{children}
</div>
</div>

View File

@@ -96,7 +96,7 @@ export default function Nav() {
</button>
{toolsOpen && (
<div role="menu" className="absolute right-0 top-[calc(100%+0.7rem)] w-80 overflow-hidden rounded-3xl border border-line bg-surface p-2 shadow-[0_22px_55px_rgba(17,20,18,0.16)]">
<p className="px-3 pb-2 pt-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-ink-soft">Alle Zufalls-Tools</p>
<p className="px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-[0.15em] text-ink-soft">Alle Zufalls-Tools</p>
{TOOLS.map((tool) => (
<Link key={tool.href} href={tool.href} role="menuitem" onClick={closeMenus} className="group flex items-center justify-between rounded-2xl px-3 py-2.5 transition-colors hover:bg-[#e6f7f0]">
<span><span className="block text-sm font-semibold text-ink group-hover:text-[#0d7a57]">{tool.label}</span><span className="block text-xs text-ink-soft">{tool.hint}</span></span>
@@ -121,7 +121,7 @@ export default function Nav() {
{mobileOpen && (
<div id="mobile-menu" className="border-t border-line/70 bg-paper px-5 pb-6 pt-4 shadow-xl sm:hidden">
<div className="mx-auto max-w-6xl">
<p className="mb-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-ink-soft">Tools auswählen</p>
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.15em] text-ink-soft">Tools auswählen</p>
<div className="grid gap-1">
{TOOLS.map((tool) => <Link key={tool.href} href={tool.href} onClick={closeMenus} className="flex items-center justify-between rounded-2xl px-4 py-3 hover:bg-[#e6f7f0]"><span><span className="block font-semibold text-ink">{tool.label}</span><span className="text-xs text-ink-soft">{tool.hint}</span></span><span className="text-[#0d7a57]"></span></Link>)}
</div>

View File

@@ -124,7 +124,7 @@ export default function Playground() {
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-[#0d7a57]/20 bg-[#e6f7f0] text-[#0d7a57] shadow-sm transition-all duration-300 group-hover:scale-110 group-hover:border-[#0d7a57] group-hover:bg-[#0d7a57] group-hover:text-white">
<Icon className="h-6 w-6" strokeWidth={2.1} />
</div>
<span className="inline-flex items-center rounded-full border border-line bg-paper px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-ink-soft">
<span className="inline-flex items-center rounded-full border border-line bg-paper px-3 py-1 text-xs font-semibold uppercase tracking-wider text-ink-soft">
{tool.badge}
</span>
</div>

View File

@@ -139,7 +139,7 @@ export default function Preloader() {
</svg>
<p
ref={wordRef}
className="mt-6 text-[11px] font-semibold uppercase tracking-[0.28em] text-ink-soft opacity-0"
className="mt-6 text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft opacity-0"
>
Entscheidomat
</p>

View File

@@ -130,9 +130,10 @@ export default function CoinFlip() {
<input
value={frontLabel}
onChange={(e) => setFrontLabel(e.target.value)}
disabled={flipping}
maxLength={14}
placeholder="Kopf"
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label className="block">
@@ -142,9 +143,10 @@ export default function CoinFlip() {
<input
value={backLabel}
onChange={(e) => setBackLabel(e.target.value)}
disabled={flipping}
maxLength={14}
placeholder="Zahl"
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
</div>

View File

@@ -208,7 +208,8 @@ export default function DiceRoller() {
key={m.id}
type="button"
onClick={() => setMode(m.id)}
className={`relative flex-1 cursor-pointer rounded-full px-3 py-2 text-sm font-semibold transition-colors duration-200 ${
disabled={rolling}
className={`relative flex-1 cursor-pointer rounded-full px-3 py-2 text-sm font-semibold transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-60 ${
mode === m.id ? "text-white" : "text-ink-soft hover:text-ink"
}`}
>
@@ -236,7 +237,7 @@ export default function DiceRoller() {
<div className="mt-4 grid grid-cols-2 gap-2">
{customFaces.map((face, i) => (
<label key={i} className="block">
<span className="mb-1 block text-[11px] font-medium text-ink-soft">
<span className="mb-1 block text-xs font-medium text-ink-soft">
Seite {i + 1}
</span>
<input
@@ -248,9 +249,10 @@ export default function DiceRoller() {
),
)
}
disabled={rolling}
maxLength={18}
placeholder={`Seite ${i + 1}`}
className="w-full rounded-xl border border-line bg-surface px-3 py-2 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
className="w-full rounded-xl border border-line bg-surface px-3 py-2 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
))}

View File

@@ -20,7 +20,6 @@ export default function ListPicker() {
const [display, setDisplay] = useState<string | null>(null);
const [drawing, setDrawing] = useState(false);
const [winner, setWinner] = useState<string | null>(null);
const [error, setError] = useState(false);
const lastPaintRef = useRef(0);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -32,10 +31,13 @@ export default function ListPicker() {
.map((s) => s.trim())
.filter(Boolean);
const insufficientItems = items.length < 2;
const draw = () => {
if (drawing) return;
if (items.length < 2) {
setError(true);
if (insufficientItems) {
setDisplay(null);
setWinner(null);
play("tick");
gsap.fromTo(
textareaRef.current,
@@ -44,7 +46,6 @@ export default function ListPicker() {
);
return;
}
setError(false);
const pick = items[Math.floor(Math.random() * items.length)];
setDrawing(true);
@@ -74,11 +75,16 @@ export default function ListPicker() {
play("win");
celebrateConfetti();
if (removeAfterDraw) {
setText(
items
.filter((item) => item !== pick)
.join("\n"),
);
setText((prevText) => {
const currentItems = prevText
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
const idx = currentItems.indexOf(pick);
if (idx === -1) return prevText;
currentItems.splice(idx, 1);
return currentItems.join("\n");
});
}
},
});
@@ -95,11 +101,12 @@ export default function ListPicker() {
ref={textareaRef}
value={text}
onChange={(e) => setText(e.target.value)}
disabled={drawing}
rows={8}
placeholder={"Ein Eintrag pro Zeile, z. B.\nAnna\nBen\nClara"}
aria-label="Liste der Einträge, ein Eintrag pro Zeile"
className={`mt-4 w-full resize-none rounded-xl border bg-surface px-4 py-3 font-mono text-sm leading-relaxed outline-none transition focus:ring-2 ${
error
className={`mt-4 w-full resize-none rounded-xl border bg-surface px-4 py-3 font-mono text-sm leading-relaxed outline-none transition focus:ring-2 disabled:cursor-not-allowed disabled:opacity-60 ${
insufficientItems
? "border-clay/60 focus:border-clay/60 focus:ring-clay/20"
: "border-line focus:border-primary/60 focus:ring-primary/20"
}`}
@@ -109,12 +116,13 @@ export default function ListPicker() {
type="checkbox"
checked={removeAfterDraw}
onChange={(e) => setRemoveAfterDraw(e.target.checked)}
className="h-4 w-4 cursor-pointer accent-primary"
disabled={drawing}
className="h-4 w-4 cursor-pointer accent-primary disabled:cursor-not-allowed"
/>
Gezogene Einträge entfernen (ohne Zurücklegen)
</label>
<p className={`mt-3 text-xs leading-relaxed ${error ? "font-semibold text-clay" : "text-ink-soft"}`}>
{error
<p className={`mt-3 text-xs leading-relaxed ${insufficientItems ? "font-semibold text-clay" : "text-ink-soft"}`}>
{insufficientItems
? "Bitte mindestens 2 Einträge eintragen."
: "Ideal für Teams, Gruppen-Reihenfolgen oder Gewinnspiele."}
</p>
@@ -133,7 +141,7 @@ export default function ListPicker() {
>
<ListChecks className="h-8 w-8 text-primary/50" />
<p className="font-display text-xl font-medium text-ink-soft">
{items.length >= 2
{!insufficientItems
? `${items.length} Einträge warten auf die Ziehung.`
: "Trage links mindestens 2 Einträge ein."}
</p>

View File

@@ -1,6 +1,6 @@
"use client";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Pencil, Plus, Sparkles, X } from "lucide-react";
import { gsap } from "@/lib/gsap";
@@ -36,6 +36,8 @@ export default function Magic8Ball() {
const [editing, setEditing] = useState(false);
const ballRef = useRef<HTMLDivElement>(null);
const answerInputRefs = useRef<Array<HTMLInputElement | null>>([]);
const justAddedRef = useRef(false);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
@@ -90,8 +92,17 @@ export default function Magic8Ball() {
const addAnswer = () => {
if (answers.length >= MAX_ANSWERS) return;
setAnswers((list) => [...list, ""]);
justAddedRef.current = true;
};
useEffect(() => {
if (!justAddedRef.current) return;
justAddedRef.current = false;
const last = answerInputRefs.current[answers.length - 1];
last?.scrollIntoView({ block: "nearest", behavior: "smooth" });
last?.focus();
}, [answers.length]);
const removeAnswer = (i: number) => {
if (answers.length <= MIN_ANSWERS) return;
setAnswers((list) => list.filter((_, idx) => idx !== i));
@@ -138,7 +149,7 @@ export default function Magic8Ball() {
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(6px)" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="text-center font-display text-[13px] font-semibold leading-snug text-[#dde5dd]"
className="text-center font-display text-sm font-semibold leading-snug text-[#dde5dd]"
>
{answer}
</motion.p>
@@ -210,6 +221,9 @@ export default function Magic8Ball() {
{answers.map((a, i) => (
<div key={i} className="flex items-center gap-2">
<input
ref={(el) => {
answerInputRefs.current[i] = el;
}}
value={a}
onChange={(e) => updateAnswer(i, e.target.value)}
maxLength={60}

View File

@@ -12,6 +12,11 @@ import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
const randInt = (lo: number, hi: number) =>
lo + Math.floor(Math.random() * (hi - lo + 1));
const toInt = (raw: string) => {
const n = Math.round(Number(raw));
return Number.isFinite(n) ? n : 0;
};
export default function RandomNumber() {
const [min, setMin] = useLocalStorage("num-min", 1);
const [max, setMax] = useLocalStorage("num-max", 100);
@@ -120,9 +125,11 @@ export default function RandomNumber() {
</span>
<input
type="number"
step={1}
value={min}
onChange={(e) => setMin(Number(e.target.value))}
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
onChange={(e) => setMin(toInt(e.target.value))}
disabled={rolling}
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
<label className="block">
@@ -131,9 +138,11 @@ export default function RandomNumber() {
</span>
<input
type="number"
step={1}
value={max}
onChange={(e) => setMax(Number(e.target.value))}
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
onChange={(e) => setMax(toInt(e.target.value))}
disabled={rolling}
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
/>
</label>
</div>

View File

@@ -1,6 +1,6 @@
"use client";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Plus, RotateCcw, Sparkles, X } from "lucide-react";
import { gsap } from "@/lib/gsap";
@@ -51,6 +51,8 @@ export default function WheelOfFortune() {
const pointerRef = useRef<HTMLDivElement>(null);
const rotationRef = useRef(0);
const lastTickRef = useRef(-1);
const optionInputRefs = useRef<Array<HTMLInputElement | null>>([]);
const justAddedRef = useRef(false);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
@@ -115,20 +117,49 @@ export default function WheelOfFortune() {
});
};
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeTag = document.activeElement?.tagName.toLowerCase();
if (activeTag === "input" || activeTag === "textarea") return;
if (e.code === "Space") {
e.preventDefault();
spin();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [spinning, n]);
const updateOption = (i: number, value: string) => {
if (spinning) return;
setOptions((opts) => opts.map((o, idx) => (idx === i ? value : o)));
setWinner(null);
};
const addOption = () => {
if (n >= MAX_OPTIONS) return;
if (spinning || n >= MAX_OPTIONS) return;
setOptions((opts) => [...opts, ""]);
setWinner(null);
justAddedRef.current = true;
};
useEffect(() => {
if (!justAddedRef.current) return;
justAddedRef.current = false;
const last = optionInputRefs.current[options.length - 1];
last?.scrollIntoView({ block: "nearest", behavior: "smooth" });
last?.focus();
}, [options.length]);
const removeOption = (i: number) => {
if (spinning || n <= MIN_OPTIONS) return;
setOptions((opts) => opts.filter((_, idx) => idx !== i));
setWinner(null);
};
const removeOption = (i: number) => {
if (n <= MIN_OPTIONS) return;
setOptions((opts) => opts.filter((_, idx) => idx !== i));
const resetOptions = () => {
if (spinning) return;
setOptions(DEFAULT_OPTIONS);
setWinner(null);
};
@@ -141,14 +172,19 @@ export default function WheelOfFortune() {
<div className="relative w-full max-w-[400px]">
{/* Zeiger */}
<div
ref={pointerRef}
ref={pointerRef}
aria-hidden
className="absolute -top-1.5 left-1/2 z-10 h-9 w-7 -translate-x-1/2 bg-primary shadow-[0_6px_16px_rgb(108_92_231/0.5)]"
className="absolute -top-1.5 left-1/2 z-10 h-9 w-7 -translate-x-1/2 bg-primary shadow-[0_6px_16px_rgb(13_122_87/0.35)]"
style={{
clipPath: "polygon(50% 100%, 0 0, 100% 0)",
}}
/>
<svg viewBox="0 0 420 420" className="w-full drop-shadow-[0_20px_36px_rgb(23_23_28/0.14)]">
<svg
viewBox="0 0 420 420"
role="img"
aria-label="Glücksrad mit Zufalls-Optionen"
className="w-full drop-shadow-[0_20px_36px_rgb(23_23_28/0.14)]"
>
{/* äußerer Ring */}
<circle cx={C} cy={C} r={206} fill="#ffffff" stroke="#e9e9e4" />
<g ref={wheelRef}>
@@ -199,7 +235,7 @@ export default function WheelOfFortune() {
</g>
{/* Nabe */}
<circle cx={C} cy={C} r={30} fill="#ffffff" stroke="#e9e9e4" strokeWidth={2} />
<circle cx={C} cy={C} r={9} fill="#6c5ce7" />
<circle cx={C} cy={C} r={9} fill="#0d7a57" />
</svg>
</div>
@@ -234,18 +270,16 @@ export default function WheelOfFortune() {
</div>
{/* Optionen-Editor */}
<div className="rounded-3xl border border-line bg-cream/70 p-5">
<div className="rounded-3xl border border-line bg-surface p-5 shadow-soft">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold uppercase tracking-[0.16em] text-ink-soft">
Optionen ({n}/{MAX_OPTIONS})
</h3>
<button
type="button"
onClick={() => {
setOptions(DEFAULT_OPTIONS);
setWinner(null);
}}
className="cursor-pointer text-xs font-medium text-ink-soft underline-offset-2 transition-colors hover:text-primary hover:underline"
onClick={resetOptions}
disabled={spinning}
className="cursor-pointer text-xs font-medium text-ink-soft underline-offset-2 transition-colors hover:text-[#0d7a57] hover:underline disabled:cursor-not-allowed disabled:opacity-40"
>
Zurücksetzen
</button>
@@ -260,17 +294,21 @@ export default function WheelOfFortune() {
style={{ background: PALETTE[i % PALETTE.length] }}
/>
<input
ref={(el) => {
optionInputRefs.current[i] = el;
}}
value={option}
onChange={(e) => updateOption(i, e.target.value)}
disabled={spinning}
maxLength={24}
placeholder={`Option ${i + 1}`}
aria-label={`Option ${i + 1}`}
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
className="w-full rounded-xl border border-line bg-paper/50 px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-[#0d7a57] focus:bg-surface focus:ring-2 focus:ring-[#0d7a57]/20 disabled:cursor-not-allowed disabled:opacity-60"
/>
<button
type="button"
onClick={() => removeOption(i)}
disabled={n <= MIN_OPTIONS}
disabled={spinning || n <= MIN_OPTIONS}
aria-label={`Option ${i + 1} entfernen`}
className="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-xl text-ink-soft transition-colors hover:bg-clay-soft hover:text-clay disabled:cursor-not-allowed disabled:opacity-30"
>
@@ -283,8 +321,8 @@ export default function WheelOfFortune() {
<button
type="button"
onClick={addOption}
disabled={n >= MAX_OPTIONS}
className="mt-3 inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl border-2 border-dashed border-line px-4 py-2.5 text-sm font-semibold text-ink-soft transition-colors hover:border-primary/50 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40"
disabled={spinning || n >= MAX_OPTIONS}
className="mt-3 inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl border-2 border-dashed border-line px-4 py-2.5 text-sm font-semibold text-ink-soft transition-colors hover:border-[#0d7a57] hover:bg-[#e6f7f0]/50 hover:text-[#0d7a57] disabled:cursor-not-allowed disabled:opacity-40"
>
<Plus className="h-4 w-4" />
Option hinzufügen