Initialisieren

This commit is contained in:
2026-07-23 21:59:15 +02:00
commit 2d961adad4
76 changed files with 14318 additions and 0 deletions

53
lib/confetti.ts Normal file
View File

@@ -0,0 +1,53 @@
import confetti from "canvas-confetti";
export const CONFETTI_COLORS = [
"#2f4a3a",
"#b4633f",
"#d9b36a",
"#7c9077",
"#ffffff",
];
/** Dezenter Burst bei Ergebnissen. */
export function popConfetti() {
confetti({
particleCount: 70,
spread: 60,
startVelocity: 32,
scalar: 0.9,
origin: { y: 0.55 },
colors: CONFETTI_COLORS,
disableForReducedMotion: true,
});
}
/** Großer Gewinner-Regen mit zwei Seitenkanonen. */
export function celebrateConfetti() {
const defaults: confetti.Options = {
spread: 70,
startVelocity: 42,
colors: CONFETTI_COLORS,
disableForReducedMotion: true,
};
confetti({ ...defaults, particleCount: 120, origin: { y: 0.55 } });
setTimeout(
() =>
confetti({
...defaults,
particleCount: 60,
angle: 60,
origin: { x: 0, y: 0.7 },
}),
150,
);
setTimeout(
() =>
confetti({
...defaults,
particleCount: 60,
angle: 120,
origin: { x: 1, y: 0.7 },
}),
250,
);
}

8
lib/gsap.ts Normal file
View File

@@ -0,0 +1,8 @@
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
export { gsap, ScrollTrigger };

17
lib/motion.ts Normal file
View File

@@ -0,0 +1,17 @@
/**
* Zentrale Motion-Tokens (aus dem Landing-Page-Animations-Skill).
* Micro: 120220ms · UI: 300600ms · Story: 8001400ms
*/
export const DUR = {
micro: 0.18,
ui: 0.45,
story: 1.1,
} as const;
export const EASE = "power3.out";
/** Langer, physikalisch wirkender Auslauf (Glücksrad). */
export const wheelEase = (t: number) => 1 - Math.pow(1 - t, 5);
/** Münz-Auslauf: dramatisches Slow-down am Ende. */
export const coinEase = (t: number) => 1 - Math.pow(1 - t, 4);

78
lib/og-image.tsx Normal file
View File

@@ -0,0 +1,78 @@
export const ogImageSize = { width: 1200, height: 630 };
type OgImageContentProps = {
visualSrc: string;
};
/** Gemeinsames Layout für Open-Graph- und X/Twitter-Vorschauen. */
export function OgImageContent({ visualSrc }: OgImageContentProps) {
return (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
position: "relative",
overflow: "hidden",
backgroundColor: "#f7f4ed",
fontFamily: "Arial, sans-serif",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element -- next/image is unsupported inside next/og's ImageResponse (Satori) rendering */}
<img
src={visualSrc}
alt=""
width={1200}
height={630}
style={{ display: "flex", position: "absolute", inset: 0, width: "100%", height: "100%" }}
/>
<div
style={{
display: "flex",
position: "absolute",
inset: 0,
width: 705,
backgroundImage: "linear-gradient(90deg, rgba(247,244,237,0.98) 0%, rgba(247,244,237,0.92) 78%, rgba(247,244,237,0) 100%)",
}}
/>
<div
style={{
display: "flex",
position: "relative",
flexDirection: "column",
width: 650,
padding: "63px 0 58px 76px",
color: "#111412",
}}
>
<div style={{ display: "flex", alignItems: "center", fontSize: 40, fontWeight: 700, letterSpacing: -1.5 }}>
<span>Entscheid</span>
<span style={{ color: "#0d7a57", fontFamily: "Georgia, serif", fontStyle: "italic", fontWeight: 400 }}>omat</span>
</div>
<div style={{ display: "flex", flexDirection: "column", marginTop: 78, fontSize: 66, lineHeight: 1.04, fontWeight: 700, letterSpacing: -3.2 }}>
<span>Lass den</span>
<span>
<span style={{ color: "#0d7a57", fontFamily: "Georgia, serif", fontStyle: "italic", fontWeight: 400, marginRight: 16 }}>Zufall</span>entscheiden.
</span>
</div>
<div style={{ display: "flex", marginTop: 27, color: "#4c5c54", fontSize: 24, lineHeight: 1.35 }}>
Münze werfen, Glücksrad drehen, würfeln &amp; mehr.
</div>
<div style={{ display: "flex", marginTop: "auto", alignItems: "center" }}>
<div style={{ display: "flex", padding: "12px 20px", borderRadius: 999, backgroundColor: "#0d7a57", color: "#ffffff", fontSize: 18, fontWeight: 700, letterSpacing: 1.2 }}>
7 TOOLS · KOSTENLOS · OHNE ANMELDUNG
</div>
</div>
</div>
<div style={{ display: "flex", position: "absolute", right: 540, bottom: 34, color: "#405249", fontSize: 16, fontWeight: 700, letterSpacing: 0.4 }}>
entscheidomat.com
</div>
</div>
);
}

122
lib/sounds.ts Normal file
View File

@@ -0,0 +1,122 @@
/**
* Prozedurale Sound-Engine auf Basis der WebAudio API.
* Keine Audio-Dateien nötig alles wird live synthetisiert.
*/
export type SoundName = "tick" | "coin" | "dice" | "pop" | "win" | "whoosh";
let ctx: AudioContext | null = null;
function ac(): AudioContext | null {
if (typeof window === "undefined") return null;
try {
if (!ctx) {
const AC =
window.AudioContext ||
(window as unknown as { webkitAudioContext?: typeof AudioContext })
.webkitAudioContext;
if (!AC) return null;
ctx = new AC();
}
if (ctx.state === "suspended") {
ctx.resume().catch(() => {});
}
return ctx;
} catch {
return null;
}
}
function tone(
c: AudioContext,
freq: number,
type: OscillatorType,
dur: number,
peak = 0.2,
when = 0,
slideTo?: number,
) {
const t0 = c.currentTime + when;
const osc = c.createOscillator();
const gain = c.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t0);
if (slideTo) {
osc.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur);
}
gain.gain.setValueAtTime(0, t0);
gain.gain.linearRampToValueAtTime(peak, t0 + 0.008);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(gain).connect(c.destination);
osc.start(t0);
osc.stop(t0 + dur + 0.05);
}
function noiseBurst(
c: AudioContext,
dur: number,
peak: number,
filterFreq: number,
when = 0,
sweepTo?: number,
) {
const t0 = c.currentTime + when;
const len = Math.max(1, Math.floor(c.sampleRate * dur));
const buffer = c.createBuffer(1, len, c.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
const src = c.createBufferSource();
src.buffer = buffer;
const filter = c.createBiquadFilter();
filter.type = "bandpass";
filter.frequency.setValueAtTime(filterFreq, t0);
filter.Q.value = 1.1;
if (sweepTo) {
filter.frequency.exponentialRampToValueAtTime(sweepTo, t0 + dur);
}
const gain = c.createGain();
gain.gain.setValueAtTime(0, t0);
gain.gain.linearRampToValueAtTime(peak, t0 + 0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
src.connect(filter).connect(gain).connect(c.destination);
src.start(t0);
}
export function playSound(name: SoundName) {
const c = ac();
if (!c) return;
try {
switch (name) {
case "tick":
tone(c, 1900, "square", 0.035, 0.07);
break;
case "coin":
// Metallischer Doppelton + Glitzer
tone(c, 2093, "triangle", 0.55, 0.16);
tone(c, 2637, "triangle", 0.45, 0.1, 0.012);
tone(c, 3520, "sine", 0.3, 0.05, 0.02);
break;
case "dice":
// Prasselndes Klackern
[0, 0.09, 0.21, 0.31].forEach((w, i) =>
noiseBurst(c, 0.05, 0.2 - i * 0.03, 1600 + Math.random() * 1000, w),
);
break;
case "pop":
tone(c, 420, "sine", 0.14, 0.22, 0, 880);
break;
case "whoosh":
noiseBurst(c, 0.55, 0.1, 500, 0, 2600);
break;
case "win":
// Kleine Fanfare C5E5G5C6
[523.25, 659.25, 783.99, 1046.5].forEach((f, i) =>
tone(c, f, "triangle", 0.4, 0.13, i * 0.09),
);
break;
}
} catch {
// Sound ist Dekoration niemals crashen
}
}

10
lib/tool-pages.ts Normal file
View File

@@ -0,0 +1,10 @@
/** Dedizierte SEO-Landingpages der Tools, für konsistentes internes Linking. */
export const TOOL_PAGES = [
{ slug: "ja-nein-generator", label: "Ja-Nein-Generator", lastModified: "2026-07-23" },
{ slug: "gluecksrad", label: "Glücksrad", lastModified: "2026-07-23" },
{ slug: "wuerfel-online", label: "Würfel Online", lastModified: "2026-07-23" },
{ slug: "namen-auslosen", label: "Namen-Auslosung", lastModified: "2026-07-23" },
{ slug: "muenze-werfen", label: "Münze werfen", lastModified: "2026-07-23" },
{ slug: "zufallszahl-generator", label: "Zufallszahl-Generator", lastModified: "2026-07-23" },
{ slug: "magic-8-ball", label: "Magic 8-Ball", lastModified: "2026-07-23" },
] as const;