import { useState, useEffect, useRef, useCallback } from „react“;
const QR_URL = „https://elkg.de/wiesengottesdienst26“;
const MAX_CHARS = 30;
function groupPrayers(prayers) {
const map = new Map();
for (const p of prayers) {
const key = p.text.trim().toLowerCase();
if (map.has(key)) map.get(key).count++;
else map.set(key, { text: p.text.trim(), count: 1, firstTs: p.ts });
}
return Array.from(map.values()).sort((a, b) => b.count – a.count || a.firstTs – b.firstTs);
}
function cardStyle(count, maxCount) {
if (maxCount <= 1) return { fontSize: "1.05rem", padding: "0.72rem 1rem" };
const t = (count - 1) / (maxCount - 1);
return {
fontSize: `${(1.05 + t * 1.25).toFixed(2)}rem`,
padding: `${(0.72 + t * 0.6).toFixed(2)}rem ${(1.0 + t * 0.85).toFixed(2)}rem`,
};
}
const FONTS = `@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;1,400&family=DM+Sans:wght@300;400;500&display=swap');`;
const CSS = `
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body { background: #f5f2ec; font-family: 'DM Sans', sans-serif; color: #2a2620; overflow: hidden; }
button { font-family: 'DM Sans', sans-serif; }
.screen { min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 3rem 1.5rem; background: #f5f2ec; overflow-y: auto; }
.event-label { font-size: 0.68rem; letter-spacing: 0.22em; text-transform: uppercase; color: #a09a90; font-weight: 300; margin-bottom: 2.5rem; text-align: center; }
/* SUBMIT */
.submit-heading { font-family: 'EB Garamond', serif; font-size: 1.9rem; font-weight: 400; font-style: italic; color: #2a2620; text-align: center; margin-bottom: 0.4rem; }
.submit-hint { font-size: 0.75rem; font-weight: 300; color: #9a9488; text-align: center; margin-bottom: 2.2rem; }
.input-wrap { width: 100%; max-width: 400px; position: relative; }
.single-input { width: 100%; background: #fff; border: 1px solid #ddd8d0; border-radius: 0.65rem; padding: 0.95rem 3rem 0.95rem 1rem; color: #2a2620; font-family: 'EB Garamond', serif; font-size: 1.2rem; font-style: italic; line-height: 1.4; outline: none; display: block; transition: border-color 0.2s; box-shadow: 0 1px 4px rgba(0,0,0,0.05); }
.single-input:focus { border-color: #b0a898; }
.single-input::placeholder { color: #c8c2b8; }
.char-badge { position: absolute; right: 0.8rem; top: 50%; transform: translateY(-50%); font-size: 0.62rem; color: #c0b8ae; pointer-events: none; }
.char-badge.warn { color: #c08060; }
.primary-btn { width: 100%; max-width: 400px; margin-top: 0.8rem; padding: 0.82rem; background: #2a2620; border: none; border-radius: 0.65rem; font-size: 0.8rem; letter-spacing: 0.13em; text-transform: uppercase; color: #f5f2ec; cursor: pointer; transition: opacity 0.2s; }
.primary-btn:hover { opacity: 0.82; }
.primary-btn:disabled { opacity: 0.25; cursor: not-allowed; }
.ok-msg { margin-top: 1rem; font-size: 0.8rem; color: #9a9488; text-align: center; animation: fadeUp 0.35s ease; }
/* ADMIN */
.admin-title { font-family: 'EB Garamond', serif; font-size: 1.5rem; font-weight: 400; color: #2a2620; margin-bottom: 0.25rem; text-align: center; }
.admin-sub { font-size: 0.73rem; color: #9a9488; margin-bottom: 2.5rem; text-align: center; font-weight: 300; }
.admin-actions { display: flex; gap: 0.6rem; margin-bottom: 2.5rem; flex-wrap: wrap; justify-content: center; }
.action-btn { padding: 0.55rem 1.1rem; background: #fff; border: 1px solid #ddd8d0; border-radius: 0.5rem; font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: #706860; cursor: pointer; transition: all 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.action-btn:hover { border-color: #b0a898; color: #2a2620; }
.action-btn.hi { background: #2a2620; color: #f5f2ec; border-color: #2a2620; box-shadow: none; }
.action-btn.hi:hover { opacity: 0.82; }
.action-btn.danger { color: #b05040; border-color: #e8d0c8; background: #fff; }
.action-btn.danger:hover { border-color: #b05040; background: #fdf5f3; }
.prayer-list { width: 100%; max-width: 520px; }
.list-title { font-size: 0.65rem; letter-spacing: 0.18em; text-transform: uppercase; color: #b0a898; margin-bottom: 1rem; }
.prayer-item { padding: 0.75rem 1rem; border: 1px solid #e8e2d8; border-radius: 0.6rem; margin-bottom: 0.4rem; background: #fff; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 1px 3px rgba(0,0,0,0.03); }
.prayer-item-text { font-family: 'EB Garamond', serif; font-size: 1rem; font-style: italic; color: #3a3430; }
.prayer-item-count { font-size: 0.65rem; color: #b0a898; margin-left: 0.5rem; white-space: nowrap; }
.del-link { font-size: 0.62rem; color: #c8b0a8; cursor: pointer; margin-left: 1rem; flex-shrink: 0; }
.del-link:hover { color: #b05040; }
.empty-list { font-size: 0.8rem; color: #c0b8ae; text-align: center; padding: 2rem 0; font-style: italic; font-family: 'EB Garamond', serif; }
/* BEAMER */
.beamer { width: 100vw; height: 100vh; overflow: hidden; background: #f0ece4; position: relative; display: flex; flex-direction: column; }
.b-canvas { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.b-top { position: relative; z-index: 2; padding: 1.4rem 2rem 0; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
.b-event { font-size: 0.65rem; letter-spacing: 0.22em; text-transform: uppercase; color: #c0b8ae; font-weight: 300; }
.b-nav { display: flex; gap: 0.4rem; align-items: center; }
.b-btn { background: rgba(255,255,255,0.7); border: 1px solid #ddd8d0; border-radius: 0.4rem; padding: 0.28rem 0.6rem; font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase; color: #a09890; cursor: pointer; transition: all 0.2s; }
.b-btn:hover { border-color: #b0a898; color: #2a2620; }
.b-btn.danger { color: #c07060; border-color: #eaccc4; background: rgba(255,255,255,0.7); }
.b-btn.danger:hover { border-color: #c07060; color: #b05040; }
.b-wall { flex: 1; position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; padding: 1rem 2rem; overflow: hidden; }
.b-grid { display: flex; flex-wrap: wrap; gap: 0.55rem; align-content: center; justify-content: center; max-width: 1200px; width: 100%; }
.p-card {
background: #ffffff;
border: 1px solid #e4ddd4;
border-radius: 0.75rem;
font-family: 'EB Garamond', serif;
font-style: italic;
color: #5a5248;
line-height: 1.25;
white-space: nowrap;
position: relative;
box-shadow: 0 1px 6px rgba(0,0,0,0.06);
transition: font-size 0.5s ease, padding 0.5s ease, border-color 0.4s ease, color 0.4s ease, box-shadow 0.4s ease;
animation: cardPop 0.45s cubic-bezier(0.34,1.5,0.64,1) both;
}
.p-card.popular { border-color: #d4ccc0; color: #3a3228; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
.p-card.top { border-color: #c4b8a8; color: #2a2218; box-shadow: 0 3px 16px rgba(0,0,0,0.10); }
.p-card.bump { animation: bumpPulse 0.4s ease; }
.p-count { position: absolute; top: -7px; right: -7px; background: #f0ece4; border: 1px solid #d8d0c4; border-radius: 999px; padding: 1px 6px; font-family: 'DM Sans', sans-serif; font-style: normal; font-size: 0.58rem; font-weight: 500; color: #9a9080; line-height: 1.6; pointer-events: none; }
.p-count.hi { background: #e8e0d4; border-color: #c0b4a4; color: #6a6058; }
.b-empty { position: relative; z-index: 2; flex: 1; display: flex; align-items: center; justify-content: center; }
.b-empty-text { font-family: 'EB Garamond', serif; font-size: 1.6rem; font-style: italic; color: #d0c8bc; }
.b-bottom { position: relative; z-index: 2; flex-shrink: 0; display: flex; justify-content: space-between; align-items: flex-end; padding: 0.5rem 2rem 1.5rem; }
.b-count-label { font-size: 0.6rem; letter-spacing: 0.15em; text-transform: uppercase; color: #c0b8ae; }
.qr-wrap { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
.qr-box { width: 78px; height: 78px; border-radius: 0.4rem; border: 1px solid #ddd8d0; overflow: hidden; background: #fff; display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
.qr-hint { font-size: 0.55rem; letter-spacing: 0.14em; text-transform: uppercase; color: #b0a898; }
@keyframes cardPop { from { opacity: 0; transform: scale(0.75); } to { opacity: 1; transform: scale(1); } }
@keyframes bumpPulse { 0% { transform: scale(1); } 40% { transform: scale(1.07); } 100% { transform: scale(1); } }
@keyframes fadeUp { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
`;
function Particles() {
const ref = useRef();
useEffect(() => {
const c = ref.current; const ctx = c.getContext(„2d“);
const resize = () => { c.width = innerWidth; c.height = innerHeight; }; resize();
window.addEventListener(„resize“, resize);
const pts = Array.from({ length: 30 }, () => ({
x: Math.random() * innerWidth, y: Math.random() * innerHeight,
r: Math.random() * 1.4 + 0.3,
vy: -(Math.random() * 0.18 + 0.04), vx: (Math.random() – 0.5) * 0.07,
a: Math.random() * 0.12 + 0.02, ph: Math.random() * Math.PI * 2,
}));
let raf;
const draw = () => {
ctx.clearRect(0, 0, c.width, c.height);
pts.forEach(p => {
p.ph += 0.018;
const a = p.a * (0.5 + 0.5 * Math.sin(p.ph));
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(160,148,128,${a.toFixed(3)})`; ctx.fill();
p.x += p.vx; p.y += p.vy;
if (p.y < -5) { p.y = c.height + 5; p.x = Math.random() * c.width; }
});
raf = requestAnimationFrame(draw);
}; draw();
return () => { window.removeEventListener(„resize“, resize); cancelAnimationFrame(raf); };
}, []);
return ;
}
function QRCanvas({ url, size = 72 }) {
const ref = useRef();
useEffect(() => {
let dead = false;
const init = () => {
if (dead || !ref.current) return;
if (window.QRCode) {
ref.current.innerHTML = „“;
new window.QRCode(ref.current, { text: url, width: size, height: size, colorDark: „#2a2620“, colorLight: „#ffffff“, correctLevel: window.QRCode.CorrectLevel.M });
} else {
const s = document.createElement(„script“);
s.src = „https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js“;
s.onload = () => { if (!dead) init(); }; document.head.appendChild(s);
}
}; init();
return () => { dead = true; };
}, [url, size]);
return
}
function PrayerCard({ item, maxCount, bumped }) {
const s = cardStyle(item.count, maxCount);
const cls = [„p-card“, item.count >= 3 ? „top“ : item.count >= 2 ? „popular“ : „“, bumped ? „bump“ : „“].filter(Boolean).join(“ „);
return (
{item.count > 1 && = 3 ? “ hi“ : „“}`}>×{item.count}}
);
}
export default function App() {
const [page, setPage] = useState(„beamer“);
const [prayers, setPrayers] = useState([]);
const [liveMode, setLiveMode] = useState(true);
const [prevCounts, setPrevCounts] = useState({});
const [bumpSet, setBumpSet] = useState(new Set());
const [text, setText] = useState(„“);
const [sent, setSent] = useState(false);
const [busy, setBusy] = useState(false);
const loadLive = useCallback(async () => {
if (!liveMode) return;
try {
const res = await window.storage.list(„pr:“, true);
if (!res?.keys?.length) { setPrayers([]); return; }
const items = (await Promise.all(res.keys.map(async k => {
try { const r = await window.storage.get(k, true); return r ? JSON.parse(r.value) : null; } catch { return null; }
}))).filter(Boolean).sort((a, b) => a.ts – b.ts);
setPrayers(items);
} catch {}
}, [liveMode]);
useEffect(() => { if (!liveMode) return; loadLive(); const iv = setInterval(loadLive, 3000); return () => clearInterval(iv); }, [liveMode, loadLive]);
const grouped = groupPrayers(prayers);
const maxCount = grouped[0]?.count || 1;
useEffect(() => {
const bumped = new Set();
grouped.forEach(g => { const k = g.text.toLowerCase(); if (prevCounts[k] !== undefined && g.count > prevCounts[k]) bumped.add(k); });
if (bumped.size) { setBumpSet(bumped); setTimeout(() => setBumpSet(new Set()), 450); }
const next = {}; grouped.forEach(g => { next[g.text.toLowerCase()] = g.count; }); setPrevCounts(next);
}, [prayers]);
const goLive = async () => {
setLiveMode(true); setPrayers([]);
try {
const res = await window.storage.list(„pr:“, true); if (!res?.keys?.length) return;
const items = (await Promise.all(res.keys.map(async k => { try { const r = await window.storage.get(k, true); return r ? JSON.parse(r.value) : null; } catch { return null; } }))).filter(Boolean).sort((a, b) => a.ts – b.ts);
setPrayers(items);
} catch {}
};
const send = async () => {
if (!text.trim() || busy) return; setBusy(true);
try {
const key = `pr:${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
await window.storage.set(key, JSON.stringify({ id: Date.now(), text: text.trim(), ts: Date.now() }), true);
setText(„“); setSent(true); setTimeout(() => setSent(false), 3000);
if (liveMode) await loadLive();
} catch (e) { console.error(e); }
setBusy(false);
};
const clearAll = async () => {
if (!confirm(„Alle Anliegen löschen?“)) return;
if (liveMode) { try { const res = await window.storage.list(„pr:“, true); if (res?.keys) await Promise.all(res.keys.map(k => window.storage.delete(k, true))); } catch {} }
setPrayers([]);
};
const deleteGroup = async (groupText) => {
if (liveMode) {
try { const res = await window.storage.list(„pr:“, true); for (const k of res?.keys || []) { try { const r = await window.storage.get(k, true); if (r && JSON.parse(r.value).text.trim().toLowerCase() === groupText.toLowerCase()) await window.storage.delete(k, true); } catch {} } } catch {}
}
setPrayers(p => p.filter(x => x.text.trim().toLowerCase() !== groupText.toLowerCase()));
};
if (page === „submit“) return (
<>
Wiesengottesdienst 2026
Gebetsanliegen
Teile dein Anliegen – wir beten gemeinsam
= MAX_CHARS – 5 ? “ warn“ : „“}`}>{MAX_CHARS – text.length}
{sent &&
Danke – dein Anliegen wurde empfangen.
}
>
);
if (page === „beamer“) return (
<>
{prayers.length > 0 && }
{grouped.length === 0
?
Wartet auf Anliegen …
:
))}
}
Anliegen einreichen
>
);
return (
<>
Wiesengottesdienst 2026
Verwaltung
Gebetsanliegen verwalten
{prayers.length > 0 && }
{grouped.length === 0 ? „Keine Anliegen“ : `${grouped.length} Anliegen · ${prayers.length} Einreichungen`}
{grouped.length === 0 ?
Noch nichts eingereicht.
: grouped.map(g => (
{g.count > 1 && ×{g.count}}
deleteGroup(g.text)}>löschen
))
}
>
);
}

