Neue Features>: zu viele um sie zu beschrieben :D
This commit is contained in:
376
AGENTS.md
376
AGENTS.md
@@ -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
406
README.md
@@ -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>
|
||||
|
||||
Binary file not shown.
202
backend/app.py
202
backend/app.py
@@ -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__":
|
||||
|
||||
11
backend/static/assets/README.md
Normal file
11
backend/static/assets/README.md
Normal 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>
|
||||
BIN
backend/static/assets/image-1.png
Normal file
BIN
backend/static/assets/image-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
backend/static/assets/image.png
Normal file
BIN
backend/static/assets/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
225
backend/templates/datenschutz.html
Normal file
225
backend/templates/datenschutz.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
80
backend/templates/host_area.html
Normal file
80
backend/templates/host_area.html
Normal 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 %}
|
||||
63
backend/templates/impressum.html
Normal file
63
backend/templates/impressum.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
13
backend/templates/welcome.html
Normal file
13
backend/templates/welcome.html
Normal 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.
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user