<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>30 Jahre Glanz – Julias Geburtstag</title>
<meta name="description" content="RSVP für Julias 30. Geburtstag – 60s Glam, Breakfast at Tiffany’s Vibes. Zusage + Essenswünsche + Begleitpersonen." />
<!-- Optional: Social Preview -->
<meta property="og:title" content="30 Jahre Glanz – Julias Geburtstag" />
<meta property="og:description" content="Bitte sag kurz Bescheid, ob du kommst – inkl. Begleitpersonen & Essenswunsch (Fleisch/Vegetarisch/Vegan)." />
<!-- <meta property="og:image" content="https://30jahreglanz.fun/assets/einladung.jpg" /> -->
<style>
/* ====== Tiffany-Glam Styling (60s) ====== */
:root{
--tiffany: #7fd6da;
--tiffany-dark: #38b8bf;
--ink: #0e1216;
--paper: #f7fbff;
--gold: #c8a858;
--line: rgba(14,18,22,.12);
--shadow: 0 14px 40px rgba(14,18,22,.14);
--radius: 22px;
--radius-sm: 14px;
--max: 1060px;
}
*{ box-sizing: border-box; }
html,body{ height:100%; }
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
color: var(--ink);
background:
radial-gradient(1200px 800px at 20% 10%, rgba(127,214,218,.40), transparent 55%),
radial-gradient(900px 700px at 80% 20%, rgba(200,168,88,.18), transparent 55%),
radial-gradient(1200px 900px at 50% 110%, rgba(127,214,218,.25), transparent 60%),
linear-gradient(180deg, #ffffff, var(--paper));
overflow-x:hidden;
}
/* Fancy subtle sparkle noise via CSS-only */
.grain{
position: fixed;
inset: 0;
pointer-events:none;
opacity:.06;
background-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
mix-blend-mode: multiply;
}
a{ color: inherit; }
.wrap{ max-width: var(--max); margin: 0 auto; padding: 22px 18px 70px; }
header{
position: relative;
border-radius: var(--radius);
padding: 28px 24px 22px;
overflow:hidden;
background:
linear-gradient(135deg, rgba(127,214,218,.38), rgba(255,255,255,.65) 55%, rgba(200,168,88,.14)),
radial-gradient(700px 400px at 12% 0%, rgba(56,184,191,.28), transparent 55%);
border: 1px solid rgba(14,18,22,.10);
box-shadow: var(--shadow);
}
.topbar{
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
flex-wrap:wrap;
margin-bottom: 14px;
}
.badge{
display:inline-flex;
align-items:center;
gap:10px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(255,255,255,.75);
border: 1px solid rgba(14,18,22,.10);
backdrop-filter: blur(8px);
font-weight: 650;
letter-spacing: .2px;
}
.dot{
width:10px; height:10px;
border-radius: 999px;
background: var(--tiffany-dark);
box-shadow: 0 0 0 3px rgba(127,214,218,.35);
}
.cta-row{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
align-items:center;
}
.btn{
appearance:none;
border:0;
cursor:pointer;
border-radius: 999px;
padding: 12px 16px;
font-weight: 700;
letter-spacing: .2px;
display:inline-flex;
align-items:center;
gap:10px;
transition: transform .08s ease, box-shadow .18s ease, background .18s ease, border-color .18s ease;
text-decoration:none;
user-select:none;
white-space: nowrap;
}
.btn:active{ transform: translateY(1px) scale(.99); }
.btn-primary{
background: linear-gradient(180deg, var(--tiffany), var(--tiffany-dark));
color: #042a2d;
box-shadow: 0 10px 22px rgba(56,184,191,.28);
}
.btn-ghost{
background: rgba(255,255,255,.72);
border: 1px solid rgba(14,18,22,.12);
}
.btn:hover{ box-shadow: 0 14px 30px rgba(14,18,22,.10); }
.hero{
display:grid;
grid-template-columns: 1.2fr .8fr;
gap: 18px;
align-items:center;
margin-top: 8px;
}
@media (max-width: 900px){
.hero{ grid-template-columns: 1fr; }
}
.h-eyebrow{
font-weight: 800;
letter-spacing: .18em;
text-transform: uppercase;
color: rgba(14,18,22,.72);
font-size: 12px;
}
h1{
margin: 10px 0 10px;
font-size: clamp(30px, 4.6vw, 56px);
line-height: 1.03;
letter-spacing: -0.02em;
}
.sub{
margin: 0 0 16px;
font-size: 16px;
line-height: 1.55;
color: rgba(14,18,22,.80);
max-width: 62ch;
}
.quote{
margin-top: 14px;
padding: 14px 16px;
border-radius: var(--radius-sm);
border: 1px solid rgba(14,18,22,.10);
background: rgba(255,255,255,.70);
}
.quote b{ letter-spacing: .02em; }
.quote p{ margin: 0; }
.poster{
border-radius: var(--radius);
border: 1px solid rgba(14,18,22,.10);
background:
radial-gradient(500px 260px at 50% 20%, rgba(200,168,88,.22), transparent 60%),
linear-gradient(180deg, rgba(255,255,255,.70), rgba(127,214,218,.10));
overflow:hidden;
box-shadow: 0 18px 44px rgba(14,18,22,.14);
min-height: 320px;
position: relative;
}
.poster::after{
content:"";
position:absolute;
inset:-60px;
background:
radial-gradient(circle at 20% 30%, rgba(255,255,255,.8), transparent 25%),
radial-gradient(circle at 60% 20%, rgba(255,255,255,.65), transparent 20%),
radial-gradient(circle at 75% 55%, rgba(255,255,255,.55), transparent 22%),
radial-gradient(circle at 35% 75%, rgba(255,255,255,.45), transparent 22%);
opacity:.45;
transform: rotate(12deg);
pointer-events:none;
}
.poster-inner{
position:absolute;
inset:0;
display:flex;
flex-direction:column;
justify-content:space-between;
padding: 18px;
}
.poster-top{
display:flex;
justify-content:space-between;
gap:10px;
align-items:flex-start;
}
.diamond{
width: 36px; height: 36px;
border-radius: 12px;
background: rgba(255,255,255,.75);
border: 1px solid rgba(14,18,22,.10);
display:grid;
place-items:center;
font-size: 18px;
}
.poster-title{
font-weight: 850;
letter-spacing: .06em;
text-transform: uppercase;
font-size: 12px;
color: rgba(14,18,22,.78);
}
.poster-big{
font-size: 40px;
font-weight: 900;
letter-spacing: -0.02em;
margin-top: 6px;
}
.img-slot{
position:absolute;
inset:0;
z-index:-1;
opacity:.9;
background:
linear-gradient(180deg, rgba(255,255,255,.65), rgba(255,255,255,.18)),
radial-gradient(900px 420px at 50% -10%, rgba(127,214,218,.35), transparent 60%);
}
/* Wenn du ein echtes Bild nutzen willst:
- lege es als assets/einladung.jpg ab
- und ersetze unten background-image entsprechend. */
/* .img-slot{ background-image: url("assets/einladung.jpg"); background-size: cover; background-position: center; } */
main{ margin-top: 22px; display:grid; gap: 18px; }
.grid{
display:grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
@media (max-width: 900px){
.grid{ grid-template-columns: 1fr; }
}
.card{
background: rgba(255,255,255,.78);
border: 1px solid rgba(14,18,22,.10);
border-radius: var(--radius);
box-shadow: 0 12px 34px rgba(14,18,22,.08);
padding: 18px;
backdrop-filter: blur(10px);
}
.card h2{
margin: 0 0 12px;
font-size: 20px;
letter-spacing: -0.01em;
}
.facts{
display:grid;
gap: 10px;
}
.fact{
display:flex;
gap: 10px;
align-items:flex-start;
padding: 12px 12px;
border-radius: var(--radius-sm);
border: 1px solid rgba(14,18,22,.09);
background: rgba(255,255,255,.68);
}
.icon{
width: 36px; height: 36px;
border-radius: 12px;
display:grid;
place-items:center;
background: rgba(127,214,218,.22);
border: 1px solid rgba(56,184,191,.18);
font-size: 18px;
flex: 0 0 auto;
}
.fact b{ display:block; }
.muted{ color: rgba(14,18,22,.70); }
.pill{
display:inline-flex;
align-items:center;
gap:8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(127,214,218,.18);
border: 1px solid rgba(56,184,191,.22);
font-weight: 750;
margin-top: 10px;
}
/* ===== Form ===== */
form{ display:grid; gap: 14px; }
.row{
display:grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 720px){
.row{ grid-template-columns: 1fr; }
}
label{
display:grid;
gap: 7px;
font-weight: 700;
font-size: 13px;
letter-spacing: .02em;
}
input, select, textarea{
width:100%;
border-radius: 14px;
border: 1px solid rgba(14,18,22,.14);
background: rgba(255,255,255,.78);
padding: 12px 12px;
font: inherit;
outline: none;
transition: box-shadow .18s ease, border-color .18s ease;
}
textarea{ min-height: 96px; resize: vertical; }
input:focus, select:focus, textarea:focus{
border-color: rgba(56,184,191,.65);
box-shadow: 0 0 0 4px rgba(127,214,218,.25);
}
.segmented{
display:flex;
gap:10px;
flex-wrap:wrap;
padding: 10px;
border-radius: 18px;
background: rgba(255,255,255,.65);
border: 1px solid rgba(14,18,22,.10);
}
.segmented input{ display:none; }
.segmented label{
margin:0;
cursor:pointer;
user-select:none;
display:inline-flex;
align-items:center;
gap:8px;
padding: 10px 12px;
border-radius: 999px;
border: 1px solid rgba(14,18,22,.12);
background: rgba(255,255,255,.70);
font-weight: 850;
font-size: 13px;
transition: transform .08s ease, background .18s ease, border-color .18s ease;
}
.segmented label:active{ transform: translateY(1px); }
.segmented input:checked + label{
background: linear-gradient(180deg, rgba(127,214,218,.45), rgba(56,184,191,.25));
border-color: rgba(56,184,191,.55);
}
.guests{
display:grid;
gap: 12px;
}
.guest{
border-radius: var(--radius);
border: 1px solid rgba(14,18,22,.10);
background: rgba(255,255,255,.65);
padding: 12px;
display:grid;
gap: 10px;
}
.guest-head{
display:flex;
justify-content:space-between;
align-items:center;
gap: 12px;
}
.guest-title{
font-weight: 900;
letter-spacing: -0.01em;
}
.btn-small{
padding: 10px 12px;
border-radius: 999px;
font-weight: 850;
border: 1px solid rgba(14,18,22,.12);
background: rgba(255,255,255,.75);
}
.divider{
height:1px;
background: rgba(14,18,22,.10);
margin: 8px 0;
}
.notice{
padding: 12px 14px;
border-radius: var(--radius);
background: rgba(127,214,218,.18);
border: 1px solid rgba(56,184,191,.26);
color: rgba(14,18,22,.85);
}
.fine{
font-size: 12px;
color: rgba(14,18,22,.65);
line-height: 1.45;
}
.footer{
text-align:center;
margin-top: 20px;
color: rgba(14,18,22,.62);
font-size: 12px;
}
/* ===== Modal ===== */
dialog{
width: min(680px, calc(100% - 28px));
border: 1px solid rgba(14,18,22,.14);
border-radius: 22px;
padding: 0;
box-shadow: 0 24px 70px rgba(14,18,22,.30);
background: rgba(255,255,255,.94);
overflow:hidden;
}
dialog::backdrop{
background: rgba(14,18,22,.45);
backdrop-filter: blur(6px);
}
.modal{
padding: 18px;
}
.modal h3{ margin: 0 0 10px; font-size: 18px; }
.summary{
border-radius: 18px;
border: 1px solid rgba(14,18,22,.12);
background: rgba(255,255,255,.70);
padding: 12px;
line-height: 1.55;
white-space: pre-wrap;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
}
.modal-actions{
display:flex;
gap: 10px;
justify-content:flex-end;
flex-wrap:wrap;
padding: 14px 18px 18px;
border-top: 1px solid rgba(14,18,22,.10);
background: rgba(127,214,218,.10);
}
/* ===== Sparkle animation on success ===== */
.sparkles{
position: fixed;
inset: 0;
pointer-events:none;
overflow:hidden;
display:none;
z-index: 9999;
}
.sparkles.show{ display:block; }
.sparkle{
position:absolute;
width: 10px; height: 10px;
border-radius: 999px;
background: radial-gradient(circle at 30% 30%, #fff, rgba(255,255,255,.1) 55%, transparent 70%),
radial-gradient(circle at 70% 70%, rgba(200,168,88,.9), transparent 60%);
opacity: .9;
animation: floatUp 1.1s ease-out forwards;
}
@keyframes floatUp{
from{ transform: translateY(0) scale(.9); opacity:.95; }
to{ transform: translateY(-180px) scale(1.25); opacity:0; }
}
</style>
</head>
<body>
<div class="grain" aria-hidden="true"></div>
<div class="sparkles" id="sparkles" aria-hidden="true"></div>
<div class="wrap">
<header>
<div class="topbar">
<div class="badge"><span class="dot"></span> 30 Jahre Glanz · 60s Glam</div>
<div class="cta-row">
<a class="btn btn-ghost" href="#details">Infos</a>
<a class="btn btn-primary" href="#rsvp">Jetzt zusagen 💎</a>
</div>
</div>
<div class="hero">
<div>
<div class="h-eyebrow">Welcome, Darling!</div>
<h1>Julia wird 30 –<br/>und das feiern wir mit Glanz.</h1>
<p class="sub">
Im Stil von „Breakfast at Tiffany’s“: stilvolle Sounds, glamouröse Gespräche und funkelnde Momente.
Werft euch gern in eure schönsten <b>60er-Jahre Outfits</b> und stoßt mit Julia auf drei Jahrzehnte Leben,
Liebe und Stil an. 🥂✨
</p>
<div class="quote">
<p><b>Wunschzettel:</b> Musik machen! Dein Geschenk fließt in neues Equipment oder Instrumente – damit aus Moon-River-Momenten neue Musik entsteht.</p>
</div>
<div class="pill">Bitte Rückmeldung bis <b>01.04.2026</b> ✍️</div>
</div>
<div class="poster" role="img" aria-label="Glamour Poster Bereich">
<div class="img-slot" aria-hidden="true"></div>
<div class="poster-inner">
<div class="poster-top">
<div>
<div class="poster-title">Save the Date</div>
<div class="poster-big">09.05.2026</div>
<div class="muted" style="font-weight:750;">ab 18:00 Uhr</div>
</div>
<div class="diamond">💎</div>
</div>
<div class="muted" style="font-weight:750;">
„Holly Golightly lässt grüßen!“
</div>
</div>
</div>
</div>
</header>
<main>
<section id="details" class="grid">
<div class="card">
<h2>Details</h2>
<div class="facts">
<div class="fact">
<div class="icon">📅</div>
<div><b>Datum</b><div class="muted">09.05.2026</div></div>
</div>
<div class="fact">
<div class="icon">🕕</div>
<div><b>Uhrzeit</b><div class="muted">18:00 Uhr</div></div>
</div>
<div class="fact">
<div class="icon">🪩</div>
<div><b>Dresscode</b><div class="muted">60er Jahre</div></div>
</div>
<div class="fact">
<div class="icon">📍</div>
<div>
<b>Location</b>
<div class="muted">FC Laufach · Am Eisenhammer 18 · 63846 Laufach</div>
<div class="fine" style="margin-top:6px;">
Tipp: Du kannst dir die Adresse direkt in Maps öffnen (Button unten).
</div>
</div>
</div>
</div>
<div class="divider"></div>
<div class="cta-row" style="justify-content:flex-start;">
<a class="btn btn-ghost" target="_blank" rel="noopener"
href="https://www.google.com/maps/search/?api=1&query=FC+Laufach%2C+Am+Eisenhammer+18%2C+63846+Laufach">
Route öffnen 🗺️
</a>
<a class="btn btn-primary" href="#rsvp">Zur Rückmeldung →</a>
</div>
</div>
<div class="card">
<h2>Kurzer Hinweis</h2>
<div class="notice">
Kinder sind herzlich willkommen. 💛<br/>
Für die Planung: Bitte gib pro Person den Essenswunsch an (Fleisch / Vegetarisch / Vegan) und ggf. Allergien.
</div>
<p class="fine" style="margin-top:12px;">
<b>Datenschutz:</b> Diese Seite ist für die Partyplanung gedacht. Deine Angaben werden nur dafür genutzt.
(Aktuell speichert dieser Prototyp im Browser; später können wir das zentral speichern.)
</p>
<div class="divider"></div>
<div class="facts">
<div class="fact">
<div class="icon">🎁</div>
<div><b>Geschenk-Idee</b><div class="muted">Musik machen – Equipment/Instrumente</div></div>
</div>
<div class="fact">
<div class="icon">✨</div>
<div><b>Vibe</b><div class="muted">Glamour · 60s · Tiffany-Vibes</div></div>
</div>
</div>
</div>
</section>
<section id="rsvp" class="card">
<h2>Rückmeldung</h2>
<form id="rsvpForm" novalidate>
<div class="row">
<label>
Dein Name (Vor- & Nachname) *
<input type="text" name="hostName" id="hostName" placeholder="z. B. Max Mustermann" required />
</label>
<label>
Kontakt (WhatsApp oder E-Mail) <span class="muted" style="font-weight:650;">(optional)</span>
<input type="text" name="contact" id="contact" placeholder="z. B. 0176… oder name@mail.de" />
</label>
</div>
<div>
<div class="fine" style="margin-bottom:8px;"><b>Kommst du?</b> *</div>
<div class="segmented" role="radiogroup" aria-label="Zusage">
<input type="radio" id="attendYes" name="attendance" value="yes" required />
<label for="attendYes">✅ Ja, ich komme</label>
<input type="radio" id="attendNo" name="attendance" value="no" />
<label for="attendNo">❌ Leider nein</label>
</div>
</div>
<div id="ifNo" style="display:none;">
<label>
Nachricht an Julia <span class="muted" style="font-weight:650;">(optional)</span>
<textarea name="messageNo" id="messageNo" placeholder="z. B. Ich denke an euch und feiere aus der Ferne…"></textarea>
</label>
</div>
<div id="ifYes" style="display:none;">
<div class="divider"></div>
<div class="row">
<label>
Kinder dabei? (Anzahl)
<input type="number" name="kidsCount" id="kidsCount" min="0" value="0" />
</label>
<label>
Trinkst du Alkohol?
<select name="alcohol" id="alcohol">
<option value="egal">Egal / weiß ich noch nicht</option>
<option value="yes">Ja</option>
<option value="no">Nein</option>
</select>
</label>
</div>
<div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<div>
<div style="font-weight:900;">Personen & Essenswünsche *</div>
<div class="fine">Füge alle Personen hinzu, die du mitbringst – inkl. Essenswunsch & Allergien.</div>
</div>
<button type="button" class="btn btn-ghost" id="addGuestBtn">+ Begleitperson hinzufügen</button>
</div>
<div class="guests" id="guests"></div>
</div>
<label>
Nachricht an Julia <span class="muted" style="font-weight:650;">(optional)</span>
<textarea name="messageYes" id="messageYes" placeholder="z. B. Wir kommen etwas später / bringen Kuchen mit / …"></textarea>
</label>
</div>
<label style="display:flex; gap:10px; align-items:flex-start; font-weight:750;">
<input type="checkbox" id="consent" required style="width:auto; margin-top:2px;" />
<span>
Ich bin einverstanden, dass meine Angaben zur Eventplanung gespeichert werden. *
<div class="fine">Nur für die Organisation dieser Feier.</div>
</span>
</label>
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<button class="btn btn-primary" type="submit">Glanz-Antwort abschicken 💎</button>
<button class="btn btn-ghost" type="button" id="viewLocalBtn">Gespeicherte Rückmeldungen (dieser Browser)</button>
</div>
<div class="fine">
* Pflichtfelder. Rückmeldefrist: <b>01.04.2026</b>.
</div>
</form>
</section>
<div class="footer">
Made with ✨ for Julias 30. · 30jahreglanz.fun
</div>
</main>
</div>
<dialog id="modal">
<div class="modal">
<h3>Deine Rückmeldung</h3>
<div class="summary" id="summaryBox"></div>
<p class="fine" style="margin:10px 0 0;">
(Prototyp) Diese Rückmeldung wurde lokal gespeichert. Später können wir das zentral an Julia schicken.
</p>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" id="closeModalBtn" type="button">Schließen</button>
<button class="btn btn-primary" id="copyBtn" type="button">Zusammenfassung kopieren</button>
</div>
</dialog>
<dialog id="localModal">
<div class="modal">
<h3>Gespeicherte Rückmeldungen (dieser Browser)</h3>
<div class="summary" id="localBox"></div>
<p class="fine" style="margin:10px 0 0;">
Hinweis: Das ist nur lokal auf diesem Gerät/Browser sichtbar.
</p>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" id="closeLocalBtn" type="button">Schließen</button>
<button class="btn btn-primary" id="clearLocalBtn" type="button">LocalStorage leeren</button>
</div>
</dialog>
<script>
// ===== Helpers =====
const $ = (sel, el=document) => el.querySelector(sel);
const $$ = (sel, el=document) => [...el.querySelectorAll(sel)];
const form = $("#rsvpForm");
const guestsEl = $("#guests");
const ifYes = $("#ifYes");
const ifNo = $("#ifNo");
const modal = $("#modal");
const summaryBox = $("#summaryBox");
const localModal = $("#localModal");
const localBox = $("#localBox");
const sparkles = $("#sparkles");
function uid(){
return Math.random().toString(16).slice(2) + "-" + Date.now().toString(16);
}
function escapeHtml(str){
return String(str ?? "")
.replaceAll("&","&")
.replaceAll("<","<")
.replaceAll(">",">")
.replaceAll('"',""")
.replaceAll("'","'");
}
function showSparkles(){
sparkles.innerHTML = "";
sparkles.classList.add("show");
const n = 18;
for(let i=0;i<n;i++){
const s = document.createElement("div");
s.className = "sparkle";
s.style.left = (Math.random()*100) + "vw";
s.style.top = (70 + Math.random()*20) + "vh";
s.style.animationDelay = (Math.random()*0.12) + "s";
s.style.transform = `translateY(0) scale(${0.8 + Math.random()*0.8})`;
sparkles.appendChild(s);
}
setTimeout(()=> sparkles.classList.remove("show"), 1200);
}
// ===== Attendance toggle =====
function updateAttendanceUI(){
const attendance = (new FormData(form)).get("attendance");
if(attendance === "yes"){
ifYes.style.display = "block";
ifNo.style.display = "none";
} else if(attendance === "no"){
ifYes.style.display = "none";
ifNo.style.display = "block";
} else {
ifYes.style.display = "none";
ifNo.style.display = "none";
}
}
$$("input[name='attendance']").forEach(r => r.addEventListener("change", updateAttendanceUI));
// ===== Guests repeater =====
function guestTemplate({id, name="", meal="vegetarisch", allergies=""}, index, isSelf=false){
const title = isSelf ? "Person 1 (du)" : `Begleitperson ${index+1}`;
return `
<div class="guest" data-guest-id="${id}">
<div class="guest-head">
<div class="guest-title">${escapeHtml(title)}</div>
${isSelf ? "" : `<button type="button" class="btn-small" data-action="remove">Entfernen</button>`}
</div>
<div class="row">
<label>
Name *
<input type="text" data-field="name" value="${escapeHtml(name)}" placeholder="Vor- & Nachname" required />
</label>
<label>
Essen *
<select data-field="meal" required>
<option value="fleisch" ${meal==="fleisch"?"selected":""}>Fleisch</option>
<option value="vegetarisch" ${meal==="vegetarisch"?"selected":""}>Vegetarisch</option>
<option value="vegan" ${meal==="vegan"?"selected":""}>Vegan</option>
</select>
</label>
</div>
<label>
Allergien/Unverträglichkeiten <span class="muted" style="font-weight:650;">(optional)</span>
<input type="text" data-field="allergies" value="${escapeHtml(allergies)}" placeholder="z. B. Nüsse, Laktose, Gluten …" />
</label>
</div>
`;
}
function getGuestsFromUI(){
return $$(".guest", guestsEl).map((g) => {
const id = g.getAttribute("data-guest-id");
const name = $("input[data-field='name']", g)?.value?.trim() || "";
const meal = $("select[data-field='meal']", g)?.value || "";
const allergies = $("input[data-field='allergies']", g)?.value?.trim() || "";
return { id, name, meal, allergies };
});
}
function setGuestsToUI(guests){
guestsEl.innerHTML = guests.map((guest, idx) => {
const isSelf = idx === 0;
return guestTemplate(guest, idx, isSelf);
}).join("");
// Bind remove handlers
$$(".guest [data-action='remove']", guestsEl).forEach(btn => {
btn.addEventListener("click", () => {
const box = btn.closest(".guest");
if(box) box.remove();
// re-render titles (so "Begleitperson 2" etc stimmt)
const current = getGuestsFromUI();
setGuestsToUI(current);
});
});
}
function ensureSelfGuest(){
const hostName = $("#hostName").value.trim();
const current = getGuestsFromUI();
if(current.length === 0){
setGuestsToUI([{ id: uid(), name: hostName, meal:"vegetarisch", allergies:"" }]);
} else {
// keep guest[0] synced with host name if empty or identical
if(!current[0].name || current[0].name === "" || current[0].name === $("#hostName").dataset.lastValue){
current[0].name = hostName;
}
setGuestsToUI(current);
}
$("#hostName").dataset.lastValue = hostName;
}
$("#hostName").addEventListener("input", () => {
if($("#attendYes").checked) ensureSelfGuest();
});
$("#addGuestBtn").addEventListener("click", () => {
const current = getGuestsFromUI();
current.push({ id: uid(), name:"", meal:"vegetarisch", allergies:"" });
setGuestsToUI(current);
});
// initialize with one self guest (hidden until yes is selected)
setGuestsToUI([{ id: uid(), name:"", meal:"vegetarisch", allergies:"" }]);
// ===== Storage =====
const STORAGE_KEY = "rsvp_30jahreglanz_v1";
function readAll(){
try{
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
} catch {
return [];
}
}
function writeAll(items){
localStorage.setItem(STORAGE_KEY, JSON.stringify(items, null, 2));
}
function buildPayload(){
const fd = new FormData(form);
const attendance = fd.get("attendance");
const hostName = (fd.get("hostName") || "").toString().trim();
const contact = (fd.get("contact") || "").toString().trim();
const consent = $("#consent").checked;
const base = {
id: uid(),
createdAt: new Date().toISOString(),
hostName,
contact,
attendance,
consent
};
if(attendance === "no"){
return {
...base,
message: (fd.get("messageNo") || "").toString().trim()
};
}
const kidsCount = Number(fd.get("kidsCount") || 0);
const alcohol = (fd.get("alcohol") || "egal").toString();
const guests = getGuestsFromUI().map(g => ({
name: g.name.trim(),
meal: g.meal,
allergies: g.allergies.trim()
}));
const message = (fd.get("messageYes") || "").toString().trim();
return {
...base,
kidsCount: isFinite(kidsCount) ? Math.max(0, kidsCount) : 0,
alcohol,
guests,
message
};
}
function validatePayload(payload){
const errors = [];
if(!payload.hostName) errors.push("Bitte deinen Namen angeben.");
if(payload.attendance !== "yes" && payload.attendance !== "no") errors.push("Bitte auswählen, ob du kommst.");
if(payload.attendance === "yes"){
if(!Array.isArray(payload.guests) || payload.guests.length < 1){
errors.push("Bitte mindestens eine Person angeben (du).");
} else {
payload.guests.forEach((g, i) => {
if(!g.name) errors.push(`Bitte Name für Person ${i+1} angeben.`);
if(!g.meal) errors.push(`Bitte Essenswunsch für Person ${i+1} wählen.`);
});
}
}
if(!payload.consent) errors.push("Bitte Datenschutz/Einverständnis bestätigen.");
return errors;
}
function payloadToHumanSummary(payload){
if(payload.attendance === "no"){
return [
"RÜCKMELDUNG",
"—".repeat(36),
`Name: ${payload.hostName}`,
`Kontakt: ${payload.contact || "—"}`,
`Status: ABSAGE`,
payload.message ? `Nachricht: ${payload.message}` : "Nachricht: —",
"",
"Danke fürs Bescheid sagen. 💛"
].join("\n");
}
const counts = payload.guests.reduce((acc,g)=>{
acc[g.meal] = (acc[g.meal]||0)+1; return acc;
}, {});
const mealLine = `Essen: Fleisch ${counts.fleisch||0} · Vegetarisch ${counts.vegetarisch||0} · Vegan ${counts.vegan||0}`;
const guestLines = payload.guests.map((g,i)=> {
const a = g.allergies ? ` (Allergien: ${g.allergies})` : "";
return ` ${i+1}. ${g.name} – ${g.meal}${a}`;
});
return [
"RÜCKMELDUNG",
"—".repeat(36),
`Name: ${payload.hostName}`,
`Kontakt: ${payload.contact || "—"}`,
`Status: ZUSAGE`,
`Anzahl Personen: ${payload.guests.length} (+ Kinder: ${payload.kidsCount || 0})`,
mealLine,
`Alkohol: ${payload.alcohol || "egal"}`,
"",
"Personen:",
...guestLines,
"",
payload.message ? `Nachricht: ${payload.message}` : "Nachricht: —",
"",
"Danke, Darling! ✨🥂"
].join("\n");
}
// ===== Submit =====
form.addEventListener("submit", async (e) => {
e.preventDefault();
updateAttendanceUI();
if($("#attendYes").checked) ensureSelfGuest();
const payload = buildPayload();
const errors = validatePayload(payload);
if(errors.length){
alert("Bitte noch kurz prüfen:\n\n• " + errors.join("\n• "));
return;
}
// Store locally (prototype)
const all = readAll();
all.push(payload);
writeAll(all);
// OPTIONAL: Later send to server endpoint
// await fetch("/api/rsvp", { method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(payload) });
const summary = payloadToHumanSummary(payload);
summaryBox.textContent = summary;
showSparkles();
modal.showModal();
// Reset form (but keep a bit of UX friendliness)
form.reset();
ifYes.style.display = "none";
ifNo.style.display = "none";
setGuestsToUI([{ id: uid(), name:"", meal:"vegetarisch", allergies:"" }]);
$("#kidsCount").value = 0;
$("#alcohol").value = "egal";
});
// ===== Modals =====
$("#closeModalBtn").addEventListener("click", () => modal.close());
$("#copyBtn").addEventListener("click", async () => {
try{
await navigator.clipboard.writeText(summaryBox.textContent || "");
$("#copyBtn").textContent = "Kopiert ✓";
setTimeout(()=> $("#copyBtn").textContent = "Zusammenfassung kopieren", 1200);
} catch {
alert("Kopieren hat nicht geklappt – bitte manuell markieren & kopieren.");
}
});
$("#viewLocalBtn").addEventListener("click", () => {
const all = readAll();
if(!all.length){
localBox.textContent = "Keine Rückmeldungen gespeichert.";
} else {
const lines = all.map((p, idx) => {
const when = new Date(p.createdAt).toLocaleString("de-DE");
if(p.attendance === "no"){
return `#${idx+1} · ${when}\nABSAGE · ${p.hostName}\nKontakt: ${p.contact || "—"}\nNachricht: ${p.message || "—"}\n`;
}
const counts = (p.guests||[]).reduce((acc,g)=>{ acc[g.meal]=(acc[g.meal]||0)+1; return acc; },{});
return `#${idx+1} · ${when}\nZUSAGE · ${p.hostName}\nPersonen: ${(p.guests||[]).length} (+ Kinder ${p.kidsCount||0})\nEssen: Fleisch ${counts.fleisch||0} · Vegetarisch ${counts.vegetarisch||0} · Vegan ${counts.vegan||0}\nKontakt: ${p.contact || "—"}\n`;
}).join("\n" + "—".repeat(36) + "\n\n");
localBox.textContent = lines.trim();
}
localModal.showModal();
});
$("#closeLocalBtn").addEventListener("click", () => localModal.close());
$("#clearLocalBtn").addEventListener("click", () => {
if(confirm("Wirklich alle lokal gespeicherten Rückmeldungen löschen?")){
localStorage.removeItem(STORAGE_KEY);
localBox.textContent = "Gelöscht.";
}
});
// Keep UI consistent on load
updateAttendanceUI();
</script>
</body>
</html>