Compare commits

..

20 Commits

Author SHA1 Message Date
a7808b6602 g 2026-03-21 17:31:12 +00:00
7b70f4c760 fix: New Groups and Members + Styl: Flashmassage cleaned up 2026-03-21 16:50:37 +00:00
6eb12d02ef hhff 2026-03-09 20:19:24 +00:00
c1a424a4d9 h 2026-03-09 19:47:07 +00:00
f98aad15f0 Final Fix 2026-03-07 09:22:32 +00:00
361592a8fb Final pre taxi 2026-03-06 20:36:37 +00:00
146a9bda99 yay 2026-03-05 21:45:40 +00:00
39c80a0253 hhh 2026-03-04 19:17:46 +00:00
3610011eaa h 2026-03-03 20:32:20 +00:00
19e64e61e8 webfonda ds 2026-03-03 20:22:22 +00:00
963fd24c96 Taxi jpeg 2026-03-03 20:19:08 +00:00
cee7a638d1 vor bild 2026-03-03 20:05:34 +00:00
7f08b99b9f nice 2026-03-03 18:54:41 +00:00
a5c5c6daf5 nice look 2026-03-03 18:40:55 +00:00
8152072bec feat: Clock 2026-03-03 17:38:51 +00:00
225ac5e441 hoi 2026-03-02 20:19:46 +00:00
705d02a15f swords to README guest list" gelöscht 2026-03-01 20:52:46 +00:00
3cd7b78995 Massiv + 2026-03-01 20:51:26 +00:00
a0bdcda7bf Gästeliste 2026-03-01 18:02:18 +00:00
fee9f65f78 Add group passwords to README guest list 2026-03-01 18:01:43 +00:00
35 changed files with 3191 additions and 391 deletions

50
GUESTS.md Normal file
View File

@@ -0,0 +1,50 @@
# Guests README
Aktueller Stand der Login-Gruppen aus `backend/app.py` (`DEFAULT_INVITATION_GROUPS`).
Anzahl Gruppen: **40**
| Username | Rolle | Passwort | Members |
|---|---|---|---|
| Bubus | admin | `Bubu!Herz24#` | Svenja, Dominic |
| Remi | guest | `Remi#Ring24!` | Remi |
| Chantal | guest | `Chan!Tanz24#` | Chantal |
| Madeleine | guest | `Madi$Rose24!` | Madeleine |
| Julie & Daniel | guest | `Juli&Dan24!#` | Julie, Daniel |
| Tim & Sophie | guest | `Tim+Sofi24!#` | Tim, Sophie |
| Marcel & Kathrin | guest | `Marc&Kath24#` | Marcel, Kathrin |
| Marie & Kai | guest | `Mari&Kai24!#` | Marie, Kai |
| Familie Olsem | guest | `Olse!Fam24#?` | Laura, Sven, Lena, Finn |
| Maxime | guest | `Maxi#Love24!` | Maxime |
| Familie Löster | guest | `Loes@Ring24#` | Claudia, Mario, Mélodie |
| Familie Thiels | guest | `Thie$Fest24!` | Matthias, Opa Bernd, Oma Heidi |
| Familie Gollor | guest | `Goll%Herz24!` | Michael, Christin, Bruno |
| Monika | guest | `Moni!Rose24#` | Monika |
| Familie Konrad | guest | `Konr#Fest24!` | Michael, Sandra, Christoph, Alexander |
| Mark | guest | `Mark!Gold24#` | Mark |
| Elias | guest | `Elia$Ring24!` | Elias |
| Milan | guest | `Mila#Tanz24!` | Milan |
| Familie Wolff | guest | `Wolf!Herz24#` | Anja, Bodo |
| Anna & Leon | guest | `Anna&Leo24!#` | Anna, Leon |
| Aryan | guest | `Arya!Fest24#` | Aryan |
| Sebastian | guest | `Seba$Ring24!` | Sebastian, Olivia |
| Flo | guest | `Flo!Liebe24#` | Flo |
| Kiki | guest | `Kiki!Rose24#` | Kiki |
| Lana & Eric | guest | `Lan&Eric24!#` | Lana, Eric |
| Britta | guest | `Brit!Tanz24#` | Britta |
| Holzi | guest | `Holz!Ring24#` | Holzi |
| Eirene | guest | `Eire$Fest24!` | Eirene |
| Family Hynes | guest | `Hyne&Fam24!#` | Michael, Traci, Bethany, Josiah, Nicholas |
| Steven & Martha | guest | `Stev&Mart24!#` | Steven, Martha |
| Timbo | guest | `Timb!Rose24#` | Timbo |
| Karen & Jay | guest | `Kare&Jay24!#` | Karen, Jay |
| Alina | guest | `Alin!Gold24#` | Alina |
| Max | guest | `Max!Liebe24#` | Max |
| Paul & Alix | guest | `Paul&Alx24!#` | Paul, Alix |
| Alfred & Nadia | guest | `Alfr&Nad24!#` | Alfred, Nadia |
| Anne-Marie & Erny | guest | `Anne&Ern24!#` | Anne-Marie, Erny |
| Familie Kieffer | guest | `Kief!Fest24#` | Anny, John |
| Peter | guest | `Peter!Fest26#` | Peter |
| Johanna | guest | `Joha!Fest26#` | Johanna |
Hinweis: Diese Datei enthält Klartext-Passwörter und sollte nicht öffentlich geteilt werden.

122
README.md
View File

@@ -494,8 +494,6 @@ Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unt
</p>
Neu:
Nutzer Rollen Authentifikationsorinzip:
Jeder account = Eine einladung (einladungen gehen an Familien und an Einzelpersonen. Familien trotzdem ein zugang.)
@@ -509,3 +507,123 @@ Bilder Downloadfunktion: Einen Button um Bilder zu downloaden. -> Bilder sollen
Zusätzlicher Button: Zurück beispielsweise in form eines Pfeils. damit man im dashboard zurück manövirieren kann. (Soll den Abmeldebutton aber nicht ersetzen)
Adminnutzer:
Bubus (Admingruppe)
Mitglieder: Svenja, Dominic
Standardnutzer:
Remi
Mitglieder: Remi
Chantal
Mitglieder: Chantal
Madeleine
Mitglieder: Madeleine
Julie & Daniel
Mitglieder: Julie, Daniel
Tim & Sophie
Mitglieder: Tim, Sophie
Marcel & Kathrin
Mitglieder: Marcel, Kathrin
Marie & Kai
Mitglieder: Marie, Kai
Familie Olsem
Mitglieder: Laura, Sven, Lena, Finn
Maxime
Mitglieder: Maxime, Freund
Familie Löster
Mitglieder: Claudia, Mario, Mélodie
Familie Thiels
Mitglieder: Matthias, Opa Bernd, Oma Heidi
Familie Gollor
Mitglieder: Michael, Christin, Bruno
Monika
Mitglieder: Monika
Familie Konrad
Mitglieder: Michael, Sandra, Christoph, Alexander
Mark
Mitglieder: Mark
Elias
Mitglieder: Elias
Milan
Mitglieder: Milan
Familie Wolff
Mitglieder: Anja, Bodo
Anna & Leon
Mitglieder: Anna, Leon
Aryan
Mitglieder: Aryan
Sebastian
Mitglieder: Sebastian, Olivia
Leander & Heni
Mitglieder: Leander, Heni
Flo
Mitglieder: Flo
Nico & Pia
Mitglieder: Nico, Pia
Kiki
Mitglieder: Kiki
Lana & Eric
Mitglieder: Lana, Eric
Britta
Mitglieder: Britta
Holzi
Mitglieder: Holzi
Eirene
Mitglieder: Eirene
Family Hynes
Mitglieder: Steven, Martha, William, Tim, Steven Jr.
Timbo
Mitglieder: Timbo
Karen & Jay
Mitglieder: Karen, Jay
Alina
Mitglieder: Alina
Max
Mitglieder: Max
Paul & Alix
Mitglieder: Paul, Alix
Alfred & Nadia
Mitglieder: Alfred, Nadia
Anne-Marie & Erny
Mitglieder: Anne-Marie, Erny
Familie Kieffer
Mitglieder: Anny, John, Jana

View File

@@ -10,4 +10,4 @@ RUN uv sync --frozen --no-dev
COPY . .
EXPOSE 8000
CMD ["uv", "run", "gunicorn", "-b", "0.0.0.0:8000", "--timeout", "300", "--graceful-timeout", "30", "--keep-alive", "5", "app:app"]
CMD ["uv", "run", "gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--timeout", "300", "--graceful-timeout", "30", "--keep-alive", "5", "--error-logfile", "-", "--access-logfile", "-", "app:app"]

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -7,4 +7,5 @@ requires-python = ">=3.12"
dependencies = [
"flask>=3.1.2",
"gunicorn>=23.0.0",
"pillow>=12.1.1",
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 700" role="img" aria-label="Kartenvorschau">
<defs>
<linearGradient id="land" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#d8e6de" />
<stop offset="1" stop-color="#cadcd2" />
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#1f2a24" flood-opacity="0.16"/>
</filter>
</defs>
<rect width="1600" height="700" fill="url(#land)"/>
<path d="M0 420 C120 380, 200 420, 300 390 C420 360, 530 440, 670 410 C770 390, 860 350, 980 380 C1110 420, 1210 370, 1330 410 C1430 440, 1500 410, 1600 430 L1600 700 L0 700 Z" fill="#b8d2c2" opacity="0.8"/>
<path d="M40 80 L520 560" stroke="#f5f5f2" stroke-width="28" stroke-linecap="round"/>
<path d="M580 50 L980 650" stroke="#f7f7f4" stroke-width="26" stroke-linecap="round"/>
<path d="M1050 70 L1540 520" stroke="#f4f5f2" stroke-width="24" stroke-linecap="round"/>
<path d="M160 640 L620 170" stroke="#f2f4ef" stroke-width="20" stroke-linecap="round"/>
<path d="M30 120 L500 590" stroke="#9eb8aa" stroke-width="5" stroke-dasharray="12 14"/>
<path d="M600 90 L1000 670" stroke="#9fb9ab" stroke-width="5" stroke-dasharray="12 14"/>
<path d="M1070 100 L1555 540" stroke="#9db6a8" stroke-width="5" stroke-dasharray="12 14"/>
<path d="M180 655 L640 185" stroke="#9db7a9" stroke-width="5" stroke-dasharray="12 14"/>
<circle cx="1080" cy="240" r="55" fill="#b0cbba" opacity="0.9"/>
<circle cx="1200" cy="500" r="48" fill="#abc6b5" opacity="0.85"/>
<circle cx="860" cy="540" r="45" fill="#aec8b8" opacity="0.88"/>
<g filter="url(#shadow)">
<rect x="105" y="78" width="560" height="168" rx="16" fill="#ffffff" opacity="0.96"/>
<text x="135" y="125" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="#20372d" font-weight="700">Klostermühle Kiedrich</text>
<text x="135" y="168" font-size="30" font-family="Arial, Helvetica, sans-serif" fill="#385246">An d. Klostermühle 3, 65399 Kiedrich</text>
<text x="135" y="210" font-size="28" font-family="Arial, Helvetica, sans-serif" fill="#4f6a5d">Kartenvorschau</text>
</g>
<g transform="translate(980 320)">
<circle cx="0" cy="0" r="34" fill="#c83c38"/>
<circle cx="0" cy="0" r="14" fill="#f8d9d7"/>
<path d="M0 34 L-17 98 L17 98 Z" fill="#c83c38"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 640" role="img" aria-labelledby="title desc">
<title id="title">Pile of money</title>
<desc id="desc">Stylized stack of money bundles.</desc>
<defs>
<linearGradient id="bg" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#224234"/>
<stop offset="100%" stop-color="#173127"/>
</linearGradient>
<linearGradient id="bill" x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stop-color="#d4f2c2"/>
<stop offset="100%" stop-color="#9cd18f"/>
</linearGradient>
</defs>
<rect width="960" height="640" rx="36" fill="url(#bg)"/>
<g opacity="0.15">
<circle cx="130" cy="120" r="120" fill="#fff"/>
<circle cx="850" cy="80" r="100" fill="#fff"/>
</g>
<g transform="translate(120 180)">
<g transform="translate(0 220)">
<rect x="0" y="0" width="720" height="140" rx="18" fill="url(#bill)"/>
<rect x="285" y="0" width="150" height="140" fill="#6ca95f"/>
<circle cx="360" cy="70" r="34" fill="#e8f9db"/>
<text x="360" y="82" text-anchor="middle" font-size="36" font-family="Arial, sans-serif" fill="#2f5e35">$</text>
</g>
<g transform="translate(45 130)">
<rect x="0" y="0" width="640" height="130" rx="18" fill="url(#bill)"/>
<rect x="255" y="0" width="130" height="130" fill="#6ca95f"/>
<circle cx="320" cy="65" r="30" fill="#e8f9db"/>
<text x="320" y="76" text-anchor="middle" font-size="32" font-family="Arial, sans-serif" fill="#2f5e35">$</text>
</g>
<g transform="translate(110 45)">
<rect x="0" y="0" width="560" height="120" rx="16" fill="url(#bill)"/>
<rect x="225" y="0" width="110" height="120" fill="#6ca95f"/>
<circle cx="280" cy="60" r="27" fill="#e8f9db"/>
<text x="280" y="69" text-anchor="middle" font-size="28" font-family="Arial, sans-serif" fill="#2f5e35">$</text>
</g>
</g>
<text x="480" y="90" text-anchor="middle" font-size="54" font-family="Arial, sans-serif" fill="#f0db8e">Gift Fund</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

109
backend/static/countdown.js Normal file
View File

@@ -0,0 +1,109 @@
(() => {
function pad2(value) {
return String(value).padStart(2, "0");
}
function splitCountdown(ms) {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return { days, hours, minutes, seconds };
}
function setAnimatedValue(node, nextValue) {
if (!node) {
return;
}
if (node.textContent === nextValue) {
return;
}
node.textContent = nextValue;
node.classList.remove("is-updated");
void node.offsetWidth;
node.classList.add("is-updated");
}
function initTimer(widget) {
const targetIso = widget.dataset.countdownTarget;
const startedLabel = widget.dataset.countdownStarted || "";
const toggle = widget.querySelector("[data-countdown-toggle]");
const popover = widget.querySelector("[data-countdown-popover]");
const subline = widget.querySelector("[data-countdown-subline]");
const daysNode = widget.querySelector("[data-countdown-days]");
const hoursNode = widget.querySelector("[data-countdown-hours]");
const minutesNode = widget.querySelector("[data-countdown-minutes]");
const secondsNode = widget.querySelector("[data-countdown-seconds]");
if (!targetIso || !toggle || !popover || !subline || !daysNode || !hoursNode || !minutesNode || !secondsNode) {
return;
}
const targetMs = Date.parse(targetIso);
if (Number.isNaN(targetMs)) {
subline.textContent = "--";
return;
}
const update = () => {
const now = Date.now();
const delta = targetMs - now;
if (delta <= 0) {
setAnimatedValue(daysNode, "0");
setAnimatedValue(hoursNode, "00");
setAnimatedValue(minutesNode, "00");
setAnimatedValue(secondsNode, "00");
subline.textContent = startedLabel;
return;
}
const parts = splitCountdown(delta);
setAnimatedValue(daysNode, String(parts.days));
setAnimatedValue(hoursNode, pad2(parts.hours));
setAnimatedValue(minutesNode, pad2(parts.minutes));
setAnimatedValue(secondsNode, pad2(parts.seconds));
};
const close = () => {
popover.hidden = true;
toggle.setAttribute("aria-expanded", "false");
};
const open = () => {
popover.hidden = false;
toggle.setAttribute("aria-expanded", "true");
update();
};
toggle.addEventListener("click", (event) => {
event.preventDefault();
if (popover.hidden) {
open();
} else {
close();
}
});
document.addEventListener("click", (event) => {
if (!widget.contains(event.target)) {
close();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
close();
}
});
update();
window.setInterval(update, 1000);
}
document.addEventListener("DOMContentLoaded", () => {
const widgets = document.querySelectorAll(".toolbar-timer");
widgets.forEach(initTimer);
});
})();

View File

@@ -0,0 +1,31 @@
(() => {
function setupDashboardAnimations() {
const dashboardGrid = document.querySelector(".dashboard-grid");
if (!dashboardGrid) {
return;
}
const dashboardLinks = document.querySelectorAll(".dashboard-link-card[href]");
dashboardLinks.forEach((link, index) => {
link.style.setProperty("--stagger-delay", `${index * 45}ms`);
});
dashboardGrid.classList.remove("is-ready", "is-animated", "is-focusing");
dashboardLinks.forEach((link) => {
link.classList.remove("is-opening");
link.style.removeProperty("transition");
link.style.removeProperty("transform");
link.style.removeProperty("opacity");
link.style.removeProperty("filter");
link.style.removeProperty("pointer-events");
});
dashboardGrid.classList.add("is-ready");
window.requestAnimationFrame(() => {
dashboardGrid.classList.add("is-animated");
});
}
document.addEventListener("DOMContentLoaded", setupDashboardAnimations);
window.addEventListener("pageshow", setupDashboardAnimations);
})();

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Source+Sans+3:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css', v='20260321a') }}" />
</head>
<body>
<header class="topbar">
@@ -22,9 +22,56 @@
<button class="btn btn-ghost" type="submit">EN</button>
</form>
{% if guest_name %}
<form method="post" action="{{ url_for('logout') }}">
<button class="btn btn-ghost" type="submit">{{ t('logout') }}</button>
</form>
<div
class="toolbar-timer"
data-countdown-target="{{ wedding_countdown_iso }}"
data-countdown-started="{{ t('countdown_started') }}"
>
<button
class="btn btn-ghost toolbar-nav-btn toolbar-timer-btn"
type="button"
aria-label="{{ t('countdown_button_label') }}"
title="{{ t('countdown_button_label') }}"
data-countdown-toggle
aria-expanded="false"
>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M15 2H9v2h6V2zm-3 4a8 8 0 1 0 8 8 8 8 0 0 0-8-8zm3.4 11.4L11 13V8h2v4.2l3.8 3.8z" />
</svg>
</button>
<div class="toolbar-timer-popover" data-countdown-popover hidden>
<p class="toolbar-timer-label">{{ t('countdown_until') }}</p>
<div class="toolbar-timer-grid" data-countdown-grid>
<div class="toolbar-timer-unit">
<span class="toolbar-timer-value" data-countdown-days>0</span>
<span class="toolbar-timer-unit-label">{{ t('countdown_days') }}</span>
</div>
<div class="toolbar-timer-unit">
<span class="toolbar-timer-value" data-countdown-hours>00</span>
<span class="toolbar-timer-unit-label">{{ t('countdown_hours') }}</span>
</div>
<div class="toolbar-timer-unit">
<span class="toolbar-timer-value" data-countdown-minutes>00</span>
<span class="toolbar-timer-unit-label">{{ t('countdown_minutes') }}</span>
</div>
<div class="toolbar-timer-unit">
<span class="toolbar-timer-value" data-countdown-seconds>00</span>
<span class="toolbar-timer-unit-label">{{ t('countdown_seconds') }}</span>
</div>
</div>
<p class="toolbar-timer-subline" data-countdown-subline>{{ t('countdown_subline') }}</p>
</div>
</div>
<div class="toolbar-session-actions">
<a class="btn btn-ghost toolbar-nav-btn" href="{{ url_for('guest_area') }}" aria-label="{{ t('dashboard') }}" title="{{ t('dashboard') }}">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M3 3h8v8H3V3zm10 0h8v5h-8V3zM3 13h5v8H3v-8zm7 0h11v8H10v-8z" />
</svg>
</a>
<form method="post" action="{{ url_for('logout') }}">
<button class="btn btn-ghost" type="submit">{{ t('logout') }}</button>
</form>
</div>
{% endif %}
</div>
</header>
@@ -46,5 +93,7 @@
<a href="{{ url_for('datenschutz') }}">{{ t('privacy') }}</a>
<a href="{{ url_for('impressum') }}">{{ t('imprint') }}</a>
</footer>
<script src="{{ url_for('static', filename='dashboard-transition.js', v='20260303a') }}" defer></script>
<script src="{{ url_for('static', filename='countdown.js', v='20260303c') }}" defer></script>
</body>
</html>

View File

@@ -4,45 +4,210 @@
{% if lang == 'en' %}
<header class="legal-header">
<h1>Privacy Policy</h1>
<p>This privacy policy explains how personal data is processed when you use this website.</p>
<p>
In this privacy policy, we inform you about the processing of personal data when using this website.
</p>
<p><strong>{{ t('legal_de_authoritative') }}</strong></p>
</header>
<article class="legal-section">
<article class="legal-section" id="controller">
<h2>Controller</h2>
<p>
Dominic Thiels, Wiesbadener Strasse 70b, 65510 Idstein, Germany<br />
Phone: +49 151 70616118<br />
Email: <a href="mailto:d.thiels@freenet.de">d.thiels@freenet.de</a>
The controller responsible for data processing is:<br />
Dominic Thiels, Wiesbadener Straße 70b, 65510 Idstein, +49 151 70616118 and
<a href="mailto:d.thiels@freenet.de">d.thiels@freenet.de</a>
</p>
</article>
<article class="legal-section">
<h2>Data Processed During Website Access</h2>
<p>When you visit this website, technical access data may be processed, including IP address, request date/time, transmitted data volume, referrer, browser type/version, and operating system.</p>
<p>Legal basis: Art. 6(1)(f) GDPR (legitimate interest in secure and stable operation of the website).</p>
<article class="legal-section" id="personal-data">
<h2>Personal Data</h2>
<p>
Personal data means any information relating to an identified or identifiable natural person (data subject). A
natural person is considered identifiable if they can be identified, directly or indirectly, in particular by
reference to an identifier such as a name, an identification number, location data, an online identifier, or to
one or more factors specific to the physical, physiological, genetic, mental, economic, cultural, or social
identity of that natural person.
</p>
</article>
<article class="legal-section">
<h2>Cookies</h2>
<p>Only technically necessary cookies are used to maintain the website and user session. No tracking cookies are used.</p>
<article class="legal-section" id="data-on-website-access">
<h2>Data Collected During Website Access</h2>
<p>
If you use this website only for information purposes and do not submit data, we process only the data required
to display the website on your internet-enabled device. This includes in particular:
</p>
<ul>
<li>IP address</li>
<li>date and time of the request</li>
<li>amount of data transferred in each case</li>
<li>the website from which the request originates</li>
<li>browser type and browser version</li>
<li>operating system</li>
</ul>
<p>
The legal basis for processing this data is legitimate interest pursuant to Art. 6(1)(f) GDPR in order to enable
the display of the website.
</p>
<p>
In addition, you can use various services on the website for which further personal and non-personal data may be
processed.
</p>
</article>
<article class="legal-section">
<h2>Google Maps</h2>
<p>Google Maps is only loaded after an explicit user click on the location page. Once activated, data such as your IP address may be transferred to Google.</p>
<p>More information: <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Google Privacy Policy</a></p>
</article>
<article class="legal-section">
<h2>Photo Uploads</h2>
<p>When guests upload photos, the uploaded file, optional metadata, and technical access data may be processed to provide the shared wedding gallery.</p>
</article>
<article class="legal-section">
<article class="legal-section" id="data-subject-rights">
<h2>Your Rights</h2>
<p>Under GDPR, you may have rights of access, rectification, erasure, restriction, portability, objection, and the right to lodge a complaint with a supervisory authority.</p>
<p>As a data subject, you have the following rights:</p>
<ul>
<li>Access to personal data concerning you (Art. 15 GDPR)</li>
<li>Rectification of inaccurate or incomplete data (Art. 16 GDPR)</li>
<li>Erasure (Art. 17 GDPR)</li>
<li>Restriction of processing (Art. 18 GDPR)</li>
<li>Data portability (Art. 20 GDPR)</li>
<li>Objection to processing (Art. 21 GDPR)</li>
<li>
Not to be subject to a decision based solely on automated processing (including profiling) (Art. 22 GDPR)
</li>
<li>
Right to lodge a complaint with a supervisory authority (Art. 77 GDPR). The competent authority is the
supervisory authority at your habitual residence, place of work, or the place of the alleged infringement.
</li>
</ul>
</article>
<article class="legal-section" id="cookies">
<h2>Use of Cookies</h2>
<p>
When visiting the website, cookies may be stored on your device. Cookies are small text files stored by the
browser you use. Cookies cannot execute programs and cannot transfer viruses to your device. However, the party
setting the cookie may receive certain information through it. In particular, cookies can be used to recognize
the device from which this website was accessed when you revisit the site.
</p>
<p>
You can restrict or prevent the storage of cookies via your browser settings. For example, you can block only
third-party cookies or all cookies. However, blocking cookies may mean that not all functions of this website are
available. In the following sections of this privacy policy, we explain where and for which purposes cookies are
used on this website.
</p>
<p>
<strong>
Only technically necessary cookies are used that are required for operation of the website and maintenance of
the user session.
</strong>
</p>
</article>
<article class="legal-section" id="google-maps">
<h2>Google Maps</h2>
<p>
This website integrates a service provided by Google Ireland Limited, Gordon House, Barrow Street, Dublin 4,
Ireland, to display geographic information visually (Google Maps).
</p>
<p>
When using Google Maps, information about your use of this website, including your IP address, is transmitted to
Google servers and stored there. These servers may also be located in the USA. Google may transfer this
information to third parties where required by law or where such third parties process the data on behalf of
Google.
</p>
<p>
The use of Google Maps is in the interest of an appealing presentation of event locations and easy findability of
the places indicated on this website. This constitutes a legitimate interest pursuant to Art. 6(1)(f) GDPR.
</p>
<p>
If consent is requested (e.g., via a consent mechanism), processing is based exclusively on Art. 6(1)(a) GDPR.
Consent can be withdrawn at any time.
</p>
<p>
Further information about data processing by Google can be found in Google's privacy policy:
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener">https://policies.google.com/privacy</a>
</p>
</article>
<article class="legal-section" id="google-fonts">
<h2>Google Fonts</h2>
<p>
This website uses Google Fonts for the uniform display of fonts, provided by Google Ireland Limited, Gordon
House, Barrow Street, Dublin 4, Ireland.
</p>
<p>
When loading the page, your browser loads required fonts from Google servers. In this process, technical data,
in particular your IP address, may be transmitted to Google.
</p>
<p>
Use is based on Art. 6(1)(f) GDPR (legitimate interest in a consistent and appealing website presentation).
</p>
<p>
Further information about data processing by Google:
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener">https://policies.google.com/privacy</a>
</p>
</article>
<article class="legal-section" id="external-links">
<h2>External Links</h2>
<p>
This website contains links to external websites (e.g., the location website). Clicking such a link causes your
browser to establish a direct connection to the respective provider.
</p>
<p>
In this process, technical data such as your IP address and, depending on browser settings, the referrer URL may
be transmitted to the external site.
</p>
<p>
Legal basis is Art. 6(1)(f) GDPR (legitimate interest in providing additional information about the venue).
</p>
<p>
Once you leave this website, only the privacy policy of the respective external provider applies.
</p>
</article>
<article class="legal-section" id="photo-upload">
<h2>Photo Uploads</h2>
<p>
This website provides the option to upload photos, for example as part of a shared wedding gallery.
</p>
<p>When uploading photos, the following personal data may be processed:</p>
<ul>
<li>the uploaded image file</li>
<li>
metadata that may be contained in the file (e.g., capture date, camera information, or location data/GPS
coordinates)
</li>
<li>the name entered by the user (if applicable)</li>
<li>technical access data (e.g., IP address, upload timestamp)</li>
</ul>
<p>
Processing is carried out to provide a shared photo gallery for event guests. Legal basis is Art. 6(1)(b) GDPR
(performance of the website function) and Art. 6(1)(f) GDPR (legitimate interest in providing a shared memories
platform).
</p>
<p>
Please note that uploaded photos may include personal data of third parties (e.g., images of persons). The user
uploading content is responsible for ensuring that publication is permissible under data protection law and that
consent of depicted persons has been obtained where required.
</p>
<p>
Photos are stored exclusively for the intended purpose and are not shared with third parties unless there is a
legal obligation.
</p>
</article>
<footer class="legal-footer">
<p><small>Source: Template privacy policy by anwalt.de</small></p>
</footer>
{% else %}
<header class="legal-header">
<h1>Datenschutzerklärung</h1>
@@ -180,6 +345,45 @@
</p>
</article>
<article class="legal-section" id="google-fonts">
<h2>Google Fonts</h2>
<p>
Diese Website nutzt zur einheitlichen Darstellung von Schriftarten den Dienst Google Fonts der Google Ireland
Limited, Gordon House, Barrow Street, Dublin 4, Irland.
</p>
<p>
Beim Aufruf der Seite lädt Ihr Browser die benötigten Schriftarten von Google-Servern. Dabei können technische
Daten, insbesondere Ihre IP-Adresse, an Google übermittelt werden.
</p>
<p>
Die Nutzung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an einer einheitlichen
und ansprechenden Darstellung der Website).
</p>
<p>
Weitere Informationen zur Datenverarbeitung durch Google finden Sie unter:
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener">https://policies.google.com/privacy</a>
</p>
</article>
<article class="legal-section" id="externe-links">
<h2>Externe Links</h2>
<p>
Diese Website enthält Verlinkungen zu externen Webseiten (z. B. zur Website der Location). Beim Anklicken eines
solchen Links stellt Ihr Browser eine direkte Verbindung zum jeweiligen Anbieter her.
</p>
<p>
Dabei können technische Daten wie Ihre IP-Adresse und je nach Browser-Einstellung auch die Referrer-URL an die
externe Seite übermittelt werden.
</p>
<p>
Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an weiterführenden Informationen zum
Veranstaltungsort).
</p>
<p>
Ab dem Verlassen dieser Website gilt ausschließlich die Datenschutzerklärung des jeweiligen externen Anbieters.
</p>
</article>
<article class="legal-section" id="foto-upload">
<h2>Upload von Fotos durch Nutzer</h2>
<p>

View File

@@ -1,18 +1,18 @@
{% extends 'base.html' %}
{% block content %}
<section class="card">
<h1>{{ t('guest_area') }}</h1>
<p>{{ t('hello_guest').format(name=guest_name) }}</p>
</section>
<section class="card-grid">
<a class="card link-card" href="{{ url_for('rsvp') }}">{{ t('rsvp') }}</a>
<a class="card link-card" href="{{ url_for('upload') }}">{{ t('upload') }}</a>
<a class="card link-card" href="{{ url_for('gallery') }}">{{ t('gallery') }}</a>
<a class="card link-card" href="{{ url_for('info', page='schedule') }}">{{ t('schedule') }}</a>
<a class="card link-card" href="{{ url_for('info', page='hotels') }}">{{ t('hotels') }}</a>
<a class="card link-card" href="{{ url_for('info', page='taxi') }}">{{ t('taxi') }}</a>
<a class="card link-card" href="{{ url_for('info', page='location') }}">{{ t('location') }}</a>
<a class="card link-card" href="{{ url_for('host_area') }}">{{ t('host_area') }}</a>
<section class="card-grid dashboard-grid">
<a class="card link-card dashboard-link-card" href="{{ url_for('rsvp') }}">{{ t('rsvp') }}</a>
<a class="card link-card dashboard-link-card" href="{{ url_for('upload') }}">{{ t('upload') }}</a>
<a class="card link-card dashboard-link-card" href="{{ url_for('gallery') }}">{{ t('gallery') }}</a>
<a class="card link-card dashboard-link-card" href="{{ url_for('info', page='schedule') }}">{{ t('schedule') }}</a>
<a class="card link-card dashboard-link-card" href="{{ url_for('info', page='hotels') }}">{{ t('hotels') }}</a>
<a class="card link-card dashboard-link-card" href="{{ url_for('info', page='taxi') }}">{{ t('taxi') }}</a>
<a class="card link-card dashboard-link-card" href="{{ url_for('info', page='location') }}">{{ t('location') }}</a>
{% if not is_admin %}
<a class="card link-card dashboard-link-card" href="{{ url_for('info', page='gifts') }}">{{ t('gifts') }}</a>
{% endif %}
{% if is_admin %}
<a class="card link-card dashboard-link-card" href="{{ url_for('host_area') }}">{{ t('host_area') }}</a>
{% endif %}
</section>
{% endblock %}

View File

@@ -1,21 +1,5 @@
{% extends 'base.html' %}
{% block content %}
<section class="card">
<h1>{{ t('host_access_title') }}</h1>
<p>{{ t('host_access_note') }}</p>
</section>
{% if not unlocked %}
<section class="card form-card">
<form method="post" action="{{ url_for('host_area') }}" class="form-grid">
<label>
{{ t('host_password') }}
<input type="password" name="host_password" required />
</label>
<button class="btn" type="submit">{{ t('host_access_submit') }}</button>
</form>
</section>
{% else %}
<section class="stats-grid">
<article class="card stat-card">
<h2>{{ t('total_guests') }}</h2>
@@ -33,10 +17,6 @@
<h2>{{ t('attending_open') }}</h2>
<p>{{ stats.attending_open }}</p>
</article>
<article class="card stat-card">
<h2>{{ t('plus_one_total') }}</h2>
<p>{{ stats.plus_one_total }}</p>
</article>
</section>
<section class="card">
@@ -45,29 +25,31 @@
<table class="guest-table">
<thead>
<tr>
<th>{{ t('host_table_name') }}</th>
<th>{{ t('host_table_group') }}</th>
<th>{{ t('host_table_member') }}</th>
<th>{{ t('host_table_status') }}</th>
<th>{{ t('host_table_plus_one') }}</th>
<th>{{ t('host_table_age') }}</th>
</tr>
</thead>
<tbody>
{% for guest in guests %}
{% for member in members %}
<tr>
<td>{{ guest["name"] }}</td>
<td>{{ member["group_name"] }}</td>
<td>{{ member["name"] }}</td>
<td>
{% if guest["attending"] == 1 %}
{% if member["attending"] == 1 %}
{{ t('status_yes') }}
{% elif guest["attending"] == 0 %}
{% elif member["attending"] == 0 %}
{{ t('status_no') }}
{% else %}
{{ t('status_open') }}
{% endif %}
</td>
<td>
{% if guest["attending"] == 1 and guest["plus_one"] == 1 %}
{{ t('yes') }}
{% if member["child_age"] is not none %}
{{ member["child_age"] }}
{% else %}
{{ t('no') }}
-
{% endif %}
</td>
</tr>
@@ -76,5 +58,4 @@
</table>
</div>
</section>
{% endif %}
{% endblock %}

View File

@@ -2,38 +2,50 @@
{% block content %}
<section class="legal-page card" lang="{{ lang }}">
{% if lang == 'en' %}
<h1>Imprint</h1>
<p>Information according to Section 5 DDG.</p>
<h1>Imprint</h1>
<p>Information pursuant to Section 5 DDG</p>
<p>Dominic Thiels<br>
<br>
Wiesbadener Straße 70b<br>
65510 Idstein <br>
</p>
<p> <strong>Represented by: </strong><br>
Dominic Thiels<br>
</p>
<p><strong>Contact:</strong> <br>
Phone: +49-151 70616118<br>
Email: <a href="mailto:d.thiels@freenet.de">d.thiels@freenet.de</a></br></p>
<p><strong>Consumer dispute resolution / Universal arbitration board</strong>
<br>We do not participate in dispute resolution proceedings before a consumer arbitration board and are not obliged to do so.
</p>
<p><strong>Disclaimer: </strong>
<br><br><strong>Liability for content</strong><br>
The contents of our pages were created with the greatest care. However, we cannot guarantee the accuracy,
completeness, or timeliness of the content. As a service provider, we are responsible for our own content on these
pages according to general laws pursuant to Section 7(1) DDG. According to Sections 8 to 10 DDG, however, we are not
obliged as a service provider to monitor transmitted or stored third-party information or to investigate circumstances
that indicate illegal activity. Obligations to remove or block the use of information under general laws remain
unaffected. Liability in this respect is only possible from the time of knowledge of a specific infringement. Upon
becoming aware of corresponding infringements, we will remove such content immediately.<br><br><strong>Liability for links</strong><br>
Our offer contains links to external third-party websites over whose content we have no influence. Therefore, we
cannot assume any liability for this external content. The respective provider or operator of the linked pages is
always responsible for their content. The linked pages were checked for possible legal violations at the time of
linking. Illegal content was not recognizable at the time of linking. However, permanent monitoring of the content
of linked pages is not reasonable without concrete indications of a legal violation. Upon becoming aware of legal
violations, we will remove such links immediately.<br><br><strong>Copyright</strong><br>
The content and works created by the site operators on these pages are subject to German copyright law.
Reproduction, editing, distribution, and any kind of use outside the limits of copyright law require the written
consent of the respective author or creator. Downloads and copies of this site are only permitted for private,
non-commercial use. Insofar as the content on this site was not created by the operator, third-party copyrights are
respected. In particular, third-party content is marked as such. Should you nevertheless become aware of a copyright
infringement, please inform us accordingly. Upon becoming aware of legal violations, we will remove such content
immediately.</p>
<p>Created with the <a href="https://impressum-generator.de" rel="dofollow">Imprint Generator</a> by WebsiteWissen.com, the guide for
<a href="https://websitewissen.com/website-erstellen" rel="dofollow">website creation</a>, <a href="https://websitewissen.com/homepage-baukasten-vergleich" rel="dofollow">website builders</a>, and
<a href="https://websitewissen.com/shopsysteme-vergleich" rel="dofollow">shop systems</a>. Legal text by
<a href="https://www.kanzlei-hasselbach.de/" rel="dofollow">Kanzlei Hasselbach</a>.
</p>
<p><strong>{{ t('legal_de_authoritative') }}</strong></p>
<h2>Provider</h2>
<p>
Dominic Thiels<br />
Wiesbadener Strasse 70b<br />
65510 Idstein<br />
Germany
</p>
<h2>Represented by</h2>
<p>Dominic Thiels</p>
<h2>Contact</h2>
<p>
Phone: +49 151 70616118<br />
Email: <a href="mailto:d.thiels@freenet.de">d.thiels@freenet.de</a>
</p>
<h2>Consumer Dispute Resolution</h2>
<p>We do not participate in dispute resolution proceedings before a consumer arbitration board and are not obliged to do so.</p>
<h2>Liability for Content</h2>
<p>The contents of this website were created with due care. However, no guarantee is given for correctness, completeness or timeliness.</p>
<h2>Liability for Links</h2>
<p>This website contains links to external third-party websites. We have no influence on their contents. The respective provider is responsible for those contents.</p>
<h2>Copyright</h2>
<p>The content created by the site operator is subject to German copyright law. Any use beyond statutory limits requires prior written consent.</p>
{% else %}
<h1>Impressum</h1>
<p>Angaben gemäß § 5 DDG</p>

View File

@@ -6,26 +6,284 @@
{% if page == 'hotels' %}{{ t('hotels') }}{% endif %}
{% if page == 'taxi' %}{{ t('taxi') }}{% endif %}
{% if page == 'location' %}{{ t('location') }}{% endif %}
{% if page == 'gifts' %}{{ t('gifts') }}{% endif %}
</h1>
{% if page == 'schedule' %}
<p>{{ t('schedule_text') }}</p>
{% elif page == 'hotels' %}
<p>{{ t('hotels_text') }}</p>
{% elif page == 'taxi' %}
<p>{{ t('taxi_text') }}</p>
{% elif page == 'location' %}
<p><strong>{{ location_name }}</strong></p>
<p>{{ location_address }}</p>
<div class="map-wrap">
<iframe
src="{{ google_maps_embed_url }}"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
allowfullscreen
></iframe>
<p>{{ t('schedule_intro') }}</p>
<div class="schedule-timeline">
<article class="schedule-item" style="--schedule-card-image: url('{{ url_for('static', filename='assets/ankunft.png') }}'); --schedule-card-position: center 18%;">
<p class="schedule-time">15:00</p>
<h2>{{ t('schedule_arrival_title') }}</h2>
<p>{{ t('schedule_arrival_text') }}</p>
</article>
<article class="schedule-item" style="--schedule-card-image: url('{{ url_for('static', filename='assets/trauzermonie.png') }}'); --schedule-card-position: center 25%;">
<p class="schedule-time">15:30</p>
<h2>{{ t('schedule_ceremony_title') }}</h2>
<p>{{ t('schedule_ceremony_text') }}</p>
</article>
<article class="schedule-item" style="--schedule-card-image: url('{{ url_for('static', filename='assets/sektempfang.png') }}'); --schedule-card-position: center 32%;">
<p class="schedule-time">16:00</p>
<h2>{{ t('schedule_reception_title') }}</h2>
<p>{{ t('schedule_reception_text') }}</p>
</article>
<article class="schedule-item" style="--schedule-card-image: url('{{ url_for('static', filename='assets/buffet.png') }}'); --schedule-card-position: center 40%;">
<p class="schedule-time">18:00</p>
<h2>{{ t('schedule_buffet_title') }}</h2>
<p>{{ t('schedule_buffet_text') }}</p>
</article>
<article class="schedule-item" style="--schedule-card-image: url('{{ url_for('static', filename='assets/kuchen.png') }}'); --schedule-card-position: center 48%;">
<p class="schedule-time">22:00</p>
<h2>{{ t('schedule_cake_title') }}</h2>
<p>{{ t('schedule_cake_text') }}</p>
</article>
<article class="schedule-item" style="--schedule-card-image: url('{{ url_for('static', filename='assets/party.png') }}'); --schedule-card-position: center 56%;">
<p class="schedule-time">{{ t('schedule_afterwards_time') }}</p>
<h2>{{ t('schedule_party_title') }}</h2>
<p>{{ t('schedule_party_text') }}</p>
</article>
</div>
<a class="btn" href="{{ location_website_url }}" target="_blank" rel="noopener">{{ t('visit_location') }}</a>
{% elif page == 'hotels' %}
<p>{{ t('hotels_intro') }}</p>
<div class="hotel-list">
{% for hotel in hotels %}
<article class="hotel-card">
<h2>{{ hotel.name }}</h2>
<p class="hotel-address">{{ hotel.address }}</p>
<p>{{ hotel.description }}</p>
<div class="hotel-badges">
<span>{{ hotel.drive_badge }}</span>
<span>{{ hotel.walk_badge }}</span>
</div>
<div class="hotel-actions">
<a class="btn" href="{{ hotel.website_url }}" target="_blank" rel="noopener">{{ t('hotel_visit_website') }}</a>
<a
class="btn btn-ghost"
href="{{ hotel.drive_route_url }}"
target="_blank"
rel="noopener"
data-route-popup
data-route-src="{{ hotel.drive_route_embed_url }}"
>{{ t('hotel_route_drive') }}</a>
<a
class="btn btn-ghost"
href="{{ hotel.walk_route_url }}"
target="_blank"
rel="noopener"
data-route-popup
data-route-src="{{ hotel.walk_route_embed_url }}"
>{{ t('hotel_route_walk') }}</a>
</div>
</article>
{% endfor %}
</div>
<div class="route-modal" data-route-modal hidden>
<div class="route-modal-backdrop" data-route-close></div>
<section class="route-modal-panel" role="dialog" aria-modal="true" aria-label="{{ t('hotel_route_modal_title') }}">
<header class="route-modal-head">
<h2>{{ t('hotel_route_modal_title') }}</h2>
<button class="btn btn-ghost route-modal-close" type="button" data-route-close>{{ t('hotel_route_modal_close') }}</button>
</header>
<div class="route-modal-body">
<iframe title="{{ t('hotel_route_modal_title') }}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen data-route-frame></iframe>
<div class="route-map-actions">
<a class="btn route-open-btn" href="#" target="_blank" rel="noopener" data-route-open-external>{{ t('hotel_route_open_maps') }}</a>
</div>
</div>
</section>
</div>
<script>
(() => {
const modal = document.querySelector("[data-route-modal]");
if (!modal) return;
const frame = modal.querySelector("[data-route-frame]");
const externalLink = modal.querySelector("[data-route-open-external]");
const closeNodes = modal.querySelectorAll("[data-route-close]");
const triggerNodes = document.querySelectorAll("[data-route-popup][data-route-src]");
if (!frame || !externalLink || triggerNodes.length === 0) return;
const closeModal = () => {
modal.hidden = true;
document.body.classList.remove("has-route-modal");
frame.src = "";
};
triggerNodes.forEach((node) => {
node.addEventListener("click", (event) => {
event.preventDefault();
const src = node.getAttribute("data-route-src");
const href = node.getAttribute("href");
if (!src) return;
frame.src = src;
externalLink.href = href || "#";
modal.hidden = false;
document.body.classList.add("has-route-modal");
});
});
closeNodes.forEach((node) => {
node.addEventListener("click", closeModal);
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !modal.hidden) {
closeModal();
}
});
})();
</script>
{% elif page == 'taxi' %}
<div class="taxi-coming-soon{% if lang == 'en' %} taxi-coming-soon--en{% else %} taxi-coming-soon--de{% endif %}">
<img
src="{{ url_for('static', filename='assets/bauerbeiter-eng-sticker.png' if lang == 'en' else 'assets/bauarbeiter-sticker.png') }}"
class="{% if lang == 'en' %}taxi-sticker--en{% endif %}"
alt="{{ t('taxi_sticker_alt') }}"
loading="lazy"
decoding="async"
>
</div>
<p>{{ t('taxi_text') }}</p>
{% elif page == 'gifts' %}
<section class="gift-fun" data-gift-fun>
<p class="gift-lead">{{ t('gifts_teaser') }}</p>
<button class="btn gift-reveal-btn" type="button" data-gift-reveal>{{ t('gifts_reveal_button') }}</button>
<div class="gift-reveal" hidden data-gift-reveal-panel>
<div class="gift-image-wrap">
<img
src="{{ url_for('static', filename='assets/money-pile.svg') }}"
alt="{{ t('gifts_image_alt') }}"
loading="lazy"
decoding="async"
>
</div>
<p class="gift-caption">{{ t('gifts_caption') }}</p>
<p>{{ t('gifts_text') }}</p>
<div class="money-rain" aria-hidden="true">
<span class="bill" style="--idx: 1;"></span>
<span class="bill" style="--idx: 2;"></span>
<span class="bill" style="--idx: 3;"></span>
<span class="bill" style="--idx: 4;"></span>
<span class="bill" style="--idx: 5;"></span>
<span class="bill" style="--idx: 6;"></span>
<span class="bill" style="--idx: 7;"></span>
<span class="bill" style="--idx: 8;"></span>
<span class="bill" style="--idx: 9;"></span>
<span class="bill" style="--idx: 10;"></span>
<span class="bill" style="--idx: 11;"></span>
<span class="bill" style="--idx: 12;"></span>
</div>
</div>
</section>
<script>
(() => {
const root = document.querySelector("[data-gift-fun]");
if (!root) return;
const button = root.querySelector("[data-gift-reveal]");
const panel = root.querySelector("[data-gift-reveal-panel]");
if (!button || !panel) return;
button.addEventListener("click", () => {
panel.hidden = false;
panel.classList.add("is-active");
button.setAttribute("disabled", "disabled");
}, { once: true });
})();
</script>
{% elif page == 'location' %}
<h2>{{ t('location_story_title') }}</h2>
<p>{{ t('location_story_text') }}</p>
<div
class="map-wrap map-consent"
data-map-consent
data-route-denied="{{ t('route_location_denied') }}"
data-route-unavailable="{{ t('route_location_unavailable') }}"
>
<button
class="map-preview"
type="button"
data-map-load
aria-label="{{ t('maps_load_button') }}"
title="{{ t('maps_load_button') }}"
style="--map-preview-image: url('{{ url_for('static', filename='assets/location-map-preview.svg') }}');"
>
<span class="map-preview-overlay">{{ t('maps_load_button') }}</span>
</button>
<div class="map-embed-target" data-map-embed-target></div>
<div class="location-actions">
<a class="btn btn-ghost" href="{{ location_route_url }}" target="_blank" rel="noopener" data-location-route-live>{{ t('route_from_current') }}</a>
<a class="btn" href="{{ location_website_url }}" target="_blank" rel="noopener">{{ t('visit_location') }}</a>
</div>
</div>
<script>
(() => {
const wrapper = document.querySelector("[data-map-consent]");
if (!wrapper) return;
const loadButtons = Array.from(wrapper.querySelectorAll("[data-map-load]"));
const previewButton = wrapper.querySelector(".map-preview");
const target = wrapper.querySelector("[data-map-embed-target]");
const src = {{ google_maps_embed_url|tojson }};
const destination = {{ location_address|tojson }};
const liveRouteLink = wrapper.querySelector("[data-location-route-live]");
const deniedMsg = wrapper.dataset.routeDenied || "";
const unavailableMsg = wrapper.dataset.routeUnavailable || "";
let loaded = false;
const loadMap = () => {
if (loaded) return;
const iframe = document.createElement("iframe");
iframe.src = src;
iframe.loading = "lazy";
iframe.referrerPolicy = "no-referrer-when-downgrade";
iframe.allowFullscreen = true;
target.appendChild(iframe);
if (previewButton) {
previewButton.remove();
}
loadButtons.forEach((button) => button.remove());
loaded = true;
};
loadButtons.forEach((button) => {
button.addEventListener("click", loadMap);
});
if (liveRouteLink) {
liveRouteLink.addEventListener("click", (event) => {
event.preventDefault();
if (!navigator.geolocation) {
if (unavailableMsg) {
window.alert(unavailableMsg);
}
return;
}
navigator.geolocation.getCurrentPosition(
({ coords }) => {
const origin = `${coords.latitude},${coords.longitude}`;
const query = new URLSearchParams({
api: "1",
origin,
destination,
travelmode: "driving",
});
window.open(`https://www.google.com/maps/dir/?${query.toString()}`, "_blank", "noopener");
},
(geoError) => {
if (geoError && geoError.code === 1 && deniedMsg) {
window.alert(deniedMsg);
return;
}
if (unavailableMsg) {
window.alert(unavailableMsg);
}
},
{ enableHighAccuracy: true, timeout: 4500, maximumAge: 120000 }
);
});
}
})();
</script>
{% endif %}
</section>
{% endblock %}

View File

@@ -1,26 +1,103 @@
{% extends 'base.html' %}
{% block content %}
<div class="login-layout">
<section class="hero card">
<h1>{{ t('subtitle') }}</h1>
<p>{{ t('login_note') }}</p>
</section>
<section class="card form-card">
<h2>{{ t('login') }}</h2>
<section class="card login-shell">
<header class="login-intro">
<h1>{{ t('subtitle') }}</h1>
<p>{{ t('login_note') }}</p>
</header>
<hr class="login-divider" />
<h2 class="login-title">{{ t('login') }}</h2>
<form method="post" action="{{ url_for('login') }}" class="form-grid">
<label>
{{ t('name') }}
<input type="text" name="name" required />
{{ t('group_name') }}
<input type="text" name="group_name" required />
</label>
<label>
{{ t('event_password') }}
<input type="password" name="event_password" required />
{{ t('group_password') }}
<div class="password-input-wrap">
<input type="password" name="group_password" required data-password-input />
<button
type="button"
class="password-toggle"
data-password-toggle
aria-label="{{ t('show_password') }}"
title="{{ t('show_password') }}"
hidden
>
<svg class="icon-eye icon-eye-open" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M2 12c2.7-4.2 6-6.3 10-6.3s7.3 2.1 10 6.3c-2.7 4.2-6 6.3-10 6.3S4.7 16.2 2 12z" />
<circle cx="12" cy="12" r="3.1" />
</svg>
<svg class="icon-eye icon-eye-closed" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M3 3l18 18" />
<path d="M2.4 12c1.1-1.8 2.4-3.2 4-4.3" />
<path d="M9.4 6.2a9 9 0 0 1 2.6-.4c4 0 7.3 2.1 10 6.3a16.2 16.2 0 0 1-3 3.6" />
<path d="M14.7 14.7a3.8 3.8 0 0 1-5.4-5.4" />
</svg>
</button>
</div>
</label>
<button class="btn" type="submit">{{ t('login_submit') }}</button>
</form>
</section>
</div>
<aside class="login-privacy-banner" data-login-privacy-banner>
<p class="login-privacy-title">{{ t('login_privacy_title') }}</p>
<p class="login-privacy-text">
{{ t('login_privacy_text') }}
<a href="{{ url_for('datenschutz') }}">{{ t('privacy') }}</a>
</p>
<button class="btn login-privacy-btn" type="button" data-login-privacy-accept>{{ t('login_privacy_accept') }}</button>
</aside>
<script>
(() => {
const banner = document.querySelector("[data-login-privacy-banner]");
const acceptBtn = document.querySelector("[data-login-privacy-accept]");
if (banner && acceptBtn) {
const storageKey = "login_privacy_banner_v1_ack";
if (localStorage.getItem(storageKey) === "1") {
banner.hidden = true;
} else {
acceptBtn.addEventListener("click", () => {
localStorage.setItem(storageKey, "1");
banner.hidden = true;
});
}
}
const passwordInput = document.querySelector("[data-password-input]");
const passwordToggle = document.querySelector("[data-password-toggle]");
if (!passwordInput || !passwordToggle) return;
const labelShow = "{{ t('show_password') }}";
const labelHide = "{{ t('hide_password') }}";
const syncState = () => {
const hasValue = passwordInput.value.length > 0;
passwordToggle.hidden = !hasValue;
passwordToggle.classList.toggle("is-visible", hasValue);
if (!hasValue) {
passwordInput.type = "password";
passwordToggle.setAttribute("aria-label", labelShow);
passwordToggle.setAttribute("title", labelShow);
passwordToggle.classList.remove("is-active");
}
};
passwordToggle.addEventListener("click", () => {
const showPassword = passwordInput.type === "password";
passwordInput.type = showPassword ? "text" : "password";
const label = showPassword ? labelHide : labelShow;
passwordToggle.setAttribute("aria-label", label);
passwordToggle.setAttribute("title", label);
passwordToggle.classList.toggle("is-active", showPassword);
});
passwordInput.addEventListener("input", syncState);
syncState();
})();
</script>
{% endblock %}

View File

@@ -1,24 +1,82 @@
{% extends 'base.html' %}
{% block content %}
<section class="card form-card">
<section class="card">
<h1>{{ t('rsvp') }}</h1>
<p>{{ t('rsvp_members_intro') }}</p>
<form method="post" class="form-grid">
<label class="radio-row">
<input type="radio" name="attending" value="yes" {% if guest and guest['attending'] == 1 %}checked{% endif %} />
{{ t('attending') }}
</label>
{% for member in members %}
<article class="member-card" data-member-card>
<h2 class="member-name">{{ member['name'] }}</h2>
<div class="member-choice-row">
<label class="radio-row">
<input
type="radio"
name="attending_{{ member['id'] }}"
value="yes"
{% if member['attending'] == 1 %}checked{% endif %}
data-attendance-input
data-member-id="{{ member['id'] }}"
/>
{{ t('attending') }}
</label>
<label class="radio-row">
<input type="radio" name="attending" value="no" {% if guest and guest['attending'] == 0 %}checked{% endif %} />
{{ t('not_attending') }}
</label>
<label class="radio-row">
<input
type="radio"
name="attending_{{ member['id'] }}"
value="no"
{% if member['attending'] == 0 %}checked{% endif %}
data-attendance-input
data-member-id="{{ member['id'] }}"
/>
{{ t('not_attending') }}
</label>
</div>
<label>
<input type="checkbox" name="plus_one" {% if guest and guest['plus_one'] == 1 %}checked{% endif %} />
{{ t('plus_one') }}
</label>
{% if member['requires_age'] == 1 %}
<label class="member-age-wrap" data-age-wrap data-member-id="{{ member['id'] }}">
{{ t('member_age') }}
<input
type="number"
min="0"
max="17"
step="1"
name="age_{{ member['id'] }}"
value="{{ member['child_age'] if member['child_age'] is not none else '' }}"
/>
<small>{{ t('member_age_hint') }}</small>
</label>
{% endif %}
</article>
{% endfor %}
<button class="btn" type="submit">{{ t('save') }}</button>
</form>
</section>
<script>
(() => {
const inputs = Array.from(document.querySelectorAll("[data-attendance-input]"));
const ageWraps = Array.from(document.querySelectorAll("[data-age-wrap]"));
const updateAgeVisibility = () => {
ageWraps.forEach((wrap) => {
const memberId = wrap.getAttribute("data-member-id");
const yesRadio = document.querySelector(`input[name="attending_${memberId}"][value="yes"]`);
const ageInput = wrap.querySelector("input[type='number']");
const shouldShow = Boolean(yesRadio && yesRadio.checked);
wrap.classList.toggle("is-visible", shouldShow);
if (ageInput) {
ageInput.required = shouldShow;
if (!shouldShow) {
ageInput.value = "";
}
}
});
};
inputs.forEach((input) => input.addEventListener("change", updateAgeVisibility));
updateAgeVisibility();
})();
</script>
{% endblock %}

View File

@@ -1,77 +1,113 @@
{% extends 'base.html' %}
{% block content %}
<section class="card form-card">
<section class="card upload-card">
<h1>{{ t('upload') }}</h1>
<form method="post" enctype="multipart/form-data" class="form-grid">
<label>
{{ t('file') }}
<input id="photo-input" type="file" name="photo" accept="image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif" multiple />
<p class="upload-intro">{{ t('upload_intro') }}</p>
<form id="upload-form" method="post" enctype="multipart/form-data" class="form-grid">
<label class="upload-picker" for="photo-input">
<span class="upload-picker-title">{{ t('file') }}</span>
<span class="upload-picker-subtitle">{{ t('upload_picker_hint') }}</span>
<input id="photo-input" class="sr-only" type="file" name="photo" accept="image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif" multiple />
</label>
<p class="upload-hint">{{ t('upload_multi_hint') }}</p>
<div id="extra-file-inputs"></div>
<button id="add-file-input" class="btn btn-ghost" type="button">{{ t('add_more_files') }}</button>
<p id="upload-selected-count" class="upload-count"></p>
<p id="upload-ready-hint" class="upload-ready">{{ t('upload_ready') }}</p>
<ul id="upload-file-list" class="upload-file-list"></ul>
<button class="btn" type="submit">{{ t('upload_submit') }}</button>
<button id="upload-submit-btn" class="btn" type="submit" disabled>{{ t('upload_submit') }}</button>
</form>
</section>
<script>
(() => {
const addBtn = document.getElementById("add-file-input");
const extraInputs = document.getElementById("extra-file-inputs");
const form = document.getElementById("upload-form");
const fileInput = document.getElementById("photo-input");
const countEl = document.getElementById("upload-selected-count");
const readyEl = document.getElementById("upload-ready-hint");
const listEl = document.getElementById("upload-file-list");
const submitBtn = document.getElementById("upload-submit-btn");
const countTpl = {{ t('upload_selected_count')|tojson }};
const selectedFiles = [];
const allInputs = () => Array.from(document.querySelectorAll('input[name="photo"]'));
const fileKey = (file) => `${file.name}__${file.size}__${file.lastModified}`;
const createExtraInput = () => {
const wrapper = document.createElement("label");
wrapper.className = "extra-file-input";
wrapper.textContent = {{ t('file')|tojson }};
const input = document.createElement("input");
input.type = "file";
input.name = "photo";
input.accept = "image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif";
input.required = false;
input.addEventListener("change", renderSelection);
wrapper.appendChild(input);
extraInputs.appendChild(wrapper);
const syncInputFiles = () => {
const transfer = new DataTransfer();
selectedFiles.forEach((file) => transfer.items.add(file));
fileInput.files = transfer.files;
};
const renderSelection = () => {
const names = [];
allInputs().forEach((input) => {
Array.from(input.files || []).forEach((file) => names.push(file.name));
});
if (!names.length) {
if (!selectedFiles.length) {
countEl.textContent = "";
listEl.innerHTML = "";
if (readyEl) {
readyEl.classList.remove("is-visible");
}
if (submitBtn) {
submitBtn.disabled = true;
}
return;
}
countEl.textContent = countTpl.replace("{count}", String(names.length));
countEl.textContent = countTpl.replace("{count}", String(selectedFiles.length));
listEl.innerHTML = "";
names.slice(0, 20).forEach((name) => {
if (readyEl) {
readyEl.classList.add("is-visible");
}
if (submitBtn) {
submitBtn.disabled = false;
}
selectedFiles.slice(0, 20).forEach((file, index) => {
const item = document.createElement("li");
item.textContent = name;
const label = document.createElement("span");
label.textContent = file.name;
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className = "upload-file-remove";
removeBtn.setAttribute("aria-label", "remove file");
removeBtn.textContent = "×";
removeBtn.addEventListener("click", () => {
selectedFiles.splice(index, 1);
syncInputFiles();
renderSelection();
});
item.appendChild(label);
item.appendChild(removeBtn);
listEl.appendChild(item);
});
if (names.length > 20) {
if (selectedFiles.length > 20) {
const more = document.createElement("li");
more.textContent = `+ ${names.length - 20} weitere`;
more.textContent = `+ ${selectedFiles.length - 20} weitere`;
listEl.appendChild(more);
}
};
allInputs().forEach((input) => input.addEventListener("change", renderSelection));
addBtn.addEventListener("click", () => {
createExtraInput();
fileInput.addEventListener("change", () => {
const existingKeys = new Set(selectedFiles.map(fileKey));
Array.from(fileInput.files || []).forEach((file) => {
const key = fileKey(file);
if (!existingKeys.has(key)) {
selectedFiles.push(file);
existingKeys.add(key);
}
});
syncInputFiles();
fileInput.value = "";
renderSelection();
});
form.addEventListener("submit", (event) => {
if (!selectedFiles.length) {
event.preventDefault();
renderSelection();
return;
}
syncInputFiles();
});
renderSelection();
})();
</script>
{% endblock %}

View File

@@ -5,9 +5,10 @@
{% if wedding_date %}
<p class="hero-kicker">{{ wedding_date }}</p>
{% endif %}
<h1>{{ t('hero_headline') }}</h1>
<p>{{ t('hero_text') }}</p>
<h1>{{ welcome_headline or t('hero_headline') }}</h1>
<p>{{ welcome_text or t('hero_text') }}</p>
<a class="btn hero-cta" href="{{ url_for('guest_area') }}">{{ t('to_guest_area') }}</a>
</div>
</section>
<p class="hero-hint-below">{{ welcome_hint }}</p>
{% endblock %}

71
backend/uv.lock generated
View File

@@ -9,12 +9,14 @@ source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "gunicorn" },
{ name = "pillow" },
]
[package.metadata]
requires-dist = [
{ name = "flask", specifier = ">=3.1.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "pillow", specifier = ">=12.1.1" },
]
[[package]]
@@ -169,6 +171,75 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pillow"
version = "12.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.5"

Binary file not shown.

View File

@@ -25,5 +25,11 @@ services:
- EVENT_PASSWORD=${EVENT_PASSWORD:-wedding2026}
- HOST_PASSWORD=${HOST_PASSWORD:-gastgeber2026}
- WEDDING_DATE=${WEDDING_DATE:-}
- WEDDING_COUNTDOWN_LOCAL=${WEDDING_COUNTDOWN_LOCAL:-2026-09-04 15:00}
- WEDDING_TIMEZONE=${WEDDING_TIMEZONE:-Europe/Berlin}
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}
- MAX_UPLOAD_BYTES=${MAX_UPLOAD_BYTES:-268435456}
- LOCATION_NAME=${LOCATION_NAME:-Klostermühle}
- LOCATION_ADDRESS=${LOCATION_ADDRESS:-An d. Klostermühle 3, 65399 Kiedrich}
- LOCATION_WEBSITE_URL=${LOCATION_WEBSITE_URL:-https://www.klostermuehle.de/}
- GOOGLE_MAPS_EMBED_URL=${GOOGLE_MAPS_EMBED_URL:-https://www.google.com/maps?q=Klostermuehle+Kiedrich+Eltville&output=embed}