Neue Features>: zu viele um sie zu beschrieben :D

This commit is contained in:
2026-02-22 16:36:37 +00:00
parent 396055a705
commit 9e2716ba9c
19 changed files with 1269 additions and 249 deletions

376
AGENTS.md
View File

@@ -1,8 +1,17 @@
AGENTS.md
💍 Wedding App Agent Specification (Modern UI Edition)
💍 AGENTS.md
Wedding App Agent Specification (Hero + Secure Edition)
1. Project Overview
A private wedding web app for invited guests. (Link: https://www.svenja-dominic-hochzeit.de/)
A private wedding web application for invited guests.
Production URL:
https://www.svenja-dominic-hochzeit.de/
The entire platform is protected by login.
There is no public content accessible without authentication.
Core goals:
@@ -12,115 +21,190 @@ RSVP + plus-one selection
photo upload + shared gallery
modern information pages (schedule, hotels, taxi)
modern information pages (schedule, hotels, taxi, location)
embedded Google Maps location
embedded Google Maps (2-click solution)
external link to the official location website
external link to official location website
button to switch the language of the webapp from german to english
language switch (German / English)
visually polished, modern design (mobile-first)
visually polished, modern, mobile-first UI
2. Access Model (IMPORTANT)
The entire site must be login-protected.
2. Tech Stack (Required)
No publicly accessible landing page.
Python 3.12
Flow:
Flask
User visits root URL → redirected to login
Gunicorn
User enters:
uv (dependency management)
event password
SQLite
guest name
Docker + Docker Compose
On success:
Frontend approach:
guest stored in database (if new)
Use server-rendered templates (Jinja2)
session created
Use modern CSS (prefer one of the following):
redirect to internal start page
Tailwind CSS via CDN (fastest)
All internal routes must require authentication.
or a small handcrafted CSS design system (preferred if no CDN)
3. Internal Start Page Structure (Hero + Dashboard Concept)
No heavy JS frameworks required.
After login, the start page consists of two sections:
3. UI / UX Design Requirements (IMPORTANT)
The site must look modern, elegant, and “wedding-like”:
Visual Style
Clean typography (Google Fonts allowed)
Soft spacing, rounded cards, subtle shadows
Consistent color palette (e.g., beige / cream / dark green / gold accents)
Smooth hover states and transitions
High-quality hero section on landing page
Layout
Mobile-first (works on phones)
Clear navigation
Dashboard cards for features (RSVP, Upload, Info)
Minimal clutter, lots of whitespace
Pages
Must implement at least:
Landing / Login page (with hero design)
Dashboard
RSVP page (or dashboard section)
Upload page
Gallery page
Info pages (schedule, hotels, taxi, location)
4. Location Page Requirements
Section 1 Hero Area (Emotional Welcome)
Must include:
Embedded Google Maps iframe
large background image
Address (configurable)
headline:
“Willkommen zu unserer Hochzeit”
A prominent button:
wedding date
“Zur Location-Webseite” (or “Visit Location Website”)
short personal text
open in new tab (target="_blank" rel="noopener")
smooth scroll transition to dashboard
The location website URL must be configurable via environment variable:
Purpose:
Make the platform feel emotional and elegant, not like a business app.
LOCATION_WEBSITE_URL
Section 2 Dashboard Area
Optionally also:
Below the hero section:
Card-based grid layout containing:
RSVP
Upload
Gallery
Ablauf (Schedule)
Hotels
Taxi
Location
The dashboard must:
use rounded cards
soft shadows
consistent spacing
mobile-first responsive layout
4. Tech Stack (Required)
Python 3.12
Flask
Gunicorn
uv (dependency management)
SQLite
Docker + Docker Compose
Frontend:
Jinja2 templates
Tailwind via CDN OR lightweight custom CSS
No heavy JS frameworks
Minimal JavaScript only where needed
5. UI / UX Requirements (Very Important)
Visual style:
elegant and modern
wedding-like aesthetic
soft spacing
rounded elements
subtle shadows
warm color palette (cream / beige / dark green / gold accents)
clean typography (Google Fonts allowed)
Mobile-first design required.
Minimal clutter.
Smooth hover transitions.
6. Language Switch (DE / EN)
Must include:
language toggle in header
switch stored in session
no automatic geo-detection
static text controlled via simple translation dictionary or structure
7. Location Page Requirements
Must include:
Location name (env variable)
Address (env variable)
Google Maps embed (iframe)
Prominent button:
“Zur Location-Webseite” / “Visit Location Website”
target="_blank"
rel="noopener"
Environment variables:
LOCATION_NAME
LOCATION_ADDRESS
LOCATION_WEBSITE_URL
GOOGLE_MAPS_EMBED_URL
5. Authentication Requirements
Google Maps Privacy Requirement
Registration/login requires event password
Google Maps must NOT load automatically.
Implement a 2-click solution:
Show placeholder container
Display privacy notice:
“Zur Anzeige der Karte werden Daten an Google übertragen.”
Only after user click → load iframe dynamically
No global cookie banner required.
8. Authentication Requirements
Event password stored in environment variable:
@@ -130,13 +214,19 @@ Guest provides:
event password
guest name
name
Use Flask sessions
Use Flask sessions.
Keep it simple (no email verification, no roles)
No:
6. Database Schema (Minimum)
email verification
role system
admin panel
9. Database Schema
Table: guests
@@ -160,92 +250,142 @@ uploaded_by (guest id)
uploaded_at (timestamp)
SQLite is sufficient.
SQLite only.
7. RSVP Logic
10. RSVP Logic
In the UI:
Guest selects:
guest selects attending Yes/No
attending Yes / No
plus_one option only shown if attending Yes
plus_one only visible if attending Yes
persist to database
Persist to database.
8. Upload Requirements
11. Upload Requirements
allowed types: jpg, jpeg, png
Allowed types:
configurable max upload size
jpg
jpeg
png
Must:
validate file extension
validate MIME type
limit file size (configurable)
sanitize filenames
prevent path traversal
store in /uploads
store files in /uploads
store upload reference in DB
store reference in database
9. Gallery Requirements
Optional but recommended:
all guests can view uploaded images
remove EXIF metadata before saving
show thumbnails in a responsive grid
12. Gallery Requirements
click opens a larger view (simple modal or dedicated page)
All guests see all images
10. Dependency Management Rules
Responsive grid layout
Use uv
Click → larger view (simple modal)
Dependencies defined in pyproject.toml
No download tracking
Commit uv.lock
13. Legal Pages (Important)
No requirements.txt
Must implement:
In Docker:
/datenschutz
/impressum
Both:
accessible without login (legal requirement)
linked in footer
always visible in footer
No cookie banner required because:
only technically necessary session cookies used
Google Maps loaded via 2-click solution
14. Dependency Management Rules
Use uv.
Dependencies defined in:
pyproject.toml
Commit:
uv.lock
No requirements.txt allowed.
Docker must run:
uv sync --frozen --no-dev
11. Docker Requirements
15. Docker Requirements
Dockerfile must:
Base image:
Base: python:3.12-slim
python:3.12-slim
Install uv
Must:
Copy pyproject.toml + uv.lock first (cache-friendly)
install uv
Run uv sync --frozen --no-dev
copy pyproject.toml + uv.lock first (cache optimization)
Start with:
run uv sync --frozen --no-dev
expose port 8000
start with:
uv run gunicorn -b 0.0.0.0:8000 app:app
Uploads + SQLite database must be persistent via volumes.
Uploads + SQLite database must use persistent volumes.
12. Non-Goals (Do NOT implement)
16. Non-Goals (Strict)
Do NOT implement:
Admin dashboards
Email systems
Payments
Payment systems
OAuth
External cloud storage
Cloud storage
Microservices
13. Design Philosophy
Tracking tools
Aesthetic first, but not overengineered
Analytics tools
Simple, maintainable code
17. Design Philosophy
Minimal dependencies
Good UX on mobile
Aesthetic first, but not overengineered.
Simple, maintainable code.
Minimal dependencies.
Excellent mobile UX.
Elegant, but not playful.

406
README.md
View File

@@ -1,65 +1,69 @@
💍 Wedding App Svenja & Dominic
Eine private, moderne Hochzeits-Webanwendung für eingeladene Gäste.
Private, moderne Hochzeits-Webanwendung für eingeladene Gäste.
Die Anwendung ermöglicht eine geschützte Registrierung, RSVP-Verwaltung, Foto-Uploads sowie den Zugriff auf alle relevanten Informationen rund um die Hochzeit in einem eleganten, mobilen Design.
Die Anwendung stellt eine geschlossene Event-Plattform dar, über die Gäste sich einloggen, ihre Teilnahme verwalten, Fotos hochladen und alle relevanten Informationen zur Hochzeit abrufen können.
Die gesamte Website ist nur nach erfolgreichem Login zugänglich.
🎯 Projektziel
Die Wedding-App soll:
unter dem Link https://www.svenja-dominic-hochzeit.de/ zum login führen.
unter https://www.svenja-dominic-hochzeit.de/ direkt zum Login führen
Gästen einen passwortgeschützten Zugang bieten
ausschließlich eingeladene Gäste mit Event-Passwort zulassen
Zu- und Absagen verwalten (inkl. Begleitperson)
eine strukturierte RSVP-Verwaltung ermöglichen
eine gemeinsame Fotogalerie bereitstellen
Informationen zur Location, Hotels in der Nähe, Taxi und Ablauf anzeigen
Location-, Hotel-, Taxi- und Ablauf-Informationen anzeigen
eine eingebettete Google Maps Karte enthalten
eine Google Maps Einbindung enthalten
einen prominenten Link zur offiziellen Website der Hochzeitslocation anzeigen
einen prominenten Link zur offiziellen Website der Location anzeigen
Einen Switch auf Englisch haben.
eine DE/EN Sprachumschaltung bieten
Die Seite soll modern, ästhetisch und mobil optimiert sein.
modern, mobiloptimiert und ästhetisch wirken
🎨 Design-Anforderung (WICHTIG)
🏗 Seitenarchitektur
🔐 Geschlossene Plattform
Die Anwendung soll:
Die gesamte Anwendung ist nur nach Login erreichbar.
Es existiert kein öffentlich zugänglicher Bereich.
modern, elegant und hochwertig wirken
🏡 Login / Landing Page (Hero + Dashboard Konzept)
mobil-first entwickelt werden
Nach erfolgreichem Login besteht die Startseite aus zwei Bereichen:
viel Weißraum verwenden
1⃣ Hero-Section (emotionaler Bereich)
runde Karten mit sanften Schatten nutzen
Großes Hintergrundbild
eine konsistente Farbpalette haben (z. B. Beige, Creme, Dunkelgrün, Gold-Akzente)
„Willkommen zu unserer Hochzeit“
klare Typografie verwenden (z. B. Google Fonts)
Datum
dezente Hover-Animationen besitzen
kurzer persönlicher Text
Seitenstruktur
Scroll-Hinweis oder sanfter Übergang
Mindestens folgende Seiten müssen umgesetzt werden:
Ziel: Emotionale Begrüßung statt App-Charakter.
Login / Landing Page (mit Hero-Section)
2⃣ Dashboard-Bereich
Dashboard
Nach dem Scrollen folgt das funktionale Gästedashboard:
RSVP-Bereich
Kartenstruktur mit:
Upload-Seite
RSVP
Upload
Galerie
Informationsseiten:
Ablauf
Hotels
@@ -68,45 +72,54 @@ Taxi
Location
🗺 Location-Seite Anforderungen
Das Dashboard bleibt funktional strukturiert, wirkt jedoch durch den vorgelagerten Hero-Bereich nicht wie eine Business-Webapp.
Die Location-Seite muss enthalten:
🎨 Design-Philosophie
Google Maps Embed (iframe)
Die Anwendung soll:
Name der Location
modern und hochwertig wirken
Adresse
mobil-first entwickelt werden
Einen deutlich sichtbaren Button:
viel Weißraum verwenden
„Zur Location-Webseite“
runde Karten mit weichen Schatten nutzen
Dieser Button muss:
eine warme, konsistente Farbpalette verwenden
(z. B. Creme, Beige, Dunkelgrün, Gold-Akzente)
in neuem Tab öffnen (target="_blank")
klare Typografie einsetzen (z. B. Google Fonts)
rel="noopener" verwenden
dezente Hover-Animationen nutzen
URL aus Environment Variable beziehen
elegant, aber nicht verspielt wirken
Konfigurierbare Variablen:
Design-Leitsatz:
LOCATION_NAME
Schön vor komplex
Einfach vor überengineered
Wartbar vor clever
LOCATION_ADDRESS
🌍 Mehrsprachigkeit
LOCATION_WEBSITE_URL
Umschaltbar zwischen Deutsch und Englisch
GOOGLE_MAPS_EMBED_URL
Sprachwahl per Switch im Header
Umsetzung über Flask-Session oder i18n-Lösung
Kein automatisches Geo-Redirect
🔐 Login & Registrierung
Zugriff nur mit Event-Passwort
Zugriff ausschließlich mit Event-Passwort.
Passwort wird als Environment Variable gespeichert:
Environment Variable:
EVENT_PASSWORD
HOST_PASSWORD
WEDDING_DATE (optional)
Registrierung:
@@ -114,48 +127,101 @@ Gast gibt Event-Passwort ein
Gast gibt seinen Namen ein
Nach erfolgreicher Prüfung → Weiterleitung zum Dashboard
Bei korrekter Validierung → Speicherung in Datenbank
Session-basiert (Flask Sessions)
Session-basierter Login (Flask Sessions)
Keine E-Mail-Verifikation
Kein Rollen-System
Keine öffentliche Registrierung
Nicht implementiert:
E-Mail-Verifikation
Rollen-System
öffentliche Registrierung
Admin-Panel
📝 RSVP-System
Im Dashboard kann der Gast auswählen:
Im Dashboard auswählbar:
Ich komme
Ich komme nicht
Ich bringe eine Begleitperson mit (nur wenn attending = true)
Ich bringe eine Begleitperson mit
(nur wenn attending = true)
Daten werden persistent in der Datenbank gespeichert.
Daten werden persistent gespeichert.
📸 Foto-Upload
Erlaubte Formate: jpg, jpeg, png
Erlaubte Formate:
Dateigröße begrenzen (konfigurierbar)
jpg
Speicherung in /uploads
jpeg
Dateinamen müssen sanitisiert werden
png
Sicherheitsanforderungen:
Dateityp validieren
Dateigröße limitieren
Dateinamen sanitizen
Path Traversal verhindern
Upload wird in Datenbank referenziert
Upload-Referenz in Datenbank speichern
Speicherung in /uploads
Optional empfohlen:
Entfernen von EXIF-Metadaten beim Upload (Datenschutz)
🖼 Galerie
Alle Gäste sehen alle Bilder
Responsive Grid
Responsives Grid
Optional: einfache Modal-Ansicht bei Klick
Optionale Modal-Ansicht
Kein Download-Tracking
🗺 Location-Seite
Enthält:
Google Maps Embed (iframe)
Name der Location
Adresse
Button „Zur Location-Webseite“
Der Button muss:
target="_blank"
rel="noopener"
URL aus Environment Variable beziehen
Konfigurierbare Variablen:
LOCATION_NAME
LOCATION_ADDRESS
LOCATION_WEBSITE_URL
GOOGLE_MAPS_EMBED_URL
Empfohlen:
Google Maps erst nach Nutzerklick laden (2-Klick-Lösung)
🧱 Datenbank (SQLite)
Tabelle: guests
@@ -180,35 +246,6 @@ uploaded_by (Guest ID)
uploaded_at (Timestamp)
🧪 Entwicklungsphasen
Phase 1 MVP
Authentifizierung
Datenbank
RSVP
Phase 2
Upload
Galerie
Phase 3
Informationsseiten
Location + Google Maps
Externer Location-Link
Phase 4
UI-Feinschliff
Sicherheitsverbesserungen
🧰 Technischer Stack
Python 3.12
@@ -240,30 +277,27 @@ uv.lock wird committed
Docker nutzt:
uv sync --frozen --no-dev
🐳 Docker
🐳 Docker Anforderungen
Base Image:
Base Image: python:3.12-slim
python:3.12-slim
uv im Container installiert
Gunicorn Start:
Gunicorn als Produktionsserver
uv run gunicorn -b 0.0.0.0:8000 app:app
Persistente Volumes für:
Persistente Volumes:
uploads
SQLite-Datenbank
Startbefehl:
uv run gunicorn -b 0.0.0.0:8000 app:app
🔒 Sicherheitsanforderungen
Event-Passwort darf niemals im Frontend erscheinen
Upload-Dateitypen validieren
Upload-Dateien validieren
Dateigröße limitieren
@@ -271,6 +305,35 @@ HTTPS verpflichtend im Produktivbetrieb
Keine unnötigen Features implementieren
📑 DSGVO & Rechtliches
Struktur
Eigene Route für Datenschutzerklärung
Platzierung im Footer neben Impressum
Dauerhaft erreichbar
Enthält: -> Vorlage ist schon im unteren abschnitt des textes erhalten.
Verantwortlicher
Verarbeitung beim Websiteaufruf
Google Maps Nutzung
Foto-Upload inkl. möglicher Metadaten
Rechte der Betroffenen
Cookie-Hinweis
Optional empfohlen:
EXIF-Metadaten beim Upload entfernen
Google Maps erst nach Einwilligung laden
❌ Nicht-Ziele
Kein Admin-Panel
@@ -285,14 +348,147 @@ Keine Cloud-Storage-Integration
Keine Microservices
💡 Design-Philosophie
Schön vor komplex
DSGVO erklärung: --> erstellung eines weiteren verzeichnisses auf dem die dsgco erklärung erreicht werden kann. Platzierung am Footer mit Impressum.
Einfach vor überengineered
Datenschutzerklärung
Wartbar vor clever
In dieser Datenschutzerklärung informieren wir Sie über die Verarbeitung personenbezogener Daten bei der Nutzung dieser Website.
Modern, aber nicht verspielt
Verantwortlicher
Mobil optimiert
Verantwortlich für die Datenverarbeitung ist:
Dominic Thiels, Wiesbadener Straße 70b, 65510, Idstein, +49 151 70616118 und d.thiels@freenet.de
Personenbezogene Daten
Personenbezogene Daten sind alle Informationen, die sich auf eine identifizierte oder identifizierbare natürliche Person (betroffene Person) beziehen. Als identifizierbar wird eine natürliche Person angesehen, die direkt oder indirekt, insbesondere mittels Zuordnung zu einer Kennung wie einem Namen, zu einer Kennnummer, zu Standortdaten, zu einer Online-Kennung oder zu einem oder mehreren besonderen Merkmalen identifiziert werden kann, die Ausdruck der physischen, physiologischen, genetischen, psychischen, wirtschaftlichen, kulturellen oder sozialen Identität dieser natürlichen Person sind.
Daten beim Websiteaufruf
Wenn Sie diese Website nur nutzen, um sich zu informieren und keine Daten angeben, dann verarbeiten wir nur die Daten, die zur Anzeige der Website auf dem von Ihnen verwendeten internetfähigen Gerät erforderlich sind. Das sind insbesondere:
IP-Adresse
Datum und Uhrzeit der Anfrage
jeweils übertragene Datenmenge
die Website, von der die Anforderung kommt
Browsertyp und Browserversion
Betriebssystem
Rechtsgrundlage für die Verarbeitung dieser Daten sind berechtigte Interessen gemäß Art. 6 Abs. 1 lit. f) DSGVO, um die Darstellung der Website grundsätzlich zu ermöglichen.
Darüber hinaus können Sie verschiedene Leistungen auf der Website nutzen, bei der weitere personenbezogene und nicht personenbezogene Daten verarbeitet werden.
Ihre Rechte
Als betroffene Person haben Sie folgende Rechte:
Sie haben ein Auskunftsrecht bezüglich der Sie betreffenden personenbezogenen Daten, die der Verantwortliche verarbeitet (Art. 15 DSGVO),
Sie haben das Recht auf Berichtigung der Sie betreffenden Daten, wenn diese unrichtig oder unvollständig gespeichert werden (Art. 16 DSGVO),
Sie haben das Recht auf Löschung (Art. 17 DSGVO),
Sie haben das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen (Art. 18 DSGVO),
Sie haben das Recht auf Datenübertragbarkeit (Art. 20 DSGVO),
Sie haben ein Widerspruchsrecht gegen die Verarbeitung der Sie betreffenden personenbezogenen Daten (Art. 21 DSGVO),
Sie haben das Recht, nicht einer ausschließlich auf einer automatisierten Verarbeitung einschließlich Profiling beruhenden Entscheidung unterworfen zu werden, die Ihnen gegenüber rechtliche Wirkung entfaltet oder Sie in ähnlicher Weise erheblich beeinträchtigt (Art. 22 DSGVO),
Sie haben das Recht, sich bei einem vermuteten Verstoß gegen das Datenschutzrecht bei der zuständigen Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde an Ihrem üblichen Aufenthaltsort, Arbeitsplatz oder am Ort des vermuteten Verstoßes.
Einsatz von Cookies
Beim Besuch der Website können Cookies auf Ihrem Gerät gespeichert werden. Cookies sind kleine Textdateien, die von dem von Ihnen verwendeten Browser gespeichert werden. Cookies können keine Programme ausführen und auch keine Viren auf Ihr Gerät übertragen. Die Stelle, die den Cookie setzt, kann darüber jedoch bestimmte Informationen erhalten. So kann mithilfe von Cookies insbesondere das Gerät, mit dem diese Website aufgerufen wurde, bei einem erneuten Aufruf erkannt werden.
Durch die Browsereinstellungen lässt sich das Setzen von Cookies einschränken oder verhindern. So kann z. B. nur die Annahme von Cookies, die von Drittanbietern stammen, blockiert werden oder aber auch die Annahme von allen Cookies. Durch das Blockieren sind jedoch möglicherweise nicht mehr alle Funktionen dieser Website nutzbar. Im weiteren Text dieser Datenschutzerklärung informieren wir Sie konkret, an welchen Stellen und zu welchen Zwecken Cookies auf den Seiten zum Einsatz kommen.
Es werden ausschließlich technisch notwendige Cookies verwendet, die für den Betrieb der Website und die Aufrechterhaltung der Benutzersitzung erforderlich sind.
Google Maps
Auf dieser Website ist ein Dienst der Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland, eingebunden, um geographische Informationen visuell darzustellen (Google Maps).
Bei der Nutzung von Google Maps werden Informationen über die Nutzung dieser Website einschließlich Ihrer IP-Adresse an Server von Google übertragen und dort gespeichert. Diese Server können sich auch in den USA befinden. Google kann diese Informationen gegebenenfalls an Dritte übertragen, sofern dies gesetzlich vorgeschrieben ist oder soweit Dritte diese Daten im Auftrag von Google verarbeiten.
Die Nutzung von Google Maps erfolgt im Interesse einer ansprechenden Darstellung der Veranstaltungsorte sowie einer leichten Auffindbarkeit der auf der Website angegebenen Orte. Dies stellt ein berechtigtes Interesse im Sinne von Art. 6 Abs. 1 lit. f DSGVO dar.
Sofern eine entsprechende Einwilligung abgefragt wird (z. B. über ein Cookie-Banner), erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO. Die Einwilligung kann jederzeit widerrufen werden.
Weitere Informationen zur Datenverarbeitung durch Google finden Sie in der Datenschutzerklärung von Google:
https://policies.google.com/privacy
Upload von Fotos durch Nutzer
Auf dieser Website besteht die Möglichkeit, Fotos hochzuladen, beispielsweise im Rahmen einer gemeinsamen Hochzeitsgalerie.
Beim Hochladen von Fotos werden folgende personenbezogene Daten verarbeitet:
die hochgeladene Bilddatei
gegebenenfalls enthaltene Metadaten (z. B. z. B. Aufnahmedatum, Kamerainformationen oder gegebenenfalls Standortdaten/GPS-Koordinaten)
der vom Nutzer angegebene Name (sofern vorgesehen)
technische Zugriffsdaten (z. B. IP-Adresse, Zeitpunkt des Uploads)
Die Verarbeitung erfolgt zum Zweck der Bereitstellung einer gemeinsamen Fotogalerie für Gäste der Veranstaltung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Erfüllung der Nutzungsfunktion der Website) sowie Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an der Bereitstellung einer gemeinsamen Erinnerungsplattform).
Es wird darauf hingewiesen, dass hochgeladene Fotos personenbezogene Daten Dritter enthalten können (z. B. Abbildungen von Personen). Der hochladende Nutzer ist dafür verantwortlich, dass die Veröffentlichung dieser Bilder datenschutzrechtlich zulässig ist und insbesondere die Einwilligung der abgebildeten Personen vorliegt.
Die Fotos werden ausschließlich für den vorgesehenen Zweck gespeichert und nicht an Dritte weitergegeben, sofern keine gesetzliche Verpflichtung besteht.
Quelle: Muster-Datenschutzerklärung von anwalt.de
Impressum
Angaben gemäß § 5 DDG
Dominic Thiels
Wiesbadener Straße 70b
65510 Idstein
Vertreten durch:
Dominic Thiels
Kontakt:
Telefon: +49-151 70616118
E-Mail: d.thiels@freenet.de
Verbraucherstreitbeilegung / Universalschlichtungsstelle
Wir nehmen nicht an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teil und sind dazu auch nicht verpflichtet.
Haftungsausschluss:
Haftung für Inhalte
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs.1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
Haftung für Links
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
Urheberrecht
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
Erstellt mit dem Impressum-Generator von WebsiteWissen.com, dem Ratgeber für Website-Erstellung, Homepage-Baukästen und Shopsysteme. Rechtstext von der Kanzlei Hasselbach.
HTML:
<h1>Impressum</h1>
<p>Angaben gemäß § 5 DDG</p>
<p>Dominic Thiels<br>
<br>
Wiesbadener Straße 70b<br>
65510 Idstein <br>
</p>
<p> <strong>Vertreten durch: </strong><br>
Dominic Thiels<br>
</p>
<p><strong>Kontakt:</strong> <br>
Telefon: +49-151 70616118<br>
E-Mail: <a>d.thiels@freenet.de</a></br></p>
<p><strong>Verbraucherstreitbeilegung / Universalschlichtungsstelle</strong>
<br>Wir nehmen nicht an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teil und sind dazu auch nicht verpflichtet.
</p>
<p><strong>Haftungsausschluss: </strong>
<br><br><strong>Haftung für Inhalte</strong><br>
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs.1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.<br><br><strong>Haftung für Links</strong><br>
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.<br><br><strong>Urheberrecht</strong><br>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.</p>
<p>Erstellt mit dem <a href="https://impressum-generator.de" rel="dofollow">Impressum-Generator</a> von WebsiteWissen.com, dem Ratgeber für <a href="https://websitewissen.com/website-erstellen" rel="dofollow">Website-Erstellung</a>, <a href="https://websitewissen.com/homepage-baukasten-vergleich" rel="dofollow">Homepage-Baukästen</a> und <a href="https://websitewissen.com/shopsysteme-vergleich" rel="dofollow">Shopsysteme</a>. Rechtstext von der <a href="https://www.kanzlei-hasselbach.de/" rel="dofollow">Kanzlei Hasselbach</a>.
</p>

View File

@@ -1,6 +1,7 @@
import os
import sqlite3
import uuid
from hmac import compare_digest
from datetime import datetime
from functools import wraps
from pathlib import Path
@@ -24,6 +25,7 @@ app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "change-me-in-production
app.config["DB_PATH"] = os.environ.get("DB_PATH", str(base_dir / "app.sqlite3"))
app.config["UPLOAD_FOLDER"] = os.environ.get("UPLOAD_FOLDER", str(base_dir / "uploads"))
app.config["EVENT_PASSWORD"] = os.environ.get("EVENT_PASSWORD", "wedding2026")
app.config["HOST_PASSWORD"] = os.environ.get("HOST_PASSWORD", "gastgeber2026")
app.config["MAX_CONTENT_LENGTH"] = int(os.environ.get("MAX_UPLOAD_BYTES", str(8 * 1024 * 1024)))
app.config["LOCATION_NAME"] = os.environ.get("LOCATION_NAME", "Schlossgarten Venue")
app.config["LOCATION_ADDRESS"] = os.environ.get(
@@ -36,6 +38,8 @@ app.config["GOOGLE_MAPS_EMBED_URL"] = os.environ.get(
"GOOGLE_MAPS_EMBED_URL",
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d0",
)
app.config["WEDDING_DATE"] = os.environ.get("WEDDING_DATE", "")
app.config["HERO_IMAGE_FILENAME"] = os.environ.get("HERO_IMAGE_FILENAME")
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png"}
@@ -43,15 +47,18 @@ TEXTS = {
"de": {
"brand": "Svenja & Dominic",
"subtitle": "Willkommen zu unserer Hochzeits-App",
"login_note": "Passwortgeschuetzter Zugriff fuer unsere Gaeste.",
"login": "Login",
"name": "Dein Name",
"event_password": "Event-Passwort",
"login_submit": "Weiter zum Dashboard",
"dashboard": "Dashboard",
"login_submit": "Weiter zum Gaestebereich",
"guest_area": "Gaestebereich",
"hello_guest": "Hallo {name}.",
"logout": "Abmelden",
"rsvp": "RSVP",
"upload": "Upload",
"gallery": "Galerie",
"host_area": "Gastgeberbereich",
"info": "Infos",
"save": "Speichern",
"attending": "Ich komme",
@@ -64,19 +71,60 @@ TEXTS = {
"taxi": "Taxi",
"location": "Location",
"visit_location": "Zur Location-Webseite",
"privacy": "Datenschutz",
"imprint": "Impressum",
"hero_headline": "Willkommen zu unserer Hochzeit",
"hero_text": "Wir freuen uns riesig, diesen besonderen Tag mit euch zu feiern.",
"to_guest_area": "Zum Gaestebereich",
"schedule_text": "15:00 Trauung, 17:00 Empfang, 19:00 Dinner.",
"hotels_text": "Empfehlungen folgen. Bitte fruehzeitig buchen.",
"taxi_text": "Taxi-Service: 01234 / 567890 (24/7).",
"gallery_uploaded_by": "von {name}",
"gallery_empty": "Noch keine Bilder vorhanden.",
"gallery_image_alt": "Upload von {name}",
"flash_enter_name": "Bitte Namen eingeben.",
"flash_invalid_password": "Ungueltiges Event-Passwort.",
"flash_rsvp_select": "Bitte eine RSVP-Auswahl treffen.",
"flash_rsvp_saved": "RSVP gespeichert.",
"flash_select_image": "Bitte eine Bilddatei auswaehlen.",
"flash_allowed_types": "Nur JPG/JPEG/PNG sind erlaubt.",
"flash_upload_success": "Upload erfolgreich.",
"flash_invalid_host_password": "Ungueltiges Gastgeber-Passwort.",
"host_access_title": "Gastgeberbereich",
"host_access_note": "Dieser Bereich ist nur fuer das Brautpaar vorgesehen.",
"host_password": "Gastgeber-Passwort",
"host_access_submit": "Adminbereich oeffnen",
"host_stats_title": "Uebersicht",
"total_guests": "Gaeste gesamt",
"attending_yes": "Zusagen",
"attending_no": "Absagen",
"attending_open": "Noch offen",
"plus_one_total": "Begleitpersonen",
"host_table_name": "Name",
"host_table_status": "RSVP",
"host_table_plus_one": "Begleitperson",
"status_yes": "Kommt",
"status_no": "Kommt nicht",
"status_open": "Offen",
"yes": "Ja",
"no": "Nein",
"legal_de_authoritative": "Bei rechtlichen Unklarheiten gilt die deutsche Fassung.",
},
"en": {
"brand": "Svenja & Dominic",
"subtitle": "Welcome to our wedding app",
"login_note": "Password-protected access for our guests.",
"login": "Login",
"name": "Your name",
"event_password": "Event password",
"login_submit": "Open dashboard",
"dashboard": "Dashboard",
"login_submit": "Open guest area",
"guest_area": "Guest Area",
"hello_guest": "Hello {name}.",
"logout": "Logout",
"rsvp": "RSVP",
"upload": "Upload",
"gallery": "Gallery",
"host_area": "Host Area",
"info": "Info",
"save": "Save",
"attending": "I will attend",
@@ -89,6 +137,44 @@ TEXTS = {
"taxi": "Taxi",
"location": "Location",
"visit_location": "Visit location website",
"privacy": "Privacy",
"imprint": "Imprint",
"hero_headline": "Welcome to our wedding",
"hero_text": "We are so excited to celebrate this special day with you.",
"to_guest_area": "Open guest area",
"schedule_text": "3:00 PM ceremony, 5:00 PM reception, 7:00 PM dinner.",
"hotels_text": "Recommendations will follow. Please book early.",
"taxi_text": "Taxi service: 01234 / 567890 (24/7).",
"gallery_uploaded_by": "by {name}",
"gallery_empty": "No photos available yet.",
"gallery_image_alt": "Uploaded by {name}",
"flash_enter_name": "Please enter your name.",
"flash_invalid_password": "Invalid event password.",
"flash_rsvp_select": "Please choose an RSVP option.",
"flash_rsvp_saved": "RSVP saved.",
"flash_select_image": "Please select an image file.",
"flash_allowed_types": "Only JPG/JPEG/PNG are allowed.",
"flash_upload_success": "Upload successful.",
"flash_invalid_host_password": "Invalid host password.",
"host_access_title": "Host Area",
"host_access_note": "This section is intended for the wedding hosts only.",
"host_password": "Host password",
"host_access_submit": "Open admin area",
"host_stats_title": "Overview",
"total_guests": "Total guests",
"attending_yes": "Attending",
"attending_no": "Declined",
"attending_open": "Pending",
"plus_one_total": "Plus-ones",
"host_table_name": "Name",
"host_table_status": "RSVP",
"host_table_plus_one": "Plus-one",
"status_yes": "Attending",
"status_no": "Not attending",
"status_open": "Pending",
"yes": "Yes",
"no": "No",
"legal_de_authoritative": "In case of legal ambiguity, the German version shall prevail.",
},
}
@@ -102,6 +188,22 @@ def t(key: str) -> str:
return TEXTS[get_lang()].get(key, key)
def get_hero_image_asset() -> str:
assets_dir = base_dir / "static" / "assets"
configured = app.config.get("HERO_IMAGE_FILENAME")
candidates = []
if configured:
candidates.append(configured)
candidates.extend(["hero.jpg", "hero.jpeg", "hero.png", "image.png", "image-1.png"])
for filename in candidates:
if (assets_dir / filename).is_file():
return f"assets/{filename}"
return "assets/hero.jpg"
@app.context_processor
def inject_common() -> dict:
return {
@@ -112,6 +214,8 @@ def inject_common() -> dict:
"location_address": app.config["LOCATION_ADDRESS"],
"location_website_url": app.config["LOCATION_WEBSITE_URL"],
"google_maps_embed_url": app.config["GOOGLE_MAPS_EMBED_URL"],
"wedding_date": app.config["WEDDING_DATE"],
"hero_image_url": url_for("static", filename=get_hero_image_asset()),
}
@@ -201,7 +305,7 @@ def health():
@app.get("/")
def landing():
if "guest_id" in session:
return redirect(url_for("dashboard"))
return redirect(url_for("welcome"))
return render_template("login.html")
@@ -211,17 +315,18 @@ def login():
event_password = request.form.get("event_password") or ""
if not name:
flash("Bitte Namen eingeben.")
flash(t("flash_enter_name"))
return redirect(url_for("landing"))
if event_password != app.config["EVENT_PASSWORD"]:
flash("Ungültiges Event-Passwort.")
flash(t("flash_invalid_password"))
return redirect(url_for("landing"))
guest_id = upsert_guest(name)
session["guest_id"] = guest_id
session["guest_name"] = name
return redirect(url_for("dashboard"))
session.pop("is_host", None)
return redirect(url_for("welcome"))
@app.post("/logout")
@@ -237,10 +342,67 @@ def set_lang(code: str):
return redirect(request.referrer or url_for("landing"))
@app.get("/welcome")
@login_required
def welcome():
return render_template("welcome.html")
@app.get("/gaestebereich")
@login_required
def guest_area():
return render_template("guest_area.html")
@app.route("/gastgeberbereich", methods=["GET", "POST"])
@login_required
def host_area():
if request.method == "POST":
host_password = request.form.get("host_password") or ""
expected = app.config.get("HOST_PASSWORD", "")
if not expected or not compare_digest(host_password, expected):
flash(t("flash_invalid_host_password"))
return redirect(url_for("host_area"))
session["is_host"] = True
return redirect(url_for("host_area"))
if not session.get("is_host"):
return render_template("host_area.html", unlocked=False)
db = get_db()
stats_row = db.execute(
"""
SELECT
COUNT(*) AS total_guests,
SUM(CASE WHEN attending = 1 THEN 1 ELSE 0 END) AS attending_yes,
SUM(CASE WHEN attending = 0 THEN 1 ELSE 0 END) AS attending_no,
SUM(CASE WHEN attending IS NULL THEN 1 ELSE 0 END) AS attending_open,
SUM(CASE WHEN attending = 1 AND plus_one = 1 THEN 1 ELSE 0 END) AS plus_one_total
FROM guests
"""
).fetchone()
guests = db.execute(
"""
SELECT name, attending, plus_one
FROM guests
ORDER BY name COLLATE NOCASE ASC
"""
).fetchall()
stats = {
"total_guests": int(stats_row["total_guests"] or 0),
"attending_yes": int(stats_row["attending_yes"] or 0),
"attending_no": int(stats_row["attending_no"] or 0),
"attending_open": int(stats_row["attending_open"] or 0),
"plus_one_total": int(stats_row["plus_one_total"] or 0),
}
return render_template("host_area.html", unlocked=True, stats=stats, guests=guests)
@app.get("/dashboard")
@login_required
def dashboard():
return render_template("dashboard.html")
return redirect(url_for("guest_area"))
@app.route("/rsvp", methods=["GET", "POST"])
@@ -253,7 +415,7 @@ def rsvp():
plus_one = 1 if request.form.get("plus_one") == "on" else 0
if attending_raw not in {"yes", "no"}:
flash("Bitte eine RSVP-Auswahl treffen.")
flash(t("flash_rsvp_select"))
return redirect(url_for("rsvp"))
attending = 1 if attending_raw == "yes" else 0
@@ -265,7 +427,7 @@ def rsvp():
(attending, plus_one, session["guest_id"]),
)
db.commit()
flash("RSVP gespeichert.")
flash(t("flash_rsvp_saved"))
return redirect(url_for("rsvp"))
guest = db.execute(
@@ -282,11 +444,11 @@ def upload():
if request.method == "POST":
file = request.files.get("photo")
if file is None or file.filename == "":
flash("Bitte eine Bilddatei auswählen.")
flash(t("flash_select_image"))
return redirect(url_for("upload"))
if not is_allowed_file(file.filename):
flash("Nur JPG/JPEG/PNG sind erlaubt.")
flash(t("flash_allowed_types"))
return redirect(url_for("upload"))
safe_name = secure_filename(file.filename)
@@ -304,7 +466,7 @@ def upload():
)
db.commit()
flash("Upload erfolgreich.")
flash(t("flash_upload_success"))
return redirect(url_for("gallery"))
return render_template("upload.html")
@@ -336,10 +498,20 @@ def serve_upload(filename: str):
def info(page: str):
allowed = {"schedule", "hotels", "taxi", "location"}
if page not in allowed:
return redirect(url_for("dashboard"))
return redirect(url_for("guest_area"))
return render_template("info.html", page=page)
@app.get("/datenschutz")
def datenschutz():
return render_template("datenschutz.html")
@app.get("/impressum")
def impressum():
return render_template("impressum.html")
init_db()
if __name__ == "__main__":

View File

@@ -0,0 +1,11 @@
Place the hero image for the welcome page in this folder.
Auto-detected filenames:
- hero.jpg
- hero.jpeg
- hero.png
- image.png
- image-1.png
Optional override via env:
- HERO_IMAGE_FILENAME=<your-file-name>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -16,6 +16,9 @@ body {
color: var(--ink);
font-family: "Source Sans 3", sans-serif;
background: radial-gradient(circle at top, #fff, var(--cream));
min-height: 100vh;
display: flex;
flex-direction: column;
}
h1,
@@ -55,6 +58,41 @@ h3 {
.container {
width: min(960px, 92vw);
margin: 1.5rem auto 3rem;
flex: 1;
}
.hero-banner {
min-height: 72vh;
border-radius: 24px;
margin-bottom: 1.2rem;
overflow: hidden;
position: relative;
display: flex;
align-items: flex-end;
background-image:
linear-gradient(to top, rgba(20, 30, 22, 0.72), rgba(20, 30, 22, 0.2)),
var(--hero-image);
background-size: cover;
background-position: center 28%;
box-shadow: 0 14px 38px rgba(39, 66, 53, 0.18);
}
.hero-overlay {
width: 100%;
padding: 1.1rem;
color: #fff;
}
.hero-kicker {
margin: 0 0 0.25rem;
letter-spacing: 0.06em;
text-transform: uppercase;
font-size: 0.78rem;
opacity: 0.9;
}
.hero-cta {
margin-top: 0.6rem;
}
.card {
@@ -163,6 +201,45 @@ input[type="file"] {
border-radius: 12px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.8rem;
margin-bottom: 1rem;
}
.stat-card h2 {
margin-bottom: 0.35rem;
font-size: 1rem;
}
.stat-card p {
margin: 0;
font-size: 1.6rem;
font-weight: 700;
color: var(--forest);
}
.table-wrap {
overflow-x: auto;
}
.guest-table {
width: 100%;
border-collapse: collapse;
}
.guest-table th,
.guest-table td {
text-align: left;
padding: 0.65rem 0.55rem;
border-bottom: 1px solid rgba(39, 66, 53, 0.12);
}
.guest-table th {
font-weight: 600;
}
.map-wrap iframe {
width: 100%;
min-height: 320px;
@@ -170,3 +247,39 @@ input[type="file"] {
border-radius: 12px;
margin: 0.8rem 0;
}
.site-footer {
border-top: 1px solid rgba(39, 66, 53, 0.12);
background: rgba(255, 255, 255, 0.78);
backdrop-filter: blur(4px);
padding: 0.9rem 1rem;
display: flex;
gap: 1rem;
justify-content: center;
}
.site-footer a {
color: var(--forest);
text-decoration: none;
font-weight: 600;
}
.site-footer a:hover {
text-decoration: underline;
}
.legal-card h2 {
margin-top: 1.2rem;
}
@media (min-width: 768px) {
.hero-overlay {
max-width: 54ch;
padding: 2rem;
}
.hero-banner {
min-height: 78vh;
background-position: center 24%;
}
}

View File

@@ -12,8 +12,7 @@
<body>
<header class="topbar">
<div>
<a class="brand" href="{{ url_for('dashboard') if guest_name else url_for('landing') }}">{{ t('brand') }}</a>
<div class="host">{{ request.host }}</div>
<a class="brand" href="{{ url_for('welcome') if guest_name else url_for('landing') }}">{{ t('brand') }}</a>
</div>
<div class="toolbar">
<form method="post" action="{{ url_for('set_lang', code='de') }}">
@@ -41,5 +40,10 @@
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
<a href="{{ url_for('datenschutz') }}">{{ t('privacy') }}</a>
<a href="{{ url_for('impressum') }}">{{ t('imprint') }}</a>
</footer>
</body>
</html>

View File

@@ -0,0 +1,225 @@
{% extends 'base.html' %}
{% block content %}
<section class="legal-page card" lang="{{ lang }}">
{% 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><strong>{{ t('legal_de_authoritative') }}</strong></p>
</header>
<article class="legal-section">
<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>
</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>
<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>
<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">
<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>
</article>
{% else %}
<header class="legal-header">
<h1>Datenschutzerklärung</h1>
<p>
In dieser Datenschutzerklärung informieren wir Sie über die Verarbeitung personenbezogener Daten bei der Nutzung
dieser Website.
</p>
</header>
<article class="legal-section" id="verantwortlicher">
<h2>Verantwortlicher</h2>
<p>
Verantwortlich für die Datenverarbeitung ist:<br />
Dominic Thiels, Wiesbadener Straße 70b, 65510 Idstein, +49 151 70616118 und
<a href="mailto:d.thiels@freenet.de">d.thiels@freenet.de</a>
</p>
</article>
<article class="legal-section" id="personenbezogene-daten">
<h2>Personenbezogene Daten</h2>
<p>
Personenbezogene Daten sind alle Informationen, die sich auf eine identifizierte oder identifizierbare natürliche
Person (betroffene Person) beziehen. Als identifizierbar wird eine natürliche Person angesehen, die direkt oder
indirekt, insbesondere mittels Zuordnung zu einer Kennung wie einem Namen, zu einer Kennnummer, zu Standortdaten,
zu einer Online-Kennung oder zu einem oder mehreren besonderen Merkmalen identifiziert werden kann, die Ausdruck
der physischen, physiologischen, genetischen, psychischen, wirtschaftlichen, kulturellen oder sozialen Identität
dieser natürlichen Person sind.
</p>
</article>
<article class="legal-section" id="daten-beim-websiteaufruf">
<h2>Daten beim Websiteaufruf</h2>
<p>
Wenn Sie diese Website nur nutzen, um sich zu informieren und keine Daten angeben, dann verarbeiten wir nur die
Daten, die zur Anzeige der Website auf dem von Ihnen verwendeten internetfähigen Gerät erforderlich sind. Das sind
insbesondere:
</p>
<ul>
<li>IP-Adresse</li>
<li>Datum und Uhrzeit der Anfrage</li>
<li>jeweils übertragene Datenmenge</li>
<li>die Website, von der die Anforderung kommt</li>
<li>Browsertyp und Browserversion</li>
<li>Betriebssystem</li>
</ul>
<p>
Rechtsgrundlage für die Verarbeitung dieser Daten sind berechtigte Interessen gemäß Art. 6 Abs. 1 lit. f DSGVO, um
die Darstellung der Website grundsätzlich zu ermöglichen.
</p>
<p>
Darüber hinaus können Sie verschiedene Leistungen auf der Website nutzen, bei der weitere personenbezogene und
nicht personenbezogene Daten verarbeitet werden.
</p>
</article>
<article class="legal-section" id="rechte-der-betroffenen">
<h2>Ihre Rechte</h2>
<p>Als betroffene Person haben Sie folgende Rechte:</p>
<ul>
<li>Auskunft über die Sie betreffenden personenbezogenen Daten (Art. 15 DSGVO)</li>
<li>Berichtigung unrichtiger oder unvollständiger Daten (Art. 16 DSGVO)</li>
<li>Löschung (Art. 17 DSGVO)</li>
<li>Einschränkung der Verarbeitung (Art. 18 DSGVO)</li>
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
<li>Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)</li>
<li>
Nicht einer ausschließlich automatisierten Entscheidung (einschließlich Profiling) unterworfen zu werden
(Art. 22 DSGVO)
</li>
<li>
Beschwerderecht bei einer Aufsichtsbehörde (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde an Ihrem üblichen
Aufenthaltsort, Arbeitsplatz oder am Ort des vermuteten Verstoßes.
</li>
</ul>
</article>
<article class="legal-section" id="cookies">
<h2>Einsatz von Cookies</h2>
<p>
Beim Besuch der Website können Cookies auf Ihrem Gerät gespeichert werden. Cookies sind kleine Textdateien, die
von dem von Ihnen verwendeten Browser gespeichert werden. Cookies können keine Programme ausführen und auch keine
Viren auf Ihr Gerät übertragen. Die Stelle, die den Cookie setzt, kann darüber jedoch bestimmte Informationen
erhalten. So kann mithilfe von Cookies insbesondere das Gerät, mit dem diese Website aufgerufen wurde, bei einem
erneuten Aufruf erkannt werden.
</p>
<p>
Durch die Browsereinstellungen lässt sich das Setzen von Cookies einschränken oder verhindern. So kann z. B. nur
die Annahme von Cookies, die von Drittanbietern stammen, blockiert werden oder aber auch die Annahme von allen
Cookies. Durch das Blockieren sind jedoch möglicherweise nicht mehr alle Funktionen dieser Website nutzbar. Im
weiteren Text dieser Datenschutzerklärung informieren wir Sie konkret, an welchen Stellen und zu welchen Zwecken
Cookies auf den Seiten zum Einsatz kommen.
</p>
<p>
<strong>
Es werden ausschließlich technisch notwendige Cookies verwendet, die für den Betrieb der Website und die
Aufrechterhaltung der Benutzersitzung erforderlich sind.
</strong>
</p>
</article>
<article class="legal-section" id="google-maps">
<h2>Google Maps</h2>
<p>
Auf dieser Website ist ein Dienst der Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland,
eingebunden, um geographische Informationen visuell darzustellen (Google Maps).
</p>
<p>
Bei der Nutzung von Google Maps werden Informationen über die Nutzung dieser Website einschließlich Ihrer
IP-Adresse an Server von Google übertragen und dort gespeichert. Diese Server können sich auch in den USA
befinden. Google kann diese Informationen gegebenenfalls an Dritte übertragen, sofern dies gesetzlich vorgeschrieben
ist oder soweit Dritte diese Daten im Auftrag von Google verarbeiten.
</p>
<p>
Die Nutzung von Google Maps erfolgt im Interesse einer ansprechenden Darstellung der Veranstaltungsorte sowie einer
leichten Auffindbarkeit der auf der Website angegebenen Orte. Dies stellt ein berechtigtes Interesse im Sinne von
Art. 6 Abs. 1 lit. f DSGVO dar.
</p>
<p>
Sofern eine entsprechende Einwilligung abgefragt wird (z. B. über ein Cookie-Banner), erfolgt die Verarbeitung
ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO. Die Einwilligung kann jederzeit widerrufen werden.
</p>
<p>
Weitere Informationen zur Datenverarbeitung durch Google finden Sie in der Datenschutzerklärung von 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="foto-upload">
<h2>Upload von Fotos durch Nutzer</h2>
<p>
Auf dieser Website besteht die Möglichkeit, Fotos hochzuladen, beispielsweise im Rahmen einer gemeinsamen
Hochzeitsgalerie.
</p>
<p>Beim Hochladen von Fotos werden folgende personenbezogene Daten verarbeitet:</p>
<ul>
<li>die hochgeladene Bilddatei</li>
<li>
gegebenenfalls enthaltene Metadaten (z. B. Aufnahmedatum, Kamerainformationen oder gegebenenfalls
Standortdaten/GPS-Koordinaten)
</li>
<li>der vom Nutzer angegebene Name (sofern vorgesehen)</li>
<li>technische Zugriffsdaten (z. B. IP-Adresse, Zeitpunkt des Uploads)</li>
</ul>
<p>
Die Verarbeitung erfolgt zum Zweck der Bereitstellung einer gemeinsamen Fotogalerie für Gäste der Veranstaltung.
Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Erfüllung der Nutzungsfunktion der Website) sowie Art. 6 Abs. 1
lit. f DSGVO (berechtigtes Interesse an der Bereitstellung einer gemeinsamen Erinnerungsplattform).
</p>
<p>
Es wird darauf hingewiesen, dass hochgeladene Fotos personenbezogene Daten Dritter enthalten können (z. B.
Abbildungen von Personen). Der hochladende Nutzer ist dafür verantwortlich, dass die Veröffentlichung dieser Bilder
datenschutzrechtlich zulässig ist und insbesondere die Einwilligung der abgebildeten Personen vorliegt.
</p>
<p>
Die Fotos werden ausschließlich für den vorgesehenen Zweck gespeichert und nicht an Dritte weitergegeben, sofern
keine gesetzliche Verpflichtung besteht.
</p>
</article>
<footer class="legal-footer">
<p><small>Quelle: Muster-Datenschutzerklärung von anwalt.de</small></p>
</footer>
{% endif %}
</section>
{% endblock %}

View File

@@ -7,14 +7,14 @@
{% for image in images %}
<figure class="gallery-item">
<a href="{{ url_for('serve_upload', filename=image['filename']) }}" target="_blank" rel="noopener">
<img src="{{ url_for('serve_upload', filename=image['filename']) }}" alt="Upload von {{ image['uploaded_by'] }}" loading="lazy" />
<img src="{{ url_for('serve_upload', filename=image['filename']) }}" alt="{{ t('gallery_image_alt').format(name=image['uploaded_by']) }}" loading="lazy" />
</a>
<figcaption>von {{ image['uploaded_by'] }}</figcaption>
<figcaption>{{ t('gallery_uploaded_by').format(name=image['uploaded_by']) }}</figcaption>
</figure>
{% endfor %}
</div>
{% else %}
<p>Noch keine Bilder vorhanden.</p>
<p>{{ t('gallery_empty') }}</p>
{% endif %}
</section>
{% endblock %}

View File

@@ -1,8 +1,8 @@
{% extends 'base.html' %}
{% block content %}
<section class="card">
<h1>{{ t('dashboard') }}</h1>
<p>Hallo {{ guest_name }}.</p>
<h1>{{ t('guest_area') }}</h1>
<p>{{ t('hello_guest').format(name=guest_name) }}</p>
</section>
<section class="card-grid">
@@ -13,5 +13,6 @@
<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>
{% endblock %}

View File

@@ -0,0 +1,80 @@
{% 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>
<p>{{ stats.total_guests }}</p>
</article>
<article class="card stat-card">
<h2>{{ t('attending_yes') }}</h2>
<p>{{ stats.attending_yes }}</p>
</article>
<article class="card stat-card">
<h2>{{ t('attending_no') }}</h2>
<p>{{ stats.attending_no }}</p>
</article>
<article class="card stat-card">
<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">
<h2>{{ t('host_stats_title') }}</h2>
<div class="table-wrap">
<table class="guest-table">
<thead>
<tr>
<th>{{ t('host_table_name') }}</th>
<th>{{ t('host_table_status') }}</th>
<th>{{ t('host_table_plus_one') }}</th>
</tr>
</thead>
<tbody>
{% for guest in guests %}
<tr>
<td>{{ guest["name"] }}</td>
<td>
{% if guest["attending"] == 1 %}
{{ t('status_yes') }}
{% elif guest["attending"] == 0 %}
{{ t('status_no') }}
{% else %}
{{ t('status_open') }}
{% endif %}
</td>
<td>
{% if guest["attending"] == 1 and guest["plus_one"] == 1 %}
{{ t('yes') }}
{% else %}
{{ t('no') }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,63 @@
{% extends 'base.html' %}
{% block content %}
<section class="legal-page card" lang="{{ lang }}">
{% if lang == 'en' %}
<h1>Imprint</h1>
<p>Information according to Section 5 DDG.</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>
<p>Dominic Thiels<br>
<br>
Wiesbadener Straße 70b<br>
65510 Idstein <br>
</p>
<p> <strong>Vertreten durch: </strong><br>
Dominic Thiels<br>
</p>
<p><strong>Kontakt:</strong> <br>
Telefon: +49-151 70616118<br>
E-Mail: <a>d.thiels@freenet.de</a></br></p>
<p><strong>Verbraucherstreitbeilegung / Universalschlichtungsstelle</strong>
<br>Wir nehmen nicht an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teil und sind dazu auch nicht verpflichtet.
</p>
<p><strong>Haftungsausschluss: </strong>
<br><br><strong>Haftung für Inhalte</strong><br>
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs.1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.<br><br><strong>Haftung für Links</strong><br>
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.<br><br><strong>Urheberrecht</strong><br>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.</p>
<p>Erstellt mit dem <a href="https://impressum-generator.de" rel="dofollow">Impressum-Generator</a> von WebsiteWissen.com, dem Ratgeber für <a href="https://websitewissen.com/website-erstellen" rel="dofollow">Website-Erstellung</a>, <a href="https://websitewissen.com/homepage-baukasten-vergleich" rel="dofollow">Homepage-Baukästen</a> und <a href="https://websitewissen.com/shopsysteme-vergleich" rel="dofollow">Shopsysteme</a>. Rechtstext von der <a href="https://www.kanzlei-hasselbach.de/" rel="dofollow">Kanzlei Hasselbach</a>.
</p>
{% endif %}
</section>
{% endblock %}

View File

@@ -9,11 +9,11 @@
</h1>
{% if page == 'schedule' %}
<p>15:00 Trauung, 17:00 Empfang, 19:00 Dinner.</p>
<p>{{ t('schedule_text') }}</p>
{% elif page == 'hotels' %}
<p>Empfehlungen folgen. Bitte frühzeitig buchen.</p>
<p>{{ t('hotels_text') }}</p>
{% elif page == 'taxi' %}
<p>Taxi-Service: 01234 / 567890 (24/7).</p>
<p>{{ t('taxi_text') }}</p>
{% elif page == 'location' %}
<p><strong>{{ location_name }}</strong></p>
<p>{{ location_address }}</p>

View File

@@ -2,7 +2,7 @@
{% block content %}
<section class="hero card">
<h1>{{ t('subtitle') }}</h1>
<p>Passwortgeschützter Zugriff für unsere Gäste.</p>
<p>{{ t('login_note') }}</p>
</section>
<section class="card form-card">

View File

@@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<section class="hero-banner" aria-label="{{ t('hero_headline') }}" style="--hero-image: url('{{ hero_image_url }}');">
<div class="hero-overlay">
{% if wedding_date %}
<p class="hero-kicker">{{ wedding_date }}</p>
{% endif %}
<h1>{{ t('hero_headline') }}</h1>
<p>{{ t('hero_text') }}</p>
<a class="btn hero-cta" href="{{ url_for('guest_area') }}">{{ t('to_guest_area') }}</a>
</div>
</section>
{% endblock %}

Binary file not shown.

View File

@@ -23,4 +23,6 @@ services:
- DB_PATH=/app/db/app.sqlite3
- UPLOAD_FOLDER=/app/uploads
- EVENT_PASSWORD=${EVENT_PASSWORD:-wedding2026}
- HOST_PASSWORD=${HOST_PASSWORD:-gastgeber2026}
- WEDDING_DATE=${WEDDING_DATE:-}
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}