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

2
.audit-next.err.log Normal file
View File

@@ -0,0 +1,2 @@
⚠ "next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.
^C

8
.audit-next.out.log Normal file
View File

@@ -0,0 +1,8 @@
> webseite@0.1.0 start
> next start -p 3100
▲ Next.js 16.2.11
- Local: http://localhost:3100
- Network: http://10.5.0.2:3100
✓ Ready in 249ms

12
.claude/launch.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3000,
"autoPort": true
}
]
}

32
.dockerignore Normal file
View File

@@ -0,0 +1,32 @@
# dependencies
node_modules
.pnp
.pnp.*
# next.js build output
.next
out
build
# git
.git
.gitignore
# env files (pass secrets via docker-compose env_file / environment instead)
.env
.env.*
!.env.example
# docs and planning files (not needed inside the image)
*.md
*.csv
# editor/OS
.DS_Store
.vscode
.idea
# docker itself
Dockerfile
.dockerignore
docker-compose.yml

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

5
AGENTS.md Normal file
View File

@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000 \
HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Binary file not shown.
1 Keyword Stats 2026-07-22 at 23_08_06
2 1. Juli 2025 - 30. Juni 2026
3 Keyword Currency Avg. monthly searches Änderung über drei Monate Änderung im Jahresvergleich Competition Competition (indexed value) Top of page bid (low range) Top of page bid (high range) Ad impression share Organic impression share Organic average position In account? In plan? Searches: Jul 2025 Searches: Aug 2025 Searches: Sep 2025 Searches: Oct 2025 Searches: Nov 2025 Searches: Dec 2025 Searches: Jan 2026 Searches: Feb 2026 Searches: Mar 2026 Searches: Apr 2026 Searches: May 2026 Searches: Jun 2026
4 entscheidungsfinder EUR 500 0% 0% Niedrig 1
5 stundensatz rechner EUR 500 -90% -90% Niedrig 4 0,37 1,52
6 wichtel generator EUR 500 0% 0% Niedrig 17 0,99 1,68
7 deckblatt ersteller EUR 500 0% 0% Niedrig 10 0,21 0,92
8 prozentrechner EUR 500000 0% 0% Niedrig 0 1,75 1,75
9 wortzaehler EUR 50 0% 0% Niedrig 18
10 namensgenerator EUR 5000 0% 0% Niedrig 3 0,24 1,90
11 gruppen generator EUR 500 0% 0% Niedrig 0
12 schriftarten generator EUR 50000 0% 0% Niedrig 1 0,29 1,37
13 trinkgeld rechner EUR 500 0% 0% Niedrig 0
14 prozentrechnung EUR 50000 0% 0% Niedrig 0 1,75 1,75
15 prozentrechnung formel EUR 50000 0% 0% Niedrig 2 1,75 1,75
16 prozentrechnung formeln EUR 50000 0% 0% Niedrig 2 1,75 1,75
17 namen generator EUR 5000 0% 0% Niedrig 4 0,32 1,49
18 kreative namen generator EUR 500 0% 0% Niedrig 20 0,16 1,09
19 prozentrechnen formel EUR 50000 0% 0% Niedrig 2 1,75 1,75
20 prozentrechnen EUR 5000 0% 0% Niedrig 0 1,75 1,75
21 formel prozentrechnung EUR 5000 0% 0% Niedrig 2 1,75 1,75
22 prozentrechner kredit EUR 50 0% 0% Mittel 61 0,64 1,28
23 prozentrechnen dreisatz EUR 5000 0% 0% Niedrig 2 0,33 1,75
24 dreisatz prozent EUR 5000 0% 0% Niedrig 0 1,75 1,75
25 prozent rechnung EUR 50000 0% 0% Niedrig 0 1,75 1,75
26 schriftgenerator EUR 50000 0% 0% Niedrig 0 0,42 1,69
27 freelancer stundensatz rechner EUR 50 0% 0% Niedrig 29 0,13 1,49
28 ki namensgenerator EUR 50 0% -90% Mittel 37 0,26 1,43
29 selbstständig stundensatz rechner EUR 50 0% -90% Niedrig 30 0,23 1,89
30 schriftarten zum kopieren EUR 50000 -90% -90% Niedrig 0 0,39 1,04
31 schriftart generator EUR 50000 0% 0% Niedrig 1 0,37 0,76
32 schriftgenerator instagram EUR 500 0% -90% Niedrig 0
33 namensgenerator produkt EUR 50 0% 0% Hoch 71
34 aktien prozentrechner EUR 50 0% 0% Niedrig 17 0,89 5,86
35 instagram schrift generator EUR 5000 0% 0% Niedrig 0
36 kredit prozentrechner EUR 50 0% 0% Hoch 75 0,62 1,62
37 prozentwert berechnen EUR 5000 0% 0% Niedrig 1 1,75 1,75
38 prozente berechnen formel EUR 500 0% 0% Niedrig 1 1,75 1,75
39 prozentaufgaben EUR 500 -90% -90% Niedrig 0
40 übungsaufgaben prozentrechnung EUR 500 0% 0% Niedrig 5
41 schriftarten kopieren EUR 5000 0% 0% Niedrig 0
42 dreisatz formel prozent EUR 500 0% 0% Niedrig 1
43 dreisatz prozentrechnung EUR 5000 0% 0% Niedrig 2 0,33 1,75
44 prozentrechnung online EUR 50000 0% 0% Niedrig 0 1,75 1,75
45 stundensatz berechnen selbstständig EUR 50 0% -90% Mittel 35 0,15 1,39
46 schöne schrift zum kopieren EUR 5000 0% 0% Niedrig 0 0,41 1,20
47 schriftarten instagram zum kopieren EUR 500 0% 0% Niedrig 0
48 mathe prozentrechnung EUR 500 0% 0% Niedrig 1 1,75 1,75
49 taschenrechner mit prozentrechnung EUR 5000 0% 0% Niedrig 4 0,07 0,45
50 prozentrechnung textaufgaben EUR 500 0% 0% Niedrig 1
51 prozentrechnung kredit EUR 50 0% 0% Hoch 86
52 taschenrechner mit prozent EUR 500 0% 0% Mittel 61 0,07 0,69
53 namensgenerator firma englisch EUR 50 0% -100% Unbekannt
54 textaufgaben prozentrechnung EUR 500 0% 0% Niedrig 1
55 prozentsatz formel EUR 5000 0% 0% Niedrig 1 1,75 1,75
56 prozentformel EUR 5000 0% 0% Niedrig 1 1,75 1,75
57 1 grad in prozent EUR 50 0% 0% Niedrig 5
58 10 prozent rechnen EUR 50 0% 0% Niedrig 0
59 19 prozent rechnen EUR 50 0% 0% Niedrig 9
60 2 namen generator EUR 50 0% 0% Niedrig 0
61 3d schriftart generator EUR 500 0% 0% Niedrig 8 0,49 1,47
62 3d schriftarten generator EUR 50 0% 0% Niedrig 6
63 a schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
64 airline namen generator EUR 50 0% 0% Niedrig 0
65 album namen generator EUR 50 0% 0% Niedrig 0
66 alias namen generator EUR 50 0% 0% Niedrig 0
67 allianz namen generator EUR 50 0% 0% Niedrig 0
68 allura schriftart generator EUR 50 0% 0% Niedrig 0
69 altdeutsche namen generator EUR 50 0% 0% Niedrig 0
70 altdeutsche schrift converter EUR 50 0% 0% Niedrig 0
71 altdeutsche schrift umwandler EUR 50 0% 0% Niedrig 0
72 altdeutsche schriftart generator EUR 5000 0% 0% Niedrig 0
73 alte schriftarten generator EUR 50 0% 0% Niedrig 0
74 alte schriften generator EUR 500 0% 0% Niedrig 0
75 andere schrift kopieren EUR 500 0% -90% Niedrig 0
76 andere schrift zum kopieren EUR 500 0% -90% Niedrig 0
77 andere schriftarten kopieren EUR 50 900% 0% Niedrig 1
78 andere schriftarten zum kopieren EUR 50 900% 0% Niedrig 1
79 anwendungsaufgaben prozentrechnung EUR 50 0% 0% Niedrig 0
80 arbeitsblätter prozentrechnung EUR 500 0% 0% Niedrig 6
81 arbeitsblätter prozentrechnung mit lösungen EUR 50 0% 0% Niedrig 4
82 assassinen namen generator EUR 50 0% 0% Niedrig 0
83 assi namen generator EUR 50 0% 0% Niedrig 0
84 auf 100 prozent rechnen EUR 50 0% Niedrig 0
85 aufgabenfuchs prozentrechnung EUR 500 0% -90% Niedrig 0
86 auto namen generator EUR 50 0% 0% Niedrig 0
87 avatar namen generator EUR 50 0% 0% Niedrig 0
88 beispielaufgaben prozentrechnung EUR 50 0% 0% Niedrig 3
89 berechne den grundwert EUR 5000 0% 0% Niedrig 1 1,58 1,75
90 berechne den prozentsatz EUR 5000 0% -90% Niedrig 0 1,75 1,75
91 berechne den prozentwert EUR 5000 0% -90% Niedrig 0 1,75 1,75
92 berechnen des grundwertes EUR 5000 0% 0% Niedrig 1 1,58 1,75
93 berechnen des prozentsatzes EUR 5000 0% -90% Niedrig 0 1,75 1,75
94 berechnen des prozentwertes EUR 5000 0% 0% Niedrig 1 1,75 1,75
95 berechnen prozentwert EUR 5000 0% -90% Niedrig 0 1,75 1,75
96 besondere schrift zum kopieren EUR 50 0% 0% Niedrig 0
97 besondere schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
98 blitzrechner prozent EUR 5000 0% 0% Niedrig 0
99 bot namen generator EUR 50 0% 0% Niedrig 0
100 brutto prozent rechner EUR 50 0% 0% Niedrig 0
101 brutto stundensatz berechnen EUR 50 -100% -100% Unbekannt
102 buchstaben generator schrift EUR 50 0% 0% Niedrig 0
103 buchstaben schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
104 bullet journal schrift generator EUR 50 0% 0% Niedrig 0
105 böse namen generator EUR 50 -100% -100% Unbekannt
106 bösewicht namen generator EUR 50 0% 0% Niedrig 0
107 casio fx 85 de plus prozent rechnen EUR 50 0% 0% Niedrig 0
108 char namen generator EUR 50 0% 0% Niedrig 0
109 charakter namen generator EUR 50 0% 0% Niedrig 0
110 charakter namensgenerator EUR 50 0% 0% Niedrig 1
111 chicano schriftart generator EUR 500 0% 0% Niedrig 1
112 chinesischer namensgenerator EUR 50 0% 0% Niedrig 0
113 clown namen generator EUR 50 0% 0% Niedrig 0
114 coole instagram namen generator EUR 50 0% 0% Niedrig 0
115 coole namen generator EUR 500 0% 0% Niedrig 1
116 coole schrift generator EUR 50 0% 0% Niedrig 0
117 coole schrift zum kopieren EUR 500 0% 0% Niedrig 1
118 coole schriftarten generator EUR 50 0% 0% Niedrig 5
119 coole schriftarten zum kopieren EUR 500 0% 0% Niedrig 0
120 coole schriften kopieren EUR 500 0% 0% Niedrig 0
121 coole youtube namen generator EUR 50 0% 0% Niedrig 0
122 cooler schriftgenerator EUR 50 0% Niedrig 0
123 cornelsen prozentrechnung EUR 50 0% 0% Niedrig 0
124 cowboy namen generator EUR 50 0% 0% Niedrig 0
125 d&d namen generator EUR 50 0% 0% Niedrig 0
126 daniel jung mathe prozentrechnung EUR 50 -100% -100% Unbekannt
127 deckblatt online erstellen kostenlos EUR 50 0% Niedrig 14
128 deckblatt selber gestalten kostenlos EUR 50 0% 0% Unbekannt
129 deckblatt selbst gestalten online EUR 0 0% -100% Unbekannt
130 deckblätter kostenlos online erstellen EUR 50 0% -100% Unbekannt
131 dein schriftgenerator EUR 500 0% 0% Niedrig 0
132 detektiv namen generator EUR 50 0% 0% Niedrig 0
133 deutsche frakturschrift generator EUR 50 0% 0% Niedrig 0
134 deutsche schrift generator EUR 50 0% 0% Niedrig 0
135 deutscher name generator EUR 50 0% 0% Niedrig 2
136 die formeln für prozentrechnung EUR 50 0% 0% Niedrig 0
137 die prozentrechnung EUR 50 0% 0% Niedrig 0
138 digitale schrift generator EUR 50 0% 0% Niedrig 0
139 discord namen generator EUR 50 0% 0% Niedrig 0
140 disney schrift kopieren EUR 50 0% 0% Niedrig 0
141 disney schrift zum kopieren EUR 500 900% 0% Niedrig 0
142 disney schriftart generator EUR 500 0% 0% Niedrig 0
143 disney schriftart generator zum kopieren EUR 50 0% 0% Niedrig 0
144 disney schriftgenerator EUR 50 0% 0% Niedrig 0
145 dreisatz formel prozent berechnen EUR 50 0% 0% Niedrig 0
146 dreisatz formel prozentsatz EUR 50 0% 0% Niedrig 1
147 dreisatz grundwert prozentwert prozentsatz EUR 50 0% 0% Niedrig 2
148 dreisatz lehrer schmidt EUR 50 0% -90% Niedrig 8
149 dreisatz mit prozent EUR 5000 0% 0% Niedrig 0 1,75 1,75
150 dreisatz prozent formel EUR 500 0% 0% Niedrig 1
151 dreisatz prozent rechner EUR 500 0% 0% Niedrig 0
152 dreisatz prozentrechnung formel EUR 500 0% 0% Niedrig 2 0,43 1,75
153 dreisatz prozentsatz EUR 500 0% 0% Niedrig 1
154 dreisatz prozentwert EUR 50 0% -90% Niedrig 0
155 dreisatz rechnen prozent EUR 5000 0% -90% Niedrig 0 1,75 1,75
156 dreisatz und prozentrechnung EUR 50 0% 0% Niedrig 2
157 dreisatz zinsrechnung prozentrechnung EUR 50 -100% -100% Unbekannt
158 dreisatzrechner prozent EUR 500 0% 0% Niedrig 0
159 dreisatzrechnung prozent EUR 50 0% 0% Niedrig 1
160 ein deckblatt erstellen EUR 50 0% 0% Niedrig 0
161 einfach prozentrechnen EUR 500 0% 0% Niedrig 1
162 einfach prozentrechnen im kopf EUR 50 0% 0% Niedrig 4
163 einfacher dreisatz prozent EUR 50 0% 0% Niedrig 0
164 elegante schriftarten zum kopieren EUR 500 0% 0% Niedrig 1
165 erklärvideo prozentrechnung EUR 50 0% 0% Niedrig 4
166 facebook schrift fett generator EUR 50 0% 0% Niedrig 0
167 facebook schriftart generator EUR 50 0% 0% Niedrig 0
168 facebook schriftgenerator EUR 500 0% -90% Niedrig 0
169 fanclub namen generator EUR 50 -100% -100% Unbekannt
170 fancy schrift generator EUR 50 0% 0% Niedrig 1
171 fantasie namen generator EUR 500 0% -90% Niedrig 1
172 farbige schrift generator zum kopieren EUR 50 0% 0% Niedrig 0
173 farm namen generator EUR 50 0% 0% Niedrig 0
174 fe schrift generator EUR 50 0% 0% Niedrig 0
175 femibion namensgenerator EUR 50 0% 0% Niedrig 0
176 fett schrift generator EUR 50 0% -90% Niedrig 0
177 font schrift generator EUR 50 0% 0% Niedrig 2
178 font schrift kopieren EUR 50 -100% -100% Unbekannt
179 font schrift zum kopieren EUR 50 0% 0% Niedrig 0
180 fonts schriftarten kopieren EUR 50 0% 0% Niedrig 0
181 fonts schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
182 formel der prozentrechnung EUR 50000 0% 0% Niedrig 2 1,75 1,75
183 formel dreisatz prozent EUR 500 0% 0% Niedrig 1
184 formel für den prozentwert EUR 50 0% 0% Niedrig 1
185 formel für die prozentrechnung EUR 50000 0% 0% Niedrig 2 1,75 1,75
186 formel für grundwert EUR 500 0% 0% Niedrig 0
187 formel für prozent EUR 50 0% 0% Niedrig 1
188 formel für prozentrechnen EUR 500 0% 0% Niedrig 1 0,02 1,75
189 formel für prozentwert EUR 500 0% 0% Niedrig 1
190 formel prozent EUR 5000 0% 0% Niedrig 1 1,75 1,75
191 formel prozent rechnung EUR 5000 0% 0% Niedrig 2 1,75 1,75
192 formel prozent von EUR 50 0% 0% Niedrig 1
193 formel prozent von etwas EUR 50 0% 0% Niedrig 4
194 formel prozentrechner EUR 5000 0% 0% Niedrig 2 1,75 1,75
195 formel prozentrechnung dreisatz EUR 50 0% 0% Niedrig 0
196 formel prozentrechnung prozentsatz EUR 50 0% 0% Niedrig 0
197 formel prozentwert EUR 500 0% 0% Niedrig 1 1,75 1,75
198 formel prozentwert berechnen EUR 50 0% 0% Niedrig 1
199 formel von prozentrechnung EUR 50000 0% 0% Niedrig 2 1,75 1,75
200 formel zum prozentrechnen EUR 50000 0% 0% Niedrig 2 1,75 1,75
201 formel zur prozentrechnung EUR 50000 0% 0% Niedrig 2 1,75 1,75
202 formeln grundwert prozentwert prozentsatz EUR 50 0% 0% Niedrig 1
203 formeln mathe prozentrechnung EUR 50 0% -90% Niedrig 1
204 formeln prozentrechnung grundformel EUR 50 0% 0% Unbekannt
205 formeln zu prozentrechnung EUR 50 0% 0% Niedrig 3
206 formelsammlung mathe prozentrechnung EUR 50 0% 0% Niedrig 0
207 formelsammlung prozentrechnung EUR 50 0% 0% Niedrig 0
208 fraktur schrift generator EUR 500 0% 0% Niedrig 0
209 frakturschrift generator EUR 500 0% 0% Niedrig 0
210 freelance gehalt rechner EUR 50 0% 0% Niedrig 23 0,46 2,79
211 freelancer tagessatz rechner EUR 50 0% 0% Niedrig 31
212 freiberufler stundensatz berechnen EUR 50 0% 0% Mittel 37
213 freiberufler stundensatz rechner EUR 50 0% 0% Niedrig 30 0,16 1,54
214 fußball namen generator EUR 50 0% 0% Niedrig 0
215 gehalt prozent rechner EUR 50 0% 0% Niedrig 1
216 gehaltserhöhung in prozent rechner EUR 50 0% 0% Niedrig 4
217 gehaltserhöhung prozent rechner EUR 500 0% 0% Niedrig 4 0,63 1,38
218 geheim schriftarten generator EUR 50 -100% 0% Unbekannt
219 geheime schriftarten generator EUR 50 0% 0% Unbekannt
220 geile schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
221 generator schriftarten EUR 50000 0% 0% Niedrig 1 0,29 1,37
222 generierte schrift generator EUR 50 0% 0% Unbekannt
223 germanische namen generator EUR 50 0% 0% Niedrig 0
224 geschichten namen generator EUR 50 0% 0% Niedrig 0
225 geschwungene handschrift generator EUR 50 0% 0% Niedrig 0
226 geschwungene schrift generator EUR 50 0% 0% Niedrig 1
227 geschwungene schriftarten generator EUR 500 0% 0% Niedrig 1
228 gilden namen generator EUR 50 0% 0% Niedrig 0
229 gleichungen prozentrechnung EUR 50 0% 0% Niedrig 1
230 glitzerschrift generator kostenlos EUR 50 -100% -100% Unbekannt
231 google schrift generator EUR 50 0% 0% Niedrig 0
232 google schriftarten generator EUR 50 0% 0% Niedrig 1
233 gothic schrift kopieren EUR 50 0% 0% Niedrig 0
234 gothic schriftart generator EUR 500 0% 0% Niedrig 1
235 gotische schrift generator EUR 50 0% 0% Niedrig 3
236 grad in prozent rechner EUR 50 0% 0% Niedrig 4
237 grad prozent rechner EUR 500 0% 0% Niedrig 4 0,01 0,02
238 graffiti schrift kopieren EUR 50 0% 0% Niedrig 0
239 graffiti schrift zum kopieren EUR 50 0% 0% Niedrig 0
240 graffiti schriftart generator EUR 5000 -90% -90% Niedrig 2 0,21 3,20
241 graffiti schriftarten generator EUR 50 0% 0% Niedrig 0
242 graffiti schriftgenerator EUR 50 0% 0% Niedrig 6
243 griechische schriftart generator EUR 50 0% 0% Niedrig 0
244 grundaufgaben der prozentrechnung EUR 50 0% 0% Niedrig 1
245 grundaufgaben prozentrechnung EUR 50 0% 0% Niedrig 1
246 grundformel prozentrechnung EUR 500 0% 0% Niedrig 1
247 grundgleichung der prozentrechnung EUR 500 0% 0% Niedrig 1
248 grundlagen der prozentrechnung EUR 50 0% 0% Niedrig 8
249 grundwert EUR 500 0% 0% Niedrig 0
250 grundwert berechnen EUR 5000 0% 0% Niedrig 1 1,58 1,75
251 grundwert berechnen dreisatz EUR 500 0% 0% Niedrig 1
252 grundwert berechnen formel EUR 500 0% 0% Niedrig 1
253 grundwert berechnen online EUR 50 0% 0% Niedrig 0
254 grundwert berechnen textaufgaben EUR 50 0% 0% Niedrig 0
255 grundwert formel EUR 500 0% -90% Niedrig 0
256 grundwert herausfinden EUR 50 0% 0% Niedrig 0
257 grundwert mit dreisatz berechnen EUR 50 0% 0% Niedrig 0
258 grundwert prozentrechnung EUR 500 0% 0% Niedrig 1
259 grundwert prozentsatz prozentwert EUR 5000 0% 0% Niedrig 1 0,79 1,75
260 grundwert prozentwert EUR 50 0% 0% Niedrig 1
261 grundwert prozentwert prozentsatz EUR 5000 0% 0% Niedrig 1 0,79 1,75
262 grundwert prozentwert prozentsatz formel EUR 500 0% 0% Niedrig 1 1,75 1,75
263 grundwert prozentwert und prozentsatz EUR 50 0% 0% Niedrig 1
264 grundwert rechner EUR 50 0% 0% Niedrig 1
265 grundwerte berechnen EUR 50 0% 0% Niedrig 1
266 gruselige schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
267 gta rp namen generator EUR 50 0% 0% Niedrig 0
268 gulp stundensatz rechner EUR 50 Niedrig 0
269 gute instagram namen generator EUR 50 0% Niedrig 0
270 hacker namen generator EUR 50 0% 0% Niedrig 0
271 handlettering schriftarten generator EUR 500 0% -90% Niedrig 4
272 handlettering schriftarten zum kopieren EUR 50 0% 0% Niedrig 14
273 handschrift generator kopieren EUR 50 0% 0% Niedrig 0
274 handy taschenrechner prozent EUR 500 0% 0% Niedrig 1
275 harry potter schriftart generator EUR 500 0% 0% Niedrig 0
276 hasen namen generator EUR 50 Niedrig 0
277 haus namen generator EUR 50 0% 0% Niedrig 0
278 hawaiianische namen generator EUR 50 -100% -100% Unbekannt
279 hipster namen generator EUR 50 0% 0% Unbekannt
280 hotel namen generator EUR 50 0% 0% Niedrig 30
281 hunde namen generator EUR 50 0% 0% Niedrig 1
282 hübsche schrift zum kopieren EUR 50 0% 0% Niedrig 0
283 indianer namen generator EUR 50 0% 0% Niedrig 0
284 instagram namen generator EUR 50 0% -90% Niedrig 0
285 instagram namensgenerator EUR 50 0% -90% Niedrig 0
286 instagram schrift zum kopieren EUR 500 -90% -90% Niedrig 0
287 instagram schriftart kopieren EUR 50 -90% -90% Niedrig 0
288 instagram schriftarten generator EUR 500 0% 900% Niedrig 0
289 instagram schriftarten zum kopieren EUR 500 -90% -90% Niedrig 0
290 instagram schriftgenerator EUR 500 0% 0% Niedrig 0
291 internet namen generator EUR 50 0% 0% Niedrig 0
292 isländische namen generator EUR 50 0% 0% Niedrig 0
293 japanische namen generator EUR 50 0% -90% Niedrig 0
294 japanische schriftart generator EUR 50 0% 0% Niedrig 0
295 kalligraphie schriftarten generator EUR 50 0% 0% Niedrig 4
296 kartenmacherei namensgenerator EUR 50 Niedrig 0
297 kaufmännisch prozentrechnen EUR 50 0% 0% Niedrig 2
298 kaufmännisches rechnen prozentrechnung EUR 50 -100% -100% Unbekannt
299 kegelclub namen generator EUR 50 0% 0% Unbekannt
300 keltische namen generator EUR 50 0% 0% Niedrig 0
301 klassenarbeiten prozentrechnung EUR 50 0% -90% Niedrig 17
302 kleine schriftart generator EUR 500 0% 0% Niedrig 1
303 kobold namen generator EUR 50 -100% -100% Unbekannt
304 kopfrechnen prozentaufgaben EUR 50 0% Niedrig 0
305 kopfrechnen prozentrechnung EUR 50 0% 0% Niedrig 2
306 kostenlose schriftarten kopieren EUR 50 0% 0% Niedrig 0
307 kostenlose schriftarten zum kopieren EUR 50 -90% 0% Niedrig 0
308 kostenloser prozentrechner EUR 50 0% 0% Niedrig 0
309 kostenloser schriftgenerator EUR 500 0% 0% Niedrig 0
310 kursiv schrift generator EUR 500 -90% -90% Niedrig 0
311 kursive schrift zum kopieren EUR 500 -90% -90% Niedrig 0
312 königreich namen generator EUR 50 0% 0% Niedrig 0
313 leichte prozentaufgaben EUR 50 0% 0% Niedrig 0
314 lernplakat prozentrechnung EUR 50 0% 0% Niedrig 10
315 lettering schrift generator EUR 50 0% 0% Niedrig 2
316 lohnerhöhung prozent rechner EUR 50 0% 0% Niedrig 0
317 lovoo schrift generator EUR 50 0% 0% Niedrig 0
318 lustige gamer namen generator EUR 50 0% 0% Niedrig 0
319 lustige namen generator EUR 50 0% 0% Niedrig 1
320 lösungen prozentrechnung EUR 0 0% 0% Unbekannt
321 maskottchen namen generator EUR 50 0% 0% Niedrig 0
322 mathe formel prozentrechnung EUR 50 0% -90% Niedrig 1
323 mathe formeln prozentrechnung EUR 50 0% -90% Niedrig 1
324 mathe grundwert prozentwert prozentsatz formel EUR 50 0% 0% Niedrig 0
325 mathe prozent EUR 500 0% 0% Niedrig 0
326 mathe prozent formeln EUR 50 0% 0% Niedrig 0
327 mathe prozent und zinsrechnung EUR 50 0% 0% Niedrig 1
328 mathe prozentaufgaben EUR 50 0% 0% Niedrig 0
329 mathe prozentrechnen EUR 50 0% 0% Niedrig 0
330 mathe prozentrechnung arbeitsblätter EUR 50 0% 0% Niedrig 0
331 mathe prozentrechnung formel EUR 50 0% -90% Niedrig 1
332 mathe prozentrechnung textaufgaben EUR 50 0% 0% Niedrig 0
333 mathe textaufgaben prozentrechnung EUR 50 0% 0% Niedrig 0
334 mathe trainer prozentrechnung EUR 50 0% 0% Mittel 36
335 mathe übungsaufgaben prozentrechnung EUR 50 0% 0% Niedrig 0
336 mathearbeit prozentrechnung EUR 50 0% 0% Hoch 100
337 matheaufgaben prozentrechnen EUR 50 0% Niedrig 0
338 matheaufgaben prozentrechnung EUR 500 0% 0% Niedrig 3
339 matheaufgaben prozentrechnung mit lösungen EUR 50 0% 0% Niedrig 0
340 mathefritz prozentrechnung EUR 50 -100% -100% Unbekannt
341 mathematik prozent und zinsrechnung EUR 50 -100% -100% Unbekannt
342 mathematik prozentrechnen EUR 50 0% 0% Niedrig 3
343 mathematik prozentrechnung formeln EUR 50 0% 0% Niedrig 0
344 mathetest prozentrechnung EUR 50 0% Niedrig 14
345 mein schriftgenerator EUR 500 0% 0% Niedrig 0
346 mexikanische namen generator EUR 50 0% 0% Niedrig 0
347 mini schrift kopieren EUR 50 0% 0% Niedrig 0
348 mini schrift zum kopieren EUR 50 0% 0% Niedrig 0
349 mit prozenten rechnen EUR 50 0% 0% Niedrig 0
350 mit taschenrechner prozent rechnen EUR 500 0% 0% Niedrig 1
351 mittelalterliche schrift generator EUR 50 0% 0% Niedrig 0
352 moderne schrift generator EUR 50 0% 0% Niedrig 0
353 moderne schriftarten generator EUR 50 0% Niedrig 0
354 moderne schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
355 motorrad club namen generator EUR 50 0% 0% Niedrig 0
356 motorrad namen generator EUR 50 0% 0% Niedrig 0
357 nachnamens generator EUR 500 0% 0% Niedrig 1
358 name schrift generator EUR 50 0% 0% Niedrig 0
359 namen generator instagram EUR 50 0% 0% Niedrig 0
360 namen generator online EUR 50 0% 0% Niedrig 25
361 namen generator vor und nachname EUR 50 0% 0% Niedrig 1
362 namen generator youtube EUR 500 0% 0% Niedrig 1
363 namen schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
364 namensgenerator aus 2 namen EUR 50 0% 0% Niedrig 0
365 namensgenerator aus vornamen EUR 50 0% 0% Niedrig 0
366 namensgenerator aus wörtern EUR 50 -100% 0% Unbekannt
367 namensgenerator aus zwei namen EUR 50 0% 0% Niedrig 2
368 namensgenerator elternnamen EUR 50 0% 0% Niedrig 0
369 namensgenerator für instagram EUR 50 0% 0% Niedrig 0
370 namensgenerator für produkte EUR 50 -100% -100% Unbekannt
371 namensgenerator für youtube EUR 50 0% 0% Niedrig 0
372 namensgenerator gruppe EUR 50 0% 0% Niedrig 0
373 namensgenerator instagram EUR 50 0% 0% Niedrig 4
374 namensgenerator mit vorgabe EUR 50 0% 0% Niedrig 10
375 namensgenerator nachnamen EUR 50 0% -90% Niedrig 1
376 namensgenerator online EUR 50 0% 0% Niedrig 3
377 namensgenerator produkte EUR 50 0% 0% Hoch 71
378 namensgenerator projekt EUR 50 0% -100% Unbekannt
379 namensgenerator vor und nachname EUR 500 0% -90% Niedrig 1
380 namensgenerator vorname EUR 50 0% 0% Niedrig 2
381 namensgenerator wow EUR 500 0% 0% Niedrig 0
382 netflix schriftart generator EUR 50 0% 0% Niedrig 0
383 netto prozent rechner EUR 50 0% 0% Niedrig 0
384 nickname schriftarten EUR 50 0% 0% Niedrig 0
385 niederländische namen generator EUR 50 0% 0% Niedrig 0
386 ninjago schriftart generator EUR 50 0% 0% Niedrig 0
387 old english schrift kopieren EUR 50 0% Niedrig 0
388 old english schriftart generator EUR 500 0% 0% Niedrig 0
389 online namen generator EUR 50 0% 0% Niedrig 2
390 online namensgenerator EUR 50 0% 0% Niedrig 31
391 online prozentrechner EUR 50000 0% 0% Niedrig 0 1,75 1,75
392 online prozentrechnung EUR 50000 0% 0% Niedrig 0 1,75 1,75
393 online rechner prozent EUR 50000 0% 0% Niedrig 0 1,75 1,75
394 online schrift generator EUR 50 -90% 0% Niedrig 1
395 online schriftarten generator EUR 50 0% 0% Niedrig 1
396 online schriftgenerator EUR 50 0% 0% Niedrig 2
397 online taschenrechner mit prozent EUR 50 0% 0% Niedrig 0
398 online wichtel generator EUR 50 0% 0% Niedrig 14
399 ork namen generator EUR 50 0% 0% Niedrig 0
400 pampers namensgenerator EUR 50 0% 0% Niedrig 0
401 pferde namen generator EUR 50 0% 0% Niedrig 0
402 pflanzen namen generator EUR 50 0% 0% Niedrig 0
403 planeten namen generator EUR 500 0% 0% Niedrig 0
404 plus prozent rechnen EUR 50 0% 0% Niedrig 0
405 pokemon schriftart generator EUR 50 0% 0% Niedrig 0
406 porsche schriftart generator EUR 50 0% 0% Niedrig 0
407 preiserhöhung in prozent berechnen rechner EUR 50 0% Niedrig 0
408 preiserhöhung in prozent rechner EUR 50 0% 0% Niedrig 0
409 producer namen generator EUR 50 0% 0% Niedrig 1
410 projekt namensgenerator EUR 50 -100% 0% Unbekannt
411 prozent berechnen EUR 50000 0% -90% Niedrig 0 1,75 1,75
412 prozent dreisatz EUR 5000 0% 0% Niedrig 0 1,75 1,75
413 prozent dreisatz formel EUR 500 0% 0% Niedrig 1
414 prozent dreisatz rechner EUR 500 0% 900% Niedrig 0
415 prozent mathe EUR 500 0% 0% Niedrig 0
416 prozent mit taschenrechner EUR 500 0% 0% Mittel 61 0,07 0,69
417 prozent rechenaufgaben EUR 50 0% 0% Niedrig 0
418 prozent rechnen EUR 5000 0% 0% Niedrig 0 1,75 1,75
419 prozent rechnen einfach EUR 500 0% 900% Niedrig 1
420 prozent rechnen im kopf EUR 50 0% 0% Niedrig 0
421 prozent rechnen mit taschenrechner EUR 50 0% 0% Niedrig 0
422 prozent rechnen taschenrechner EUR 500 0% 0% Niedrig 1
423 prozent rechner EUR 500000 0% 0% Niedrig 0 1,75 1,75
424 prozent rechner euro EUR 5000 0% 0% Niedrig 0
425 prozent rechner formel EUR 50 0% 0% Niedrig 1
426 prozent rechner gehalt EUR 50 0% -90% Niedrig 1
427 prozent rechner online EUR 50000 0% 0% Niedrig 0 1,75 1,75
428 prozent rechnung formeln EUR 50000 0% 0% Niedrig 2 1,75 1,75
429 prozent rechnung online EUR 50000 0% 0% Niedrig 0 1,75 1,75
430 prozent taschenrechner EUR 500 0% 0% Mittel 61 0,07 0,69
431 prozent taschenrechner online EUR 50 0% 0% Niedrig 0
432 prozent textaufgaben EUR 50 0% 0% Niedrig 1
433 prozent und zinsrechnung EUR 500 0% 0% Niedrig 1
434 prozent und zinsrechnung formeln EUR 50 0% 0% Niedrig 0
435 prozent und zinsrechnung lösungen EUR 50 0% -100% Unbekannt
436 prozent und zinsrechnung rechner EUR 50 -100% -100% Unbekannt
437 prozent von etwas berechnen EUR 500 0% 0% Niedrig 0
438 prozent von etwas berechnen formel EUR 50 0% 0% Niedrig 0
439 prozent von formel EUR 50 0% 0% Niedrig 4
440 prozent übungsaufgaben EUR 50 0% 0% Niedrig 0
441 prozentaufgabe EUR 50 0% 0% Niedrig 0
442 prozentaufgaben lösen EUR 50 -100% -100% Unbekannt
443 prozentaufgaben mit lösungen EUR 50 0% 0% Niedrig 5
444 prozentaufgaben online EUR 50 0% 0% Niedrig 0
445 prozentaufgaben rechnen EUR 50 -100% -100% Unbekannt
446 prozentaufgaben textaufgaben mit lösungen EUR 50 -100% 0% Unbekannt
447 prozente berechnen online EUR 50 0% 0% Niedrig 0
448 prozente berechnen taschenrechner EUR 50 0% 0% Niedrig 0
449 prozente im alltag EUR 500 0% 0% Niedrig 0
450 prozente im kopf rechnen EUR 50 0% 0% Niedrig 1
451 prozente mathe EUR 500 0% 0% Niedrig 0
452 prozentfaktor EUR 50 0% 0% Niedrig 0
453 prozentformeln EUR 500 0% 0% Niedrig 1
454 prozentrechnen am taschenrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
455 prozentrechnen arbeitsblätter EUR 500 0% 0% Niedrig 6
456 prozentrechnen arbeitsblätter zum ausdrucken EUR 50 0% 0% Niedrig 0
457 prozentrechnen auf dem taschenrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
458 prozentrechnen auf taschenrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
459 prozentrechnen beispielaufgaben EUR 50 0% 0% Niedrig 3
460 prozentrechnen dreisatz formel EUR 500 0% 0% Niedrig 2 0,43 1,75
461 prozentrechnen einfach EUR 500 0% 0% Niedrig 1
462 prozentrechnen einfach im kopf EUR 50 -100% -100% Unbekannt
463 prozentrechnen formel einfach EUR 500 0% 0% Niedrig 0
464 prozentrechnen für anfänger EUR 50 0% 0% Niedrig 13
465 prozentrechnen für dumme EUR 5000 0% 0% Niedrig 1 0,77 1,75
466 prozentrechnen für dummies EUR 50 0% 0% Niedrig 23 0,03 1,75
467 prozentrechnen grundlagen EUR 50 0% 0% Niedrig 8
468 prozentrechnen grundschule EUR 50 0% 0% Niedrig 21
469 prozentrechnen grundwert EUR 50 0% 0% Niedrig 3
470 prozentrechnen im kopf EUR 500 0% 0% Niedrig 0
471 prozentrechnen mit brüchen EUR 50 0% 0% Niedrig 3
472 prozentrechnen mit dem taschenrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
473 prozentrechnen mit dreisatz EUR 5000 0% 0% Niedrig 2 0,33 1,75
474 prozentrechnen mit formel EUR 50000 0% 0% Niedrig 2 1,75 1,75
475 prozentrechnen mit lösungen EUR 50 0% 0% Niedrig 1
476 prozentrechnen mit lösungsweg EUR 50 -100% 0% Unbekannt
477 prozentrechnen mit taschenrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
478 prozentrechnen mit taschenrechner prozenttaste EUR 50 0% 0% Niedrig 0
479 prozentrechnen rechner EUR 5000 -90% -90% Niedrig 0
480 prozentrechnen taschenrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
481 prozentrechnen taschenrechner ohne prozenttaste EUR 50 0% 0% Niedrig 5
482 prozentrechnen taschenrechner prozenttaste EUR 50 0% 0% Unbekannt
483 prozentrechnen textaufgaben EUR 500 0% 0% Niedrig 1
484 prozentrechnen textaufgaben mit lösungen EUR 50 0% 0% Niedrig 0
485 prozentrechnen wie EUR 5000 -90% -90% Niedrig 1 0,59 1,75
486 prozentrechnen übungsaufgaben EUR 500 0% 0% Niedrig 5
487 prozentrechnen übungsaufgaben mit lösungen EUR 50 0% 0% Niedrig 5
488 prozentrechner aktien EUR 50 0% 0% Niedrig 8
489 prozentrechner brutto netto EUR 50 0% 0% Niedrig 12 0,02 0,11
490 prozentrechner download EUR 50 -100% -100% Unbekannt
491 prozentrechner download kostenlos EUR 50 0% -100% Unbekannt
492 prozentrechner dreisatz EUR 5000 0% 0% Niedrig 2 0,33 1,75
493 prozentrechner euro EUR 5000 0% 0% Niedrig 0
494 prozentrechner formel EUR 50000 0% 0% Niedrig 2 1,75 1,75
495 prozentrechner für dummies EUR 50 0% 0% Niedrig 0
496 prozentrechner geld EUR 500 0% -90% Niedrig 0
497 prozentrechner grundwert EUR 50 0% 0% Niedrig 3
498 prozentrechner kostenlos EUR 50 0% 0% Niedrig 0
499 prozentrechner mit formel EUR 50000 0% 0% Niedrig 2 1,75 1,75
500 prozentrechner mit rechenweg EUR 500 0% 0% Niedrig 0
501 prozentrechner netto brutto EUR 50 0% 0% Niedrig 9
502 prozentrechner preiserhöhung EUR 50 0% 0% Niedrig 0
503 prozentrechner rechner EUR 5000 -90% -90% Niedrig 0
504 prozentrechner taschenrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
505 prozentrechner € EUR 5000 0% 0% Niedrig 0
506 prozentrechnung 1 EUR 50 0% 0% Niedrig 0
507 prozentrechnung aktien EUR 50 0% 0% Niedrig 8
508 prozentrechnung alle formeln EUR 50 0% 0% Niedrig 0
509 prozentrechnung anwendungsaufgaben EUR 50 0% 0% Niedrig 0
510 prozentrechnung auf 100 formel EUR 50 Niedrig 0
511 prozentrechnung auf hundert EUR 50 0% 0% Niedrig 0
512 prozentrechnung beispielaufgaben mit lösung EUR 50 0% 0% Niedrig 0
513 prozentrechnung berechnen EUR 50 0% 0% Niedrig 0
514 prozentrechnung brutto netto EUR 50 0% 0% Niedrig 11
515 prozentrechnung brutto netto gehalt EUR 50 0% Niedrig 5
516 prozentrechnung daniel jung EUR 50 -100% -100% Unbekannt
517 prozentrechnung dreisatz grundwert EUR 50 0% 0% Niedrig 0
518 prozentrechnung einfach EUR 500 0% 0% Niedrig 1
519 prozentrechnung einzelhandel EUR 50 0% 0% Niedrig 0
520 prozentrechnung formel dreisatz EUR 500 0% 0% Niedrig 2 0,43 1,75
521 prozentrechnung formel grundwert EUR 50 0% -90% Niedrig 2
522 prozentrechnung formel online EUR 50 -100% -100% Unbekannt
523 prozentrechnung formel prozentsatz EUR 50 0% -90% Niedrig 0
524 prozentrechnung formel prozentwert EUR 50 0% 0% Niedrig 1
525 prozentrechnung formel realschule EUR 50 0% 0% Niedrig 0
526 prozentrechnung formelsammlung EUR 50 0% 0% Niedrig 6
527 prozentrechnung für dummies EUR 50 0% 0% Niedrig 23 0,03 1,75
528 prozentrechnung ganz einfach EUR 50 0% 0% Niedrig 5
529 prozentrechnung gehalt EUR 50 0% 0% Niedrig 0
530 prozentrechnung grundformel EUR 500 0% 0% Niedrig 1
531 prozentrechnung grundwert EUR 500 0% 0% Niedrig 1
532 prozentrechnung grundwert berechnen EUR 500 0% 0% Niedrig 1
533 prozentrechnung grundwert formel EUR 50 0% -90% Niedrig 2
534 prozentrechnung handlungsorientiert EUR 50 0% 0% Mittel 50
535 prozentrechnung hauptschule EUR 50 0% 0% Niedrig 0
536 prozentrechnung im alltag EUR 50 0% 0% Niedrig 0
537 prozentrechnung im dreisatz EUR 5000 0% 0% Niedrig 2 0,33 1,75
538 prozentrechnung in hundert EUR 50 0% 0% Niedrig 3
539 prozentrechnung kaufmännisch EUR 50 0% 0% Niedrig 0
540 prozentrechnung kopfrechnen EUR 50 0% 0% Niedrig 2
541 prozentrechnung lernzettel EUR 50 -90% 0% Niedrig 2
542 prozentrechnung lösung EUR 50 0% 0% Niedrig 1
543 prozentrechnung lösungen EUR 50 0% 0% Niedrig 1
544 prozentrechnung mathe EUR 500 0% 0% Niedrig 1 1,75 1,75
545 prozentrechnung mit dem dreisatz EUR 50 0% 0% Niedrig 0
546 prozentrechnung mit kommazahlen EUR 50 0% 0% Niedrig 4
547 prozentrechnung mit rechenweg EUR 500 -90% -90% Niedrig 2
548 prozentrechnung online rechner EUR 500 0% 900% Niedrig 0
549 prozentrechnung preiserhöhung EUR 50 0% 0% Niedrig 0
550 prozentrechnung prozentsatz EUR 50 0% 0% Niedrig 0
551 prozentrechnung prozentsatz berechnen EUR 50 0% -90% Niedrig 1
552 prozentrechnung prozentwert EUR 50 0% 0% Niedrig 0
553 prozentrechnung prozentwert berechnen EUR 50 0% 0% Niedrig 0
554 prozentrechnung realschule EUR 50 0% 0% Niedrig 5
555 prozentrechnung rechenweg EUR 500 -90% -90% Niedrig 2
556 prozentrechnung sachaufgaben EUR 500 -90% -90% Niedrig 1
557 prozentrechnung sachaufgaben mit lösung EUR 50 0% 0% Niedrig 0
558 prozentrechnung schlussrechnung EUR 50 0% 0% Niedrig 0
559 prozentrechnung schnell EUR 50 0% 0% Niedrig 3
560 prozentrechnung schnell im kopf EUR 50 0% 0% Niedrig 0
561 prozentrechnung serlo EUR 50 0% 0% Niedrig 3
562 prozentrechnung simpleclub EUR 50 0% 0% Niedrig 0
563 prozentrechnung steuer EUR 50 0% 0% Niedrig 0
564 prozentrechnung tabelle EUR 500 0% 0% Niedrig 1
565 prozentrechnung taschenrechner mit prozenttaste EUR 50 0% 0% Niedrig 10
566 prozentrechnung taschenrechner online EUR 50 0% 0% Niedrig 0
567 prozentrechnung textaufgaben mit lösung EUR 50 0% 0% Niedrig 0
568 prozentrechnung um und auf EUR 50 0% 0% Niedrig 0
569 prozentrechnung umgekehrt EUR 50 0% 0% Niedrig 0
570 prozentrechnung und dreisatz übungsaufgaben EUR 50 0% 0% Niedrig 0
571 prozentrechnung und zinsrechnung EUR 50 0% 0% Niedrig 2
572 prozentrechnung unterrichtsentwurf EUR 50 0% 0% Niedrig 21
573 prozentrechnung verhältnisgleichung EUR 50 0% 0% Niedrig 0
574 prozentrechnung von 100 EUR 50 0% 0% Niedrig 0
575 prozentrechnung von hundert EUR 50 0% 0% Niedrig 0
576 prozentrechnung wie EUR 5000 -90% -90% Niedrig 1 0,59 1,75
577 prozentrechnung wieviel prozent sind x von y EUR 50 0% 0% Niedrig 0
578 prozentrechnung zinsrechnung EUR 50 0% 0% Niedrig 2
579 prozentrechnung übungskönig EUR 50 0% 0% Niedrig 0
580 prozentsatz berechnen dreisatz EUR 500 0% 0% Niedrig 1
581 prozentsatz berechnen mit dreisatz EUR 50 0% 0% Niedrig 0
582 prozentsatz berechnen taschenrechner EUR 50 0% 0% Niedrig 0
583 prozentsatz im kopf berechnen EUR 50 0% 0% Niedrig 1
584 prozentsatz mit dreisatz berechnen EUR 50 0% 0% Niedrig 0
585 prozentsatz rechner EUR 500 0% 0% Niedrig 0
586 prozentsatz und prozentwert EUR 500 0% 0% Niedrig 1
587 prozentschreibweise EUR 50 0% -90% Niedrig 0
588 prozenttaste am taschenrechner EUR 50 -100% 0% Unbekannt
589 prozenttaste auf taschenrechner EUR 50 -100% -100% Unbekannt
590 prozenttaste taschenrechner EUR 500 0% 0% Hoch 94 0,09 0,22
591 prozentwert EUR 5000 0% 0% Niedrig 0
592 prozentwert berechnen dreisatz EUR 500 -90% -90% Niedrig 1
593 prozentwert berechnen formel EUR 500 0% 0% Niedrig 2 0,31 1,75
594 prozentwert berechnen tabelle EUR 50 0% 0% Niedrig 0
595 prozentwert berechnen taschenrechner EUR 50 0% 0% Niedrig 0
596 prozentwert berechnen textaufgaben EUR 50 0% 0% Niedrig 0
597 prozentwert dreisatz EUR 50 0% -90% Niedrig 0
598 prozentwert formel EUR 5000 -90% -90% Niedrig 1 0,69 1,75
599 prozentwert grundwert prozentsatz EUR 5000 0% 0% Niedrig 1 0,79 1,75
600 prozentwert prozentsatz grundwert EUR 5000 0% 0% Niedrig 1 0,79 1,75
601 prozentwert prozentsatz grundwert formel EUR 500 0% 0% Niedrig 1 1,75 1,75
602 prozentwert rechner EUR 50 0% -90% Niedrig 1
603 prozentwert taschenrechner EUR 50 0% 0% Niedrig 0
604 prozentwert textaufgaben EUR 50 0% 0% Niedrig 0
605 prozentwerte berechnen EUR 5000 0% 0% Niedrig 1 1,75 1,75
606 prozentzeichen taschenrechner EUR 50 0% 0% Niedrig 22
607 rammstein schriftart generator EUR 50 0% Niedrig 0
608 random wichtel generator EUR 50 -100% 0% Unbekannt
609 rechenaufgaben prozentrechnung EUR 50 0% 0% Niedrig 0
610 rechenbeispiele prozentrechnung EUR 50 0% Niedrig 0
611 rechenformel prozentrechnung EUR 50 0% 0% Niedrig 0
612 rechenweg prozentrechnung EUR 500 -90% -90% Niedrig 2
613 rechnen mit prozent EUR 50000 0% -90% Niedrig 0 1,75 1,75
614 rechnen prozent EUR 50000 0% -90% Niedrig 0 1,75 1,75
615 rechnen prozent taschenrechner EUR 50 0% 0% Niedrig 0
616 rechner für prozentrechnung EUR 5000 -90% -90% Niedrig 0
617 rechner in prozent EUR 50 0% 0% Niedrig 0
618 rechner mit prozent EUR 5000 0% 0% Niedrig 0
619 rechner prozent EUR 5000 0% 0% Niedrig 0
620 rechner prozentrechner EUR 5000 -90% -90% Niedrig 0
621 rechnung prozent EUR 50000 0% -90% Niedrig 0 1,75 1,75
622 rechnung prozentsatz EUR 50 0% 0% Niedrig 2
623 richtig prozentrechnen EUR 50 0% 0% Niedrig 0
624 rp namen generator EUR 500 0% 0% Niedrig 0
625 russische namen generator EUR 50 0% 0% Niedrig 0
626 russische schrift tattoo generator EUR 50 0% 0% Niedrig 1
627 russische schriftarten generator EUR 50 0% 0% Niedrig 0
628 sachaufgaben prozentrechnung EUR 500 -90% -90% Niedrig 1
629 sachaufgaben zur prozentrechnung EUR 500 -90% -90% Niedrig 1
630 schnell prozentrechnen EUR 50 0% 0% Niedrig 1
631 schnörkel schrift generator EUR 500 0% 0% Niedrig 0
632 schnörkelschrift generator EUR 500 0% 0% Niedrig 0
633 schreibmaschine schriftart generator EUR 50 0% 0% Niedrig 0
634 schreibschrift font generator EUR 50 0% 0% Niedrig 0
635 schreibschrift generator instagram EUR 50 0% 0% Niedrig 0
636 schreibschrift generator zum kopieren EUR 50 0% 0% Niedrig 0
637 schreibschrift schriftgenerator EUR 50 0% 0% Niedrig 0
638 schrift art generator EUR 50 0% 0% Niedrig 2
639 schrift arten zum kopieren EUR 50 0% 0% Niedrig 0
640 schrift design generator EUR 50 0% 0% Niedrig 3
641 schrift design zum kopieren EUR 50 0% 0% Niedrig 0
642 schrift editor kostenlos EUR 50 0% 0% Niedrig 0
643 schrift generator arial EUR 50 0% -100% Unbekannt
644 schrift generator copy paste EUR 50 -100% -100% Unbekannt
645 schrift generator druckschrift EUR 50 0% 0% Unbekannt
646 schrift generator free EUR 50 0% 0% Niedrig 0
647 schrift generator kopieren EUR 50 0% 0% Niedrig 0
648 schrift generator kostenlos EUR 500 0% 0% Niedrig 1
649 schrift generator kursiv EUR 50 0% 0% Niedrig 0
650 schrift generator online EUR 50 0% 0% Niedrig 1
651 schrift generator online kostenlos EUR 50 0% 0% Niedrig 0
652 schrift generator schreibschrift EUR 50 0% 0% Niedrig 0
653 schrift generator zahlen EUR 50 0% 0% Niedrig 0
654 schrift generator zum kopieren EUR 500 0% 0% Niedrig 0
655 schrift generieren EUR 500 0% 0% Niedrig 5 0,39 1,86
656 schrift instagram generator EUR 50 -100% -100% Unbekannt
657 schrift konfigurator EUR 50 0% -90% Mittel 37 0,36 0,96
658 schrift konverter EUR 500 0% 0% Niedrig 0
659 schrift kopieren für instagram EUR 50 Niedrig 0
660 schrift mit schatten generator EUR 50 0% 0% Niedrig 2
661 schrift online generator EUR 50 0% 0% Niedrig 1
662 schrift simulator EUR 50 0% -90% Niedrig 0
663 schrift style generator EUR 50 -100% -100% Unbekannt
664 schrift tattoo generator EUR 5000 0% 0% Niedrig 3 0,52 1,33
665 schrift umwandler EUR 500 0% 0% Niedrig 1
666 schrift verkleinern generator EUR 50 0% 0% Niedrig 0
667 schrift zahlen generator EUR 50 0% 0% Niedrig 0
668 schrift zum kopieren EUR 5000 0% 0% Niedrig 0 0,45 1,01
669 schrift zum kopieren und einfügen EUR 50 0% 0% Niedrig 0
670 schrift ändern generator EUR 50 0% 0% Niedrig 0
671 schriftart altdeutsch generator EUR 50 0% 0% Niedrig 0
672 schriftart generator instagram EUR 500 0% -90% Niedrig 0
673 schriftart kopieren EUR 5000 0% 0% Niedrig 0
674 schriftart kopieren einfügen EUR 50 0% 0% Niedrig 0
675 schriftart maker EUR 500 -90% 0% Niedrig 3
676 schriftart online generieren EUR 50 -100% -100% Unbekannt
677 schriftart zum kopieren instagram EUR 50 0% 0% Niedrig 0
678 schriftarten altdeutsch generator EUR 50 0% 0% Niedrig 3
679 schriftarten für instagram kopieren EUR 50 0% Niedrig 0
680 schriftarten geheimschrift generator EUR 50 0% -100% Unbekannt
681 schriftarten generator altdeutsch EUR 50 0% 0% Niedrig 0
682 schriftarten generator disney EUR 50 0% 0% Niedrig 0
683 schriftarten generator facebook EUR 50 0% 0% Niedrig 0
684 schriftarten generator geheimschrift EUR 50 -100% -100% Unbekannt
685 schriftarten generator graffiti EUR 50 0% 0% Niedrig 0
686 schriftarten generator handlettering EUR 50 0% 900% Niedrig 1
687 schriftarten generator handschrift EUR 50 0% 0% Niedrig 0
688 schriftarten generator instagram EUR 50 0% -90% Niedrig 0
689 schriftarten generator kopieren EUR 50 0% 0% Niedrig 0
690 schriftarten generator kostenlos EUR 500 0% 0% Niedrig 0
691 schriftarten generator online EUR 50 0% 0% Niedrig 1
692 schriftarten generator schreibschrift EUR 50 0% 0% Niedrig 0
693 schriftarten generator whatsapp EUR 50 0% 0% Niedrig 0
694 schriftarten generator zahlen EUR 50 0% 0% Niedrig 0
695 schriftarten generator zum kopieren EUR 50 0% 0% Niedrig 0
696 schriftarten generieren EUR 50 0% 0% Niedrig 2
697 schriftarten graffiti generator EUR 50 0% 0% Niedrig 24
698 schriftarten handschrift generator EUR 50 0% 0% Niedrig 2
699 schriftarten kalligraphie generator EUR 50 0% 0% Niedrig 3
700 schriftarten kopieren einfügen EUR 50 0% 0% Niedrig 0
701 schriftarten kopieren für instagram EUR 50 0% -100% Unbekannt
702 schriftarten kopieren kostenlos EUR 50 0% 0% Niedrig 0
703 schriftarten kopieren schreibschrift EUR 50 0% 0% Niedrig 0
704 schriftarten kopieren und einfügen EUR 50 0% 0% Niedrig 1
705 schriftarten messletters EUR 50 0% 0% Niedrig 0
706 schriftarten nickname EUR 50 0% 0% Niedrig 0
707 schriftarten online generieren EUR 50 0% 0% Niedrig 2
708 schriftarten schreibschrift generator EUR 50 0% 0% Niedrig 0
709 schriftarten schreibschrift kopieren EUR 50 0% 0% Niedrig 0
710 schriftarten schreibschrift zum kopieren EUR 500 0% 0% Niedrig 0
711 schriftarten zum kopieren für instagram EUR 50 0% 0% Niedrig 0
712 schriftarten zum kopieren instagram EUR 50 0% 0% Niedrig 0
713 schriftarten zum kopieren kostenlos EUR 500 0% 0% Niedrig 0
714 schriftarten zum kopieren schreibschrift EUR 50 0% 0% Niedrig 0
715 schriftarten zum kopieren und einfügen EUR 50 0% 0% Niedrig 1
716 schriftartgenerator EUR 500 0% 0% Niedrig 0
717 schriften generieren online EUR 50 -100% -100% Unbekannt
718 schriften kombinieren generator EUR 50 Niedrig 0
719 schriften kopieren instagram EUR 50 0% 0% Niedrig 0
720 schriftgenerator altdeutsch EUR 500 900% 0% Niedrig 0
721 schriftgenerator altdeutsche schrift EUR 50 0% 0% Niedrig 0
722 schriftgenerator disney instagram EUR 50 0% -100% Unbekannt
723 schriftgenerator disney kopieren EUR 50 0% -100% Unbekannt
724 schriftgenerator disney zum kopieren EUR 50 0% 0% Niedrig 0
725 schriftgenerator druckschrift EUR 50 0% 0% Niedrig 0
726 schriftgenerator durchgestrichen EUR 50 0% 0% Niedrig 0
727 schriftgenerator fonts EUR 50 0% 0% Niedrig 0
728 schriftgenerator für facebook EUR 50 0% 0% Niedrig 0
729 schriftgenerator für instagram EUR 50 0% 0% Niedrig 0
730 schriftgenerator für whatsapp EUR 50 0% 0% Niedrig 0
731 schriftgenerator für zahlen EUR 50 0% 0% Niedrig 0
732 schriftgenerator geschwungene schrift EUR 50 0% Niedrig 0
733 schriftgenerator handlettering EUR 50 0% 0% Niedrig 1
734 schriftgenerator instagram kopieren EUR 50 0% 0% Niedrig 0
735 schriftgenerator iphone EUR 50 0% 0% Niedrig 0
736 schriftgenerator kalligraphie EUR 50 0% 0% Niedrig 2
737 schriftgenerator klein EUR 50 0% 0% Niedrig 0
738 schriftgenerator kleine schrift EUR 50 0% 0% Niedrig 0
739 schriftgenerator kopieren EUR 50 0% 0% Niedrig 0
740 schriftgenerator kostenlos EUR 500 0% 0% Niedrig 0
741 schriftgenerator kursiv EUR 50 0% 0% Niedrig 0
742 schriftgenerator messletters EUR 50 0% 0% Niedrig 0
743 schriftgenerator mini schrift EUR 50 0% 0% Niedrig 0
744 schriftgenerator miniaturschrift EUR 50 0% 0% Niedrig 0
745 schriftgenerator online EUR 50 -90% 0% Niedrig 0
746 schriftgenerator schreibmaschine EUR 50 0% 0% Niedrig 0
747 schriftgenerator schreibschrift EUR 500 0% 0% Niedrig 0
748 schriftgenerator schreibschrift zum kopieren EUR 50 0% 0% Niedrig 0
749 schriftgenerator schönschrift EUR 50 0% 0% Niedrig 0
750 schriftgenerator zahlen EUR 50 0% 0% Niedrig 0
751 schön schrift generator EUR 500 0% 0% Niedrig 0
752 schön schrift kopieren EUR 5000 0% 900% Niedrig 0
753 schöne schreibschrift generator EUR 50 0% 0% Niedrig 0
754 schöne schriftarten generator EUR 50 0% 0% Niedrig 5
755 schöne schriftarten kopieren EUR 5000 0% 0% Niedrig 1
756 schöne schriftarten kostenlos generator EUR 50 -100% 0% Unbekannt
757 schöne schriftarten zum kopieren EUR 5000 0% 0% Niedrig 1
758 serlo prozentrechnung EUR 50 0% 0% Niedrig 3
759 simpleclub prozentrechnung EUR 50 -100% -100% Unbekannt
760 snapchat namen generator EUR 50 0% 0% Niedrig 0
761 sonderzeichen schrift generator EUR 50 0% 0% Niedrig 0
762 spezielle schrift zum kopieren EUR 50 0% 0% Niedrig 0
763 squad namen generator EUR 50 0% 0% Niedrig 0
764 stempel schrift generator EUR 50 0% 0% Niedrig 14
765 stencil schrift generator EUR 50 0% 0% Niedrig 1
766 studyflix prozentrechnung EUR 50 0% 0% Niedrig 1
767 stunden in prozent rechnen EUR 50 0% 0% Niedrig 0
768 stunden in prozent rechner EUR 50 0% 0% Niedrig 0
769 stunden prozent rechner EUR 50 0% 0% Niedrig 0
770 stundenlohn rechner für selbstständige EUR 50 0% Mittel 57
771 stundenlohnrechner selbstständige EUR 50 0% 0% Mittel 37 0,12 2,80
772 stundensatz berechnen arbeitgeber EUR 50 0% Niedrig 7
773 stundensatz berechnen online EUR 50 0% Niedrig 0
774 stundensatz lohn berechnen EUR 50 0% 0% Niedrig 14
775 stundensatz mitarbeiter berechnen EUR 50 0% 0% Niedrig 31
776 stundensatz monatslohn berechnen EUR 50 0% 0% Niedrig 0
777 stundensatz rechner für selbstständige EUR 50 0% -90% Niedrig 30 0,23 1,89
778 stundensatz rechner selbständige EUR 50 0% 0% Mittel 44 0,30 1,55
779 stundensatzkalkulation rechner EUR 50 0% 0% Niedrig 4
780 stundensatzrechner EUR 500 0% 0% Niedrig 19 0,23 0,75
781 stundenverrechnungssatz rechner EUR 50 0% 0% Niedrig 16 0,19 2,22
782 tafelwerk prozentrechnung EUR 50 0% 0% Niedrig 0
783 tag schrift generator EUR 50 0% 0% Niedrig 0
784 tagessatz rechner freelancer EUR 50 0% 0% Niedrig 21
785 tannenberg schriftart generator EUR 500 0% 900% Niedrig 0
786 taschenrechner für prozentrechnung EUR 50 0% 0% Mittel 51
787 taschenrechner mit prozent online EUR 50 0% 0% Niedrig 0
788 taschenrechner mit prozent rechnen EUR 500 0% 0% Niedrig 1
789 taschenrechner mit prozentfunktion EUR 50 0% 0% Hoch 100
790 taschenrechner mit prozenttaste EUR 500 0% 0% Hoch 94 0,09 0,22
791 taschenrechner prozent EUR 500 0% 0% Mittel 61 0,07 0,69
792 taschenrechner prozent online EUR 50 0% 0% Niedrig 0
793 taschenrechner prozent rechnen EUR 500 0% 0% Niedrig 1
794 taschenrechner prozentrechnen EUR 5000 0% 0% Niedrig 4 0,07 0,45
795 taschenrechner prozentrechner EUR 5000 0% 0% Niedrig 4 0,07 0,45
796 taschenrechner prozenttaste EUR 500 0% 0% Hoch 94 0,09 0,22
797 tavernen namen generator EUR 50 0% 0% Niedrig 0
798 tengwar schrift generator EUR 50 0% 0% Niedrig 0
799 texas instruments prozentrechnen EUR 50 -100% 0% Unbekannt
800 text generator schriftart EUR 50 0% 0% Niedrig 0
801 text schrift generator EUR 50 0% 0% Niedrig 2
802 text schriftarten zum kopieren EUR 50 0% 0% Niedrig 0
803 textaufgaben für prozentrechnung EUR 50 0% 0% Niedrig 29
804 textaufgaben mathe prozentrechnung EUR 50 0% Niedrig 0
805 textaufgaben mit prozent EUR 50 0% 0% Niedrig 1
806 textaufgaben mit prozentrechnung EUR 500 0% 0% Niedrig 1
807 textaufgaben prozent EUR 50 0% 0% Niedrig 1
808 textaufgaben prozentrechnung mit lösungen EUR 50 0% 0% Niedrig 0
809 textaufgaben prozentwert EUR 50 0% 0% Niedrig 0
810 textaufgaben prozentwert berechnen EUR 50 0% Niedrig 0
811 textaufgaben zur prozentrechnung EUR 500 0% 0% Niedrig 1
812 thug life schriftart generator EUR 50 -100% -100% Unbekannt
813 times new roman schriftgenerator EUR 50 -100% 0% Unbekannt
814 titelblatt erstellen online EUR 50 0% 0% Unbekannt
815 umgekehrte prozentrechnung EUR 50 0% 0% Niedrig 1
816 unisex namen generator EUR 50 0% -100% Unbekannt
817 unterrichtsentwurf prozentrechnung EUR 50 0% 0% Niedrig 12
818 vampir namen generator EUR 50 0% 0% Niedrig 0
819 verhältnisgleichung prozentrechnung EUR 50 0% 0% Niedrig 1
820 vermehrter grundwert EUR 500 0% 0% Niedrig 0
821 verschieden schriftarten zum kopieren EUR 500 0% 0% Niedrig 1
822 verschieden schriften zum kopieren EUR 50 0% 0% Niedrig 1
823 verschiedene schriftarten generator EUR 50 0% 0% Niedrig 0
824 verschiedene schriftarten kopieren EUR 500 0% 0% Niedrig 1
825 verschnörkelte schrift generator EUR 50 0% 0% Niedrig 0
826 viking schriftart generator EUR 50 0% 0% Niedrig 1
827 vintage schriftarten generator EUR 50 -100% -100% Unbekannt
828 wahrscheinlichkeitsrechner prozent EUR 50 0% 0% Niedrig 0
829 waltograph schrift generator EUR 50 0% 0% Niedrig 2
830 weiblicher namensgenerator EUR 50 0% 0% Niedrig 2
831 western namen generator EUR 50 0% 0% Niedrig 0
832 wichtel generator online EUR 50 -100% -100% Unbekannt
833 wichtel generator whatsapp EUR 50 0% 0% Niedrig 29
834 wichtel namen generator EUR 500 -100% -100% Unbekannt
835 wiederholung prozentrechnung EUR 50 0% 0% Niedrig 0
836 wieviel prozent von etwas EUR 50 0% 0% Niedrig 0
837 wikinger schriftart generator EUR 50 0% Niedrig 0
838 wilder westen namen generator EUR 50 0% 0% Niedrig 0
839 word schriftarten generator EUR 50 0% 0% Niedrig 0
840 wow namen generator EUR 500 0% 0% Niedrig 0
841 www messletters com schriftarten EUR 0 0% 0% Unbekannt
842 www prozentrechner EUR 50 0% 0% Niedrig 0
843 youtube name generator english EUR 50 0% 0% Unbekannt
844 youtube namen generator 2022 EUR 0 0% 0% Unbekannt
845 youtube namensgenerator EUR 50 0% 0% Niedrig 0
846 youtube namensgenerator 2022 EUR 50 0% 0% Unbekannt
847 yt namen generator EUR 500 0% 0% Niedrig 1
848 zahlen generator schrift EUR 50 0% 0% Niedrig 0
849 zahlen schrift generator EUR 50 0% 0% Niedrig 0
850 zahlen schriftarten generator EUR 50 0% 0% Niedrig 0
851 zahlengenerator schrift EUR 50 0% 0% Niedrig 0
852 zelda schriftart generator EUR 50 0% 0% Niedrig 0
853 zins prozent rechner EUR 50 0% 0% Mittel 33
854 zins prozentrechnung EUR 50 0% 0% Niedrig 13 0,53 1,61
855 zins und prozentrechnung EUR 50 0% 0% Niedrig 1
856 zinsrechnung grundwert EUR 50 0% Niedrig 5
857 zinsrechnung mit dreisatz EUR 500 0% 0% Niedrig 1
858 zinsrechnung prozentrechnung EUR 50 0% 0% Niedrig 2
859 zinsrechnung und prozentrechnung EUR 50 0% 0% Niedrig 2
860 zufalls namensgenerator EUR 50 0% 0% Niedrig 3
861 zufälliger name generator EUR 50 0% 0% Niedrig 0
862 zusammengesetzte prozentrechnung EUR 50 0% 0% Niedrig 0
863 ähnliche namen generator EUR 50 0% -100% Unbekannt
864 übungsaufgaben dreisatz prozentrechnung EUR 50 0% 0% Niedrig 0
865 übungsaufgaben für prozentrechnung EUR 50 0% Niedrig 5
866 übungsaufgaben mathe prozentrechnung EUR 50 0% 0% Niedrig 0
867 übungsaufgaben prozent EUR 50 0% 0% Niedrig 0
868 übungsaufgaben prozentwert EUR 50 -100% -100% Unbekannt
869 übungsaufgaben zu prozentrechnung EUR 50 -100% -100% Unbekannt
870 übungsaufgaben zur prozentrechnung EUR 500 0% 0% Niedrig 5
871 übungskönig mathe prozentrechnung EUR 50 0% 0% Niedrig 0
872 übungskönig prozentrechnung EUR 50 0% 0% Niedrig 0

Binary file not shown.
1 Keyword Stats 2026-07-22 at 23_15_06
2 1. Juli 2025 - 30. Juni 2026
3 Keyword Currency Avg. monthly searches Änderung über drei Monate Änderung im Jahresvergleich Competition Competition (indexed value) Top of page bid (low range) Top of page bid (high range) Ad impression share Organic impression share Organic average position In account? In plan? Searches: Jul 2025 Searches: Aug 2025 Searches: Sep 2025 Searches: Oct 2025 Searches: Nov 2025 Searches: Dec 2025 Searches: Jan 2026 Searches: Feb 2026 Searches: Mar 2026 Searches: Apr 2026 Searches: May 2026 Searches: Jun 2026
4 entscheidungsfinder EUR 500 0% 0% Niedrig 1
5 entscheidungsrad EUR 5000 0% 0% Niedrig 1 0,35 3,21
6 ja nein generator EUR 50000 0% 0% Niedrig 0 0,80 1,08
7 ja nein entscheider EUR 50 0% 0% Niedrig 0
8 glücksrad drehen EUR 5000 0% 0% Niedrig 7 0,36 1,64
9 münze werfen online EUR 5000 0% 0% Niedrig 0
10 wuerfel online EUR 50000 0% 0% Niedrig 1 0,39 1,37
11 zufallsgenerator namen EUR 50000 0% 0% Niedrig 0
12 glücksrad zum drehen EUR 500 0% 900% Hoch 99 0,26 2,30
13 10er würfel online EUR 50 0% 0% Niedrig 1
14 auslosung generator namen EUR 50 0% 0% Niedrig 0
15 d&d online würfel EUR 50 0% Niedrig 0
16 elektronischer würfel online EUR 50 0% 0% Niedrig 0
17 entscheidungs rad EUR 500 0% 0% Niedrig 1
18 generator ja nein EUR 50000 0% 0% Niedrig 0 0,80 1,08
19 ja oder nein generator EUR 50000 0% 0% Niedrig 0 0,80 1,08
20 kniffel würfel online EUR 500 0% 0% Niedrig 3 0,08 0,44
21 münze online werfen EUR 50 0% 900% Niedrig 0
22 namen zufallsgenerator EUR 50000 0% 0% Niedrig 0
23 namens zufallsgenerator EUR 50000 0% 0% Niedrig 0
24 online münze werfen EUR 500 0% 0% Niedrig 0
25 online virtueller würfel EUR 50 0% 0% Niedrig 0
26 online wuerfel EUR 50 0% 0% Niedrig 2
27 online zufallsgenerator namen EUR 50 0% 0% Niedrig 0
28 zufallsgenerator gewinnspiel namen EUR 50 0% 0% Niedrig 2
29 zufallsgenerator mit namen EUR 500 0% 0% Niedrig 0
30 zufallsgenerator namen auslosen EUR 500 0% 0% Niedrig 0
31 zufallsgenerator namen ziehen EUR 50 0% 0% Niedrig 0
32 gluecksrad online EUR 50000 0% 0% Niedrig 2 0,55 2,96
33 muenzwurf online EUR -- -- Unbekannt

384
LIVE-PLAN-entscheidomat.md Normal file
View File

@@ -0,0 +1,384 @@
# Entscheidomat Allgemeiner Go-Live-Plan
**Projekt:** Entscheidomat-Webseite
**Projektordner:** `C:\Users\timo\Documents\Elena\webseite`
**Zieldomain:** `https://entscheidomat.com`
**Ziel:** Die Website sicher, rechtlich sauber, technisch stabil und öffentlich nutzbar live nehmen.
---
## Go-Live-Status heute
### Bereits vorhanden
- [x] Next.js-Projekt funktioniert lokal
- [x] Produktions-Build läuft erfolgreich
- [x] Haupt-Tool-Oberfläche vorhanden
- [x] Münze, Glücksrad, Würfel, Zufallszahl, Magic 8-Ball und Listen-Tool vorhanden
- [x] responsive Grundstruktur vorhanden
- [x] deutsche Seitensprache vorhanden
- [x] grundlegende Title- und Description-Metadaten vorhanden
### Noch offen
- [ ] echte Domain verbinden und HTTPS prüfen
- [ ] Impressum
- [ ] Datenschutz
- [ ] Kontaktmöglichkeit
- [ ] Canonical URL
- [ ] Open-Graph- und Twitter-Metadaten
- [ ] Sitemap
- [ ] Robots-Datei
- [ ] Structured Data / JSON-LD
- [ ] SEO-Unterseiten für die wichtigsten Tools
- [ ] Lint-Fehler beheben
- [ ] mobile und Accessibility-QA
- [ ] Analytics und Consent-Lösung
- [ ] Search Console
- [ ] finale Produktionsprüfung
---
# Phase 1 Entscheidungen vor dem Launch
## 1. Domain und Marke festlegen
- [ ] Ist `entscheidomat.com` definitiv die Hauptdomain?
- [ ] Soll `www.entscheidomat.com` weiterleiten oder die Hauptdomain sein?
- [ ] finalen Markennamen und Logo festlegen
- [ ] Kontakt-E-Mail unter der Domain festlegen
- [ ] Betreiber/Verantwortlichen für Impressum und Datenschutz bestimmen
**Ergebnis:** Eine einzige kanonische Domainvariante.
## 2. Zielgruppe und Produktversprechen festlegen
Finale Kurzbeschreibung:
> Entscheidomat ist ein kostenloser Zufallsgenerator und eine digitale Entscheidungshilfe mit Münzwurf, Glücksrad, Würfel, Zufallszahl, Magic 8-Ball und Listen-Auswahl.
Diese Beschreibung muss konsistent erscheinen in:
- Homepage
- Meta Description
- Open Graph
- JSON-LD
- Social Profiles
- Impressum/Datenschutz, soweit relevant
---
# Phase 2 Rechtliche Grundlage
Diese Punkte müssen vor dem öffentlichen Launch erledigt werden:
- [ ] Impressum erstellen
- [ ] Datenschutzerklärung erstellen
- [ ] Kontaktmöglichkeit ergänzen
- [ ] Erklärung zu lokalen Speicherfunktionen ergänzen
- [ ] Erklärung zu Analytics ergänzen, falls Analytics eingesetzt wird
- [ ] Erklärung zu Werbung/AdSense ergänzen, sobald Werbung aktiv ist
- [ ] Cookie-/Consent-Management für nicht notwendige Cookies und personalisierte Werbung prüfen
- [ ] EEA-, UK- und Schweiz-Anforderungen für Google-Werbung prüfen
- [ ] Footer dauerhaft mit Impressum und Datenschutz verlinken
**Go-Live-Blocker:** Ohne Impressum und Datenschutz nicht öffentlich starten.
---
# Phase 3 Produkt- und Funktionstest
## 3.1 Alle Tools testen
Für jedes Tool prüfen:
- [ ] Tool öffnet ohne Fehler
- [ ] Startzustand ist verständlich
- [ ] Button funktioniert mehrfach hintereinander
- [ ] Ergebnis wird sichtbar aktualisiert
- [ ] Ergebnis ist für Screenreader angekündigt
- [ ] Reset/Neustart funktioniert
- [ ] keine falschen oder unmöglichen Ergebnisse
- [ ] keine Endlosschleife
- [ ] keine fehlerhaften Zufallsbereiche
- [ ] keine Daten gehen beim normalen Seitenwechsel unerwartet verloren
## 3.2 Spezifische Tests
### Münzwurf
- [ ] Kopf und Zahl erscheinen korrekt
- [ ] Animation endet immer
- [ ] Ergebnis bleibt nach Animation sichtbar
- [ ] Animation funktioniert auch bei Reduced Motion
### Glücksrad
- [ ] Standardoptionen funktionieren
- [ ] eigene Optionen können hinzugefügt werden
- [ ] leere Eingaben werden abgefangen
- [ ] lange Optionen werden sauber dargestellt
- [ ] Rad bleibt innerhalb des Bildschirms
### Würfel
- [ ] Würfelanimation endet zuverlässig
- [ ] Ergebnis stimmt mit der gewählten Würfelart überein
- [ ] mehrfaches Würfeln funktioniert
### Zufallszahl
- [ ] Minimum und Maximum werden validiert
- [ ] Minimum größer als Maximum wird abgefangen
- [ ] Dezimalwerte werden sauber behandelt
- [ ] sehr große Werte verursachen keinen Layoutfehler
### Magic 8-Ball
- [ ] Frage kann eingegeben werden
- [ ] leere Frage wird sinnvoll behandelt
- [ ] Antwort wird deutlich angezeigt
- [ ] mehrfaches Fragen funktioniert
### Listen-Auswahl
- [ ] leere Liste wird abgefangen
- [ ] mehrere Einträge werden korrekt getrennt
- [ ] Sonderzeichen funktionieren
- [ ] zufällige Auswahl wird angezeigt
---
# Phase 4 Mobile, Browser und Accessibility
## 4.1 Browsermatrix
- [ ] Chrome Desktop
- [ ] Chrome Android
- [ ] Safari iPhone
- [ ] Safari macOS
- [ ] Firefox Desktop
- [ ] Edge Desktop
## 4.2 Bildschirmgrößen
- [ ] 320 px Breite
- [ ] 375 px Breite
- [ ] 390 px Breite
- [ ] 768 px Breite
- [ ] 1024 px Breite
- [ ] 1440 px Breite
## 4.3 Accessibility
- [ ] alle interaktiven Elemente per Tab erreichbar
- [ ] sichtbarer Focus-Ring vorhanden
- [ ] Buttons haben verständliche Namen
- [ ] Tabs haben korrekte ARIA-Zuordnung
- [ ] Ergebnisse werden per `aria-live` angekündigt
- [ ] keine wichtigen Inhalte nur über Farbe verständlich
- [ ] ausreichender Farbkontrast
- [ ] Reduced Motion funktioniert
- [ ] Sound wird nicht ungefragt abgespielt
- [ ] Escape/Back-Verhalten bei Overlays prüfen
---
# Phase 5 Technischer Produktions-Check
## 5.1 Codequalität
- [ ] `npm run build` erfolgreich
- [ ] `npm run lint` erfolgreich
- [ ] keine TypeScript-Fehler
- [ ] keine Browser-Console-Errors
- [ ] keine Hydration-Warnings
- [ ] keine unnötigen Debug-Logs
- [ ] keine Testdaten oder Platzhalter im Produktionsbuild
- [ ] keine privaten API-Keys im Clientcode
Aktuell besteht der Build, aber der Lint-Lauf hat noch sechs Fehler in Preloader, Sound-Provider, Dice-Roller und zwei Hooks. Diese müssen vor dem finalen Go-Live behoben werden.
## 5.2 Performance
- [ ] Lighthouse Mobile prüfen
- [ ] Largest Contentful Paint prüfen
- [ ] Cumulative Layout Shift prüfen
- [ ] Interaction to Next Paint prüfen
- [ ] JavaScript-Bundle prüfen
- [ ] Animationen auf schwachen Geräten testen
- [ ] Fonts und externe Ressourcen prüfen
- [ ] keine unnötigen Audio-/Videoressourcen laden
Zielwerte als praktische Launch-Grenze:
- Performance mindestens 80 auf Mobile
- keine kritischen Accessibility-Fehler
- keine großen Layout Shifts
- sichtbarer Hauptinhalt ohne lange Wartezeit
---
# Phase 6 SEO-Basis vor dem Launch
- [ ] finale Startseiten-H1 festlegen
- [ ] Title auf Produktkategorie und Nutzen ausrichten
- [ ] Description finalisieren
- [ ] Canonical setzen
- [ ] Open Graph setzen
- [ ] Twitter Card setzen
- [ ] Social Preview Image erstellen
- [ ] `app/robots.ts` erstellen
- [ ] `app/sitemap.ts` erstellen
- [ ] JSON-LD für WebApplication/WebSite ergänzen
- [ ] Haupt-Tools intern verlinken
- [ ] keine Testseiten indexierbar machen
- [ ] URLs für Tool-Unterseiten festlegen
## Minimale empfehlenswerte Unterseiten
- `/ja-nein-generator`
- `/gluecksrad`
- `/wuerfel-online`
- `/namen-auslosen`
- `/muenze-werfen`
- `/zufallszahl-generator`
- `/magic-8-ball`
- `/listen-generator`
- `/impressum`
- `/datenschutz`
---
# Phase 7 Analytics und Monitoring
Vor dem Launch entscheiden:
- [ ] Google Analytics 4 oder datenschutzfreundliche Alternative
- [ ] Consent-Management
- [ ] Search Console
- [ ] Bing Webmaster Tools
- [ ] Fehler-Monitoring
Sinnvolle Events:
- [ ] `tool_open`
- [ ] `tool_result`
- [ ] `tool_share`
- [ ] `tool_customized`
- [ ] `faq_open`
Keine personenbezogenen Daten in Event-Parametern speichern.
Nach dem Launch beobachten:
- Indexierung
- Impressions
- Klicks
- CTR
- Tool-Nutzung
- Absprungrate
- Core Web Vitals
- Browserfehler
- mobile Fehler
---
# Phase 8 AdSense erst nach dem Basis-Launch
AdSense nicht als allerersten Schritt einbauen. Erst muss die Seite einen klaren eigenständigen Nutzerwert und ausreichenden öffentlichen Content haben.
Vor dem AdSense-Antrag:
- [ ] Impressum und Datenschutz live
- [ ] Consent Management eingerichtet
- [ ] mehrere hochwertige Tool-Seiten live
- [ ] keine leeren oder dünnen Unterseiten
- [ ] keine Platzhaltertexte
- [ ] Kontaktmöglichkeit vorhanden
- [ ] Search Console eingerichtet
- [ ] Anzeigen nicht in interaktive Buttons integrieren
- [ ] Anzeigen klar als Werbung erkennbar
- [ ] keine Klickanreize
- [ ] keine Werbung vor dem eigentlichen Tool, wenn dadurch die Nutzung gestört wird
Das Ziel ist nicht möglichst viele Anzeigen, sondern maximaler regelkonformer Umsatz bei guter UX.
---
# Phase 9 Go-Live-Ablauf
## Einen Tag vor dem Launch
- [ ] finalen Git-Commit/Backup erstellen
- [ ] Produktionsvariablen prüfen
- [ ] Domain-DNS prüfen
- [ ] HTTPS prüfen
- [ ] Build und Lint erneut ausführen
- [ ] mobile Smoke-Test durchführen
- [ ] rechtliche Seiten prüfen
- [ ] Social Preview testen
- [ ] robots.txt testen
- [ ] sitemap.xml testen
## Zum Launch
- [ ] Domain auf Produktionsversion zeigen lassen
- [ ] Startseite öffnen
- [ ] alle Haupt-URLs mit 200 testen
- [ ] HTTPS-Redirect testen
- [ ] Canonical im HTML prüfen
- [ ] robots.txt aufrufen
- [ ] sitemap.xml aufrufen
- [ ] Search Console bestätigen
- [ ] Sitemap einreichen
- [ ] erste URL-Inspektion starten
## Direkt nach dem Launch
- [ ] Server-/Browserfehler beobachten
- [ ] Search Console Coverage prüfen
- [ ] Mobile Usability prüfen
- [ ] Social Share testen
- [ ] mindestens ein Tool auf iPhone und Android testen
- [ ] Nutzerfeedback sammeln
- [ ] keine großen Content- oder URL-Änderungen ohne Redirect-Plan
---
# Go/No-Go-Checkliste
Die Website darf live gehen, wenn alle Punkte erfüllt sind:
- [ ] Domain und HTTPS funktionieren
- [ ] Impressum vorhanden
- [ ] Datenschutz vorhanden
- [ ] Kontaktmöglichkeit vorhanden
- [ ] Build erfolgreich
- [ ] Lint erfolgreich
- [ ] keine Console Errors
- [ ] alle Kern-Tools funktionieren
- [ ] mobile Smoke-Tests bestanden
- [ ] Canonical korrekt
- [ ] robots.txt vorhanden
- [ ] sitemap.xml vorhanden
- [ ] keine versehentliche Noindex-Sperre
- [ ] Analytics/Consent-Entscheidung umgesetzt
- [ ] Backup vorhanden
**Wenn ein rechtlicher Punkt, HTTPS, Build, Kernfunktion oder schwerer Mobile-Fehler offen ist: NO-GO.**
---
# Empfohlene Reihenfolge für die nächsten konkreten Arbeiten
1. Impressum, Datenschutz und Kontakt
2. Domain-/Canonical-Konfiguration
3. Lint-Fehler beheben
4. Mobile- und Accessibility-QA
5. Sitemap, Robots, Open Graph und JSON-LD
6. die fünf wichtigsten SEO-Unterseiten erstellen
7. Search Console und Monitoring einrichten
8. Website live nehmen
9. zwei bis vier Wochen Daten sammeln
10. erst danach AdSense beantragen und vorsichtig einbauen

254
README.md Normal file
View File

@@ -0,0 +1,254 @@
# Entscheidomat
Entscheidomat ist eine deutschsprachige Next.js-Web-App mit sieben kostenlosen
Zufalls-Tools: Ja/Nein-Generator, Münze, Glücksrad, Würfel,
Zufallszahl-Generator, Magic 8-Ball und Namen-Auslosung.
Die Anwendung wird als Docker-Container gebaut und hinter einem Reverse Proxy
wie Caddy betrieben. Diese Anleitung beschreibt den kompletten Weg vom
Produktionsserver bis zum sicheren Go-live von `entscheidomat.com`.
## Überblick
```text
Browser
-> Cloudflare DNS / CDN (optional)
-> Caddy (TLS, HTTPS, Redirects)
-> Docker-Container auf Port 3000
-> Next.js-App
```
Die App selbst enthält keine Datenbank. Eigene Einstellungen der Tools werden im
Browser der Nutzenden gespeichert. Umami Cloud wird für anonyme,
aggregierte Nutzungsstatistiken geladen.
## Voraussetzungen
Auf dem Produktionsserver werden benötigt:
- Linux-Server mit aktuellem Docker und Docker Compose Plugin
- eine Domain, z. B. `entscheidomat.com`
- Zugriff auf die DNS-Verwaltung, ggf. Cloudflare
- Caddy als Reverse Proxy (oder ein gleichwertiger Proxy)
- offene Ports `80` und `443` für Caddy
Prüfen:
```bash
docker --version
docker compose version
caddy version
```
## 1. Projekt auf den Server übertragen
Lege auf dem Server ein Verzeichnis für die Anwendung an und übertrage den
Projektordner dorthin. Bei einem Git-Repository ist ein Clone am einfachsten:
```bash
git clone <DEIN-REPOSITORY-URL> /opt/entscheidomat
cd /opt/entscheidomat
```
Ohne Git kannst du den Projektordner per `rsync` oder SFTP übertragen. Nicht
mit übertragen werden müssen `node_modules` und `.next`; Docker erzeugt beides
beim Build neu.
## 2. Produktionsumgebung konfigurieren
Erstelle im Projektordner eine `.env`-Datei:
```bash
APP_PORT=127.0.0.1:3000
```
Damit ist der App-Port nur lokal auf dem Server erreichbar. Caddy leitet
Anfragen intern an die App weiter; Port 3000 muss nicht öffentlich offen sein.
> Die `.env`-Datei gehört nicht in ein öffentliches Repository. Aktuell werden
> für die Anwendung keine weiteren Server-Geheimnisse benötigt.
## 3. App bauen und starten
```bash
cd /opt/entscheidomat
docker compose up -d --build
docker compose ps
docker compose logs -f app
```
Lokaler Gesundheitscheck auf dem Server:
```bash
curl -I http://127.0.0.1:3000/
```
Erwartet wird `HTTP/1.1 200 OK`.
## 4. Caddy konfigurieren
Lege eine Caddy-Konfiguration an oder ergänze die bestehende Caddyfile. Der
folgende Aufbau leitet `www` permanent auf die Hauptdomain und nutzt Caddy für
TLS und Reverse Proxying:
```caddyfile
www.entscheidomat.com {
redir https://entscheidomat.com{uri} permanent
}
entscheidomat.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
}
```
Nach dem Ändern die Konfiguration prüfen und neu laden:
```bash
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Der genaue Pfad kann je nach Caddy-Installation abweichen.
## 5. DNS und Cloudflare
Lege für die Domain folgende DNS-Einträge an:
| Typ | Name | Ziel |
| --- | --- | --- |
| A | `@` | Öffentliche IPv4-Adresse des Servers |
| CNAME | `www` | `entscheidomat.com` |
Wenn Cloudflare verwendet wird:
1. Stelle SSL/TLS auf **Full (strict)**.
2. Lass Caddy ein gültiges Zertifikat verwalten oder nutze ein korrekt
eingerichtetes Cloudflare Origin Certificate.
3. Aktiviere erst nach erfolgreichem Origin-Test den Proxy-Modus (orange Wolke).
4. Lege keine Cache-Regel an, die HTML-Seiten oder `robots.txt`/`sitemap.xml`
dauerhaft mit einem 404-Status cached.
## 6. Go-live-Prüfung
Nach DNS-Propagation alle folgenden Befehle ausführen:
```bash
curl -I https://entscheidomat.com/
curl -I https://entscheidomat.com/ja-nein-generator
curl -I https://entscheidomat.com/robots.txt
curl -I https://entscheidomat.com/sitemap.xml
curl -I http://entscheidomat.com/
curl -I https://www.entscheidomat.com/
```
Erwartungen:
- Startseite, Toolseiten, `robots.txt` und `sitemap.xml`: `200`
- HTTP und `www`: ein permanenter Redirect auf `https://entscheidomat.com`
- keine Cloudflare- oder Proxy-404-Seiten
Danach im Browser prüfen:
- Canonical URLs
- Titel und Meta-Beschreibungen
- Open-Graph-Vorschau
- alle sieben Toolseiten
- Impressum und Datenschutzerklärung
## 7. SEO nach dem Launch
1. Property in Google Search Console und Bing Webmaster Tools bestätigen.
2. `https://entscheidomat.com/sitemap.xml` einreichen.
3. Nach einigen Tagen Coverage, Indexierung und Core Web Vitals prüfen.
4. Regelmäßig die Umami-Daten und Suchanfragen aus der Search Console auswerten.
Die App erzeugt statisch unter anderem:
- `/robots.txt`
- `/sitemap.xml`
- `/opengraph-image`
- `/twitter-image`
## Aktualisierungen deployen
Bei jeder Änderung:
```bash
cd /opt/entscheidomat
git pull
docker compose up -d --build
docker compose logs --tail=100 app
```
Ohne Git zuerst die neuen Dateien übertragen und anschließend nur die letzten
zwei Befehle ausführen.
Bei einem Fehler lässt sich der vorherige, funktionierende Stand am einfachsten
über den vorherigen Git-Commit und einen erneuten Build wiederherstellen.
## Betrieb und Diagnose
```bash
# Container-Status
docker compose ps
# Live-Logs
docker compose logs -f app
# Neustart ohne Neubau
docker compose restart app
# Lokalen Container-Check ausführen
docker compose exec app wget --spider -q http://localhost:3000/
```
Für Caddy:
```bash
sudo systemctl status caddy
sudo journalctl -u caddy -f
```
Richte zusätzlich einen externen Uptime-Monitor für mindestens diese URLs ein:
- `https://entscheidomat.com/`
- `https://entscheidomat.com/robots.txt`
- `https://entscheidomat.com/sitemap.xml`
## Datenschutz und rechtliche Launch-Checkliste
Vor der öffentlichen Veröffentlichung müssen die Angaben in Impressum und
Datenschutzerklärung auf die tatsächliche Konfiguration abgestimmt werden:
- vollständige ladungsfähige Geschäftsanschrift des Einzelunternehmens
- tatsächlicher Name des Hostinganbieters
- Serverstandort Texas, USA, sowie Speicherdauer der Server-Logfiles
- dokumentierte Grundlage für den Datenübertragungsmechanismus in die USA
- Umami Cloud mit gewählter Datenregion EU und die passende
Auftragsverarbeitungs-/Datenschutzdokumentation
- Umsatzsteuer-ID oder Wirtschafts-ID nur, falls tatsächlich vorhanden
Die vorhandenen Texte sind ein Pre-Launch-Stand und keine individuelle
Rechtsberatung. Vor dem Livegang sollte die finale Fassung rechtlich geprüft
werden.
## Lokale Entwicklung
```bash
npm ci
npm run dev
npm run lint
npm run build
```
Die lokale App ist anschließend unter `http://localhost:3000` erreichbar.
## Tech-Stack
- Next.js 16, React 19 und TypeScript
- Tailwind CSS v4
- Docker mit Multi-Stage-Build
- Caddy als Reverse Proxy
- Cloudflare optional für DNS/CDN
- Umami Cloud, Datenregion EU

View File

@@ -0,0 +1,730 @@
# Entscheidomat.com Masterplan für Livegang, SEO, AEO, GEO und AdSense
**Erstellt:** 23. Juli 2026
**Projekt:** `C:\Users\timo\Documents\Elena\webseite`
**Zieldomain:** `https://entscheidomat.com`
**Ziel:** Ein technisch sauberes, indexierbares, zitierfähiges und AdSense-taugliches Web-Tool-Portal auf Basis einer Exact-Match-Domain.
> Dieser Plan ist ein Umsetzungsplan, keine Garantie für Rankings, AI-Zitate, AdSense-Freischaltung oder einen bestimmten Umsatz. Die Domain, Suchvolumina und CPCs sind Chancen-Signale, keine Traffic- oder Erlösprognosen.
---
## 1. Executive Summary
### Aktueller Stand
Die bestehende Website ist ein visuell starkes, funktionierendes Next.js-Tool mit sechs interaktiven Zufalls-Tools:
- Münze
- Glücksrad
- Würfel
- Zufallszahl
- Magic 8-Ball
- Listen-Auswahl
Der Produktions-Build wurde erfolgreich ausgeführt:
```text
✓ Compiled successfully
✓ Finished TypeScript
✓ Generating static pages
○ / Static prerendered
```
Der Lint-Lauf ist dagegen noch nicht sauber:
```text
6 errors in:
- components/preloader.tsx
- components/providers/sound-provider.tsx
- components/tools/dice-roller.tsx
- hooks/use-local-storage.ts
- hooks/use-prefers-reduced-motion.ts
```
### Gesamturteil
| Bereich | Aktueller Stand | Live-Urteil |
|---|---:|---|
| Design und Produktidee | stark | bereit nach QA |
| Interaktive Funktionalität | gut | bereit nach QA |
| Produktions-Build | bestanden | gut |
| SEO-Technik | lückenhaft | noch nicht bereit |
| AEO/GEO/AI-Zitierfähigkeit | schwach | noch nicht bereit |
| AdSense-Voraussetzungen | unvollständig | noch nicht beantragen |
| Rechtliches | nicht erkennbar | kritischer Blocker |
**Empfehlung:** Erst P0/P1 abschließen, dann live stellen und indexieren. Nicht direkt mit einer reinen Tool-Oberfläche in AdSense gehen.
---
## 2. Quellen und Audit-Methode
Verwendet wurden:
1. aktueller Code im Projektordner
2. echter `npm run build`-Lauf
3. echter `npm run lint`-Lauf
4. gerenderte lokale Seite im Browser
5. Keyword-Planner-Export `Keyword Stats 2026-07-22 at 23_15_06.csv`
6. großer Keyword-Planner-Export `Keyword Stats 2026-07-22 at 23_08_06.csv`
7. SEO-Audit- und AI-SEO-Skills
8. öffentlich gefundene Corey-Haines-Referenzen zu Programmatic SEO: Template-Design, Datenqualität, Quality Controls und technischem SEO
9. Google Search Central und Google AdSense-Hilfedokumentation
Eine lokale Skill-Datei mit dem Namen „Corey Haynes“ wurde nicht gefunden. Deshalb wird hier kein nicht vorhandener Skill behauptet. Der Plan übernimmt den erkennbaren Corey-Haines-Ansatz: **erst Suchintention und Template-Architektur, dann skalieren; jede programmatische Seite braucht einzigartige Substanz, Qualitätskontrollen und einen echten Nutzerwert.**
---
## 3. Keyword-Planner-Befund
### 3.1 Entscheidomat-spezifischer Export
Datei:
`Keyword Stats 2026-07-22 at 23_15_06.csv`
Auswertung:
- 30 Keywords
- zusammen ausgewiesenes Volumen: **419.050 monatliche Suchen**
- 28 Keywords mit Wettbewerb „Niedrig“
- die Summe darf **nicht** als erwartbarer Traffic addiert werden, weil Varianten und Suchintentionen sich überschneiden
Wichtige Gruppen:
| Cluster | Beispielkeywords | Planner-Volumen | Wettbewerb / CPC-Signal |
|---|---|---:|---|
| Ja/Nein | `ja nein generator`, `generator ja nein`, `ja oder nein generator` | je 50.000 | niedrig, Top-of-page ca. 0,801,08 € |
| Würfel | `wuerfel online` | 50.000 | niedrig, ca. 0,391,37 € |
| Namen | `zufallsgenerator namen`, `namen zufallsgenerator`, `namens zufallsgenerator` | je 50.000 | niedrig, CPC nicht durchgehend vorhanden |
| Glücksrad | `gluecksrad online` | 50.000 | niedrig, ca. 0,552,96 € |
| Entscheidungsrad | `entscheidungsrad` | 5.000 | niedrig, ca. 0,353,21 € |
| Glücksrad drehen | `glücksrad drehen` | 5.000 | niedrig, ca. 0,361,64 € |
| Münze | `münze werfen online` | 5.000 | niedrig, kein CPC-Wert im Export |
| Entscheidung | `entscheidungsfinder` | 500 | niedrig |
### 3.2 Großer gemischter Export
Datei:
`Keyword Stats 2026-07-22 at 23_08_06.csv`
Auswertung:
- 869 Keywords
- zusammen ausgewiesenes Volumen: **2.903.900 monatliche Suchen**
- 773 Keywords mit Wettbewerb „Niedrig“
- enthält mehrere Projekte und Themen, insbesondere Prozentrechner und Schriftgeneratoren
Die größten Begriffe des großen Exports sind **nicht** Entscheidomat-Ziele:
- `prozentrechner`: 500.000
- `prozent rechner`: 500.000
- zahlreiche Prozentrechnungsvarianten: 50.000
- Schriftgenerator-Varianten: 50.000
**Konsequenz:** Die 2,9 Millionen dürfen nicht Entscheidomat zugerechnet werden. Prozentomat und Schriftgenerator gehören in separate Projekte bzw. Domains. Entscheidomat erhält ausschließlich die Zufalls-, Entscheidungs-, Würfel-, Rad-, Namen- und Münz-Cluster.
### 3.3 Keyword-Strategie für die Exact-Match-Domain
`entscheidomat.com` ist ein gutes Brand-/Category-Signal, aber eine Exact-Match-Domain ist **kein Ranking-Shortcut**. Sie ersetzt nicht:
- hilfreiche Inhalte
- gute Seitenarchitektur
- interne Links
- externe Autorität
- technische Indexierbarkeit
- E-E-A-T
Die Domain sollte als merkbare Marke für ein **Entscheidungs- und Zufallstool-Portal** positioniert werden.
---
## 4. Empfohlene Seitenarchitektur
### Startseite `/`
**Primäre Funktion:** Marke plus Überblick über das Tool-Portfolio.
Empfohlener Title:
> Entscheidomat Kostenloser Zufallsgenerator und Entscheidungshilfe
Empfohlener H1:
> Kostenloser Zufallsgenerator für schnelle Entscheidungen
Markenclaim als Unterzeile:
> Lass den Zufall entscheiden.
Die Startseite soll nicht alle Keywords unnatürlich wiederholen. Sie erklärt das Produkt, verlinkt auf die Tool-Seiten und beantwortet die wichtigste Definition direkt.
### Priorität-1-Seiten
| URL | Primärintention | Primärkeywords |
|---|---|---|
| `/ja-nein-generator` | direkte Ja-/Nein-Entscheidung | `ja nein generator`, `ja oder nein generator`, `generator ja nein` |
| `/gluecksrad` | Rad drehen / Entscheidungsrad | `gluecksrad online`, `glücksrad drehen`, `entscheidungsrad` |
| `/wuerfel-online` | virtueller Würfel | `wuerfel online`, `online virtueller würfel` |
| `/namen-auslosen` | Namen zufällig ziehen | `zufallsgenerator namen`, `namen zufallsgenerator`, `namen auslosen` |
| `/muenze-werfen` | Kopf oder Zahl | `münze werfen online`, `online münze werfen`, `münze online werfen` |
### Priorität-2-Seiten
| URL | Zweck |
|---|---|
| `/zufallszahl-generator` | Zufallszahlen mit frei wählbarem Bereich |
| `/magic-8-ball` | Fragen und zufällige Antworten |
| `/listen-generator` | Liste mischen, Einträge ziehen, Gruppen bilden |
| `/entscheidungsrad` | Alias-/Intentseite, nur wenn eigenständig nützlich |
| `/zufallsgenerator` | zentrale Kategorie-Seite, falls die Suchintention separat belegt wird |
**Wichtig:** Keine dünnen Doorway-Seiten erstellen. Jede Seite braucht ein eigenes Tool, eine eigene Erklärung, eigene Beispiele, eigene FAQs und eigene interne Linkbeziehungen.
---
## 5. P0 Kritische Live-Blocker
### P0.1 Rechtliche Seiten
Vor dem öffentlichen Launch ergänzen:
- `/impressum`
- `/datenschutz`
- optional `/nutzungsbedingungen`
- Kontaktmöglichkeit
Im Footer müssen diese Seiten dauerhaft verlinkt sein. Das ist insbesondere für eine deutsche Website und für AdSense wichtig.
### P0.2 Domain- und URL-Konfiguration
Nach Festlegung der echten Domain:
- HTTPS erzwingen
- eine kanonische Variante festlegen: `https://entscheidomat.com`
- `www` entweder sauber weiterleiten oder als kanonische Variante festlegen
- HTTP auf HTTPS weiterleiten
- lokale/Preview-Domain niemals als Canonical ausliefern
- `metadataBase` auf `https://entscheidomat.com` setzen
### P0.3 Canonical und Social Metadata
In `app/layout.tsx` ergänzen:
- `metadataBase`
- self-referencing canonical
- Open Graph title/description/url/image
- Twitter card
- locale `de_DE`
- Site name
- passende Social-Share-Grafik 1200×630
### P0.4 Robots und Sitemap
Anlegen:
- `app/robots.ts`
- `app/sitemap.ts`
Die Sitemap darf nur 200er, kanonische und indexierbare URLs enthalten. Keine Test-, Query- oder internen URLs.
Robots darf Suchcrawler nicht blockieren. Wenn AI-Zitate gewünscht sind, keine Sperren gegen GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, Google-Extended oder Bingbot einbauen, sofern keine bewusste andere Geschäftsentscheidung getroffen wird.
### P0.5 Lint-Fehler beseitigen
Vor Launch beheben:
- `preloader.tsx`: Reduced-Motion-Initialisierung ohne problematisches synchrones State-Setzen im Effect
- `sound-provider.tsx`: Local-Storage-Hydration sauber behandeln
- `dice-roller.tsx`: Transformationszustand aus einem renderbaren State ableiten, nicht `ref.current` direkt rendern
- `use-local-storage.ts`: Hydration-Strategie prüfen
- `use-prefers-reduced-motion.ts`: Media-Query-Initialisierung sauber lösen
Akzeptanzkriterium:
```text
npm run build -> exit 0
npm run lint -> exit 0
```
### P0.6 Browser- und Accessibility-QA
Prüfen:
- Chrome Desktop
- Chrome Android / iPhone-Breite
- Tastaturbedienung
- Screenreader-Landmarks
- sichtbare Focus-Zustände
- `role=tab` mit passendem `tabpanel`
- `aria-controls` und `id` für Tabs
- keine Inhalte, die nur nach Animation sichtbar werden
- reduzierte Bewegung
- Sound standardmäßig nicht ungefragt abspielen
---
## 6. P1 Onpage-SEO und Informationsarchitektur
### 6.1 Direct-Answer-Block auf der Startseite
Direkt unter dem Hero, als sichtbarer HTML-Text:
> **Was ist Entscheidomat?**
> Entscheidomat ist ein kostenloser Zufallsgenerator und eine digitale Entscheidungshilfe. Du kannst online eine Münze werfen, ein Glücksrad drehen, würfeln, eine Zufallszahl erzeugen, den Magic 8-Ball fragen oder eine eigene Liste auslosen ohne Anmeldung und direkt im Browser.
Der Absatz ist für Menschen, klassische Snippets und AI-Extraktion geschrieben. Kein Keyword-Stuffing.
### 6.2 Tool-Abschnitte auf der Startseite
Unter dem interaktiven Bereich je ein kurzer, echter Informationsabschnitt:
- Was macht das Tool?
- Wann ist es hilfreich?
- Welche Optionen kann man anpassen?
- Link zur vertiefenden Tool-Seite
### 6.3 Inhalte auf jeder Tool-Seite
Jede Tool-Seite braucht mindestens:
1. H1 mit natürlicher Suchintention
2. 4060-Wörter-Direktantwort
3. das funktionierende Tool möglichst früh
4. kurze Bedienungsanleitung
5. konkrete Beispiele
6. Vorteile und Grenzen
7. verwandte Tools
8. 58 echte FAQs
9. Aktualisierungsdatum
10. interne Links zur Startseite und zu verwandten Seiten
### 6.4 Beispiel für `/ja-nein-generator`
H1:
> Ja-Nein-Generator schnelle Entscheidungen online treffen
Direktantwort:
> Der Ja-Nein-Generator beantwortet eine Frage zufällig mit Ja oder Nein. Formuliere deine Frage, starte den Generator und erhalte sofort eine zufällige Entscheidung kostenlos, ohne Anmeldung und direkt im Browser.
FAQs:
- Wie funktioniert ein Ja-Nein-Generator?
- Ist das Ergebnis wirklich zufällig?
- Kann ich den Generator mehrfach verwenden?
- Eignet sich der Generator für Gruppenentscheidungen?
- Ersetzt ein Zufallsergebnis eine wichtige persönliche Entscheidung?
### 6.5 Title-/H1-Mapping
Jede URL bekommt einen eigenen Title, eine eigene H1 und eine eigene Description. Keine Serienseiten mit nur ausgetauschtem Keyword.
---
## 7. P1 Structured Data
### Startseite
- `WebSite`
- `Organization` oder `SoftwareApplication`
- `WebApplication` mit Name, URL, Beschreibung, Browser-Anforderung und kostenloser Nutzung
### Tool-Seiten
- `WebApplication` oder `SoftwareApplication`
- `BreadcrumbList`
- `FAQPage` nur für Fragen, die sichtbar auf der Seite stehen
Schema muss exakt mit sichtbarem Inhalt übereinstimmen. Keine erfundenen Bewertungen, Preise, Nutzerzahlen oder Auszeichnungen.
Google weist ausdrücklich darauf hin, dass Structured Data keine Garantie für Rich Results oder AI-Zitate ist. Es verbessert aber die maschinenlesbare Interpretation und die allgemeine SEO-Grundlage.
---
## 8. P1 AEO und GEO für AI-Sichtbarkeit
### Grundprinzip
Nicht separate „AI-Texte“ produzieren. Ein klarer, hilfreicher Inhalt soll gleichzeitig:
- Menschen helfen
- Google ranken können
- von AI-Systemen extrahiert werden können
- als Quelle zitiert werden können
### Content-Muster
Für jede wichtige Seite:
- Definition am Anfang
- direkte Antwort auf die Hauptfrage
- kurze Absätze
- eindeutige H2/H3-Fragen
- nummerierte Bedienungsanleitung
- Vergleichstabelle, wenn sinnvoll
- FAQ
- sichtbarer Stand/Aktualisierungszeitpunkt
- klare Verweise auf verwandte Tools
### AI-Readiness-Checkliste
- [ ] Hauptdefinition in den ersten 100 Wörtern
- [ ] jeder zentrale Absatz ohne Kontext verständlich
- [ ] keine wichtigen Informationen nur in Canvas/SVG/JavaScript
- [ ] semantisches HTML
- [ ] echte H1/H2/H3-Hierarchie
- [ ] Tool-Name und Zweck klar genannt
- [ ] FAQs sichtbar und nicht nur im Schema versteckt
- [ ] robots erlaubt gewünschte AI-Suchcrawler
- [ ] `llms.txt` optional als Zusatz, nicht als Ersatz für SEO
- [ ] keine KI-generierten Massenvarianten ohne eigenen Nutzwert
### `/llms.txt`
Optional anlegen, nachdem die echten Seiten existieren. Inhalt:
- kurze Produktdefinition
- Zielgruppe
- Liste der Tools
- kanonische URLs
- Kontakt und rechtliche Seiten
- kurze Hinweise zu kostenloser Nutzung
`llms.txt` ist ein Zusatzsignal für einige AI-Ökosysteme, aber kein offizieller Google-Ranking-Hebel und kein Ersatz für Sitemap, HTML, interne Links oder E-E-A-T.
---
## 9. P1 Corey-Haines-inspirierte Programmatic-SEO-Regeln
Die geplanten Tool-Seiten dürfen über Templates erzeugt werden, aber nur mit Qualitätskontrollen.
### Jede Seite muss eigene Daten enthalten
- eigenes Keyword-Cluster
- eigene Suchintention
- eigenes Tool-Verhalten
- eigene Beispiele
- eigene FAQs
- eigener Direct-Answer-Text
- eigene interne Linkempfehlungen
- eigene Title/Description
### Nicht skalieren, bevor eine Musterseite gut ist
Reihenfolge:
1. `/ja-nein-generator` vollständig bauen
2. als SEO-/UX-/Schema-/AdSense-Musterseite prüfen
3. `/gluecksrad` als zweite Musterseite bauen
4. erst danach Datenmodell und Seiten-Template auf alle Tools ausrollen
### Quality Gate pro programmatischer Seite
Eine Seite wird nur veröffentlicht, wenn:
- sie ein echtes funktionierendes Tool besitzt
- mindestens 500900 Wörter echter, nützlicher Kontext vorhanden sind, sofern die Intention das rechtfertigt
- der Text nicht künstlich verlängert ist
- mindestens 5 einzigartige FAQs vorhanden sind
- keine Copy-Paste-Blöcke von anderen Tool-Seiten bestehen
- Title, H1, URL und Suchintention zusammenpassen
- Links und Schema validiert sind
- mobile Darstellung geprüft wurde
Die Wortzahl ist kein Selbstzweck. Nutzerwert und Vollständigkeit gehen vor künstlicher Länge.
---
## 10. P2 AdSense-Plan
### Wichtige Realität
AdSense wird nicht durch Keyword-Volumen allein freigeschaltet. Ein Tool-Portal braucht:
- originellen, hilfreichen Content
- klare Navigation
- erreichbare rechtliche Seiten
- Datenschutz und Einwilligungsmanagement
- keine irreführenden Anzeigen
- keine ungültigen Klicks
- gute Nutzererfahrung
- ausreichende öffentliche Substanz
Es gibt keinen seriösen Weg, „maximales AdSense“ zu garantieren. Das Ziel ist **maximale regelkonforme Ertragschance bei guter Nutzererfahrung**.
### Vor AdSense-Antrag
- [ ] Domain live und stabil
- [ ] HTTPS aktiv
- [ ] Impressum live
- [ ] Datenschutz live
- [ ] Cookie-/Consent-Lösung für EEA, UK und Schweiz eingerichtet
- [ ] Google-zertifizierte CMP bzw. Google Privacy & Messaging geprüft
- [ ] Sitemap und Search Console eingerichtet
- [ ] Seiten indexierbar
- [ ] mehrere echte Tool-Seiten live
- [ ] informative Inhalte pro Tool vorhanden
- [ ] Kontaktmöglichkeit vorhanden
- [ ] keine Platzhaltertexte
- [ ] keine leeren Kategorien
- [ ] keine übermäßigen Popups oder Interstitials
### Ad-Platzierung
Nicht direkt neben einem Button, der ein Ergebnis erzeugt. Keine Platzierung, die versehentliche Klicks provoziert.
Empfohlene Reihenfolge:
1. oberhalb/unterhalb des redaktionellen Direct-Answer-Blocks, nur wenn genug Inhalt vorhanden ist
2. zwischen Inhalt und FAQ, nicht innerhalb der eigentlichen Tool-Steuerung
3. ein responsives Ad im unteren Content-Bereich
4. optional ein dezentes Banner auf langen Erklärseiten
Nicht empfehlen:
- Ads als erste sichtbare Fläche vor dem Tool
- Ads zwischen Ja-/Nein-Buttons
- Ads neben Würfel-/Rad-Steuerungen
- Anzeigen in die interaktive Oberfläche hinein
- mehrere Ads auf einer kurzen, inhaltsarmen Seite
- „Klicke hier“-Formulierungen
Google AdSense empfiehlt, Anzeigen als Anzeigen erkennbar zu halten, den eigenen Content hervorzuheben, responsive Formate zu verwenden und Bereiche mit wenig einzigartigem Content nicht mit Werbung zu überladen.
### Ertragsoptimierung nach ausreichendem Traffic
Erst nach stabilen Daten testen:
- Ad-Viewability
- Page RPM
- Session RPM
- Engagement Rate
- Tool-Nutzung vor und nach Ad-Placement
- Core Web Vitals
- mobile vs. desktop
- Consent Rate
Nicht nur CPC optimieren. Ein höherer CPC mit schlechter UX kann Umsatz und SEO verschlechtern.
---
## 11. P2 Analytics und Messung
### Technische Einrichtung
- Google Search Console
- GA4 oder datenschutzkonforme Alternative
- Consent Mode korrekt konfigurieren
- `sitemap.xml` einreichen
- Bing Webmaster Tools
- Ereignisse für Tool-Nutzung
### Events
Empfohlene Events:
- `tool_open`
- `tool_result`
- `tool_customized`
- `tool_share`
- `faq_expand`
- `outbound_click`
Keine personenbezogenen Daten in Event-Parametern speichern.
### SEO-Monitoring
Monatlich prüfen:
- Klicks und Impressionen je URL
- CTR je Keyword
- Indexierungsstatus
- Core Web Vitals
- Suchanfragen mit Impressionen, aber niedriger CTR
- neue Suchintentionen
- Seiten ohne Impressionen
- interne Links und Crawlbarkeit
### AI-Monitoring
Monatlich 1020 Queries testen:
- „kostenloser Zufallsgenerator online"
- „bester Ja Nein Generator"
- „Glücksrad online drehen"
- „Würfel online"
- „Namen zufällig auslosen"
- „Münze werfen online"
- „Entscheidungshilfe ohne Anmeldung"
In Google AI Overviews, ChatGPT, Perplexity und Bing/Copilot dokumentieren:
- wird Entscheidomat genannt?
- wird eine URL zitiert?
- welche Wettbewerber werden zitiert?
- welche Formulierung verwendet die AI?
- welche Seite muss inhaltlich verbessert werden?
---
## 12. Konkrete Umsetzungsreihenfolge
### Phase A Launch-Fundament
- [ ] Domain `entscheidomat.com` verbinden
- [ ] HTTPS und Redirect-Strategie prüfen
- [ ] `metadataBase` und Canonical setzen
- [ ] OG-/Twitter-Card erstellen
- [ ] Social Image erzeugen
- [ ] Impressum erstellen
- [ ] Datenschutz erstellen
- [ ] Footer erweitern
- [ ] `app/robots.ts` erstellen
- [ ] `app/sitemap.ts` erstellen
- [ ] gewünschte AI-Crawler nicht blockieren
- [ ] Lint-Fehler beheben
- [ ] mobile QA durchführen
### Phase B Startseite SEO-/AI-fähig machen
- [ ] Title auf Kategorie + Nutzen ausrichten
- [ ] H1 um „kostenloser Zufallsgenerator“ ergänzen
- [ ] Direct-Answer-Block hinzufügen
- [ ] sechs Tool-Erklärungen hinzufügen
- [ ] 810 FAQs hinzufügen
- [ ] WebApplication-/Organization-Schema ergänzen
- [ ] Breadcrumb-Schema für Unterseiten vorbereiten
### Phase C erste fünf Suchseiten
- [ ] `/ja-nein-generator`
- [ ] `/gluecksrad`
- [ ] `/wuerfel-online`
- [ ] `/namen-auslosen`
- [ ] `/muenze-werfen`
Für jede Seite:
- [ ] eigenes Tool
- [ ] eigener Title/H1/Description
- [ ] Direct Answer
- [ ] Bedienungsanleitung
- [ ] Beispiele
- [ ] FAQ
- [ ] interne Links
- [ ] Schema
- [ ] Mobile-/Accessibility-QA
### Phase D Search Console und AdSense
- [ ] Search Console bestätigen
- [ ] Sitemap einreichen
- [ ] Indexierung prüfen
- [ ] Analytics und Consent prüfen
- [ ] mindestens mehrere wertvolle Seiten stabil live haben
- [ ] AdSense-Policies prüfen
- [ ] AdSense beantragen
- [ ] Ads erst nach Freischaltung und in klar getrennten Content-Bereichen einbauen
- [ ] Viewability und UX messen
### Phase E Autorität und GEO
- [ ] About-/Produktseite
- [ ] glaubwürdige Projekt-/Betreiberinformationen
- [ ] echte Erwähnungen und Reviews aufbauen
- [ ] Tool-Vergleiche und How-to-Inhalte
- [ ] keine künstlichen Backlinks
- [ ] keine massenhaft generierten Thin Pages
- [ ] monatliches AI-Citation-Tracking
---
## 13. Definition of Done perfekte Live-Version
### Technik
- [ ] `npm run build` exit 0
- [ ] `npm run lint` exit 0
- [ ] HTTPS und Canonical korrekt
- [ ] keine Console Errors
- [ ] mobile Layouts geprüft
- [ ] Tastatur und Screenreader geprüft
- [ ] Core Web Vitals gemessen
### Crawlability
- [ ] `/robots.txt` 200
- [ ] `/sitemap.xml` 200
- [ ] nur kanonische URLs in Sitemap
- [ ] keine wichtigen Inhalte hinter blockierendem Client-JS
- [ ] keine versehentlichen `noindex`-Signale
### SEO
- [ ] jede indexierbare Seite hat eigenen Title
- [ ] jede Seite hat eigene Description
- [ ] genau ein sinnvoller H1
- [ ] Keywords werden natürlich eingesetzt
- [ ] interne Links sind beschreibend
- [ ] keine dünnen Doorway-Seiten
### AEO/GEO
- [ ] Definition am Seitenanfang
- [ ] direkte Antworten auf Nutzerfragen
- [ ] sichtbare FAQ
- [ ] semantische Überschriften
- [ ] Schema entspricht sichtbarem Content
- [ ] AI-Crawler-Entscheidung dokumentiert
- [ ] AI-Visibility-Baseline erstellt
### AdSense
- [ ] Impressum und Datenschutz
- [ ] Consent Management für EEA/UK/Schweiz
- [ ] keine Ads in Tool-Controls
- [ ] Ads klar erkennbar
- [ ] keine Klickanreize
- [ ] keine ungültigen Klicks
- [ ] ausreichender einzigartiger Content
- [ ] Ads nur dort, wo sie UX nicht zerstören
---
## 14. Quellen
### Google Search Central
- [AI features and your website](https://developers.google.com/search/docs/appearance/ai-features)
- [Optimizing for generative AI features](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)
- [SEO Starter Guide](https://developers.google.com/search/docs/fundamentals/seo-starter-guide)
- [Structured Data introduction](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
- [Canonical URLs](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls)
- [Sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview)
### Google AdSense
- [Best practices for ad placement](https://support.google.com/adsense/answer/1282097)
- [Viewability best practices](https://support.google.com/adsense/answer/6219980)
- [EU user consent policy / CMP](https://support.google.com/adsense/answer/7670013)
- [Consent optimization](https://support.google.com/adsense/answer/16878447)
### Keyword-Daten
- `Keyword Stats 2026-07-22 at 23_15_06.csv` entscheidomat-spezifischer Export
- `Keyword Stats 2026-07-22 at 23_08_06.csv` gemischter Portfolio-Export; nicht vollständig Entscheidomat zurechnen
---
## Schlussentscheidung
**Nicht als finale SEO-/AdSense-Version live nehmen.**
**Als funktionierender Produktprototyp:** ja.
**Als sauberer öffentlicher Launch:** nach Phase A.
**Als SEO-/AI-/AdSense-optimierte Version:** nach Phase B bis D.
**Als skalierbares Portfolio:** erst nach einer vollständig geprüften Musterseite und einem stabilen Quality Gate für programmatische Seiten.

121
ZUKUNFTSPLAN.md Normal file
View File

@@ -0,0 +1,121 @@
# 🚀 Zukunftsplan: SEO & EMD-Portfolio (Web-Tools)
Dieses Dokument dient als strategischer Zukunftsplan und Fahrplan für das Web-Tool-Portfolio auf Basis der Google Keyword Planner Analysen.
---
## 🎯 Strategische Grundausrichtung: Multi-Domain-Ansatz
Anstatt alle unterschiedlichen Tools auf einer gemischten Website zu bündeln, setzen wir auf **fokussierte Exact-Match-Domains (EMDs)**.
### Vorteile dieses Ansatzes:
- **100 % SEO-Relevanz:** Jede Domain bedient exakt ein Themenfeld. Google belohnt diese thematische Reinheit mit besseren organischen Rankings.
- **Hohe Click-Through-Rate (CTR):** Nutzer in den Suchergebnissen vertrauen prägnanten Domainnamen wie `entscheidomat.com` oder `prozentomat.de`.
- **Einfache Skalierbarkeit:** Neue Nischen können als eigenständige Marken gelaucht werden.
---
## 🏆 Die Hauptprojekte & Roadmap
```mermaid
graph TD
A[Web-Tool Portfolio] --> B[Projekt 1: entscheidomat.com]
A --> C[Projekt 2: prozentomat.de]
A --> D[Projekt 3: schriftomat.de]
A --> E[Projekt 4: nameomat.de]
B --> B1[Glücksrad / 50k]
B --> B2[Ja/Nein Generator / 50k]
B --> B3[Würfel Online / 50k]
B --> B4[Namen-Auslosung / 50k]
B --> B5[Münzwurf / 5k]
C --> C1[Prozentrechner / 500k]
C --> C2[Prozentuale Zu/Abnahme]
C --> C3[MwSt.-Rechner]
C --> C4[Dreisatz-Rechner]
```
---
## 1. 🎲 Projekt 1: `entscheidomat.com` (Sofort-Launch)
* **Fokus:** Zufallsgeneratoren, Entscheidungs-Tools & Unterhaltung.
* **Suchvolumen-Pool:** **> 250.000 Suchen / Monat** (bei geringem Wettbewerb).
* **Entwicklungsstatus:** Tools sind bereits vollständig programmiert.
### SEO-Titel & URL-Struktur:
- **Startseite (`/`):** *Entscheidomat Der Online Entscheidungsfinder & Zufallsgenerator*
- **`/gluecksrad`:** *Online Glücksrad & Entscheidungsrad drehen*
- **Keywords:** `gluecksrad online` (50.000 Suchen) | `entscheidungsrad` (5.000 Suchen, **3,21 € Max CPC**)
- **`/ja-nein-generator`:** *Ja Nein Generator Schnelle Antworten finden*
- **Keywords:** `ja nein generator` (50.000 Suchen, 1,08 € CPC) | `ja oder nein generator` (50.000 Suchen)
- **`/wuerfel-online`:** *Virtueller Würfel Online 6er & 10er Würfel rollen*
- **Keywords:** `wuerfel online` (50.000 Suchen, 1,37 € CPC)
- **`/namen-auslosen`:** *Namen Zufallsgenerator Gewinner & Teams ziehen*
- **Keywords:** `zufallsgenerator namen` (50.000 Suchen)
- **`/muenze-werfen`:** *Münze werfen online Kopf oder Zahl*
- **Keywords:** `münze werfen online` (5.000 Suchen)
---
## 2. 🧮 Projekt 2: `prozentomat.de` (Traffic- & Ertrags-Gigant)
* **Fokus:** Prozentrechnung, Rabatt-, Finanz- & Schul-Rechner.
* **Suchvolumen-Pool:** **> 500.000 Suchen / Monat**
* **Monetarisierung:** **1,75 € Max CPC** (Finanz-Segment mit sehr hohem Werbewert).
### Geplante Tools auf `prozentomat.de`:
1. **Klassischer Prozentrechner:** „Wie viel sind X % von Y?“ / „Wie viel % sind X von Y?“
2. **Prozentuale Zu- & Abnahme (Rabattrechner):** „Betrag um X % erhöhen / verringern“.
3. **Mehrwertsteuer-Rechner:** 19 % & 7 % MwSt. hinzurechnen oder herausrechnen.
4. **Dreisatz-Prozentrechner:** Berechnung von Grundwert, Prozentwert und Prozentsatz.
5. **Prozentuale Veränderung:** prozentuale Differenz zwischen zwei Werten.
---
## 3. 📦 Pipeline-Projekte (Zukunft)
### ✍️ Projekt 3: `schriftomat.de` / `fontomat.de`
- **Fokus:** Unicode-Textgenerator & Instagram-Schriftarten zum Kopieren.
- **Traffic-Potenzial:** **> 340.000 Suchen / Monat** | CPC: **1,69 €**
- **Aufwand:** Sehr gering (rein clientseitiges JavaScript).
### 💡 Projekt 4: `nameomat.de`
- **Fokus:** Namensgenerator für Marken, Firmen, Gaming & Haustiere.
- **Traffic-Potenzial:** **> 21.000 Suchen / Monat** | CPC: **1,90 €** (Höchster CPC im Generator-Segment).
---
## 💰 Finanz- & Monetarisierungs-Prognose (AdSense)
Basierend auf einer konservativen Klickrate (CTR) von 1,5 % 2,0 % und den Klickpreisen (CPC) der Google Keyword Planner Daten:
| Projekt | Monatliche Seitenaufrufe (geschätzt) | AdSense Umsatzerwartung / Monat |
| :--- | :---: | :---: |
| **`entscheidomat.com`** | ~120.000 Pageviews | **800 € 2.200 €** |
| **`prozentomat.de`** | ~150.000 Pageviews | **1.800 € 4.500 €** |
| **`schriftomat.de`** | ~100.000 Pageviews | **600 € 1.600 €** |
| **GESAMT-PORTFOLIO** | **~370.000 Pageviews** | **3.200 € 8.300 € / Monat** |
---
## 📋 Schritt-für-Schritt Fahrplan zur Umsetzung
### Phase 1: Launch von `entscheidomat.com`
- [ ] Domain `entscheidomat.com` registrieren.
- [ ] Bisheriges Tool-Set auf der Domain einrichten.
- [ ] Einzelseiten (`/gluecksrad`, `/ja-nein-generator`, `/wuerfel-online`, `/muenze-werfen`, `/namen-auslosen`) anlegen.
- [ ] SEO-Optimierung: Meta-Title, H1-Tags, Meta-Description, OpenGraph-Tags & Schema.org Markup (`SoftwareApplication` / `WebApplication`).
- [ ] In der Google Search Console anmelden & Sitemap einreichen.
### Phase 2: Aufbau von `prozentomat.de`
- [ ] Domain `prozentomat.de` registrieren.
- [ ] Schnelles React/Next.js oder HTML/JS Frontend für die 5 Prozentrechner-Formulartools bauen.
- [ ] Erklärungstexte & Formeln unter die Rechner setzen (wichtig für Google SEO Content-Relevanz).
- [ ] Google Search Console Anmeldung & Indexierung anstoßen.
### Phase 3: Monetarisierung & Ausbau
- [ ] Google AdSense Konto beantragen und Anzeigenplätze (Header, Sidebar, In-Content) optimieren.
- [ ] Affiliate-Partnerschaften einbinden (z. B. Buchhaltung / Finanzen bei Prozentomat).
- [ ] Pipeline-Projekte (`schriftomat.de`, `nameomat.de`) umsetzen.

163
app/datenschutz/page.tsx Normal file
View File

@@ -0,0 +1,163 @@
import type { Metadata } from "next";
import Link from "next/link";
import LegalLayout from "@/components/legal-layout";
export const metadata: Metadata = {
title: "Datenschutzerklärung",
description: "Datenschutzerklärung von Entscheidomat gemäß DSGVO.",
alternates: { canonical: "/datenschutz" },
};
export default function DatenschutzPage() {
return (
<LegalLayout title="Datenschutzerklärung" updated="23. Juli 2026">
<section>
<h2>1. Verantwortlicher</h2>
<p className="mt-3">
Verantwortlich für die Datenverarbeitung auf dieser Website im
Sinne der Datenschutz-Grundverordnung (DSGVO) ist:
</p>
<p className="mt-3">
Timo Knuth
<br />
E-Mail: <a href="mailto:info@entscheidomat.com">info@entscheidomat.com</a>
</p>
<p className="mt-3 font-medium text-clay">
Die vollständige ladungsfähige Geschäftsanschrift wird vor dem
öffentlichen Launch ergänzt.
</p>
<p className="mt-3">
Weitere Angaben finden Sie im{" "}
<Link href="/impressum">Impressum</Link>.
</p>
</section>
<section>
<h2>2. Grundsatz der Datenverarbeitung</h2>
<p className="mt-3">
Entscheidomat ist bewusst datensparsam aufgebaut: Die
Zufalls-Tools (Münze, Glücksrad, Würfel, Zufallszahl, Magic
8-Ball, Listen-Auswahl, Ja/Nein-Generator) laufen vollständig im
Browser. Eingaben wie eigene Beschriftungen, Listen oder
Fragen werden nicht an unseren Server übertragen.
</p>
</section>
<section>
<h2>3. Hosting und Server-Logfiles</h2>
<p className="mt-3">
Beim Aufruf dieser Website verarbeitet der Hostinganbieter
automatisch technische Zugriffsdaten (z.&nbsp;B. IP-Adresse,
Datum und Uhrzeit des Zugriffs, aufgerufene Seite, verwendeter
Browser) in Server-Logfiles. Dies ist technisch notwendig, um die
Website sicher und stabil auszuliefern (Rechtsgrundlage: Art. 6
Abs. 1 lit. f DSGVO, berechtigtes Interesse an einem
funktionsfähigen Betrieb).
</p>
<p className="mt-3 font-medium text-clay">
Der Webserver befindet sich in Texas, USA. Die Bezeichnung des
Hostinganbieters und die Speicherdauer der Logfiles werden vor dem
öffentlichen Launch ergänzt.
</p>
<h2 className="mt-6">3.1 Datenübermittlung in die USA (Drittlandtransfer)</h2>
<p className="mt-3">
Diese Website wird auf einem Server in Texas, USA gehostet. Dabei werden
personenbezogene Daten (insbesondere die technischen
Zugriffsdaten aus Ziffer 3) in ein Land außerhalb der
Europäischen Union und des Europäischen Wirtschaftsraums
(sogenanntes Drittland) übertragen.
</p>
<p className="mt-3 font-medium text-clay">
Vor dem öffentlichen Launch wird geprüft und dokumentiert, auf
welcher konkreten Grundlage die Datenübermittlung erfolgt und ob
der gewählte Hostinganbieter die erforderlichen Garantien für den
Drittlandtransfer bietet.
</p>
</section>
<section>
<h2>4. Lokale Speicherung (localStorage)</h2>
<p className="mt-3">
Eigene Einstellungen z.&nbsp;B. Münz-Beschriftungen, eigene
Würfel-Texte, Listen-Einträge, Magic-8-Ball-Antworten oder die
Sound-Einstellung werden ausschließlich im{" "}
<code>localStorage</code> Ihres Browsers auf Ihrem Gerät
gespeichert, damit sie bei erneutem Besuch erhalten bleiben.
Diese Daten verlassen Ihr Gerät nicht und werden nicht an uns
übertragen. Sie können diese Daten jederzeit über die
Browsereinstellungen (Website-Daten löschen) entfernen.
</p>
</section>
<section>
<h2>5. Cookies und Werbung</h2>
<p className="mt-3">
Entscheidomat setzt aktuell keine Cookies und keine Werbedienste
ein. Sollte künftig Werbung (z.&nbsp;B. Google AdSense)
hinzukommen, wird diese Datenschutzerklärung vorher aktualisiert
und eine Einwilligungslösung (Consent-Banner) für Nutzerinnen und
Nutzer aus der EU, dem UK und der Schweiz eingebaut, bevor der
Dienst aktiv geschaltet wird.
</p>
</section>
<section>
<h2>6. Webanalyse mit Umami</h2>
<p className="mt-3">
Wir nutzen Umami Cloud (Umami Software, Inc.) mit Datenregion EU,
um anonyme, aggregierte Nutzungsstatistiken zu erheben (z.&nbsp;B.
Seitenaufrufe, Referrer, ungefähre geografische Region, verwendetes
Gerät). Rechtsgrundlage ist unser berechtigtes Interesse an der
Analyse und Verbesserung des Angebots (Art. 6 Abs. 1 lit. f DSGVO).
</p>
<p className="mt-3">
Umami setzt <strong>keine Cookies</strong> und verwendet keine
persistente, geräteübergreifende Kennung zur Wiedererkennung
einzelner Personen. IP-Adressen werden nicht gespeichert; es
erfolgt lediglich eine tagesaktuelle, nicht rückverfolgbare
Kennzeichnung zur Unterscheidung wiederkehrender Besuche
(Hashing). Eine Zusammenführung mit anderen Diensten oder eine
Profilbildung findet nicht statt. Die Daten werden auf Servern in
der EU verarbeitet.
</p>
<p className="mt-3">
Da keine Cookies gesetzt und keine personenbezogenen Kennungen
gespeichert werden, ist nach unserer Einschätzung keine
Einwilligung nach § 25 TDDDG erforderlich.
Weitere Informationen:{" "}
<a
href="https://umami.is/privacy"
target="_blank"
rel="noopener noreferrer"
>
umami.is/privacy
</a>
.
</p>
</section>
<section>
<h2>7. Ihre Rechte</h2>
<p className="mt-3">Sie haben im Rahmen der geltenden Datenschutzgesetze jederzeit das Recht auf:</p>
<ul className="mt-3">
<li>Auskunft über Ihre bei uns gespeicherten personenbezogenen Daten (Art. 15 DSGVO)</li>
<li>Berichtigung unrichtiger Daten (Art. 16 DSGVO)</li>
<li>Löschung Ihrer bei uns gespeicherten Daten (Art. 17 DSGVO)</li>
<li>Einschränkung der Datenverarbeitung (Art. 18 DSGVO)</li>
<li>Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)</li>
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
<li>Beschwerde bei einer Datenschutz-Aufsichtsbehörde</li>
</ul>
</section>
<section>
<h2>8. Kontakt für Datenschutzanfragen</h2>
<p className="mt-3">
Bei Fragen zum Datenschutz wenden Sie sich bitte an:{" "}
info@entscheidomat.com.
</p>
</section>
</LegalLayout>
);
}

374
app/globals.css Normal file
View File

@@ -0,0 +1,374 @@
@import "tailwindcss";
@theme {
/* Typografie */
--font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-display: var(--font-cormorant), Georgia, serif;
/* Farben Warmes Porzellan-Papier mit sattem Gelb-Grün / Smaragd */
--color-paper: #faf8f3;
--color-surface: #ffffff;
--color-ink: #111412;
--color-ink-soft: #45504a;
--color-line: #e0eae4;
--color-primary: #111412;
--color-primary-strong: #000000;
--color-primary-soft: #e6f7f0;
/* Glücksspiel & Zufall Gelb-Grün / Smaragdgrün */
--color-emerald-accent: #0d7a57;
--color-emerald-strong: #095c41;
--color-emerald-soft: #e6f7f0;
--color-emerald-line: #b2e5d1;
--color-clay: #b4633f;
--color-clay-soft: #f6eae2;
/* Schatten */
--shadow-soft: 0 2px 4px rgb(17 20 18 / 0.03), 0 12px 36px rgb(17 20 18 / 0.06);
--shadow-lift: 0 4px 12px rgb(17 20 18 / 0.06), 0 24px 52px rgb(17 20 18 / 0.10);
--shadow-cta: 0 12px 28px rgb(17 20 18 / 0.22);
/* Animationen */
--animate-float: float 7s ease-in-out infinite;
--animate-coin-spin: coin-spin 10s linear infinite;
--animate-bob: bob 2.4s ease-in-out infinite;
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-16px);
}
}
@keyframes coin-spin {
from {
transform: rotateY(0deg);
}
to {
transform: rotateY(360deg);
}
}
@keyframes bob {
0%,
100% {
transform: translateY(0);
opacity: 0.7;
}
50% {
transform: translateY(6px);
opacity: 1;
}
}
@keyframes stream {
from {
stroke-dashoffset: 1000;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes shootTL {
0% {
transform: translate(0, 0);
opacity: 0;
}
20% {
opacity: 0.7;
}
80% {
opacity: 0.7;
}
100% {
transform: translate(520px, 180px);
opacity: 0;
}
}
@keyframes shootTR {
0% {
transform: translate(0, 0);
opacity: 0;
}
20% {
opacity: 0.7;
}
80% {
opacity: 0.7;
}
100% {
transform: translate(-520px, 190px);
opacity: 0;
}
}
@keyframes shootBL {
0% {
transform: translate(0, 0);
opacity: 0;
}
20% {
opacity: 0.65;
}
80% {
opacity: 0.65;
}
100% {
transform: translate(480px, -170px);
opacity: 0;
}
}
}
/* ---------- Basis ---------- */
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
body {
background: var(--color-paper);
color: var(--color-ink);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
/* Der Inline-Check im Root-Layout läuft vor der Hydration. Dadurch blitzt der
Preloader bei einem Reload innerhalb derselben Browser-Session nicht auf. */
html[data-preloader="cached"] [data-preloader-overlay] {
display: none;
}
/* Der Preloader würde auf einem gedrosselten Mobilgerät das LCP-Element
verdecken. Die Seite bleibt dort sofort lesbar; Desktop behält den Effekt. */
@media (max-width: 767px) {
[data-preloader-overlay] {
display: none;
}
}
/* Die Hero-Inhalte sind bereits vor dem Öffnen des Vorhangs im Startzustand.
So kann GSAP sie nicht erst sichtbar machen und anschließend zurücksetzen. */
@media (prefers-reduced-motion: no-preference) {
html[data-preloader="active"] .hero-el {
opacity: 0;
transform: translateY(34px);
}
.reveal {
opacity: 0;
transform: translateY(var(--reveal-y, 30px));
transition:
opacity 0.7s ease var(--reveal-delay, 0s),
transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0s);
}
.reveal--visible {
opacity: 1;
transform: translateY(0);
}
}
::selection {
background: rgb(22 21 19 / 0.12);
}
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 6px;
}
/* Dezente Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: var(--color-paper);
}
::-webkit-scrollbar-thumb {
background: #d5d0c2;
border-radius: 8px;
border: 2px solid var(--color-paper);
}
::-webkit-scrollbar-thumb:hover {
background: #c1bbab;
}
/* ---------- Hintergrund-Deko ---------- */
.bg-grid {
background-image:
linear-gradient(to right, rgb(32 30 25 / 0.045) 1px, transparent 1px),
linear-gradient(to bottom, rgb(32 30 25 / 0.045) 1px, transparent 1px);
background-size: 46px 46px;
mask-image: radial-gradient(ellipse 90% 65% at 50% 0%, black 25%, transparent 78%);
-webkit-mask-image: radial-gradient(ellipse 90% 65% at 50% 0%, black 25%, transparent 78%);
}
.bg-noise {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
opacity: 0.03;
}
/* ---------- 3D-Münze (gebürstetes Messing, flach) ---------- */
.coin-scene {
perspective: 1100px;
}
.coin {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
will-change: transform;
}
.coin-face {
position: absolute;
inset: 0;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.coin-face--front {
transform: translateZ(4px);
background: #dec998;
box-shadow:
inset 0 1px 2px rgb(255 250 235 / 0.5),
inset 0 -2px 4px rgb(120 90 40 / 0.12);
}
.coin-face--back {
background: #d6be87;
box-shadow:
inset 0 1px 2px rgb(255 248 228 / 0.5),
inset 0 -2px 4px rgb(110 82 36 / 0.12);
}
.coin-edge {
position: absolute;
inset: 0;
border-radius: 9999px;
background: #8a6f44;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.coin-engraving {
position: absolute;
inset: 8%;
border-radius: 9999px;
border: 2px solid rgb(122 94 51 / 0.38);
pointer-events: none;
}
.coin-engraving--inner {
inset: 16%;
border-width: 1px;
border-color: rgb(122 94 51 / 0.26);
}
.coin-label {
font-family: var(--font-display);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #6e5533;
text-shadow: 0 1px 0 rgb(255 246 224 / 0.8);
max-width: 78%;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ---------- 3D-Würfel ---------- */
.dice-scene {
perspective: 900px;
}
.dice {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
will-change: transform;
}
.dice-face {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #fdfcf9;
border: 1px solid var(--color-line);
border-radius: 18%;
box-shadow: inset 0 0 14px rgb(32 30 25 / 0.06);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
overflow: hidden;
padding: 8%;
}
.dice-face-label {
font-family: var(--font-display);
font-weight: 600;
font-size: clamp(9px, 1.6vw, 13px);
line-height: 1.2;
text-align: center;
color: var(--color-ink);
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ---------- Magic 8-Ball (warmes Anthrazit, flach) ---------- */
.ball-body {
background: #1d1b17;
box-shadow:
inset 0 -18px 42px rgb(0 0 0 / 0.6),
inset 0 10px 26px rgb(255 255 255 / 0.07),
0 30px 60px rgb(32 30 25 / 0.28);
}
.ball-window {
background: #12130f;
box-shadow:
inset 0 4px 18px rgb(0 0 0 / 0.75),
0 0 0 6px rgb(255 255 255 / 0.05);
}
/* ---------- Diverse ---------- */
.tabular {
font-variant-numeric: tabular-nums;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}

163
app/gluecksrad/page.tsx Normal file
View File

@@ -0,0 +1,163 @@
import type { Metadata } from "next";
import WheelOfFortune from "@/components/tools/wheel-of-fortune";
import SubpageLayout from "@/components/subpage-layout";
const SITE_URL = "https://entscheidomat.com";
const PAGE_URL = `${SITE_URL}/gluecksrad`;
const PAGE_TITLE = "Glücksrad online drehen kostenloses Entscheidungsrad";
const PAGE_DESCRIPTION =
"Glücksrad online mit bis zu 12 Optionen: eintragen, drehen und zufällig entscheiden lassen. Kostenlos, ohne Anmeldung und direkt im Browser.";
const UPDATED = "23. Juli 2026";
export const metadata: Metadata = {
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
alternates: { canonical: "/gluecksrad" },
openGraph: { type: "website", locale: "de_DE", siteName: "Entscheidomat", title: PAGE_TITLE, description: PAGE_DESCRIPTION, url: PAGE_URL, images: ["/opengraph-image"] },
twitter: { card: "summary_large_image", title: PAGE_TITLE, description: PAGE_DESCRIPTION, images: ["/twitter-image"] },
};
const FAQS = [
{
q: "Wie viele Optionen kann ich im Glücksrad eintragen?",
a: "Zwischen 2 und 12 Optionen. Du kannst sie jederzeit ändern, hinzufügen oder entfernen deine Liste bleibt automatisch im Browser gespeichert.",
},
{
q: "Wie wird die Option ausgewählt?",
a: "Beim Drehen erzeugt das Tool im Browser einen neuen Zufallswert und ordnet das Ergebnis einer sichtbaren Option zu. Das Glücksrad ist für spielerische Alltagsentscheidungen gedacht, nicht für verbindliche oder sicherheitskritische Zwecke.",
},
{
q: "Werden meine Optionen gespeichert oder übertragen?",
a: "Deine Optionen werden ausschließlich lokal im localStorage deines Browsers gespeichert und nicht an einen Server übertragen.",
},
{
q: "Kann ich das Rad auf die Standardoptionen zurücksetzen?",
a: "Ja, über den Button „Zurücksetzen“ im Optionen-Editor werden die Standardoptionen 16 wiederhergestellt.",
},
{
q: "Eignet sich das Glücksrad für Gruppen und Präsentationen?",
a: "Ja, es ist groß und deutlich lesbar. Für wichtige Entscheidungen sollte es aber nicht der einzige Entscheidungsweg sein.",
},
{
q: "Wie nutze ich das Glücksrad für eine faire Auswahl?",
a: "Trage jede Option genau einmal ein. Das Rad teilt alle Einträge in gleich große Segmente; doppelte Einträge erhöhen deshalb die Chance dieser Option. Prüfe die Liste vor dem Drehen, wenn alle dieselbe Gewinnchance haben sollen.",
},
{
q: "Was ist der Unterschied zwischen Glücksrad und Namen auslosen?",
a: "Das Glücksrad zeigt die Optionen visuell und eignet sich besonders für kleine Gruppen oder Präsentationen mit bis zu 12 Einträgen. Bei langen Listen oder mehreren Ziehungen ohne Wiederholung ist die Namen-Auslosung übersichtlicher.",
},
];
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Entscheidomat", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "Glücksrad", item: PAGE_URL },
],
},
{
"@type": "WebApplication",
name: "Glücksrad",
url: PAGE_URL,
description: PAGE_DESCRIPTION,
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
const STEPS = [
"Trage 2 bis 12 Optionen im Editor ein z. B. Namen, Aktivitäten oder Restaurants.",
"Passe die Beschriftungen jederzeit an.",
"Tippe auf „Drehen!“ das Rad dreht sich und bremst zufällig ab.",
"Der Zeiger markiert die gewonnene Option, inklusive Konfetti.",
];
const EXAMPLES = [
"„Welches Restaurant heute Abend?“",
"„Wer räumt diese Woche auf?“",
"„Welcher Film wird geschaut?“",
];
const VORTEILE = (
<p>
Das Glücksrad eignet sich besonders für Gruppenentscheidungen mit mehreren
gleichwertigen Optionen, die visuell gut dargestellt werden können. Pro Option
sind bis zu 24 Zeichen und maximal 12 Optionen möglich für sehr viele
Alternativen eignet sich eher die Listen-Auswahl.
</p>
);
const ADDITIONAL_CONTENT = (
<article className="space-y-4 text-base leading-relaxed text-ink-soft">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Glücksrad online: Entscheidungen sichtbar und fair auslosen
</h2>
<p>
Ein Online-Glücksrad ist eine Entscheidungshilfe für mehrere gleichwertige
Optionen. Du trägst zwischen zwei und zwölf Begriffe ein, das Tool verteilt sie
auf gleich große Segmente und der Zeiger bestimmt beim Drehen ein Ergebnis.
Dadurch hat jede einmal eingetragene Option dieselbe Chance. Typische Beispiele
sind Restaurants, Filme, Spiele, Aufgaben, Themen für einen Vortrag oder die
Reihenfolge in einer Gruppe.
</p>
<p>
Damit die Auswahl fair bleibt, sollte jede Möglichkeit genau einmal in der Liste
stehen. Ein doppelter Eintrag nimmt zwei Segmente ein und wird entsprechend
wahrscheinlicher ausgewählt. Das ist nützlich, wenn du bewusst gewichten willst,
aber ungeeignet für eine faire Verlosung. Prüfe deshalb die Beschriftungen vor
dem Start und erkläre der Gruppe kurz, dass alle Segmente gleich groß sind. Die
gespeicherten Optionen bleiben nur lokal in deinem Browser und werden nicht an
einen Server übertragen.
</p>
<p>
Für kleine Gruppen ist das drehende Rad oft besser als eine Liste, weil alle
Optionen gleichzeitig sichtbar sind und das Ergebnis einen gemeinsamen Moment
schafft. Bei vielen Namen oder wenn nach jeder Ziehung ein Eintrag verschwinden
soll, ist die Namen-Auslosung die bessere Wahl. Und wenn es nur um zwei
Möglichkeiten geht, sind Ja-Nein-Generator oder Münzwurf schneller. Für wichtige
Entscheidungen bleibt Zufall ein Impuls Verantwortung und Folgen sollten alle
Beteiligten vorher besprechen.
</p>
</article>
);
export default function GluecksradPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<SubpageLayout
toolSlug="gluecksrad"
toolName="Glücksrad"
h1Title="Glücksrad online drehen dein kostenloses Entscheidungsrad"
introText="Das Online-Glücksrad ist ein digitales Entscheidungsrad mit bis zu 12 frei einstellbaren Optionen. Trage deine Optionen ein, drehe das Rad und lass den Zeiger die Entscheidung treffen kostenlos, ohne Anmeldung und direkt im Browser."
steps={STEPS}
examples={EXAMPLES}
vorteileText={VORTEILE}
additionalContent={ADDITIONAL_CONTENT}
faqs={FAQS}
updatedDate={UPDATED}
>
<WheelOfFortune />
</SubpageLayout>
</>
);
}

9
app/icon.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#0d7a57"/>
<path d="M143 168h226c18 0 33 15 33 33v110c0 18-15 33-33 33H143c-18 0-33-15-33-33V201c0-18 15-33 33-33Z" fill="#faf8f3"/>
<rect x="142" y="200" width="94" height="112" rx="20" fill="#dcefe6"/>
<rect x="276" y="200" width="94" height="112" rx="20" fill="#0d7a57"/>
<path d="m300 255 17 17 31-37" fill="none" stroke="#faf8f3" stroke-linecap="round" stroke-linejoin="round" stroke-width="14"/>
<path d="M256 344v36" fill="none" stroke="#faf8f3" stroke-linecap="round" stroke-width="16"/>
<circle cx="256" cy="408" r="25" fill="#8bb79d"/>
</svg>

After

Width:  |  Height:  |  Size: 676 B

88
app/impressum/page.tsx Normal file
View File

@@ -0,0 +1,88 @@
import type { Metadata } from "next";
import LegalLayout from "@/components/legal-layout";
export const metadata: Metadata = {
title: "Impressum",
description: "Impressum und Anbieterkennzeichnung von Entscheidomat.",
alternates: { canonical: "/impressum" },
};
export default function ImpressumPage() {
return (
<LegalLayout title="Impressum" updated="23. Juli 2026">
<section>
<h2>Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG)</h2>
<p className="mt-3">
Timo Knuth
<br />
Einzelunternehmer
</p>
</section>
<section>
<h2>Kontakt</h2>
<p className="mt-3">
E-Mail: <a href="mailto:info@entscheidomat.com">info@entscheidomat.com</a>
</p>
</section>
<section>
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
<p className="mt-3">Timo Knuth</p>
</section>
<section>
<h2>Nutzungshinweis und Haftungsrahmen</h2>
<p className="mt-3">
Entscheidomat ist eine spielerische Entscheidungshilfe für
alltägliche, folgenarme Situationen. Die Tools ersetzen keine
persönliche, medizinische, rechtliche, finanzielle oder sonstige
fachliche Beratung und sind nicht für verbindliche Auslosungen,
Gewinnspiele oder sicherheitskritische Zwecke bestimmt.
</p>
<p className="mt-3">
Die Ergebnisse werden mit browserbasierten Zufallswerten erzeugt.
Es wird keine Zusage für kryptografische Zufälligkeit, rechtliche
Verbindlichkeit oder eine bestimmte Eignung für einen konkreten
Zweck gemacht. Gesetzliche Haftung, insbesondere bei Vorsatz,
grober Fahrlässigkeit sowie bei Schäden an Leben, Körper oder
Gesundheit, bleibt unberührt.
</p>
</section>
<section>
<h2>Haftung für Inhalte</h2>
<p className="mt-3">
Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene
Inhalte auf diesen Seiten nach den allgemeinen Gesetzen
verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter
jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde
Informationen zu überwachen oder nach Umständen zu forschen, die
auf eine rechtswidrige Tätigkeit hinweisen.
</p>
</section>
<section>
<h2>Haftung für Links</h2>
<p className="mt-3">
Entscheidomat enthält aktuell keine Links zu externen Webseiten
Dritter, auf deren Inhalte wir Einfluss haben. Sollten künftig
externe Links ergänzt werden, gilt: Für die Inhalte der
verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber
der Seiten verantwortlich.
</p>
</section>
<section>
<h2>Urheberrecht</h2>
<p className="mt-3">
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf
diesen Seiten unterliegen dem deutschen Urheberrecht.
Vervielfältigung, Bearbeitung, Verbreitung und jede Art der
Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der
schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
</p>
</section>
</LegalLayout>
);
}

View File

@@ -0,0 +1,175 @@
import type { Metadata } from "next";
import JaNeinGenerator from "@/components/tools/ja-nein-generator";
import SubpageLayout from "@/components/subpage-layout";
const SITE_URL = "https://entscheidomat.com";
const PAGE_URL = `${SITE_URL}/ja-nein-generator`;
const PAGE_TITLE = "Ja-Nein-Generator: Ja oder Nein online entscheiden";
const PAGE_DESCRIPTION =
"Mit dem Ja-Nein-Generator erhältst du sofort eine zufällige Antwort auf deine Frage: Ja oder Nein. Kostenlos, ohne Anmeldung, direkt im Browser.";
const UPDATED = "23. Juli 2026";
export const metadata: Metadata = {
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
alternates: { canonical: "/ja-nein-generator" },
openGraph: {
type: "website",
locale: "de_DE",
siteName: "Entscheidomat",
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
url: PAGE_URL,
images: ["/opengraph-image"],
},
twitter: {
card: "summary_large_image",
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
images: ["/twitter-image"],
},
};
const FAQS = [
{
q: "Wie funktioniert ein Ja-Nein-Generator?",
a: "Du gibst optional eine Frage ein und startest den Generator per Klick oder Tipp. Das Tool erzeugt im Browser einen neuen Zufallswert und zeigt danach „Ja“ oder „Nein“ an.",
},
{
q: "Wie entsteht das Ergebnis?",
a: "Bei jeder Anfrage erzeugt das Tool im Browser einen neuen Zufallswert. Der Ja-Nein-Generator ist für spielerische Alltagsentscheidungen gedacht, nicht für sicherheitskritische oder rechtlich verbindliche Zwecke.",
},
{
q: "Kann ich den Generator mehrfach verwenden?",
a: "Ja, beliebig oft. Der Verlauf zeigt dir die letzten zehn Ergebnisse, damit du siehst, wie oft Ja bzw. Nein gefallen ist.",
},
{
q: "Eignet sich der Generator für Gruppenentscheidungen?",
a: "Für lockere Gruppenentscheidungen (z. B. „Bestellen wir Pizza?“) eignet er sich gut. Für Entscheidungen, die alle Beteiligten mittragen müssen, ersetzt er kein gemeinsames Gespräch.",
},
{
q: "Ersetzt ein Zufallsergebnis eine wichtige persönliche Entscheidung?",
a: "Nein. Der Generator ist als spielerischer Denkanstoß für Alltagsfragen gedacht bei wichtigen persönlichen, finanziellen, rechtlichen oder gesundheitlichen Entscheidungen solltest du dich nicht auf einen Zufallsgenerator verlassen.",
},
{
q: "Wann ist ein Ja-Nein-Generator besser als ein Münzwurf?",
a: "Beide Methoden helfen bei einer Entscheidung zwischen zwei Optionen. Der Ja-Nein-Generator passt besser, wenn du eine konkrete Frage formulierst und direkt „Ja“ oder „Nein“ sehen möchtest; ein Münzwurf eignet sich eher für Kopf oder Zahl oder frei beschriftete Seiten.",
},
{
q: "Ist der Ja-Nein-Generator für wichtige Entscheidungen geeignet?",
a: "Nein. Er ist als spielerischer Denkanstoß für Alltagsfragen gedacht. Für gesundheitliche, rechtliche, finanzielle oder andere folgenreiche Entscheidungen solltest du Informationen sammeln und gegebenenfalls fachlichen Rat einholen.",
},
];
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Entscheidomat", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "Ja-Nein-Generator", item: PAGE_URL },
],
},
{
"@type": "WebApplication",
name: "Ja-Nein-Generator",
url: PAGE_URL,
description: PAGE_DESCRIPTION,
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
const STEPS = [
"Trage optional deine Frage in das Eingabefeld ein.",
"Tippe auf die Karte oder den Button „Ja oder Nein?“.",
"Der Generator wählt zufällig zwischen Ja und Nein und zeigt dir sofort das Ergebnis.",
"Frag so oft du willst der Verlauf zeigt dir die letzten zehn Ergebnisse.",
];
const EXAMPLES = [
"„Sollen wir heute Abend bestellen statt kochen?“",
"„Soll ich den Film jetzt zu Ende schauen?“",
"„Starte ich heute noch mit dem Sport?“",
];
const VORTEILE = (
<p>
Der Generator ist ideal für schnelle Alltagsfragen ohne großes Abwägen er ist
kostenlos, sofort einsatzbereit und komplett anonym, da nichts an einen Server
übertragen wird. Für wichtige persönliche, finanzielle, rechtliche oder gesundheitliche
Entscheidungen ist ein reiner Zufallsmechanismus dagegen nicht geeignet.
</p>
);
const ADDITIONAL_CONTENT = (
<article className="space-y-4 text-base leading-relaxed text-ink-soft">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Ja-Nein-Generator oder Münzwurf: Was passt wann?
</h2>
<p>
Ein Ja-Nein-Generator ist eine digitale Entscheidungshilfe für Fragen mit
genau zwei möglichen Antworten. Er eignet sich, wenn die Optionen bereits klar
sind und ihr eine kleine Pattsituation ohne langes Diskutieren auflösen möchtet:
Bestellen wir heute?, Gehen wir noch spazieren? oder Starte ich jetzt mit
der Aufgabe? Die Frage ist dabei nur ein hilfreicher Rahmen; sie verändert die
Wahrscheinlichkeit nicht.
</p>
<p>
Im Vergleich zum Münzwurf ist das Ergebnis sprachlich direkter. Statt Kopf oder
Zahl erscheint sofort Ja oder Nein praktisch auf dem Smartphone, in einer
Gruppe oder wenn ihr das Ergebnis später noch einmal im Verlauf sehen wollt.
Für zwei frei benannte Optionen wie Kino oder Couch kannst du entweder die
Frage entsprechend formulieren oder den Münzwurf mit eigenen Seiten nutzen.
Bei drei oder mehr gleichwertigen Möglichkeiten ist ein Glücksrad meist die
passendere Entscheidungshilfe.
</p>
<p>
Zufall ersetzt keine Abwägung, kann aber sichtbar machen, was du selbst willst:
Achte auf deine erste Reaktion auf das Ergebnis. Fühlt sich ein Nein unerwartet
enttäuschend an, steckt in dieser Reaktion oft schon eine Präferenz. Für
finanzielle, rechtliche, gesundheitliche oder andere folgenreiche Entscheidungen
solltest du Informationen sammeln, Alternativen vergleichen und gegebenenfalls
fachlichen Rat einholen nicht würfeln.
</p>
</article>
);
export default function JaNeinGeneratorPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<SubpageLayout
toolSlug="ja-nein-generator"
toolName="Ja-Nein-Generator"
h1Title="Ja-Nein-Generator Ja oder Nein online entscheiden"
introText="Der Ja-Nein-Generator beantwortet eine Frage zufällig mit Ja oder Nein. Formuliere deine Frage, starte den Generator und erhalte sofort eine zufällige Entscheidung kostenlos, ohne Anmeldung und direkt im Browser."
steps={STEPS}
examples={EXAMPLES}
vorteileText={VORTEILE}
additionalContent={ADDITIONAL_CONTENT}
faqs={FAQS}
updatedDate={UPDATED}
>
<JaNeinGenerator />
</SubpageLayout>
</>
);
}

121
app/layout.tsx Normal file
View File

@@ -0,0 +1,121 @@
import type { Metadata } from "next";
import { Inter, Cormorant_Garamond } from "next/font/google";
import Script from "next/script";
import "./globals.css";
import { SoundProvider } from "@/components/providers/sound-provider";
import Preloader from "@/components/preloader";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
display: "optional",
});
const cormorant = Cormorant_Garamond({
variable: "--font-cormorant",
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
style: ["normal", "italic"],
display: "optional",
});
const SITE_URL = "https://entscheidomat.com";
const SITE_NAME = "Entscheidomat";
const SITE_TITLE = "Entscheidomat Lass den Zufall entscheiden";
const SITE_DESCRIPTION =
"Münze werfen, Glücksrad drehen, würfeln, eine Zufallszahl erzeugen, den Magic 8-Ball fragen, eine Liste auslosen oder den Ja-Nein-Generator befragen wunderschön animiert, komplett anpassbar und kostenlos.";
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: {
default: SITE_TITLE,
template: "%s Entscheidomat",
},
description: SITE_DESCRIPTION,
keywords: [
"Münzwurf",
"Glücksrad",
"Würfel",
"Zufallsgenerator",
"Entscheidung",
"Kopf oder Zahl",
"Ja Nein Generator",
],
alternates: {
canonical: "/",
},
verification: {
google: "8AohGR6zT7xA_1au64S-bh1i0GDPx7d-cbMdS518NkE",
},
openGraph: {
type: "website",
locale: "de_DE",
url: SITE_URL,
siteName: SITE_NAME,
title: SITE_TITLE,
description: SITE_DESCRIPTION,
},
twitter: {
card: "summary_large_image",
title: SITE_TITLE,
description: SITE_DESCRIPTION,
},
};
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": `${SITE_URL}/#website`,
url: SITE_URL,
name: SITE_NAME,
description: SITE_DESCRIPTION,
inLanguage: "de-DE",
},
{
"@type": "Organization",
"@id": `${SITE_URL}/#organization`,
name: SITE_NAME,
url: SITE_URL,
},
],
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="de"
className={`${inter.variable} ${cormorant.variable} h-full antialiased`}
>
<body className="flex min-h-full flex-col">
<Script id="preloader-session" strategy="beforeInteractive">
{`try {
const hasSeenPreloader = sessionStorage.getItem("entscheidomat:preloader-seen") === "1";
const isMobile = window.matchMedia("(max-width: 767px)").matches;
document.documentElement.dataset.preloader = hasSeenPreloader || isMobile ? "cached" : "active";
} catch {
document.documentElement.dataset.preloader = window.matchMedia("(max-width: 767px)").matches ? "cached" : "active";
}`}
</Script>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<Script
src="https://cloud.umami.is/script.js"
data-website-id="15bab46b-b1a9-4d2f-9c69-deaef60a0cfc"
strategy="afterInteractive"
/>
<Preloader />
<SoundProvider>{children}</SoundProvider>
</body>
</html>
);
}

146
app/magic-8-ball/page.tsx Normal file
View File

@@ -0,0 +1,146 @@
import type { Metadata } from "next";
import Link from "next/link";
import Magic8Ball from "@/components/tools/magic-8-ball";
import SubpageLayout from "@/components/subpage-layout";
const SITE_URL = "https://entscheidomat.com";
const PAGE_URL = `${SITE_URL}/magic-8-ball`;
const PAGE_TITLE = "Magic 8-Ball online fragen zufällige Antworten";
const PAGE_DESCRIPTION =
"Der Magic 8-Ball beantwortet deine Frage mit einer zufälligen, anpassbaren Antwort. Fragen stellen, schütteln, Antwort erhalten kostenlos, ohne Anmeldung.";
const UPDATED = "23. Juli 2026";
export const metadata: Metadata = {
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
alternates: { canonical: "/magic-8-ball" },
openGraph: { type: "website", locale: "de_DE", siteName: "Entscheidomat", title: PAGE_TITLE, description: PAGE_DESCRIPTION, url: PAGE_URL, images: ["/opengraph-image"] },
twitter: { card: "summary_large_image", title: PAGE_TITLE, description: PAGE_DESCRIPTION, images: ["/twitter-image"] },
};
const FAQS = [
{
q: "Wie funktioniert der Magic 8-Ball?",
a: "Stelle optional deine Frage, tippe auf den Ball oder den Button der Ball schüttelt sich kurz und zeigt eine zufällig ausgewählte Antwort aus deiner Antwortliste.",
},
{
q: "Kann ich die Antworten selbst anpassen?",
a: "Ja, du kannst 2 bis 20 eigene Antworten im Editor eintragen. Sie werden automatisch lokal gespeichert.",
},
{
q: "Sind die Antworten immer eindeutig Ja oder Nein?",
a: "Nein, die klassischen 8-Ball-Antworten sind oft bewusst vage formuliert (z. B. „Frag später nochmal“). Für eine eindeutige Ja/Nein-Antwort eignet sich der Ja-Nein-Generator besser.",
},
{
q: "Werden meine Antworten gespeichert oder an einen Server übertragen?",
a: "Deine Antwortliste wird nur lokal im localStorage deines Browsers gespeichert, nicht an uns übertragen.",
},
{
q: "Kann ich den Ball mehrfach fragen?",
a: "Ja, beliebig oft hintereinander.",
},
];
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Entscheidomat", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "Magic 8-Ball", item: PAGE_URL },
],
},
{
"@type": "WebApplication",
name: "Magic 8-Ball",
url: PAGE_URL,
description: PAGE_DESCRIPTION,
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
const STEPS = [
"Trage optional deine Frage ein.",
"Tippe auf den Ball oder den Button „8-Ball schütteln“.",
"Der Ball schüttelt sich kurz und zeigt eine zufällige Antwort im Fenster.",
"Passe die möglichen Antworten im Editor an (220 Antworten).",
];
const EXAMPLES = [
"„Wird das Projekt heute fertig?“",
"„Soll ich noch mal anrufen?“",
"Spielerische Fragen in der Gruppe stellen",
];
const VORTEILE = (
<p>
Eigene Antwortliste möglich (220 Antworten, automatisch gespeichert). Die klassischen
Antworten sind oft bewusst mehrdeutig formuliert für eine eindeutige Ja- oder
Nein-Entscheidung eignet sich der{" "}
<Link href="/ja-nein-generator" className="text-[#c97d60] font-medium hover:underline underline-offset-2">
Ja-Nein-Generator
</Link>{" "}
besser.
</p>
);
const ADDITIONAL_CONTENT = (
<article className="space-y-4 text-base leading-relaxed text-ink-soft">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Magic 8-Ball online: spielerische Antworten auf deine Frage
</h2>
<p>
Der Magic 8-Ball ist eine spielerische Entscheidungshilfe mit anpassbaren Antworten.
Frage den Ball, schüttle ihn und erhalte eine zufällig ausgewählte Antwort. Mit einer
eigenen Antwortliste kannst du ihn für Spiele, Gruppenfragen oder kleine Impulse
personalisieren.
</p>
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Magic 8-Ball, Ja-Nein-Generator oder Münzwurf?
</h2>
<p>
Der Magic 8-Ball passt zu offenen und spielerischen Fragen, weil seine Antworten auch
mehrdeutig sein können. Für eine eindeutige Antwort nutze den <Link href="/ja-nein-generator" className="text-[#c97d60] font-medium hover:underline underline-offset-2">Ja-Nein-Generator</Link>; für Kopf oder Zahl oder zwei frei benannte Optionen eignet sich die <Link href="/muenze-werfen" className="text-[#c97d60] font-medium hover:underline underline-offset-2">Online-Münze</Link>.
</p>
</article>
);
export default function Magic8BallPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<SubpageLayout
toolSlug="magic-8-ball"
toolName="Magic 8-Ball"
h1Title="Magic 8-Ball online fragen zufällige Antworten erhalten"
introText="Der Magic 8-Ball beantwortet deine Frage mit einer zufälligen Antwort aus einer anpassbaren Liste. Stelle deine Frage, schüttle den Ball per Klick und erhalte sofort eine Antwort kostenlos, ohne Anmeldung, direkt im Browser."
steps={STEPS}
examples={EXAMPLES}
vorteileText={VORTEILE}
additionalContent={ADDITIONAL_CONTENT}
faqs={FAQS}
updatedDate={UPDATED}
>
<Magic8Ball />
</SubpageLayout>
</>
);
}

150
app/muenze-werfen/page.tsx Normal file
View File

@@ -0,0 +1,150 @@
import type { Metadata } from "next";
import Link from "next/link";
import CoinFlip from "@/components/tools/coin-flip";
import SubpageLayout from "@/components/subpage-layout";
const SITE_URL = "https://entscheidomat.com";
const PAGE_URL = `${SITE_URL}/muenze-werfen`;
const PAGE_TITLE = "Münze werfen online Kopf oder Zahl entscheiden";
const PAGE_DESCRIPTION =
"Münze online werfen: Die Online-Münze simuliert Kopf oder Zahl mit 3D-Animation und frei beschriftbaren Seiten. Kostenlos und ohne Anmeldung.";
const UPDATED = "23. Juli 2026";
export const metadata: Metadata = {
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
alternates: { canonical: "/muenze-werfen" },
openGraph: { type: "website", locale: "de_DE", siteName: "Entscheidomat", title: PAGE_TITLE, description: PAGE_DESCRIPTION, url: PAGE_URL, images: ["/opengraph-image"] },
twitter: { card: "summary_large_image", title: PAGE_TITLE, description: PAGE_DESCRIPTION, images: ["/twitter-image"] },
};
const FAQS = [
{
q: "Wie entsteht das Ergebnis beim Online-Münzwurf?",
a: "Bei jedem Wurf erzeugt das Tool im Browser einen neuen Zufallswert und zeigt eine der beiden Seiten an. Der Münzwurf ist für spielerische Alltagsentscheidungen gedacht, nicht für verbindliche oder sicherheitskritische Zwecke.",
},
{
q: "Kann ich die Seiten der Münze umbenennen?",
a: "Ja, du kannst beliebige Texte statt „Kopf“ und „Zahl“ verwenden, z. B. „Ja“/„Nein“ oder „Ich“/„Du“ (bis 14 Zeichen pro Seite).",
},
{
q: "Wird mein Wurfverlauf gespeichert?",
a: "Der Verlauf der letzten 10 Würfe wird nur während der aktuellen Sitzung angezeigt. Deine Beschriftungen selbst bleiben dauerhaft lokal im Browser gespeichert.",
},
{
q: "Funktioniert der Münzwurf auch ohne Ton?",
a: "Ja, Sound ist optional und über den Lautsprecher-Button in der Navigation jederzeit abschaltbar.",
},
{
q: "Ersetzt der Münzwurf eine wichtige Entscheidung?",
a: "Nein. Er ist für spielerische Alltagsentscheidungen gedacht nicht für wichtige persönliche, finanzielle oder rechtliche Entscheidungen.",
},
];
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Entscheidomat", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "Münze werfen", item: PAGE_URL },
],
},
{
"@type": "WebApplication",
name: "Münze werfen",
url: PAGE_URL,
description: PAGE_DESCRIPTION,
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
const STEPS = [
"Passe optional die Beschriftung von Vorder- und Rückseite an (bis 14 Zeichen).",
"Tippe auf die Münze oder den Button „Münze werfen“.",
"Die Münze dreht sich in 3D und landet zufällig auf einer Seite.",
"Das Ergebnis erscheint direkt darunter, der Verlauf zeigt die letzten 10 Würfe.",
];
const EXAMPLES = [
"„Wer beginnt das Spiel?“",
"„Kino oder Couch?“",
"Klassisches Kopf-oder-Zahl bei Alltagsentscheidungen",
];
const VORTEILE = (
<p>
Schnell und unkompliziert für binäre Entscheidungen, die freie Beschriftung macht
die Münze vielseitig einsetzbar. Für Entscheidungen mit mehr als zwei Optionen eignet sich eher das{" "}
<Link href="/gluecksrad" className="text-[#c97d60] font-medium hover:underline underline-offset-2">
Glücksrad
</Link>{" "}
oder der{" "}
<Link href="/wuerfel-online" className="text-[#c97d60] font-medium hover:underline underline-offset-2">
Würfel
</Link>
.
</p>
);
const ADDITIONAL_CONTENT = (
<article className="space-y-4 text-base leading-relaxed text-ink-soft">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Münze online werfen oder Ja-Nein-Generator?
</h2>
<p>
Beide Tools helfen bei einer Entscheidung zwischen zwei gleichwertigen Optionen. Wenn
du klassisch Kopf oder Zahl wählen oder die beiden Seiten selbst beschriften möchtest,
passt die Online-Münze. Für eine formulierte Frage mit einer direkten Antwort eignet
sich der <Link href="/ja-nein-generator" className="text-[#c97d60] font-medium hover:underline underline-offset-2">Ja-Nein-Generator</Link> besser.
</p>
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Für welche Entscheidungen eignet sich ein Münzwurf?
</h2>
<p>
Einen Münzwurf kannst du nutzen, um eine kleine Pattsituation aufzulösen: Wer beginnt,
Kino oder Couch, bestellen oder kochen. Bei mehr als zwei Optionen ist ein
<Link href="/gluecksrad" className="text-[#c97d60] font-medium hover:underline underline-offset-2"> Glücksrad</Link> übersichtlicher. Für wichtige persönliche, finanzielle, rechtliche oder gesundheitliche Entscheidungen sollte Zufall keine Grundlage sein.
</p>
</article>
);
export default function MuenzeWerfenPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<SubpageLayout
toolSlug="muenze-werfen"
toolName="Münze werfen"
h1Title="Münze werfen online Kopf oder Zahl entscheiden"
introText="Wirf eine Münze online und entscheide zwischen Kopf oder Zahl. Beschrifte beide Seiten frei, starte den Münzwurf per Klick und erhalte sofort ein zufälliges Ergebnis kostenlos und ohne Anmeldung."
steps={STEPS}
examples={EXAMPLES}
vorteileText={VORTEILE}
additionalContent={ADDITIONAL_CONTENT}
faqs={FAQS}
updatedDate={UPDATED}
>
<CoinFlip />
</SubpageLayout>
</>
);
}

143
app/namen-auslosen/page.tsx Normal file
View File

@@ -0,0 +1,143 @@
import type { Metadata } from "next";
import Link from "next/link";
import ListPicker from "@/components/tools/list-picker";
import SubpageLayout from "@/components/subpage-layout";
const SITE_URL = "https://entscheidomat.com";
const PAGE_URL = `${SITE_URL}/namen-auslosen`;
const PAGE_TITLE = "Zufallsgenerator Namen Namen online auslosen";
const PAGE_DESCRIPTION =
"Mit dem Zufallsgenerator für Namen ziehst du Namen oder Optionen zufällig aus deiner Liste für Gewinner, Teams und Aufgaben. Kostenlos und ohne Anmeldung.";
const UPDATED = "23. Juli 2026";
export const metadata: Metadata = {
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
alternates: { canonical: "/namen-auslosen" },
openGraph: { type: "website", locale: "de_DE", siteName: "Entscheidomat", title: PAGE_TITLE, description: PAGE_DESCRIPTION, url: PAGE_URL, images: ["/opengraph-image"] },
twitter: { card: "summary_large_image", title: PAGE_TITLE, description: PAGE_DESCRIPTION, images: ["/twitter-image"] },
};
const FAQS = [
{
q: "Wie viele Namen kann ich eintragen?",
a: "Beliebig viele mindestens 2 sind nötig. Ein Name bzw. Eintrag pro Zeile in das Textfeld eintragen.",
},
{
q: "Was bedeutet „Gezogene Einträge entfernen“?",
a: "Ist die Option aktiviert, wird ein gezogener Eintrag automatisch aus der Liste entfernt (Ziehung ohne Zurücklegen) praktisch für Teameinteilungen oder mehrere Gewinner nacheinander.",
},
{
q: "Werden meine Namen gespeichert oder an einen Server übertragen?",
a: "Deine Liste wird ausschließlich lokal im localStorage deines Browsers gespeichert, nicht an uns übertragen.",
},
{
q: "Kann ich Duplikate in der Liste verwenden?",
a: "Technisch ja ein doppelt eingetragener Name kann dadurch häufiger berücksichtigt werden. Wenn alle Namen gleich behandelt werden sollen, trage jeden Namen nur einmal ein.",
},
{
q: "Eignet sich das Tool für offizielle Gewinnspiele?",
a: "Für lockere, private Ziehungen ist es gut geeignet. Bei rechtlich bindenden Gewinnspielen solltest du eigene Teilnahmebedingungen und mögliche Dokumentationspflichten separat prüfen.",
},
];
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Entscheidomat", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "Namen-Auslosung", item: PAGE_URL },
],
},
{
"@type": "WebApplication",
name: "Namen-Auslosung",
url: PAGE_URL,
description: PAGE_DESCRIPTION,
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
const STEPS = [
"Trage mindestens 2 Namen oder Einträge ein, einen pro Zeile.",
"Aktiviere optional „Gezogene Einträge entfernen“, um ohne Zurücklegen zu ziehen (z. B. für Teameinteilung).",
"Tippe auf „Zufällig ziehen“.",
"Der gezogene Name erscheint groß im Ergebnisfeld, inklusive Konfetti.",
];
const EXAMPLES = [
"Gewinner unter Teilnehmenden auslosen",
"Reihenfolge für eine Präsentation festlegen",
"Teams zufällig zusammenstellen (mehrfach ziehen „ohne Zurücklegen“)",
];
const VORTEILE = (
<p>
Beliebig viele Einträge möglich, flexibel für Namen oder andere Texte. Die Option
ohne Zurücklegen macht wiederholtes Ziehen für Gruppenaufteilungen praktisch. Die
Ziehung nutzt einen browserbasierten Zufallswert eine Gewichtung einzelner Einträge ist nicht möglich.
</p>
);
const ADDITIONAL_CONTENT = (
<article className="space-y-4 text-base leading-relaxed text-ink-soft">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Namen zufällig ziehen für Gewinner, Teams und Reihenfolgen
</h2>
<p>
Der Zufallsgenerator für Namen zieht einen Eintrag aus deiner eigenen Liste. Damit
kannst du einen Gewinner bestimmen, eine Reihenfolge für Präsentationen festlegen oder
Personen fair nacheinander auswählen. Für mehrere Ziehungen aktiviere Gezogene
Einträge entfernen: Dann wird jeder Name nur einmal gezogen.
</p>
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
So bleibt eine Namen-Auslosung fair
</h2>
<p>
Trage jeden Namen genau einmal ein, wenn alle die gleiche Chance erhalten sollen.
Doppelte Einträge erhöhen die Chance dieses Namens. Für eine Entscheidung zwischen
festen Optionen ist das <Link href="/gluecksrad" className="text-[#c97d60] font-medium hover:underline underline-offset-2">Glücksrad</Link> eine gute Alternative; für eine zufällige Zahl nutze den <Link href="/zufallszahl-generator" className="text-[#c97d60] font-medium hover:underline underline-offset-2">Zufallszahl-Generator</Link>.
</p>
</article>
);
export default function NamenAuslosenPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<SubpageLayout
toolSlug="namen-auslosen"
toolName="Namen-Auslosung"
h1Title="Zufallsgenerator für Namen Namen zufällig auslosen"
introText="Der Zufallsgenerator für Namen zieht einen Eintrag aus deiner eigenen Liste. Trage Namen oder Optionen ein, starte die Ziehung und erhalte sofort ein zufälliges Ergebnis ideal für Gewinner, Teameinteilungen oder Aufgabenverteilung."
steps={STEPS}
examples={EXAMPLES}
vorteileText={VORTEILE}
additionalContent={ADDITIONAL_CONTENT}
faqs={FAQS}
updatedDate={UPDATED}
>
<ListPicker />
</SubpageLayout>
</>
);
}

17
app/opengraph-image.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { OgImageContent, ogImageSize } from "@/lib/og-image";
export const alt = "Entscheidomat Lass den Zufall entscheiden";
export const size = ogImageSize;
export const contentType = "image/png";
export const runtime = "nodejs";
export default async function Image() {
const image = await readFile(join(process.cwd(), "public/images/og-decision-tools.jpg"));
const visualSrc = `data:image/jpeg;base64,${image.toString("base64")}`;
return new ImageResponse(<OgImageContent visualSrc={visualSrc} />, size);
}

68
app/page.tsx Normal file
View File

@@ -0,0 +1,68 @@
import type { Metadata } from "next";
import Nav from "@/components/nav";
import Hero from "@/components/hero";
import DirectAnswer from "@/components/direct-answer";
import Playground from "@/components/playground";
import Features from "@/components/features";
import HomepageFaq, { HOMEPAGE_FAQS } from "@/components/homepage-faq";
import Footer from "@/components/footer";
import DeferredScrollFlourishes from "@/components/deferred-scroll-flourishes";
const SITE_URL = "https://entscheidomat.com";
export const metadata: Metadata = {
title: {
absolute: "Entscheidomat Kostenloser Zufallsgenerator & Entscheidungshilfe",
},
description:
"Kostenloser Zufallsgenerator und Entscheidungshilfe für schnelle Alltagsentscheidungen: Ja-Nein-Generator, Glücksrad, Würfel, Münzwurf und mehr direkt im Browser.",
alternates: { canonical: "/" },
};
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
name: "Entscheidomat",
url: SITE_URL,
description:
"Kostenloser Zufallsgenerator und digitale Entscheidungshilfe mit sieben Tools: Ja-Nein-Generator, Münze, Glücksrad, Würfel, Zufallszahl, Magic 8-Ball und Listen-Auslosung.",
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: HOMEPAGE_FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
export default function Home() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<Nav />
<main className="flex-1">
<Hero />
<DirectAnswer />
<Playground />
<Features />
<HomepageFaq />
</main>
<Footer />
<DeferredScrollFlourishes />
</>
);
}

13
app/robots.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { MetadataRoute } from "next";
const SITE_URL = "https://entscheidomat.com";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}

33
app/sitemap.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { MetadataRoute } from "next";
import { TOOL_PAGES } from "@/lib/tool-pages";
const SITE_URL = "https://entscheidomat.com";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: SITE_URL,
lastModified: "2026-07-23",
changeFrequency: "weekly",
priority: 1,
},
...TOOL_PAGES.map((t) => ({
url: `${SITE_URL}/${t.slug}`,
lastModified: t.lastModified,
changeFrequency: "monthly" as const,
priority: 0.8,
})),
{
url: `${SITE_URL}/impressum`,
lastModified: "2026-07-23",
changeFrequency: "yearly",
priority: 0.2,
},
{
url: `${SITE_URL}/datenschutz`,
lastModified: "2026-07-23",
changeFrequency: "yearly",
priority: 0.2,
},
];
}

17
app/twitter-image.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { OgImageContent, ogImageSize } from "@/lib/og-image";
export const alt = "Entscheidomat Lass den Zufall entscheiden";
export const size = ogImageSize;
export const contentType = "image/png";
export const runtime = "nodejs";
export default async function Image() {
const image = await readFile(join(process.cwd(), "public/images/og-decision-tools.jpg"));
const visualSrc = `data:image/jpeg;base64,${image.toString("base64")}`;
return new ImageResponse(<OgImageContent visualSrc={visualSrc} />, size);
}

165
app/wuerfel-online/page.tsx Normal file
View File

@@ -0,0 +1,165 @@
import type { Metadata } from "next";
import Link from "next/link";
import DiceRoller from "@/components/tools/dice-roller";
import SubpageLayout from "@/components/subpage-layout";
const SITE_URL = "https://entscheidomat.com";
const PAGE_URL = `${SITE_URL}/wuerfel-online`;
const PAGE_TITLE = "Würfel online virtuellen 6-seitigen Würfel werfen";
const PAGE_DESCRIPTION =
"Würfel online direkt im Browser: mit klassischen Augen oder eigenen Texten auf sechs Seiten würfeln. Kostenlos, ohne Anmeldung und sofort nutzbar.";
const UPDATED = "23. Juli 2026";
export const metadata: Metadata = {
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
alternates: { canonical: "/wuerfel-online" },
openGraph: { type: "website", locale: "de_DE", siteName: "Entscheidomat", title: PAGE_TITLE, description: PAGE_DESCRIPTION, url: PAGE_URL, images: ["/opengraph-image"] },
twitter: { card: "summary_large_image", title: PAGE_TITLE, description: PAGE_DESCRIPTION, images: ["/twitter-image"] },
};
const FAQS = [
{
q: "Wie entsteht das Ergebnis beim Online-Würfel?",
a: "Bei jedem Wurf erzeugt das Tool im Browser einen neuen Zufallswert und zeigt eine Würfelseite an. Der Online-Würfel ist für Spiele und spielerische Alltagsentscheidungen gedacht, nicht für verbindliche oder sicherheitskritische Zwecke.",
},
{
q: "Kann ich eigene Texte statt Zahlen verwenden?",
a: "Ja, im Modus „Eigene Texte“ kannst du bis zu 18 Zeichen pro Würfelseite eintragen z. B. für Aufgaben oder Optionen.",
},
{
q: "Werden meine eigenen Würfeltexte gespeichert?",
a: "Ja, lokal im Browser (localStorage). Sie werden nicht an einen Server übertragen und bleiben beim nächsten Besuch erhalten.",
},
{
q: "Kann ich mehrere Würfel gleichzeitig werfen?",
a: "Aktuell simuliert das Tool einen Würfel pro Wurf. Für mehrere Würfe einfach mehrfach hintereinander würfeln.",
},
{
q: "Funktioniert der Würfel auch auf dem Smartphone?",
a: "Ja, das Tool ist vollständig responsiv und funktioniert auf Smartphone, Tablet und Desktop.",
},
{
q: "Wie wahrscheinlich ist jede Seite beim Online-Würfel?",
a: "Bei einem klassischen sechsseitigen Würfel hat jede Zahl von 1 bis 6 bei jedem einzelnen Wurf die gleiche Chance von einem Sechstel. Ein Wurf beeinflusst den nächsten nicht.",
},
{
q: "Wann nutze ich einen Würfel statt Glücksrad oder Ja-Nein-Generator?",
a: "Der Würfel passt für bis zu sechs gleichwertige Optionen, für Brettspiele oder für eigene Aufgaben auf sechs Seiten. Bei zwei Optionen ist der Ja-Nein-Generator schneller; bei mehr als sechs Optionen bietet das Glücksrad mehr Platz und Übersicht.",
},
];
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Entscheidomat", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "Würfel Online", item: PAGE_URL },
],
},
{
"@type": "WebApplication",
name: "Würfel Online",
url: PAGE_URL,
description: PAGE_DESCRIPTION,
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
const STEPS = [
"Wähle zwischen „Zahlen 16“ oder „Eigene Texte“.",
"Bei eigenen Texten: trage bis zu 18 Zeichen pro Seite ein.",
"Tippe auf „Würfeln!“ der Würfel dreht sich in 3D und landet auf einer Seite.",
"Das Ergebnis erscheint direkt darunter.",
];
const EXAMPLES = [
"„Welche Aufgabe mache ich zuerst?“ (6 Aufgaben als eigene Texte)",
"Ersatzwürfel für Brett- und Gesellschaftsspiele",
"Zufallsentscheidung zwischen 6 gleichwertigen Optionen",
];
const VORTEILE = (
<p>
Praktisch als Ersatz für einen echten Würfel, jederzeit griffbereit im Browser.
Für mehr als 6 Optionen eignet sich eher das{" "}
<Link href="/gluecksrad" className="text-[#c97d60] font-medium hover:underline underline-offset-2">
Glücksrad
</Link>{" "}
oder die Listen-Auswahl.
</p>
);
const ADDITIONAL_CONTENT = (
<article className="space-y-4 text-base leading-relaxed text-ink-soft">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Online würfeln: Zufallszahl, Spielhilfe oder sechs Optionen wählen
</h2>
<p>
Ein Online-Würfel simuliert einen klassischen sechsseitigen Würfel direkt im
Browser. Bei jedem Wurf erzeugt das Tool einen neuen browserbasierten Zufallswert und
wählt damit eine Zahl von 1 bis 6 aus. Das macht den virtuellen
Würfel zu einem praktischen Ersatz, wenn kein echter Würfel zur Hand ist etwa
bei Brettspielen, unterwegs oder beim spontanen Entscheiden in einer Gruppe.
</p>
<p>
Besonders flexibel wird der Würfel mit eigenen Texten. Statt Zahlen kannst du
sechs Aufgaben, Aktivitäten oder Namen eintragen und eine Option auslosen:
kochen, aufräumen, spazieren, spielen, lesen oder Pause. Für eine
gleich behandelte Auswahl sollte jeder Eintrag nur auf einer Seite stehen. Sind die Optionen
unterschiedlich wichtig oder sollen manche öfter vorkommen, bildet ein einfacher
Würfel diese Gewichtung nicht ab. Dann solltest du die Auswahl vorher bewusst
festlegen statt ein zufälliges Ergebnis als objektiv zu behandeln.
</p>
<p>
Der Würfel eignet sich am besten für bis zu sechs gleichwertige Möglichkeiten.
Für genau zwei Antworten liefern der Ja-Nein-Generator oder ein Münzwurf ein
klareres Ergebnis. Für sieben bis zwölf Optionen ist ein Glücksrad besser
lesbar, und für lange Namenslisten hilft die Namen-Auslosung. Wie bei allen
Zufallstools gilt: Für Spiele und kleine Alltagsfragen ist der Würfel ideal;
Entscheidungen mit gesundheitlichen, rechtlichen oder finanziellen Folgen
solltest du nicht an einen Zufallswert delegieren.
</p>
</article>
);
export default function WuerfelOnlinePage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<SubpageLayout
toolSlug="wuerfel-online"
toolName="Würfel Online"
h1Title="Würfel online werfen dein virtueller 6-seitiger Würfel"
introText="Der virtuelle Würfel simuliert einen echten 6-seitigen Würfel direkt im Browser. Wähle zwischen klassischen Würfelaugen oder eigenen Texten auf den sechs Seiten und würfle mit einem Klick kostenlos und ohne Anmeldung."
steps={STEPS}
examples={EXAMPLES}
vorteileText={VORTEILE}
additionalContent={ADDITIONAL_CONTENT}
faqs={FAQS}
updatedDate={UPDATED}
>
<DiceRoller />
</SubpageLayout>
</>
);
}

View File

@@ -0,0 +1,149 @@
import type { Metadata } from "next";
import Link from "next/link";
import RandomNumber from "@/components/tools/random-number";
import SubpageLayout from "@/components/subpage-layout";
const SITE_URL = "https://entscheidomat.com";
const PAGE_URL = `${SITE_URL}/zufallszahl-generator`;
const PAGE_TITLE = "Zufallszahl-Generator Zahl im Wunschbereich erzeugen";
const PAGE_DESCRIPTION =
"Zufallszahl-Generator mit frei wählbarem Zahlenbereich. Minimum und Maximum eintragen, generieren, Ergebnis erhalten kostenlos, ohne Anmeldung.";
const UPDATED = "23. Juli 2026";
export const metadata: Metadata = {
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
alternates: { canonical: "/zufallszahl-generator" },
openGraph: { type: "website", locale: "de_DE", siteName: "Entscheidomat", title: PAGE_TITLE, description: PAGE_DESCRIPTION, url: PAGE_URL, images: ["/opengraph-image"] },
twitter: { card: "summary_large_image", title: PAGE_TITLE, description: PAGE_DESCRIPTION, images: ["/twitter-image"] },
};
const FAQS = [
{
q: "Kann ich auch negative Zahlen oder sehr große Bereiche eingeben?",
a: "Ja, Minimum und Maximum sind frei wählbare Zahlen. Die Reihenfolge der Eingabe spielt keine Rolle, das Tool sortiert automatisch.",
},
{
q: "Werden auch Dezimalzahlen unterstützt?",
a: "Nein, aktuell erzeugt der Generator ausschließlich ganze Zahlen innerhalb des gewählten Bereichs.",
},
{
q: "Wie entsteht die Zufallszahl?",
a: "Bei jeder Anfrage erzeugt das Tool im Browser einen neuen Zufallswert und leitet daraus eine ganze Zahl im gewählten Bereich ab. Der Generator ist für spielerische und alltägliche Zwecke gedacht, nicht für sicherheitskritische oder verbindliche Anwendungen.",
},
{
q: "Werden meine Einstellungen für Minimum und Maximum gespeichert?",
a: "Ja, lokal im localStorage deines Browsers. Sie werden nicht an einen Server übertragen.",
},
{
q: "Kann ich mehrere Zahlen hintereinander generieren?",
a: "Ja, beliebig oft einfach erneut auf „Zahl generieren“ tippen.",
},
];
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Entscheidomat", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "Zufallszahl-Generator", item: PAGE_URL },
],
},
{
"@type": "WebApplication",
name: "Zufallszahl-Generator",
url: PAGE_URL,
description: PAGE_DESCRIPTION,
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any (Web-Browser)",
isPartOf: { "@id": `${SITE_URL}/#website` },
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
},
{
"@type": "FAQPage",
mainEntity: FAQS.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
},
],
};
const STEPS = [
"Trage Minimum und Maximum ein die Reihenfolge ist egal.",
"Tippe auf „Zahl generieren“.",
"Eine Zufallszahl im gewählten Bereich erscheint mit kurzer Animation.",
"Passe den Zahlenraum jederzeit an und generiere erneut.",
];
const EXAMPLES = [
"Losnummern für eine Verlosung ziehen",
"„Wie viele Liegestütze mache ich?“",
"Zufällige Teamnummer oder Startreihenfolge vergeben",
];
const VORTEILE = (
<p>
Beliebiger Zahlenbereich möglich, auch mit negativen Zahlen. Der Generator erzeugt
ausschließlich ganze Zahlen für Entscheidungen zwischen benannten Optionen eignen sich eher das{" "}
<Link href="/gluecksrad" className="text-[#c97d60] font-medium hover:underline underline-offset-2">
Glücksrad
</Link>{" "}
oder die{" "}
<Link href="/namen-auslosen" className="text-[#c97d60] font-medium hover:underline underline-offset-2">
Namen-Auslosung
</Link>
.
</p>
);
const ADDITIONAL_CONTENT = (
<article className="space-y-4 text-base leading-relaxed text-ink-soft">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Zufallszahl im gewünschten Bereich erzeugen
</h2>
<p>
Lege Minimum und Maximum fest, um eine ganze Zufallszahl aus genau diesem Bereich zu
erhalten. Das ist praktisch für Startnummern, kleine Spielaufgaben oder eine zufällige
Reihenfolge. Negative Zahlen sind ebenfalls möglich; die Reihenfolge deiner Eingaben
spielt keine Rolle.
</p>
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink">
Wann passt eine Zufallszahl und wann ein anderes Tool?
</h2>
<p>
Nutze diesen Generator, wenn das Ergebnis eine Zahl sein soll. Möchtest du stattdessen
Namen oder Texte ziehen, ist die <Link href="/namen-auslosen" className="text-[#c97d60] font-medium hover:underline underline-offset-2">Namen-Auslosung</Link> passender. Bei mehreren sichtbaren Optionen hilft ein <Link href="/gluecksrad" className="text-[#c97d60] font-medium hover:underline underline-offset-2">Glücksrad</Link> dabei, die Auswahl gemeinsam nachzuvollziehen.
</p>
</article>
);
export default function ZufallszahlGeneratorPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<SubpageLayout
toolSlug="zufallszahl-generator"
toolName="Zufallszahl-Generator"
h1Title="Zufallszahl-Generator Zahl im Wunschbereich erzeugen"
introText="Der Zufallszahl-Generator erzeugt eine zufällige Ganzzahl in einem selbst gewählten Bereich. Trage Minimum und Maximum ein, generiere die Zahl per Klick und erhalte sofort ein zufälliges Ergebnis kostenlos, ohne Anmeldung, direkt im Browser."
steps={STEPS}
examples={EXAMPLES}
vorteileText={VORTEILE}
additionalContent={ADDITIONAL_CONTENT}
faqs={FAQS}
updatedDate={UPDATED}
>
<RandomNumber />
</SubpageLayout>
</>
);
}

74
components/coin-3d.tsx Normal file
View File

@@ -0,0 +1,74 @@
"use client";
import { forwardRef } from "react";
type Coin3DProps = {
frontLabel: string;
backLabel: string;
/** Kantenlänge in px */
size?: number;
/** Endlos-Idle-Drehung (Hero) */
spin?: boolean;
/**
* Achse, um die die Münze rotiert. Bestimmt, wie die Rückseite
* vorgedreht wird, damit ihr Text lesbar bleibt.
* "x" = Flip um die X-Achse (Tool), "y" = Drehung um die Y-Achse (Hero).
*/
spinAxis?: "x" | "y";
className?: string;
};
/** Gestapelte Kreisscheiben erzeugen die Münzdicke. */
const EDGE_LAYERS = [-3, -2, -1, 0, 1, 2, 3];
/**
* Echte 3D-CSS-Münze. Der forwarded ref zeigt auf das rotierende
* Innen-Element, damit GSAP `rotationX` flippen kann.
*/
const Coin3D = forwardRef<HTMLDivElement, Coin3DProps>(function Coin3D(
{ frontLabel, backLabel, size = 168, spin = false, spinAxis = "x", className = "" },
ref,
) {
return (
<div
ref={ref}
className={`coin ${spin ? "animate-coin-spin" : ""} ${className}`}
style={{ width: size, height: size }}
>
{EDGE_LAYERS.map((z) => (
<div
key={z}
aria-hidden
className="coin-edge"
style={{ transform: `translateZ(${z}px)` }}
/>
))}
<div className="coin-face coin-face--front">
<div className="coin-engraving" aria-hidden />
<div className="coin-engraving coin-engraving--inner" aria-hidden />
<span className="coin-label" style={{ fontSize: size * 0.1 }}>
{frontLabel}
</span>
</div>
<div
className="coin-face coin-face--back"
style={{
transform:
spinAxis === "y"
? "rotateY(180deg) translateZ(4px)"
: "rotateX(180deg) translateZ(4px)",
}}
>
<div className="coin-engraving" aria-hidden />
<div className="coin-engraving coin-engraving--inner" aria-hidden />
<span className="coin-label" style={{ fontSize: size * 0.1 }}>
{backLabel}
</span>
</div>
</div>
);
});
export default Coin3D;

View File

@@ -0,0 +1,26 @@
"use client";
import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
const ScrollFlourishes = dynamic(() => import("@/components/scroll-flourishes"), {
ssr: false,
});
/** Die dekorativen, aufwändigen Ranken sind nur auf größeren Displays sinnvoll. */
export default function DeferredScrollFlourishes() {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const query = window.matchMedia(
"(min-width: 768px) and (prefers-reduced-motion: no-preference)",
);
const update = () => setEnabled(query.matches);
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
return enabled ? <ScrollFlourishes /> : null;
}

View File

@@ -0,0 +1,21 @@
export default function DirectAnswer() {
return (
<section className="relative py-4 sm:py-8">
<div className="mx-auto w-full max-w-3xl px-5 sm:px-8">
<div className="rounded-3xl border border-line bg-surface/80 p-7 text-center shadow-soft sm:p-9">
<h2 className="font-display text-xl font-semibold tracking-tight text-ink">
Was ist Entscheidomat?
</h2>
<p className="mt-3 text-base leading-relaxed text-ink-soft">
Entscheidomat ist ein kostenloser Zufallsgenerator und eine
digitale Entscheidungshilfe. Du kannst online eine Münze werfen,
ein Glücksrad drehen, würfeln, eine Zufallszahl erzeugen, den
Magic 8-Ball fragen, eine eigene Liste auslosen oder den
Ja-Nein-Generator befragen ganz ohne Anmeldung und direkt im
Browser.
</p>
</div>
</div>
</section>
);
}

76
components/features.tsx Normal file
View File

@@ -0,0 +1,76 @@
import {
ShieldCheck,
SlidersHorizontal,
Smartphone,
Sparkles,
} from "lucide-react";
import Reveal from "@/components/reveal";
const FEATURES = [
{
icon: SlidersHorizontal,
title: "Komplett anpassbar",
text: "Beschriftungen, Optionen und Antworten alles passt sich deiner Entscheidung an und bleibt automatisch gespeichert.",
tint: "bg-[#e6f7f0] text-[#0d7a57]",
},
{
icon: ShieldCheck,
title: "Kein Account nötig",
text: "Kein Login, keine Werbung, kein Schnickschnack. Öffnen, werfen, entscheiden fertig.",
tint: "bg-[#e6f7f0] text-[#0d7a57]",
},
{
icon: Smartphone,
title: "Überall dabei",
text: "Ob Handy, Tablet oder Desktop: Der Entscheidomat fühlt sich auf jedem Gerät butterweich an.",
tint: "bg-[#e6f7f0] text-[#0d7a57]",
},
{
icon: Sparkles,
title: "Liebe zum Detail",
text: "Physikalische Spins, feine 3D-Animationen und Soundeffekte machen jede Entscheidung zum kleinen Erlebnis.",
tint: "bg-[#e6f7f0] text-[#0d7a57]",
},
];
export default function Features() {
return (
<section id="features" className="relative scroll-mt-20 py-24 sm:py-32">
<div className="mx-auto w-full max-w-6xl px-5 sm:px-8">
<Reveal className="mx-auto max-w-2xl text-center">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-ink-soft">
Warum Entscheidomat?
</p>
<h2 className="mt-3 text-4xl font-semibold tracking-[-0.02em] sm:text-5xl">
Weil Entscheiden auch{" "}
<em className="font-display italic text-[#0d7a57]">schön</em> sein kann.
</h2>
<p className="mt-4 text-lg leading-relaxed text-ink-soft">
Keine langweiligen Standard-Tools mehr. Hier bekommst du Zufall mit
Stil und trotzdem blitzschnell und ohne Hürden.
</p>
</Reveal>
<div className="mt-14 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{FEATURES.map((feature, i) => (
<Reveal key={feature.title} delay={i * 0.08}>
<div className="group h-full rounded-3xl border border-line bg-surface p-7 shadow-soft transition-all duration-300 hover:-translate-y-1.5 hover:shadow-lift">
<div
className={`flex h-12 w-12 items-center justify-center rounded-2xl ${feature.tint} transition-transform duration-300 group-hover:scale-110 group-hover:-rotate-6`}
>
<feature.icon className="h-6 w-6" strokeWidth={2.1} />
</div>
<h3 className="mt-5 font-display text-lg font-semibold tracking-tight">
{feature.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-ink-soft">
{feature.text}
</p>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}

30
components/footer.tsx Normal file
View File

@@ -0,0 +1,30 @@
import Link from "next/link";
import { Coins } from "lucide-react";
export default function Footer() {
return (
<footer className="border-t border-line bg-surface/60">
<div className="mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 px-5 py-8 text-sm text-ink-soft sm:flex-row sm:px-8">
<div className="flex items-center gap-2">
<Coins className="h-4 w-4 text-[#0d7a57]" />
<span className="font-display font-semibold text-ink">
Entscheid<span className="italic text-[#0d7a57]">omat</span>
</span>
<span aria-hidden>·</span>
<span>© {new Date().getFullYear()}</span>
</div>
<p>
Gebaut für alle, die sich (manchmal) nicht entscheiden können.
</p>
<nav aria-label="Rechtliches" className="flex items-center gap-4">
<Link href="/impressum" className="transition-colors hover:text-[#0d7a57]">
Impressum
</Link>
<Link href="/datenschutz" className="transition-colors hover:text-[#0d7a57]">
Datenschutz
</Link>
</nav>
</div>
</footer>
);
}

327
components/hero.tsx Normal file
View File

@@ -0,0 +1,327 @@
"use client";
import { useEffect, useRef, useState } from "react";
import {
motion,
useMotionValue,
useScroll,
useSpring,
useTransform,
} from "framer-motion";
import { ArrowDown } from "lucide-react";
import { gsap } from "@/lib/gsap";
export default function Hero() {
const sectionRef = useRef<HTMLElement>(null);
const { scrollY } = useScroll();
// Scroll-Transformationen (Reversibel)
const ringY = useTransform(scrollY, [0, 1000], [0, 180]);
const ringScale = useTransform(scrollY, [0, 1000], [1, 1.18]);
const ringOpacity = useTransform(scrollY, [0, 400, 1000, 1500], [1, 1, 0.5, 0]);
const ringRotate = useTransform(scrollY, [0, 1000], [0, 10]);
// Interaktive Cursor-Bewegung (Subtile Spring-Physik für elegantes Hovering)
const rawMouseX = useMotionValue(0);
const rawMouseY = useMotionValue(0);
const smoothMouseX = useSpring(rawMouseX, { stiffness: 60, damping: 28 });
const smoothMouseY = useSpring(rawMouseY, { stiffness: 60, damping: 28 });
// Subtile magnetische Linienverschiebung
const magnetX = useTransform(smoothMouseX, [-0.5, 0.5], [-10, 10]);
const magnetY = useTransform(smoothMouseY, [-0.5, 0.5], [-8, 8]);
// SVG-Glow-Position (in viewBox 1550 x 950 Koordinaten)
const [svgGlow, setSvgGlow] = useState({ x: 775, y: 475 });
const handleMouseMove = (e: React.MouseEvent<HTMLElement>) => {
const rect = sectionRef.current?.getBoundingClientRect();
if (!rect) return;
// Normalisierte Mauskoordinaten (-0.5 bis 0.5)
const normX = (e.clientX - rect.left) / rect.width - 0.5;
const normY = (e.clientY - rect.top) / rect.height - 0.5;
rawMouseX.set(normX);
rawMouseY.set(normY);
// Umrechnung auf SVG-ViewBox-Koordinaten für das sanfte Aufleuchten
const svgX = Math.round(((e.clientX - rect.left) / rect.width) * 1550);
const svgY = Math.round(((e.clientY - rect.top) / rect.height) * 950);
setSvgGlow({ x: svgX, y: svgY });
};
// Text-Intro startet erst, wenn der Preloader das "app:reveal"-Event feuert
useEffect(() => {
// Der Hero ist das LCP-Element. Auf Mobilgeräten bleibt er deshalb sofort
// sichtbar, statt seine endgültige Darstellung durch eine Timeline zu verzögern.
if (window.matchMedia("(max-width: 767px)").matches) return;
let ctx: gsap.Context | undefined;
let fallback: ReturnType<typeof setTimeout> | undefined;
const start = () => {
if (ctx) return;
ctx = gsap.context(() => {
gsap.fromTo(
".hero-el",
{ y: 34, opacity: 0 },
{
y: 0,
opacity: 1,
duration: 1,
stagger: 0.1,
ease: "power3.out",
},
);
}, sectionRef);
};
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
window.addEventListener("app:reveal", start, { once: true });
// Beim gecachten bzw. mobilen Preloader ist die Seite schon sichtbar,
// bevor dieser Effect registriert wird. Dann sofort animieren statt auf
// den Fallback-Timer zu warten.
if (document.documentElement.dataset.preloader !== "active") {
start();
} else {
fallback = setTimeout(start, 1200);
}
});
return () => {
mm.revert();
window.removeEventListener("app:reveal", start);
if (fallback) clearTimeout(fallback);
ctx?.revert();
};
}, []);
return (
<section
id="top"
ref={sectionRef}
onMouseMove={handleMouseMove}
className="relative flex min-h-[100svh] flex-col justify-center pt-16 cursor-default"
>
{/* Weicher Spot in der Mitte: Fadet am Rand und unten zu 100% transparent in den Seitenhintergrund aus */}
<div
aria-hidden
className="absolute inset-0 bg-[radial-gradient(ellipse_75%_55%_at_50%_35%,#ffffff_0%,transparent_100%)]"
/>
{/* Multischichtiges Ring-Design: Kreise fließen flüssig über die Sektionsgrenze nach unten */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
style={{
y: ringY,
x: magnetX,
scale: ringScale,
opacity: ringOpacity,
rotate: ringRotate,
}}
className="pointer-events-none absolute left-1/2 top-1/2 w-[min(100vw,1550px)] h-[min(115vh,1100px)] -translate-x-1/2 -translate-y-1/2"
>
<motion.div style={{ y: magnetY }} className="w-full h-full">
<svg
aria-hidden
className="w-full h-full"
viewBox="0 0 1550 1100"
fill="none"
>
<defs>
{/* Subtiler radialer Glow-Verlauf unter dem Cursor */}
<radialGradient
id="cursorLineGlow"
cx={svgGlow.x}
cy={svgGlow.y}
r="280"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor="#0d7a57" stopOpacity="0.32" />
<stop offset="50%" stopColor="#0d7a57" stopOpacity="0.12" />
<stop offset="100%" stopColor="#0d7a57" stopOpacity="0" />
</radialGradient>
</defs>
{/* 1. Basis-Ellipsen mit dezentem Ruhe-Opacity */}
<ellipse
cx="775"
cy="490"
rx="290"
ry="180"
stroke="#111412"
strokeOpacity="0.13"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="440"
ry="275"
stroke="#111412"
strokeOpacity="0.10"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="610"
ry="370"
stroke="#111412"
strokeOpacity="0.11"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="770"
ry="460"
stroke="#111412"
strokeOpacity="0.08"
strokeWidth="1"
strokeDasharray="5 5"
/>
<ellipse
cx="775"
cy="490"
rx="920"
ry="550"
stroke="#111412"
strokeOpacity="0.06"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="1080"
ry="640"
stroke="#111412"
strokeOpacity="0.05"
strokeWidth="1"
/>
{/* Dynamisch kreuzende Orbit-Bögen reichen flüssig bis in die nächste Sektion */}
<circle
cx="690"
cy="440"
r="620"
stroke="#111412"
strokeOpacity="0.07"
strokeWidth="1"
/>
<circle
cx="860"
cy="570"
r="680"
stroke="#111412"
strokeOpacity="0.06"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="1040"
ry="440"
transform="rotate(-11 775 490)"
stroke="#111412"
strokeOpacity="0.06"
strokeWidth="1"
/>
{/* Feine Horizon-Linie hinter den Buttons */}
<line
x1="390"
y1="690"
x2="1160"
y2="690"
stroke="#111412"
strokeOpacity="0.09"
strokeWidth="1"
/>
{/* 2. Subtile Aufleuchten-Ebene unter dem Cursor */}
<g stroke="url(#cursorLineGlow)" strokeWidth="1.3">
<ellipse cx="775" cy="490" rx="290" ry="180" />
<ellipse cx="775" cy="490" rx="440" ry="275" />
<ellipse cx="775" cy="490" rx="610" ry="370" />
<ellipse cx="775" cy="490" rx="770" ry="460" strokeDasharray="5 5" />
<ellipse cx="775" cy="490" rx="920" ry="550" />
<ellipse cx="775" cy="490" rx="1080" ry="640" />
<circle cx="690" cy="440" r="620" />
<circle cx="860" cy="570" r="680" />
<ellipse
cx="775"
cy="490"
rx="1040"
ry="440"
transform="rotate(-11 775 490)"
/>
<line x1="390" y1="690" x2="1160" y2="690" />
</g>
</svg>
</motion.div>
</motion.div>
<div className="bg-noise pointer-events-none absolute inset-0" aria-hidden />
<div className="relative mx-auto flex w-full max-w-5xl flex-col items-center px-5 py-16 text-center sm:px-8">
<h1 className="font-sans text-[11vw] font-medium leading-[1.02] tracking-[-0.03em] sm:text-6xl lg:text-7xl">
<span className="hero-el block">Kostenloser</span>
<span className="hero-el block font-display text-[1.06em] font-normal italic tracking-normal text-[#0d7a57]">
Zufallsgenerator
</span>
<span className="hero-el block">für schnelle Entscheidungen</span>
</h1>
<p className="hero-el mt-8 max-w-md text-base leading-relaxed text-ink-soft sm:text-lg">
Lass den Zufall entscheiden mit Münze, Glücksrad, Würfel &amp; mehr für
alle, die sich mal wieder nicht entscheiden können.
</p>
<div className="hero-el mt-10 flex flex-wrap items-center justify-center gap-3">
<motion.a
href="#tools"
whileHover={{ y: -2, scale: 1.02 }}
whileTap={{ scale: 0.97 }}
className="inline-flex items-center rounded-full bg-[#0d7a57] px-8 py-3.5 text-sm font-semibold text-white shadow-[0_12px_28px_rgba(13,122,87,0.30)] transition-colors duration-200 hover:bg-[#095c41]"
>
Jetzt entscheiden
</motion.a>
<motion.a
href="#features"
whileHover={{ y: -2 }}
whileTap={{ scale: 0.97 }}
className="inline-flex items-center rounded-full border border-ink/20 bg-surface/80 px-8 py-3.5 text-sm font-semibold text-ink transition-colors duration-200 hover:border-[#0d7a57] hover:text-[#0d7a57]"
>
Mehr erfahren
</motion.a>
</div>
<p className="hero-el mt-8 inline-flex items-center rounded-full border border-[#0d7a57]/20 bg-[#e6f7f0] px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-[#0d7a57]">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[#0d7a57] mr-2.5" />
7 Tools · Kostenlos · Ohne Anmeldung
</p>
</div>
{/* Scroll-Hinweis */}
<a
href="#tools"
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]">
Scroll
</span>
<ArrowDown className="animate-bob h-4 w-4" />
</a>
</section>
);
}

View File

@@ -0,0 +1,74 @@
import Reveal from "@/components/reveal";
export const HOMEPAGE_FAQS = [
{
q: "Was ist Entscheidomat?",
a: "Entscheidomat ist ein kostenloses Portal mit sieben Zufalls- und Entscheidungs-Tools: Ja-Nein-Generator, Münze, Glücksrad, Würfel, Zufallszahl, Magic 8-Ball und Listen-Auslosung alle direkt im Browser nutzbar.",
},
{
q: "Ist Entscheidomat wirklich kostenlos?",
a: "Ja, vollständig kostenlos. Es gibt keine versteckten Kosten und keine Pflicht-Anmeldung, um die Tools zu nutzen.",
},
{
q: "Muss ich mich registrieren?",
a: "Nein. Es ist keine Anmeldung, keine E-Mail-Adresse und kein Account nötig einfach öffnen und loslegen.",
},
{
q: "Werden meine Daten gespeichert oder weitergegeben?",
a: "Eigene Einstellungen (z. B. Beschriftungen oder Listen) werden nur lokal im localStorage deines Browsers gespeichert und nicht an einen Server übertragen. Details findest du in der Datenschutzerklärung.",
},
{
q: "Funktioniert Entscheidomat auch auf dem Handy?",
a: "Ja, alle Tools sind vollständig responsiv und funktionieren auf Smartphone, Tablet und Desktop.",
},
{
q: "Wie zufällig sind die Ergebnisse?",
a: "Jedes Tool erzeugt bei jeder Anfrage einen neuen Zufallswert, unabhängig von vorherigen Ergebnissen.",
},
{
q: "Kann ich die Tools für wichtige Entscheidungen nutzen?",
a: "Die Tools sind für spielerische Alltagsentscheidungen gedacht. Bei wichtigen persönlichen, finanziellen, rechtlichen oder gesundheitlichen Entscheidungen solltest du dich nicht auf einen Zufallsgenerator verlassen.",
},
{
q: "Gibt es Werbung auf Entscheidomat?",
a: "Aktuell zeigt Entscheidomat keine Werbung. Sollte sich das künftig ändern, wird das vorher transparent in der Datenschutzerklärung ergänzt.",
},
{
q: "Wie kann ich Feedback geben oder Kontakt aufnehmen?",
a: "Am einfachsten per E-Mail an info@entscheidomat.com die Kontaktdaten findest du auch im Impressum.",
},
];
export default function HomepageFaq() {
return (
<section id="faq" className="relative scroll-mt-20 py-20 sm:py-28">
<div className="mx-auto w-full max-w-3xl px-5 sm:px-8">
<Reveal className="text-center">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-ink-soft">
Häufige Fragen
</p>
<h2 className="mt-3 text-4xl font-medium tracking-[-0.02em] sm:text-5xl">
Noch Fragen zu{" "}
<em className="font-display italic text-[#0d7a57]">Entscheidomat</em>?
</h2>
</Reveal>
<Reveal delay={0.1} className="mt-10 space-y-3">
{HOMEPAGE_FAQS.map((faq) => (
<details
key={faq.q}
className="group rounded-2xl border border-line bg-cream/70 px-5 py-4"
>
<summary className="cursor-pointer list-none font-semibold text-ink marker:content-none">
{faq.q}
</summary>
<p className="mt-2 text-sm leading-relaxed text-ink-soft">
{faq.a}
</p>
</details>
))}
</Reveal>
</div>
</section>
);
}

View File

@@ -0,0 +1,38 @@
import type { ReactNode } from "react";
import Nav from "@/components/nav";
import Footer from "@/components/footer";
export default function LegalLayout({
title,
updated,
children,
}: {
title: string;
updated: string;
children: ReactNode;
}) {
return (
<>
<Nav />
<main className="flex-1 pt-16">
<div className="mx-auto w-full max-w-3xl px-5 py-16 sm:px-8 sm:py-24">
<h1 className="font-display text-4xl font-medium tracking-[-0.02em] sm:text-5xl">
{title}
</h1>
<p className="mt-3 text-sm text-ink-soft">Stand: {updated}</p>
<div className="mt-8 rounded-2xl border border-clay/30 bg-clay-soft px-5 py-4 text-sm font-medium leading-relaxed text-clay">
<strong>Hinweis für den Pre-Launch:</strong> Vor der öffentlichen
Veröffentlichung müssen die Angaben zum Diensteanbieter und eine
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">
{children}
</div>
</div>
</main>
<Footer />
</>
);
}

134
components/nav.tsx Normal file
View File

@@ -0,0 +1,134 @@
"use client";
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { motion } from "framer-motion";
import { ChevronDown, Menu, Volume2, VolumeX, X } from "lucide-react";
import { useSound } from "@/components/providers/sound-provider";
const TOOLS = [
{ href: "/ja-nein-generator", label: "Ja-Nein-Generator", hint: "Schnelle Entscheidungen" },
{ href: "/muenze-werfen", label: "Münze werfen", hint: "Kopf oder Zahl" },
{ href: "/gluecksrad", label: "Glücksrad", hint: "Drehen & auswählen" },
{ href: "/wuerfel-online", label: "Würfel online", hint: "Für Spiel & Zufall" },
{ href: "/zufallszahl-generator", label: "Zufallszahl", hint: "Dein Zahlenbereich" },
{ href: "/magic-8-ball", label: "Magic 8-Ball", hint: "Das kleine Orakel" },
{ href: "/namen-auslosen", label: "Namen auslosen", hint: "Listen & Gewinner" },
];
export default function Nav() {
const [scrolled, setScrolled] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { enabled, toggle, play } = useSound();
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 10);
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setToolsOpen(false);
setMobileOpen(false);
}
};
const onPointerDown = (event: MouseEvent) => {
if (!dropdownRef.current?.contains(event.target as Node)) setToolsOpen(false);
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("keydown", onKeyDown);
window.addEventListener("mousedown", onPointerDown);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("mousedown", onPointerDown);
};
}, []);
const closeMenus = () => {
setToolsOpen(false);
setMobileOpen(false);
};
const soundButton = (
<motion.button
type="button"
onClick={() => {
toggle();
if (!enabled) play("pop");
}}
whileTap={{ scale: 0.88 }}
aria-label={enabled ? "Sound ausschalten" : "Sound einschalten"}
title={enabled ? "Sound ausschalten" : "Sound einschalten"}
className="flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full border border-line bg-surface text-ink-soft shadow-soft transition-colors duration-200 hover:border-[#0d7a57]/50 hover:text-[#0d7a57]"
>
{enabled ? <Volume2 className="h-[18px] w-[18px]" /> : <VolumeX className="h-[18px] w-[18px]" />}
</motion.button>
);
return (
<header className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ${scrolled || mobileOpen ? "border-b border-line/70 bg-paper/95 shadow-[0_4px_24px_rgb(23_23_28/0.05)] backdrop-blur-xl" : "border-b border-transparent bg-transparent"}`}>
<nav className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between px-5 sm:px-8" aria-label="Hauptnavigation">
<Link href="/#top" className="group flex items-center gap-2.5" onClick={closeMenus}>
<img
src="/icon.svg"
alt=""
aria-hidden="true"
className="h-9 w-9 shrink-0 transition-transform duration-200 group-hover:scale-105"
/>
<span className="flex items-baseline">
<span className="text-lg font-semibold tracking-tight">Entscheid</span>
<span className="font-display text-lg italic text-[#0d7a57] transition-colors group-hover:text-[#095c41]">omat</span>
</span>
</Link>
<div className="hidden items-center gap-1 sm:flex">
<div ref={dropdownRef} className="relative">
<button
type="button"
onClick={() => setToolsOpen((open) => !open)}
aria-expanded={toolsOpen}
aria-haspopup="menu"
className="inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-sm font-medium text-ink-soft transition-colors duration-200 hover:bg-[#e6f7f0] hover:text-[#0d7a57]"
>
Tools <ChevronDown className={`h-4 w-4 transition-transform duration-200 ${toolsOpen ? "rotate-180" : ""}`} />
</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>
{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>
<span aria-hidden className="text-[#0d7a57] opacity-0 transition-opacity group-hover:opacity-100"></span>
</Link>
))}
</div>
)}
</div>
<Link href="/#features" className="rounded-full px-4 py-2 text-sm font-medium text-ink-soft transition-colors duration-200 hover:bg-[#e6f7f0] hover:text-[#0d7a57]">Features</Link>
{soundButton}
</div>
<div className="flex items-center gap-2 sm:hidden">
{soundButton}
<button type="button" onClick={() => setMobileOpen((open) => !open)} aria-expanded={mobileOpen} aria-controls="mobile-menu" aria-label={mobileOpen ? "Menü schließen" : "Menü öffnen"} className="flex h-10 w-10 items-center justify-center rounded-full border border-line bg-surface text-ink shadow-soft">
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</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>
<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>
<Link href="/#features" onClick={closeMenus} className="mt-3 block rounded-2xl border border-line px-4 py-3 font-semibold text-ink">Features</Link>
</div>
</div>
)}
</header>
);
}

156
components/playground.tsx Normal file
View File

@@ -0,0 +1,156 @@
"use client";
import Link from "next/link";
import {
ArrowRight,
CircleDashed,
Coins,
Dices,
Disc3,
Hash,
HelpCircle,
ListChecks,
} from "lucide-react";
import Reveal from "@/components/reveal";
const GENERATORS = [
{
id: "janein",
slug: "ja-nein-generator",
title: "Ja-Nein-Generator",
badge: "Zwei Optionen",
description:
"Stelle eine Frage und erhalte sofort eine unparteiische Antwort mit Ja oder Nein. Ideal für schnelle Alltagsentscheidungen.",
icon: HelpCircle,
},
{
id: "coin",
slug: "muenze-werfen",
title: "Münze werfen",
badge: "3D Münzwurf",
description:
"Wirf eine goldene 3D-Münze in die Luft und überlasse knifflige Wahlentscheidungen dem klassischen Kopf oder Zahl.",
icon: Coins,
},
{
id: "wheel",
slug: "gluecksrad",
title: "Glücksrad online",
badge: "Entscheidungsrad",
description:
"Erstelle dein eigenes Glücksrad mit individuellen Feldern und drehe für eine transparente, zufällige Auswahl.",
icon: Disc3,
},
{
id: "dice",
slug: "wuerfel-online",
title: "Würfel online",
badge: "Würfel-Simulator",
description:
"Rolle einen oder mehrere 6er, 10er oder benutzerdefinierte 3D-Würfel für Spiele & Entscheidungen.",
icon: Dices,
},
{
id: "number",
slug: "zufallszahl-generator",
title: "Zufallszahl-Generator",
badge: "Zahlenbereich",
description:
"Generiere eine oder mehrere echte Zufallszahlen in einem frei definierten Bereich zwischen Minimum und Maximum.",
icon: Hash,
},
{
id: "ball",
slug: "magic-8-ball",
title: "Magic 8-Ball",
badge: "Mystisches Orakel",
description:
"Befrage die magische Billardkugel und erhalte geheimnisvolle Antworten auf deine Lebensfragen.",
icon: CircleDashed,
},
{
id: "list",
slug: "namen-auslosen",
title: "Namen & Liste auslosen",
badge: "Gewinnspiel & Teams",
description:
"Mische Namen oder Optionen in einer Liste, lose Gewinner aus oder erstelle automatisch zufällige Gruppen.",
icon: ListChecks,
},
] as const;
export default function Playground() {
return (
<section id="tools" className="relative scroll-mt-20 py-20 sm:py-28">
<div className="relative mx-auto w-full max-w-6xl px-5 sm:px-8">
<Reveal className="mx-auto max-w-2xl text-center">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-ink-soft">
Die 7 Zufalls-Tools
</p>
<h2 className="mt-3 text-4xl font-semibold tracking-[-0.02em] sm:text-5xl">
Wähle deinen{" "}
<em className="font-display italic text-[#0d7a57]">Generator</em>.
</h2>
<p className="mt-4 text-lg leading-relaxed text-ink-soft">
Klicke auf ein Tool, um direkt zur interaktiven Vollversion mit
allen Einstellungen zu gelangen.
</p>
</Reveal>
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{GENERATORS.map((tool, i) => {
const Icon = tool.icon;
const isFeatured = i === 6;
return (
<Reveal
key={tool.id}
delay={i * 0.06}
className={isFeatured ? "sm:col-span-2 lg:col-span-1" : ""}
>
<Link
href={`/${tool.slug}`}
className="group relative flex h-full flex-col justify-between overflow-hidden rounded-[2rem] border border-line bg-surface p-7 sm:p-8 shadow-lift transition-all duration-300 hover:-translate-y-1.5 hover:border-[#0d7a57]/40 hover:shadow-[0_20px_45px_rgba(13,122,87,0.14)]"
>
{/* Subtiler Glow-Effekt im Hintergrund der Karte */}
<div
aria-hidden
className="pointer-events-none absolute -right-12 -top-12 h-36 w-36 rounded-full bg-[#0d7a57]/5 blur-2xl transition-all duration-500 group-hover:scale-150 group-hover:bg-[#0d7a57]/15"
/>
<div>
{/* Icon & Badge Header */}
<div className="flex items-center justify-between gap-3">
<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">
{tool.badge}
</span>
</div>
{/* Titel & Beschreibung */}
<h3 className="mt-6 font-display text-2xl font-semibold tracking-tight text-ink transition-colors duration-200 group-hover:text-[#0d7a57]">
{tool.title}
</h3>
<p className="mt-2.5 text-sm leading-relaxed text-ink-soft">
{tool.description}
</p>
</div>
{/* Action Link Footer */}
<div className="mt-8 flex items-center justify-between border-t border-line/60 pt-4 text-xs font-semibold uppercase tracking-widest text-[#0d7a57]">
<span>Generator öffnen</span>
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-[#e6f7f0] text-[#0d7a57] transition-all duration-300 group-hover:translate-x-1 group-hover:bg-[#0d7a57] group-hover:text-white">
<ArrowRight className="h-3.5 w-3.5" />
</div>
</div>
</Link>
</Reveal>
);
})}
</div>
</div>
</section>
);
}

148
components/preloader.tsx Normal file
View File

@@ -0,0 +1,148 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { gsap } from "@/lib/gsap";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
const CURL_PATH =
"M 100 30 C 88 30, 76 26, 66 22 C 56 18, 46 18, 40 24 C 34 30, 38 40, 48 40 C 56 40, 60 32, 54 28 C 49 25, 43 29, 46 34";
const SESSION_KEY = "entscheidomat:preloader-seen";
/**
* Minimalistische Ladeanimation: ein Kalligraphie-Ornament,
* das sich selbst zeichnet, danach hebt sich der Vorhang.
* Feuert "app:reveal", sobald die Seite sichtbar wird.
*/
export default function Preloader() {
const reduced = usePrefersReducedMotion();
const [animationDone, setAnimationDone] = useState(false);
const overlayRef = useRef<HTMLDivElement>(null);
const leftCurlRef = useRef<SVGPathElement>(null);
const rightCurlRef = useRef<SVGPathElement>(null);
const dotRef = useRef<SVGCircleElement>(null);
const wordRef = useRef<HTMLParagraphElement>(null);
const hasStartedRef = useRef(false);
const reveal = () => {
document.documentElement.dataset.preloader = "complete";
window.dispatchEvent(new Event("app:reveal"));
};
// Reduced Motion: keine Animation, aber Reveal-Event trotzdem dispatchen
// rein imperativ, kein setState, deshalb kein separater Effekt-Zweig nötig.
useEffect(() => {
let hasSeenPreloader = false;
try {
hasSeenPreloader = window.sessionStorage.getItem(SESSION_KEY) === "1";
} catch {
// Falls Browser-Speicher blockiert ist, bleibt der Loader funktional.
}
const shouldSkip = document.documentElement.dataset.preloader === "cached";
if (reduced || shouldSkip || (hasSeenPreloader && !hasStartedRef.current)) {
setAnimationDone(true);
reveal();
return;
}
hasStartedRef.current = true;
try {
window.sessionStorage.setItem(SESSION_KEY, "1");
} catch {
// Die Animation darf nicht von verfügbarem Browser-Speicher abhängen.
}
document.documentElement.style.overflow = "hidden";
const duration = window.matchMedia("(max-width: 767px)").matches ? 0.45 : 1;
const curls = [leftCurlRef.current, rightCurlRef.current].filter(
Boolean,
) as SVGPathElement[];
const lengths = curls.map((p) => p.getTotalLength());
curls.forEach((p, i) => {
p.style.strokeDasharray = `${lengths[i]}`;
p.style.strokeDashoffset = `${lengths[i]}`;
});
const tl = gsap.timeline({
onComplete: () => {
document.documentElement.style.overflow = "";
setAnimationDone(true);
reveal();
},
});
tl.to(curls, {
strokeDashoffset: 0,
duration: 0.35 * duration,
ease: "power2.inOut",
stagger: 0.05 * duration,
})
.fromTo(
dotRef.current,
{ scale: 0, transformOrigin: "50% 50%" },
{ scale: 1, duration: 0.15 * duration, ease: "back.out(3)" },
`-=${0.12 * duration}`,
)
.fromTo(
wordRef.current,
{ opacity: 0, y: 10, letterSpacing: "0.5em" },
{ opacity: 1, y: 0, letterSpacing: "0.28em", duration: 0.22 * duration, ease: "power2.out" },
`-=${0.1 * duration}`,
)
.to(overlayRef.current, {
yPercent: -100,
duration: 0.4 * duration,
ease: "power4.inOut",
delay: 0.08 * duration,
});
return () => {
document.documentElement.style.overflow = "";
tl.kill();
};
}, [reduced]);
if (reduced || animationDone) return null;
return (
<div
ref={overlayRef}
aria-hidden
data-preloader-overlay
className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-paper"
>
<svg
width="220"
height="66"
viewBox="0 0 200 60"
fill="none"
stroke="#161513"
strokeWidth="1.6"
strokeLinecap="round"
>
<path ref={leftCurlRef} d={CURL_PATH} />
<path
ref={rightCurlRef}
d={CURL_PATH}
transform="rotate(180 100 30)"
/>
<circle
ref={dotRef}
cx="100"
cy="30"
r="1.8"
fill="#161513"
stroke="none"
/>
</svg>
<p
ref={wordRef}
className="mt-6 text-[11px] font-semibold uppercase tracking-[0.28em] text-ink-soft opacity-0"
>
Entscheidomat
</p>
</div>
);
}

View File

@@ -0,0 +1,53 @@
"use client";
import {
createContext,
useCallback,
useContext,
useMemo,
type ReactNode,
} from "react";
import { playSound, type SoundName } from "@/lib/sounds";
import { useLocalStorage } from "@/hooks/use-local-storage";
type SoundContextValue = {
enabled: boolean;
toggle: () => void;
play: (name: SoundName) => void;
};
const SoundContext = createContext<SoundContextValue>({
enabled: false,
toggle: () => {},
play: () => {},
});
const STORAGE_KEY = "entscheidomat-sound";
export function SoundProvider({ children }: { children: ReactNode }) {
const [enabled, setEnabled] = useLocalStorage(STORAGE_KEY, true);
const toggle = useCallback(() => {
setEnabled((prev) => !prev);
}, [setEnabled]);
const play = useCallback(
(name: SoundName) => {
if (enabled) playSound(name);
},
[enabled],
);
const value = useMemo(
() => ({ enabled, toggle, play }),
[enabled, toggle, play],
);
return (
<SoundContext.Provider value={value}>{children}</SoundContext.Provider>
);
}
export function useSound() {
return useContext(SoundContext);
}

View File

@@ -0,0 +1,28 @@
import Link from "next/link";
import { TOOL_PAGES } from "@/lib/tool-pages";
export default function RelatedTools({ current }: { current: string }) {
const others = TOOL_PAGES.filter((t) => t.slug !== current);
return (
<p className="text-base text-ink-soft leading-relaxed">
Weitere Zufalls-Tools:{" "}
{others.map((t, i) => (
<span key={t.slug}>
<Link
href={`/${t.slug}`}
className="text-[#c97d60] font-medium hover:underline underline-offset-2 transition-colors"
>
{t.label}
</Link>
{i < others.length - 2
? ", "
: i === others.length - 2
? " sowie "
: "."}
</span>
))}
</p>
);
}

64
components/reveal.tsx Normal file
View File

@@ -0,0 +1,64 @@
"use client";
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
type RevealProps = {
children: ReactNode;
className?: string;
/** Verzögerung in Sekunden (für Stagger) */
delay?: number;
y?: number;
};
/**
* Leichte Scroll-Reveal-Variante ohne ScrollTrigger. IntersectionObserver
* verursacht keine fortlaufende Scroll-Arbeit und lädt keine GSAP-Plugins.
*/
export default function Reveal({
children,
className,
delay = 0,
y = 30,
}: RevealProps) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
if (reducedMotion.matches || !window.IntersectionObserver) {
const frame = window.requestAnimationFrame(() => setVisible(true));
return () => window.cancelAnimationFrame(frame);
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ rootMargin: "0px 0px -12%" },
);
observer.observe(element);
return () => observer.disconnect();
}, []);
return (
<div
ref={ref}
className={`reveal${visible ? " reveal--visible" : ""}${className ? ` ${className}` : ""}`}
style={
{
"--reveal-delay": `${delay}s`,
"--reveal-y": `${y}px`,
} as CSSProperties
}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,390 @@
"use client";
import { useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { gsap } from "@/lib/gsap";
/**
* Botanisch präzise verschnörkelte Ranken:
* Das Wachstum ist exakt auf den gesamten Scrollverlauf abgestimmt (kein zu schnelles Wachsen).
* Die Ranken entfalten sich genau synchron zur Scroll-Position und erreichen den
* untersten Rand erst, wenn man auch ganz unten angekommen ist.
*/
// Botanisch geformtes Blatt mit spitzer Silhouetten-Form, Blattader & dezentem Hover-Wiegeln
function Leaf({
x,
y,
rotate = 0,
scale = 1,
className = "",
}: {
x: number;
y: number;
rotate?: number;
scale?: number;
className?: string;
}) {
return (
<motion.g
transform={`translate(${x}, ${y}) rotate(${rotate}) scale(${scale})`}
whileHover={{ scale: scale * 1.18, rotate: rotate + 6 }}
transition={{ type: "spring", stiffness: 160, damping: 16 }}
className={`vine-leaf pointer-events-auto cursor-pointer ${className}`}
>
{/* Äußere Blattsilhouette edles Gelb-Grün / Smaragd */}
<path
d="M 0 0 C 14 -20 36 -12 42 0 C 36 12 14 20 0 0 Z"
fill="#0d7a57"
fillOpacity="0.92"
className="transition-all duration-300 hover:fill-[#109b6f]"
/>
{/* Feine Blattader */}
<path
d="M 0 0 C 15 -3 28 -2 39 0"
stroke="#ffffff"
strokeOpacity="0.75"
strokeWidth="1.2"
strokeLinecap="round"
/>
</motion.g>
);
}
// Offene Spiral-Tendril (Ranken-Kringel)
function Tendril({ d, className = "" }: { d: string; className?: string }) {
return (
<path
className={`vine-tendril ${className}`}
d={d}
fill="none"
stroke="#161513"
strokeWidth="1.4"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
);
}
// Ranke 1 (Links-Oben nach Mitte-Rechts - Hero Bereich)
const STEM_1 =
"M -40 20 C 120 70, 240 160, 320 300 C 400 440, 280 600, 420 740 C 520 860, 680 940, 820 1060";
const TENDRILS_1 = [
"M 240 160 C 275 135, 295 160, 275 185 C 255 195, 240 178, 252 165",
"M 420 740 C 465 710, 485 745, 460 770 C 435 785, 415 755, 428 738",
];
const LEAF_NODES_1 = [
{ x: 120, y: 70, rotate: -25, scale: 0.9 },
{ x: 240, y: 160, rotate: 42, scale: 1.1 },
{ x: 320, y: 300, rotate: -45, scale: 1.0 },
{ x: 400, y: 440, rotate: 32, scale: 0.95 },
{ x: 420, y: 740, rotate: -35, scale: 1.05 },
{ x: 680, y: 940, rotate: 50, scale: 0.9 },
];
// Ranke 2 (Rechts-Oben edel schwebend - Mittlerer Bereich)
const STEM_2 =
"M 920 30 C 760 150, 620 310, 710 490 C 800 680, 620 830, 740 1020 C 860 1180, 720 1340, 840 1500";
const TENDRILS_2 = [
"M 710 490 C 665 460, 640 495, 665 520 C 690 535, 710 505, 695 488",
"M 740 1020 C 695 990, 670 1025, 695 1050 C 720 1065, 740 1035, 725 1018",
];
const LEAF_NODES_2 = [
{ x: 760, y: 150, rotate: 155, scale: 1.0 },
{ x: 710, y: 490, rotate: -130, scale: 1.15 },
{ x: 620, y: 830, rotate: 140, scale: 0.9 },
{ x: 740, y: 1020, rotate: -150, scale: 1.05 },
{ x: 840, y: 1500, rotate: 135, scale: 1.1 },
];
// Ranke 3 (Untere Ranke Links Wächst erst in der 2. Seitenhälfte bis in den Footer)
const STEM_3 =
"M -30 50 C 210 180, 440 240, 520 430 C 600 620, 420 810, 610 1000 C 780 1170, 690 1340, 840 1550 C 920 1720, 780 1880, 910 2050";
const TENDRILS_3 = [
"M 520 430 C 565 400, 590 435, 565 460 C 540 475, 518 445, 532 428",
"M 610 1000 C 655 970, 680 1005, 655 1030 C 630 1045, 608 1015, 622 998",
"M 840 1550 C 885 1520, 910 1555, 885 1580 C 860 1595, 838 1565, 852 1548",
];
const LEAF_NODES_3 = [
{ x: 210, y: 180, rotate: -30, scale: 1.05 },
{ x: 440, y: 240, rotate: 35, scale: 0.9 },
{ x: 520, y: 430, rotate: -40, scale: 1.1 },
{ x: 420, y: 810, rotate: 45, scale: 1.0 },
{ x: 610, y: 1000, rotate: -35, scale: 1.15 },
{ x: 780, y: 1170, rotate: 30, scale: 0.95 },
{ x: 840, y: 1550, rotate: -25, scale: 1.1 },
{ x: 880, y: 1860, rotate: 40, scale: 0.95 },
];
// Ranke 4 (Untere Ranke Rechts Wächst erst in der 2. Seitenhälfte bis in den Footer)
const STEM_4 =
"M 900 40 C 740 220, 620 400, 700 600 C 780 800, 600 1000, 720 1220 C 840 1440, 680 1680, 780 1920 C 880 2080, 750 2200, 850 2320";
const TENDRILS_4 = [
"M 700 600 C 655 570, 630 605, 655 630 C 680 645, 700 615, 685 598",
"M 720 1220 C 675 1190, 650 1225, 675 1250 C 700 1265, 720 1235, 705 1218",
];
const LEAF_NODES_4 = [
{ x: 740, y: 220, rotate: 145, scale: 1.0 },
{ x: 700, y: 600, rotate: 125, scale: 0.95 },
{ x: 600, y: 1000, rotate: -140, scale: 1.1 },
{ x: 720, y: 1220, rotate: 135, scale: 1.05 },
{ x: 780, y: 1920, rotate: -130, scale: 0.95 },
];
export default function ScrollFlourishes() {
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
// 1. Dasharray & Offset für alle Linienpfade vorbereiten
const strokePaths = root.querySelectorAll<SVGPathElement>(
".vine-stem, .vine-tendril",
);
strokePaths.forEach((p) => {
const len = p.getTotalLength();
p.style.strokeDasharray = `${len}`;
p.style.strokeDashoffset = `${len}`;
});
const leaves = root.querySelectorAll<SVGGElement>(".vine-leaf");
gsap.set(leaves, { opacity: 0, scale: 0, transformOrigin: "0px 0px" });
// 2. Perfekt getaktete ScrollTrigger Timeline (Spannweite von 0 bis 1.0 des gesamten Dokument-Scrolls)
const tl = gsap.timeline({
scrollTrigger: {
trigger: document.body,
start: "top top",
end: "bottom bottom",
scrub: 0.7,
},
});
// Ranke 1 (Hero): Wächst im oberen Drittel (0.0 bis 0.38)
tl.to(
root.querySelectorAll(".vine-stem-1"),
{ strokeDashoffset: 0, ease: "none", duration: 0.38 },
0,
);
tl.to(
root.querySelectorAll(".vine-tendril-1"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.25,
);
// Ranke 2 (Mitte-Rechts): Wächst im mittleren Bereich (0.22 bis 0.68)
tl.to(
root.querySelectorAll(".vine-stem-2"),
{ strokeDashoffset: 0, ease: "none", duration: 0.46 },
0.22,
);
tl.to(
root.querySelectorAll(".vine-tendril-2"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.48,
);
// Ranke 3 (Untere Ranke Links): Wächst in der 2. Hälfte bis ganz nach unten (0.45 bis 1.0)
tl.to(
root.querySelectorAll(".vine-stem-3"),
{ strokeDashoffset: 0, ease: "none", duration: 0.55 },
0.45,
);
tl.to(
root.querySelectorAll(".vine-tendril-3"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.72,
);
// Ranke 4 (Untere Ranke Rechts): Wächst in der 2. Hälfte bis ganz nach unten (0.50 bis 1.0)
tl.to(
root.querySelectorAll(".vine-stem-4"),
{ strokeDashoffset: 0, ease: "none", duration: 0.5 },
0.5,
);
tl.to(
root.querySelectorAll(".vine-tendril-4"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.78,
);
// Blätter entfalten sich organisch zur jeweiligen Wachstumszeit
tl.to(
leaves,
{
opacity: 1,
scale: 1,
ease: "back.out(1.5)",
duration: 0.1,
stagger: 0.02,
},
0.1,
);
// Subtiler, ruhiger Parallax-Versatz (Ohne hektische Maus-Swayings)
tl.to(
".vine-container-1",
{ yPercent: 6, scale: 1.06, ease: "none", duration: 1 },
0,
);
tl.to(
".vine-container-2",
{ yPercent: 8, scale: 0.95, ease: "none", duration: 1 },
0,
);
tl.to(
".vine-container-3",
{ yPercent: -4, scale: 1.05, ease: "none", duration: 1 },
0,
);
tl.to(
".vine-container-4",
{ yPercent: 5, scale: 1.04, ease: "none", duration: 1 },
0,
);
});
return () => mm.revert();
}, []);
return (
<div
ref={rootRef}
aria-hidden
className="pointer-events-none fixed inset-0 -z-10 overflow-hidden select-none"
>
{/* Ranke 1: Links-Oben nach Mitte-Rechts (Hero) */}
<div className="vine-container-1 absolute left-[-4vw] top-[-2vh] w-[50vw] max-w-[750px] min-w-[320px] opacity-35 transition-opacity duration-500">
<svg
viewBox="0 0 850 1100"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-1"
d={STEM_1}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_1.map((d, i) => (
<Tendril key={i} className="vine-tendril-1" d={d} />
))}
{LEAF_NODES_1.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
{/* Ranke 2: Rechts-Oben (Mittlerer Bereich) */}
<div className="vine-container-2 absolute right-[-4vw] top-[18vh] w-[54vw] max-w-[820px] min-w-[340px] opacity-30 transition-opacity duration-500">
<svg
viewBox="0 0 950 1550"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-2"
d={STEM_2}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_2.map((d, i) => (
<Tendril key={i} className="vine-tendril-2" d={d} />
))}
{LEAF_NODES_2.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
{/* Ranke 3: Untere Ranke Links Wächst erst in der 2. Seitenhälfte bis in den Footer */}
<div className="vine-container-3 absolute left-[-3vw] top-[42vh] w-[58vw] max-w-[920px] min-w-[360px] opacity-35 transition-opacity duration-500">
<svg
viewBox="0 0 950 2100"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-3"
d={STEM_3}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_3.map((d, i) => (
<Tendril key={i} className="vine-tendril-3" d={d} />
))}
{LEAF_NODES_3.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
{/* Ranke 4: Untere Ranke Rechts Wächst erst in der 2. Seitenhälfte bis in den Footer */}
<div className="vine-container-4 absolute right-[-3vw] top-[48vh] w-[56vw] max-w-[900px] min-w-[350px] opacity-35 transition-opacity duration-500">
<svg
viewBox="0 0 950 2350"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-4"
d={STEM_4}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_4.map((d, i) => (
<Tendril key={i} className="vine-tendril-4" d={d} />
))}
{LEAF_NODES_4.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
</div>
);
}

View File

@@ -0,0 +1,210 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { ChevronDown } from "lucide-react";
import Nav from "@/components/nav";
import Footer from "@/components/footer";
import RelatedTools from "@/components/related-tools";
export type FaqItem = {
q: string;
a: string;
};
type SubpageLayoutProps = {
toolSlug: string;
toolName: string;
h1Title: string;
introText: string;
steps: string[];
examples: string[];
vorteileText: ReactNode;
additionalContent?: ReactNode;
faqs: FaqItem[];
updatedDate: string;
children: ReactNode;
};
export default function SubpageLayout({
toolSlug,
toolName,
h1Title,
introText,
steps,
examples,
vorteileText,
additionalContent,
faqs,
updatedDate,
children,
}: SubpageLayoutProps) {
const howItWorksName =
toolSlug === "muenze-werfen" ? "der Münzwurf" : toolName;
return (
<>
<Nav />
<main className="relative flex-1 pt-16 overflow-hidden">
{/* Full-width Viewport Background: Architektonisches Grid & Geometrie-Muster (wie im Referenzbild) mit sanftem Übergang nach unten */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[620px] w-full overflow-hidden [mask-image:linear-gradient(to_bottom,black_45%,transparent_98%)]"
>
{/* Weiche Aquarell-Glows (einheitlich in Salbeigrün) */}
<div className="absolute top-8 left-[12%] h-72 w-72 rounded-full bg-[#8da399]/20 blur-3xl" />
<div className="absolute top-48 left-[50%] h-64 w-64 rounded-full bg-[#8da399]/18 blur-3xl" />
{/* Vektor-Muster: Feines architektonisches Raster, Kreise & Punkte */}
<svg
className="absolute inset-0 h-full w-full opacity-75"
width="100%"
height="100%"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<pattern
id="archPattern"
width="160"
height="160"
patternUnits="userSpaceOnUse"
>
{/* Feine Rasterlinien */}
<line x1="0" y1="0" x2="160" y2="0" stroke="#747874" strokeWidth="0.5" strokeOpacity="0.22" />
<line x1="0" y1="80" x2="160" y2="80" stroke="#747874" strokeWidth="0.5" strokeOpacity="0.15" strokeDasharray="4 4" />
<line x1="0" y1="0" x2="0" y2="160" stroke="#747874" strokeWidth="0.5" strokeOpacity="0.22" />
<line x1="80" y1="0" x2="80" y2="160" stroke="#747874" strokeWidth="0.5" strokeOpacity="0.15" strokeDasharray="4 4" />
{/* Diagonale feine Hilfslinien */}
<line x1="0" y1="0" x2="160" y2="160" stroke="#8da399" strokeWidth="0.4" strokeOpacity="0.15" />
{/* Geometrische Bögen & Kreise */}
<circle cx="80" cy="80" r="50" stroke="#8da399" strokeWidth="0.5" strokeOpacity="0.3" fill="none" />
<circle cx="80" cy="80" r="75" stroke="#8da399" strokeWidth="0.4" strokeOpacity="0.2" strokeDasharray="2 4" fill="none" />
<circle cx="0" cy="0" r="40" stroke="#8da399" strokeWidth="0.4" strokeOpacity="0.22" fill="none" />
{/* Punkte auf den Schnittstellen */}
<circle cx="80" cy="80" r="2" fill="#707a65" fillOpacity="0.5" />
<circle cx="0" cy="0" r="2.2" fill="#707a65" fillOpacity="0.45" />
<circle cx="160" cy="0" r="1.6" fill="#707a65" fillOpacity="0.4" />
<circle cx="0" cy="160" r="1.6" fill="#707a65" fillOpacity="0.4" />
<circle cx="160" cy="160" r="2.2" fill="#707a65" fillOpacity="0.45" />
<circle cx="80" cy="30" r="1.6" fill="#8da399" fillOpacity="0.45" />
<circle cx="80" cy="130" r="1.6" fill="#8da399" fillOpacity="0.45" />
<circle cx="30" cy="80" r="1.6" fill="#8da399" fillOpacity="0.4" />
<circle cx="130" cy="80" r="1.6" fill="#8da399" fillOpacity="0.4" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#archPattern)" />
</svg>
</div>
<div className="relative mx-auto w-full max-w-6xl px-5 py-12 sm:px-8 sm:py-16 md:py-20">
{/* Header Section ohne Container-Box */}
<header className="relative max-w-3xl mb-12 sm:mb-16">
<nav
aria-label="Breadcrumb"
className="flex items-center gap-2 mb-3 text-xs font-semibold uppercase tracking-widest text-ink-soft"
>
<Link href="/" className="hover:text-[#c97d60] transition-colors">
Entscheidomat
</Link>
<span className="text-line">/</span>
<span className="text-ink font-medium">{toolName}</span>
</nav>
<h1 className="font-display text-4xl sm:text-5xl lg:text-6xl font-semibold leading-[1.15] tracking-tight text-ink mb-4">
{h1Title}
</h1>
<p className="text-base sm:text-lg text-ink-soft leading-relaxed">
{introText}
</p>
</header>
{/* Main Interactive Tool Container */}
<section className="mb-14 sm:mb-20 rounded-3xl border border-line bg-surface p-6 sm:p-8 md:p-10 shadow-lift relative overflow-hidden">
{children}
</section>
{/* 2-Column Content Structure: Explanations & FAQ */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-14">
{/* Left Column: Guide & Info */}
<div className="lg:col-span-7 space-y-10">
<article>
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink mb-4">
So funktioniert {howItWorksName.toLowerCase().startsWith("der") || howItWorksName.toLowerCase().startsWith("das") || howItWorksName.toLowerCase().startsWith("die") ? howItWorksName : `das ${howItWorksName}`}
</h2>
<ol className="space-y-3 text-base text-ink-soft pl-4 border-l-2 border-[#c97d60]/40">
{steps.map((step, idx) => (
<li key={idx} className="pl-2">
<span className="font-semibold text-ink">{idx + 1}. </span>
{step}
</li>
))}
</ol>
</article>
<article>
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink mb-4">
Beispiele
</h2>
<ul className="space-y-2 text-base text-ink-soft list-disc pl-5">
{examples.map((ex, idx) => (
<li key={idx}>{ex}</li>
))}
</ul>
</article>
<article>
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink mb-4">
Vorteile und Grenzen
</h2>
<div className="text-base text-ink-soft leading-relaxed">
{vorteileText}
</div>
</article>
{additionalContent}
<article className="p-6 sm:p-7 bg-paper/90 rounded-2xl border border-line shadow-soft">
<h2 className="font-display text-xl font-semibold text-ink mb-3">
Verwandte Tools
</h2>
<RelatedTools current={toolSlug} />
</article>
</div>
{/* Right Column: FAQ Accordion */}
<div className="lg:col-span-5">
<h2 className="font-display text-2xl sm:text-3xl font-semibold text-ink mb-6">
Häufige Fragen
</h2>
<div className="space-y-3">
{faqs.map((faq) => (
<details
key={faq.q}
className="group rounded-2xl border border-line bg-surface transition-colors hover:border-[#c97d60]/40 [&_summary::-webkit-details-marker]:hidden"
>
<summary className="flex cursor-pointer items-center justify-between gap-3 p-4 sm:p-5 font-display text-base font-semibold text-ink">
<span>{faq.q}</span>
<span className="shrink-0 text-ink-soft transition-transform duration-300 group-open:rotate-180">
<ChevronDown className="h-5 w-5" />
</span>
</summary>
<div className="px-4 pb-4 sm:px-5 sm:pb-5 text-sm sm:text-base leading-relaxed text-ink-soft border-t border-line/50 pt-3 mt-1">
{faq.a}
</div>
</details>
))}
</div>
<p className="mt-6 text-xs text-ink-soft">
Zuletzt aktualisiert: {updatedDate}
</p>
</div>
</div>
</div>
</main>
<Footer />
</>
);
}

View File

@@ -0,0 +1,185 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Coins } from "lucide-react";
import Coin3D from "@/components/coin-3d";
import { gsap } from "@/lib/gsap";
import { coinEase } from "@/lib/motion";
import { popConfetti } from "@/lib/confetti";
import { useSound } from "@/components/providers/sound-provider";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
export default function CoinFlip() {
const [frontLabel, setFrontLabel] = useLocalStorage("coin-front", "Kopf");
const [backLabel, setBackLabel] = useLocalStorage("coin-back", "Zahl");
const [history, setHistory] = useState<string[]>([]);
const [flipping, setFlipping] = useState(false);
const [result, setResult] = useState<string | null>(null);
const coinRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<HTMLDivElement>(null);
const rotationRef = useRef(0);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
const front = frontLabel.trim() || "Kopf";
const back = backLabel.trim() || "Zahl";
const flip = () => {
if (flipping) return;
const isBack = Math.random() < 0.5;
const label = isBack ? back : front;
setFlipping(true);
setResult(null);
play("coin");
const current = rotationRef.current;
const base = Math.ceil(current / 360) * 360;
let target = base + 360 * 5 + (isBack ? 180 : 0);
if (target - current < 360 * 4) target += 360;
const dur = reduced ? 0.3 : 2.6;
gsap.to(coinRef.current, {
rotationX: target,
duration: dur,
ease: reduced ? "power1.out" : coinEase,
onComplete: () => {
rotationRef.current = target;
setFlipping(false);
setResult(label);
setHistory((h) => [label, ...h].slice(0, 10));
play("pop");
popConfetti();
},
});
if (!reduced) {
gsap.to(stageRef.current, {
y: -54,
duration: dur / 2,
yoyo: true,
repeat: 1,
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">
<div ref={stageRef} className="coin-scene p-4">
<button
type="button"
onClick={flip}
aria-label="Münze werfen"
className="block cursor-pointer rounded-full drop-shadow-[0_10px_18px_rgba(17,20,18,0.12)] transition-transform duration-200 hover:scale-[1.04] active:scale-95"
>
<Coin3D ref={coinRef} frontLabel={front} backLabel={back} size={188} />
</button>
</div>
<div className="mt-2 h-14" aria-live="polite">
<AnimatePresence mode="wait">
{result && (
<motion.p
key={result + history.length}
initial={{ opacity: 0, scale: 0.6, y: 12 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }}
className="font-display text-3xl font-semibold tracking-tight"
>
<span className="text-primary">{result}!</span>
</motion.p>
)}
</AnimatePresence>
</div>
<motion.button
type="button"
onClick={flip}
disabled={flipping}
whileTap={{ scale: 0.95 }}
className="mt-2 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"
>
<Coins className="h-5 w-5" />
{flipping ? "Fliegt …" : "Münze werfen"}
</motion.button>
<p className="mt-3 text-xs text-ink-soft">
Tipp: Du kannst auch direkt auf die Münze tippen.
</p>
</div>
{/* Einstellungen */}
<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">
Seiten beschriften
</h3>
<div className="mt-4 grid grid-cols-2 gap-3">
<label className="block">
<span className="mb-1.5 block text-xs font-medium text-ink-soft">
Vorderseite
</span>
<input
value={frontLabel}
onChange={(e) => setFrontLabel(e.target.value)}
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"
/>
</label>
<label className="block">
<span className="mb-1.5 block text-xs font-medium text-ink-soft">
Rückseite
</span>
<input
value={backLabel}
onChange={(e) => setBackLabel(e.target.value)}
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"
/>
</label>
</div>
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
Z. B. Ja &amp; Nein, Kino &amp; Couch deine Beschriftung
bleibt gespeichert.
</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
? "bg-primary text-white"
: "bg-surface text-ink-soft ring-1 ring-line"
}`}
>
{entry}
</motion.span>
))}
</AnimatePresence>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,270 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Dices } from "lucide-react";
import { gsap } from "@/lib/gsap";
import { popConfetti } from "@/lib/confetti";
import { useSound } from "@/components/providers/sound-provider";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
const SIZE = 104;
const HALF = SIZE / 2;
/** Würfelaugen-Positionen im 3×3-Raster (viewBox 40×40). */
const PIPS: Record<number, number[]> = {
1: [4],
2: [0, 8],
3: [0, 4, 8],
4: [0, 2, 6, 8],
5: [0, 2, 4, 6, 8],
6: [0, 2, 3, 5, 6, 8],
};
/** Platzierung der Seiten im Würfel (gegenüberliegende Seiten = 7). */
const FACE_TRANSFORMS = [
`translateZ(${HALF}px)`, // 1 vorne
`rotateX(90deg) translateZ(${HALF}px)`, // 2 oben
`rotateY(90deg) translateZ(${HALF}px)`, // 3 rechts
`rotateY(-90deg) translateZ(${HALF}px)`, // 4 links
`rotateX(-90deg) translateZ(${HALF}px)`, // 5 unten
`rotateY(180deg) translateZ(${HALF}px)`, // 6 hinten
];
/** Welche Würfel-Rotation bringt Wert v nach vorne? */
const FACE_ROTATION: Record<number, { rx: number; ry: number }> = {
1: { rx: 0, ry: 0 },
2: { rx: -90, ry: 0 },
3: { rx: 0, ry: -90 },
4: { rx: 0, ry: 90 },
5: { rx: 90, ry: 0 },
6: { rx: 0, ry: 180 },
};
const DEFAULT_CUSTOM = ["Ja", "Nein", "Vielleicht", "Frag später", "Gute Idee", "Lass es"];
export default function DiceRoller() {
const [mode, setMode] = useLocalStorage<"numbers" | "custom">(
"dice-mode",
"numbers",
);
const [customFaces, setCustomFaces] = useLocalStorage<string[]>(
"dice-custom",
DEFAULT_CUSTOM,
);
const [rolling, setRolling] = useState(false);
const [result, setResult] = useState<number | null>(null);
const diceRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<HTMLDivElement>(null);
const rotRef = useRef({ rx: -18, ry: 24 }); // hübscher Startwinkel
const { play } = useSound();
const reduced = usePrefersReducedMotion();
const roll = () => {
if (rolling) return;
const value = 1 + Math.floor(Math.random() * 6);
setRolling(true);
setResult(null);
play("dice");
const { rx, ry } = rotRef.current;
const target = FACE_ROTATION[value];
const targetRx = Math.ceil(rx / 360) * 360 + 720 + target.rx;
const targetRy = Math.ceil(ry / 360) * 360 + 720 + target.ry;
const dur = reduced ? 0.35 : 1.9;
gsap.to(diceRef.current, {
rotationX: targetRx,
rotationY: targetRy,
duration: dur,
ease: reduced ? "power1.out" : "power3.out",
onComplete: () => {
rotRef.current = { rx: targetRx, ry: targetRy };
setRolling(false);
setResult(value);
play("pop");
popConfetti();
},
});
if (!reduced) {
gsap.to(stageRef.current, {
y: -40,
duration: dur / 2,
yoyo: true,
repeat: 1,
ease: "power2.out",
});
}
};
const resultLabel =
result === null
? null
: mode === "custom"
? customFaces[result - 1]?.trim() || `Seite ${result}`
: String(result);
return (
<div className="grid items-center gap-10 lg:grid-cols-[1.15fr_0.85fr]">
{/* Bühne */}
<div className="flex flex-col items-center">
<div ref={stageRef} className="dice-scene p-6">
<button
type="button"
onClick={roll}
aria-label="Würfel rollen"
className="block cursor-pointer drop-shadow-[0_22px_26px_rgb(23_23_28/0.22)] transition-transform duration-200 hover:scale-[1.05] active:scale-95"
>
<div
ref={diceRef}
className="dice"
style={{
width: SIZE,
height: SIZE,
// Startwinkel als Literal (deckt sich mit rotRef's Initialwert)
// GSAP übernimmt danach die Rotation imperativ am DOM-Node.
transform: "rotateX(-18deg) rotateY(24deg)",
}}
>
{[1, 2, 3, 4, 5, 6].map((v, i) => (
<div
key={v}
className="dice-face"
style={{ transform: FACE_TRANSFORMS[i] }}
>
{mode === "numbers" ? (
<svg viewBox="0 0 40 40" className="h-full w-full">
{PIPS[v].map((p) => (
<circle
key={p}
cx={8 + (p % 3) * 12}
cy={8 + Math.floor(p / 3) * 12}
r={3.6}
fill="#17171c"
/>
))}
</svg>
) : (
<span className="dice-face-label">
{customFaces[v - 1]?.trim() || `Seite ${v}`}
</span>
)}
</div>
))}
</div>
</button>
</div>
<div className="mt-2 h-14" aria-live="polite">
<AnimatePresence mode="wait">
{resultLabel !== null && (
<motion.p
key={resultLabel + String(result)}
initial={{ opacity: 0, scale: 0.6, y: 12 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }}
className="font-display text-3xl font-semibold tracking-tight"
>
<span className="text-primary">{resultLabel}</span>
</motion.p>
)}
</AnimatePresence>
</div>
<motion.button
type="button"
onClick={roll}
disabled={rolling}
whileTap={{ scale: 0.95 }}
className="mt-2 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"
>
<Dices className="h-5 w-5" />
{rolling ? "Rollt …" : "Würfeln!"}
</motion.button>
</div>
{/* Einstellungen */}
<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">
Würfel-Seiten
</h3>
<div className="mt-4 flex rounded-full border border-line bg-surface p-1">
{(
[
{ id: "numbers", label: "Zahlen 16" },
{ id: "custom", label: "Eigene Texte" },
] as const
).map((m) => (
<button
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 ${
mode === m.id ? "text-white" : "text-ink-soft hover:text-ink"
}`}
>
{mode === m.id && (
<motion.span
layoutId="dice-mode-pill"
transition={{ type: "spring", stiffness: 420, damping: 34 }}
className="absolute inset-0 rounded-full bg-primary"
/>
)}
<span className="relative z-10">{m.label}</span>
</button>
))}
</div>
<AnimatePresence initial={false}>
{mode === "custom" && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="overflow-hidden"
>
<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">
Seite {i + 1}
</span>
<input
value={face}
onChange={(e) =>
setCustomFaces((faces) =>
faces.map((f, idx) =>
idx === i ? e.target.value : f,
),
)
}
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"
/>
</label>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
Klassisch mit Würfelaugen oder mit eigenen Texten perfekt für
Wer spült heute ab?.
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,188 @@
"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>
);
}

View File

@@ -0,0 +1,178 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ListChecks, Sparkles } from "lucide-react";
import { gsap } from "@/lib/gsap";
import { celebrateConfetti } from "@/lib/confetti";
import { useSound } from "@/components/providers/sound-provider";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
const DEFAULT_LIST = "Anna\nBen\nClara\nDavid";
export default function ListPicker() {
const [text, setText] = useLocalStorage("list-items", DEFAULT_LIST);
const [removeAfterDraw, setRemoveAfterDraw] = useLocalStorage(
"list-remove",
false,
);
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);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
const items = text
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
const draw = () => {
if (drawing) return;
if (items.length < 2) {
setError(true);
play("tick");
gsap.fromTo(
textareaRef.current,
{ x: -7 },
{ x: 0, duration: 0.4, ease: "elastic.out(2.5, 0.35)" },
);
return;
}
setError(false);
const pick = items[Math.floor(Math.random() * items.length)];
setDrawing(true);
setWinner(null);
play("whoosh");
const dur = reduced ? 0.35 : 2.0;
const proxy = { p: 0 };
lastPaintRef.current = 0;
gsap.to(proxy, {
p: 1,
duration: dur,
ease: "power2.out",
onUpdate: () => {
const now = performance.now();
const interval = reduced ? 60 : 30 + 260 * proxy.p;
if (now - lastPaintRef.current > interval) {
lastPaintRef.current = now;
setDisplay(items[Math.floor(Math.random() * items.length)]);
}
},
onComplete: () => {
setDisplay(pick);
setWinner(pick);
setDrawing(false);
play("win");
celebrateConfetti();
if (removeAfterDraw) {
setText(
items
.filter((item) => item !== pick)
.join("\n"),
);
}
},
});
};
return (
<div className="grid items-center gap-10 lg:grid-cols-[0.9fr_1.1fr]">
{/* Liste */}
<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">
Deine Liste ({items.length})
</h3>
<textarea
ref={textareaRef}
value={text}
onChange={(e) => setText(e.target.value)}
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
? "border-clay/60 focus:border-clay/60 focus:ring-clay/20"
: "border-line focus:border-primary/60 focus:ring-primary/20"
}`}
/>
<label className="mt-3 flex cursor-pointer items-center gap-2.5 text-sm font-medium text-ink-soft">
<input
type="checkbox"
checked={removeAfterDraw}
onChange={(e) => setRemoveAfterDraw(e.target.checked)}
className="h-4 w-4 cursor-pointer accent-primary"
/>
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
? "Bitte mindestens 2 Einträge eintragen."
: "Ideal für Teams, Gruppen-Reihenfolgen oder Gewinnspiele."}
</p>
</div>
{/* Anzeige */}
<div className="flex flex-col items-center">
<div className="flex min-h-[190px] w-full items-center justify-center rounded-[2rem] border border-line bg-cream/70 px-8 py-8">
<AnimatePresence mode="wait">
{display === null ? (
<motion.div
key="idle"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center gap-3 text-center"
>
<ListChecks className="h-8 w-8 text-primary/50" />
<p className="font-display text-xl font-medium text-ink-soft">
{items.length >= 2
? `${items.length} Einträge warten auf die Ziehung.`
: "Trage links mindestens 2 Einträge ein."}
</p>
</motion.div>
) : (
<motion.p
key={display + (winner ? "-win" : "")}
initial={winner ? { scale: 0.6, opacity: 0, y: 14 } : false}
animate={{ scale: 1, opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 340, damping: 20 }}
className={`break-words text-center font-display text-5xl font-semibold tracking-tight sm:text-6xl ${
winner ? "text-primary" : "text-ink/60"
}`}
aria-live="polite"
>
{display}
</motion.p>
)}
</AnimatePresence>
</div>
<motion.button
type="button"
onClick={draw}
disabled={drawing}
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"
>
<Sparkles className="h-5 w-5" />
{drawing ? "Zieht …" : "Zufällig ziehen"}
</motion.button>
{winner && (
<p className="mt-3 text-xs text-ink-soft">
{removeAfterDraw
? `${winner}“ wurde von der Liste entfernt.`
: "Nochmal ziehen? Einfach erneut klicken."}
</p>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,252 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Pencil, Plus, Sparkles, X } from "lucide-react";
import { gsap } from "@/lib/gsap";
import { popConfetti } from "@/lib/confetti";
import { useSound } from "@/components/providers/sound-provider";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
const DEFAULT_ANSWERS = [
"Ja, definitiv.",
"Ohne Zweifel.",
"Sehr wahrscheinlich.",
"Sieht gut aus.",
"Frag später nochmal.",
"Kann ich jetzt nicht sagen.",
"Konzentrier dich und frag erneut.",
"Rechne nicht damit.",
"Meine Antwort: Nein.",
"Eher nicht.",
];
const MIN_ANSWERS = 2;
const MAX_ANSWERS = 20;
export default function Magic8Ball() {
const [answers, setAnswers] = useLocalStorage<string[]>(
"ball-answers",
DEFAULT_ANSWERS,
);
const [question, setQuestion] = useState("");
const [answer, setAnswer] = useState<string | null>(null);
const [shaking, setShaking] = useState(false);
const [editing, setEditing] = useState(false);
const ballRef = useRef<HTMLDivElement>(null);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
const validAnswers = answers.map((a) => a.trim()).filter(Boolean);
const shake = () => {
if (shaking || validAnswers.length === 0) return;
const pick =
validAnswers[Math.floor(Math.random() * validAnswers.length)];
setShaking(true);
setAnswer(null);
play("whoosh");
const reveal = () => {
setAnswer(pick);
setShaking(false);
play("pop");
popConfetti();
};
if (reduced) {
gsap.delayedCall(0.25, reveal);
return;
}
const tl = gsap.timeline({ onComplete: reveal });
tl.to(ballRef.current, {
x: -18,
rotation: -9,
duration: 0.08,
ease: "power1.inOut",
})
.to(ballRef.current, {
x: 16,
rotation: 8,
duration: 0.08,
ease: "power1.inOut",
})
.repeat(4)
.to(ballRef.current, {
x: 0,
rotation: 0,
duration: 0.22,
ease: "power2.out",
});
};
const updateAnswer = (i: number, value: string) =>
setAnswers((list) => list.map((a, idx) => (idx === i ? value : a)));
const addAnswer = () => {
if (answers.length >= MAX_ANSWERS) return;
setAnswers((list) => [...list, ""]);
};
const removeAnswer = (i: number) => {
if (answers.length <= MIN_ANSWERS) return;
setAnswers((list) => list.filter((_, idx) => idx !== i));
};
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={80}
placeholder="Deine Frage (optional) …"
aria-label="Deine Frage an den 8-Ball"
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={ballRef} className="mt-6">
<button
type="button"
onClick={shake}
aria-label="8-Ball schütteln"
className="ball-body relative flex h-64 w-64 cursor-pointer items-center justify-center rounded-full transition-transform duration-200 hover:scale-[1.03] active:scale-95 sm:h-72 sm:w-72"
>
<div className="ball-window relative flex h-32 w-32 items-center justify-center overflow-hidden rounded-full sm:h-36 sm:w-36">
{/* Prisma-Dreieck */}
<div
aria-hidden
className="absolute inset-4 bg-primary/60"
style={{
clipPath: "polygon(50% 6%, 94% 90%, 6% 90%)",
}}
/>
<div
className="relative z-10 flex h-full w-full items-center justify-center px-7"
aria-live="polite"
>
<AnimatePresence mode="wait">
{answer ? (
<motion.p
key={answer}
initial={{ opacity: 0, scale: 0.7, filter: "blur(8px)" }}
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]"
>
{answer}
</motion.p>
) : (
<motion.p
key="eight"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="font-display text-4xl font-semibold text-[#dde5dd]"
>
8
</motion.p>
)}
</AnimatePresence>
</div>
</div>
</button>
</div>
<motion.button
type="button"
onClick={shake}
disabled={shaking || validAnswers.length === 0}
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"
>
<Sparkles className="h-5 w-5" />
{shaking ? "Schüttelt …" : "8-Ball schütteln"}
</motion.button>
<p className="mt-3 text-xs text-ink-soft">
{question.trim()
? `${question.trim()}`
: "Stell eine Ja/Nein-Frage oder lass dich einfach überraschen."}
</p>
</div>
{/* Antworten-Editor */}
<div className="rounded-3xl border border-line bg-cream/70 p-5">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold uppercase tracking-[0.16em] text-ink-soft">
Antworten ({validAnswers.length})
</h3>
<button
type="button"
onClick={() => setEditing((e) => !e)}
aria-expanded={editing}
className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold transition-colors ${
editing
? "bg-primary text-white"
: "bg-surface text-ink-soft ring-1 ring-line hover:text-primary"
}`}
>
<Pencil className="h-3.5 w-3.5" />
{editing ? "Fertig" : "Bearbeiten"}
</button>
</div>
<AnimatePresence initial={false}>
{editing && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="overflow-hidden"
>
<div className="mt-4 max-h-[260px] space-y-2 overflow-y-auto pr-1">
{answers.map((a, i) => (
<div key={i} className="flex items-center gap-2">
<input
value={a}
onChange={(e) => updateAnswer(i, e.target.value)}
maxLength={60}
placeholder={`Antwort ${i + 1}`}
aria-label={`Antwort ${i + 1}`}
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
/>
<button
type="button"
onClick={() => removeAnswer(i)}
disabled={answers.length <= MIN_ANSWERS}
aria-label={`Antwort ${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"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
<button
type="button"
onClick={addAnswer}
disabled={answers.length >= MAX_ANSWERS}
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"
>
<Plus className="h-4 w-4" />
Antwort hinzufügen
</button>
</motion.div>
)}
</AnimatePresence>
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
Der Ball sagt, was du ihm vorgibst von klassisch bis fies.{" "}
{MIN_ANSWERS}{MAX_ANSWERS} Antworten, automatisch gespeichert.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,147 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Hash } from "lucide-react";
import { gsap } from "@/lib/gsap";
import { popConfetti } from "@/lib/confetti";
import { useSound } from "@/components/providers/sound-provider";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
const randInt = (lo: number, hi: number) =>
lo + Math.floor(Math.random() * (hi - lo + 1));
export default function RandomNumber() {
const [min, setMin] = useLocalStorage("num-min", 1);
const [max, setMax] = useLocalStorage("num-max", 100);
const [display, setDisplay] = useState<string | null>(null);
const [rolling, setRolling] = useState(false);
const [done, setDone] = useState(false);
const lastPaintRef = useRef(0);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
const roll = () => {
if (rolling) return;
const lo = Math.min(min, max);
const hi = Math.max(min, max);
const result = randInt(lo, hi);
setRolling(true);
setDone(false);
play("whoosh");
const dur = reduced ? 0.3 : 1.7;
const proxy = { p: 0 };
lastPaintRef.current = 0;
gsap.to(proxy, {
p: 1,
duration: dur,
ease: "power2.out",
onUpdate: () => {
const now = performance.now();
// Wechsel-Intervall wächst → Slot-Machine-Effekt
const interval = reduced ? 60 : 28 + 240 * proxy.p;
if (now - lastPaintRef.current > interval) {
lastPaintRef.current = now;
setDisplay(String(randInt(lo, hi)));
}
},
onComplete: () => {
setDisplay(String(result));
setRolling(false);
setDone(true);
play("pop");
popConfetti();
},
});
};
return (
<div className="grid items-center gap-10 lg:grid-cols-[1.15fr_0.85fr]">
{/* Anzeige */}
<div className="flex flex-col items-center">
<div className="flex min-h-[190px] items-center justify-center rounded-[2rem] border border-line bg-cream/70 px-10 py-8">
<AnimatePresence mode="wait">
{display === null ? (
<motion.p
key="idle"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center font-display text-xl font-medium text-ink-soft"
>
Bereit, wenn du es bist.
</motion.p>
) : (
<motion.p
key={display + (done ? "-done" : "")}
initial={done ? { scale: 1.35, opacity: 0 } : false}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", stiffness: 320, damping: 18 }}
className={`tabular font-display text-8xl font-semibold tracking-tight sm:text-9xl ${
done ? "text-primary" : "text-ink/70"
}`}
aria-live="polite"
>
{display}
</motion.p>
)}
</AnimatePresence>
</div>
<motion.button
type="button"
onClick={roll}
disabled={rolling}
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"
>
<Hash className="h-5 w-5" />
{rolling ? "Rollt …" : "Zahl generieren"}
</motion.button>
<p className="mt-3 text-xs text-ink-soft">
Zufallszahl zwischen {Math.min(min, max)} und {Math.max(min, max)}
</p>
</div>
{/* Einstellungen */}
<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">
Zahlenraum
</h3>
<div className="mt-4 grid grid-cols-2 gap-3">
<label className="block">
<span className="mb-1.5 block text-xs font-medium text-ink-soft">
Minimum
</span>
<input
type="number"
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"
/>
</label>
<label className="block">
<span className="mb-1.5 block text-xs font-medium text-ink-soft">
Maximum
</span>
<input
type="number"
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"
/>
</label>
</div>
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
Perfekt für Lose, Teams oder Wie viele Liegestütze?. Reihenfolge
von Min &amp; Max ist egal.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,299 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Plus, RotateCcw, Sparkles, X } from "lucide-react";
import { gsap } from "@/lib/gsap";
import { wheelEase } from "@/lib/motion";
import { celebrateConfetti } from "@/lib/confetti";
import { useSound } from "@/components/providers/sound-provider";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
const DEFAULT_OPTIONS = ["1", "2", "3", "4", "5", "6"];
/** Natürliche, gedeckte Farben weiße Schrift bleibt lesbar. */
const PALETTE = [
"#33523f",
"#a8623f",
"#b98a3c",
"#5e7a5e",
"#6b7f8c",
"#8a6b52",
"#5f8a80",
"#9a5b45",
"#4f6b8a",
"#8c7a4f",
"#5c6e4f",
"#3f5e66",
];
const C = 210;
const R = 196;
const MIN_OPTIONS = 2;
const MAX_OPTIONS = 12;
/** Winkel → Punkt auf dem Kreis (0° = oben, im Uhrzeigersinn). */
function polar(angleDeg: number, radius: number): [number, number] {
const a = (angleDeg * Math.PI) / 180;
return [C + radius * Math.sin(a), C - radius * Math.cos(a)];
}
export default function WheelOfFortune() {
const [options, setOptions] = useLocalStorage<string[]>(
"wheel-options",
DEFAULT_OPTIONS,
);
const [spinning, setSpinning] = useState(false);
const [winner, setWinner] = useState<number | null>(null);
const wheelRef = useRef<SVGGElement>(null);
const pointerRef = useRef<HTMLDivElement>(null);
const rotationRef = useRef(0);
const lastTickRef = useRef(-1);
const { play } = useSound();
const reduced = usePrefersReducedMotion();
const labels = options.map((o, i) => o.trim() || `Option ${i + 1}`);
const n = labels.length;
const seg = 360 / n;
const spin = () => {
if (spinning || n < MIN_OPTIONS) return;
const winnerIndex = Math.floor(Math.random() * n);
setWinner(null);
setSpinning(true);
play("whoosh");
lastTickRef.current = -1;
// Zufällige Position innerhalb des Gewinner-Segments
const offset = 0.18 + Math.random() * 0.64;
const targetAngle = winnerIndex * seg + seg * offset;
const current = rotationRef.current;
const currentMod = ((current % 360) + 360) % 360;
const delta =
(((360 - targetAngle - currentMod) % 360) + 360) % 360;
const target = current + 360 * 6 + delta;
const dur = reduced ? 0.5 : 5.6;
gsap.to(wheelRef.current, {
rotation: target,
svgOrigin: `${C} ${C}`,
duration: dur,
ease: reduced ? "power1.out" : wheelEase,
onUpdate: () => {
if (reduced) return;
const rot = Number(gsap.getProperty(wheelRef.current, "rotation"));
const m = ((rot % 360) + 360) % 360;
const idx = Math.floor(((360 - m) % 360) / seg);
if (idx !== lastTickRef.current) {
lastTickRef.current = idx;
play("tick");
gsap.fromTo(
pointerRef.current,
{ rotation: -16 },
{
rotation: 0,
duration: 0.22,
ease: "elastic.out(2.2, 0.4)",
transformOrigin: "50% 12%",
},
);
}
},
onComplete: () => {
rotationRef.current = target;
setSpinning(false);
setWinner(winnerIndex);
play("win");
celebrateConfetti();
},
});
};
const updateOption = (i: number, value: string) => {
setOptions((opts) => opts.map((o, idx) => (idx === i ? value : o)));
setWinner(null);
};
const addOption = () => {
if (n >= MAX_OPTIONS) return;
setOptions((opts) => [...opts, ""]);
setWinner(null);
};
const removeOption = (i: number) => {
if (n <= MIN_OPTIONS) return;
setOptions((opts) => opts.filter((_, idx) => idx !== i));
setWinner(null);
};
const fontSize = n <= 6 ? 15 : n <= 9 ? 13 : 11.5;
return (
<div className="grid items-center gap-10 lg:grid-cols-[1.1fr_0.9fr]">
{/* Rad */}
<div className="flex flex-col items-center">
<div className="relative w-full max-w-[400px]">
{/* Zeiger */}
<div
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)]"
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)]">
{/* äußerer Ring */}
<circle cx={C} cy={C} r={206} fill="#ffffff" stroke="#e9e9e4" />
<g ref={wheelRef}>
{labels.map((label, i) => {
const [x0, y0] = polar(i * seg, R);
const [x1, y1] = polar((i + 1) * seg, R);
const large = seg > 180 ? 1 : 0;
const isWinner = winner === i;
const dimmed = winner !== null && !isWinner;
return (
<g key={i}>
<path
d={`M ${C} ${C} L ${x0} ${y0} A ${R} ${R} 0 ${large} 1 ${x1} ${y1} Z`}
fill={PALETTE[i % PALETTE.length]}
stroke="#ffffff"
strokeWidth={isWinner ? 3.5 : 1.5}
className="transition-opacity duration-500"
style={{
opacity: dimmed ? 0.4 : 1,
filter: isWinner
? "drop-shadow(0 0 14px rgb(242 179 61 / 0.9))"
: undefined,
}}
/>
<g transform={`rotate(${i * seg + seg / 2} ${C} ${C})`}>
<text
x={C}
y={C - 118}
textAnchor="middle"
dominantBaseline="central"
transform={`rotate(90 ${C} ${C - 118})`}
fill="#ffffff"
fontSize={fontSize}
fontWeight={600}
fontFamily="var(--font-display)"
style={{
textShadow: "0 1px 3px rgb(0 0 0 / 0.35)",
opacity: dimmed ? 0.4 : 1,
}}
className="transition-opacity duration-500"
>
{label.length > 16 ? `${label.slice(0, 15)}` : label}
</text>
</g>
</g>
);
})}
</g>
{/* Nabe */}
<circle cx={C} cy={C} r={30} fill="#ffffff" stroke="#e9e9e4" strokeWidth={2} />
<circle cx={C} cy={C} r={9} fill="#6c5ce7" />
</svg>
</div>
<div className="mt-4 h-14" aria-live="polite">
<AnimatePresence mode="wait">
{winner !== null && (
<motion.p
key={`${winner}-${labels[winner]}`}
initial={{ opacity: 0, scale: 0.6, y: 12 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }}
className="flex items-center gap-2 font-display text-3xl font-semibold tracking-tight"
>
<Sparkles className="h-6 w-6 text-clay" />
<span className="text-primary">{labels[winner]}!</span>
</motion.p>
)}
</AnimatePresence>
</div>
<motion.button
type="button"
onClick={spin}
disabled={spinning || n < MIN_OPTIONS}
whileTap={{ scale: 0.95 }}
className="mt-2 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"
>
<RotateCcw className="h-5 w-5" />
{spinning ? "Dreht sich …" : "Drehen!"}
</motion.button>
</div>
{/* Optionen-Editor */}
<div className="rounded-3xl border border-line bg-cream/70 p-5">
<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"
>
Zurücksetzen
</button>
</div>
<div className="mt-4 max-h-[300px] space-y-2 overflow-y-auto pr-1">
{options.map((option, i) => (
<div key={i} className="flex items-center gap-2.5">
<span
aria-hidden
className="h-3.5 w-3.5 shrink-0 rounded-full ring-2 ring-white"
style={{ background: PALETTE[i % PALETTE.length] }}
/>
<input
value={option}
onChange={(e) => updateOption(i, e.target.value)}
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"
/>
<button
type="button"
onClick={() => removeOption(i)}
disabled={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"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
<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"
>
<Plus className="h-4 w-4" />
Option hinzufügen
</button>
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
{MIN_OPTIONS}{MAX_OPTIONS} Optionen. Deine Liste bleibt automatisch
gespeichert.
</p>
</div>
</div>
);
}

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
# Startet nur die Entscheidomat-App (entscheidomat.com).
# TLS und Domain-Routing übernimmt der Reverse Proxy (Caddy) auf dem
# Produktionsserver — der ist NICHT Teil dieses Setups. Caddy muss lediglich
# auf den unten published Port (Default 3000) dieses Containers zeigen, z. B.:
#
# entscheidomat.com {
# reverse_proxy localhost:3000
# }
services:
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
env_file:
- path: .env
required: false
environment:
NODE_ENV: production
ports:
- "${APP_PORT:-3000}:3000"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

18
eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@@ -0,0 +1,68 @@
"use client";
import { useCallback, useSyncExternalStore } from "react";
type Listener = () => void;
const listeners = new Map<string, Set<Listener>>();
const cache = new Map<string, unknown>();
function readFromStorage<T>(key: string, initial: T): T {
try {
const raw = window.localStorage.getItem(key);
return raw != null ? (JSON.parse(raw) as T) : initial;
} catch {
return initial;
}
}
function notify(key: string) {
listeners.get(key)?.forEach((l) => l());
}
/**
* State, der in localStorage persistiert wird.
* SSR-sicher über useSyncExternalStore: liefert serverseitig `initial`,
* hydriert clientseitig aus localStorage.
*/
export function useLocalStorage<T>(key: string, initial: T) {
const subscribe = useCallback(
(callback: Listener) => {
let set = listeners.get(key);
if (!set) {
set = new Set();
listeners.set(key, set);
}
set.add(callback);
return () => set!.delete(callback);
},
[key],
);
const getSnapshot = useCallback(() => {
if (!cache.has(key)) cache.set(key, readFromStorage(key, initial));
return cache.get(key) as T;
// eslint-disable-next-line react-hooks/exhaustive-deps -- `initial` is only used as a one-time fallback
}, [key]);
const getServerSnapshot = useCallback(() => initial, [initial]);
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const setValue = useCallback(
(next: T | ((prev: T) => T)) => {
const prev = cache.has(key) ? (cache.get(key) as T) : readFromStorage(key, initial);
const resolved = typeof next === "function" ? (next as (p: T) => T)(prev) : next;
cache.set(key, resolved);
try {
window.localStorage.setItem(key, JSON.stringify(resolved));
} catch {
// Speicher voll o.ä. egal
}
notify(key);
},
[key, initial],
);
return [value, setValue] as const;
}

View File

@@ -0,0 +1,24 @@
"use client";
import { useSyncExternalStore } from "react";
const QUERY = "(prefers-reduced-motion: reduce)";
function subscribe(callback: () => void) {
const mq = window.matchMedia(QUERY);
mq.addEventListener("change", callback);
return () => mq.removeEventListener("change", callback);
}
function getSnapshot() {
return window.matchMedia(QUERY).matches;
}
function getServerSnapshot() {
return false;
}
/** true, wenn der Nutzer reduzierte Bewegung wünscht (A11y-Gate). */
export function usePrefersReducedMotion() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

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;

7
next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

BIN
og-preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

6797
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "webseite",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"canvas-confetti": "^1.9.4",
"framer-motion": "^12.42.2",
"gsap": "^3.15.0",
"lucide-react": "^1.25.0",
"next": "16.2.11",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.11",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1 @@
google-site-verification: googleccd5315437d68a49.html

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#0d7a57"/>
<path d="M143 168h226c18 0 33 15 33 33v110c0 18-15 33-33 33H143c-18 0-33-15-33-33V201c0-18 15-33 33-33Z" fill="#faf8f3"/>
<rect x="142" y="200" width="94" height="112" rx="20" fill="#dcefe6"/>
<rect x="276" y="200" width="94" height="112" rx="20" fill="#0d7a57"/>
<path d="m300 255 17 17 31-37" fill="none" stroke="#faf8f3" stroke-linecap="round" stroke-linejoin="round" stroke-width="14"/>
<path d="M256 344v36" fill="none" stroke="#faf8f3" stroke-linecap="round" stroke-width="16"/>
<circle cx="256" cy="408" r="25" fill="#8bb79d"/>
</svg>

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

22
public/llms.txt Normal file
View File

@@ -0,0 +1,22 @@
# Entscheidomat
> Entscheidomat ist ein kostenloser Zufallsgenerator und eine digitale Entscheidungshilfe. Sieben Tools Ja-Nein-Generator, Münze, Glücksrad, Würfel, Zufallszahl, Magic 8-Ball und Listen-Auslosung laufen direkt im Browser, ohne Anmeldung und kostenlos. Zielgruppe: Alltagsentscheidungen für Einzelpersonen und Gruppen (z. B. Restaurantwahl, Teameinteilung, spielerische Fragen). Nicht gedacht für wichtige persönliche, finanzielle, rechtliche oder gesundheitliche Entscheidungen.
## Tools
- [Ja-Nein-Generator](https://entscheidomat.com/ja-nein-generator): Beantwortet eine Frage zufällig mit Ja oder Nein.
- [Glücksrad](https://entscheidomat.com/gluecksrad): Digitales Entscheidungsrad mit bis zu 12 frei einstellbaren Optionen.
- [Würfel Online](https://entscheidomat.com/wuerfel-online): Virtueller 6-seitiger Würfel mit Zahlen oder eigenen Texten.
- [Namen-Auslosung](https://entscheidomat.com/namen-auslosen): Zieht zufällig einen Eintrag aus einer eigenen Liste, wahlweise ohne Zurücklegen.
- [Münze werfen](https://entscheidomat.com/muenze-werfen): Münzwurf mit frei beschriftbaren Seiten (Standard: Kopf/Zahl).
- [Zufallszahl-Generator](https://entscheidomat.com/zufallszahl-generator): Erzeugt eine Zufallszahl in einem frei wählbaren Bereich.
- [Magic 8-Ball](https://entscheidomat.com/magic-8-ball): Zufällige Antwort aus einer anpassbaren Antwortliste (220 Antworten).
## Rechtliches
- [Impressum](https://entscheidomat.com/impressum)
- [Datenschutzerklärung](https://entscheidomat.com/datenschutz)
## Kontakt
info@entscheidomat.com

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}