This commit is contained in:
2026-03-05 21:45:40 +00:00
parent 39c80a0253
commit 146a9bda99
16 changed files with 956 additions and 24 deletions

View File

@@ -4,6 +4,7 @@ import uuid
from datetime import datetime
from functools import wraps
from pathlib import Path
from urllib.parse import urlencode
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from flask import (
@@ -209,11 +210,65 @@ DEFAULT_INVITATION_GROUPS = [
]
AGE_REQUIRED_NAMES = {"Lena", "Finn", "Fin", "Bruno"}
HOTEL_RECOMMENDATIONS = [
{
"id": "parkhotel_sonnenberg",
"name": "Parkhotel Sonnenberg",
"address": "Friedrichstraße 65, 65343 Eltville am Rhein",
"website_url": "https://parkhotel-sonnenberg.com/",
"drive_minutes": "5",
"walk_minutes": "27",
},
{
"id": "hotel_frankenbach",
"name": "Hotel Frankenbach",
"address": "Wilhelmstraße 13, 65343 Eltville am Rhein",
"website_url": "https://www.hotel-frankenbach.de/",
"drive_minutes": "5",
"walk_minutes": "19",
},
{
"id": "spritzenhaus",
"name": "Das Spritzenhaus",
"address": "Platz von Montrichard 1, 65343 Eltville am Rhein",
"website_url": "https://www.das-spritzenhaus.de/",
"drive_minutes": "5",
"walk_minutes": "22",
},
{
"id": "kronenschloesschen",
"name": "Hotel Kronenschlösschen",
"address": "Rheinallee, 65347 Eltville-Hattenheim",
"website_url": "https://www.kronenschloesschen.de/",
"drive_minutes": "6",
"walk_minutes": "63",
},
{
"id": "kloster_eberbach",
"name": "Kloster Eberbach",
"address": "Kloster-Eberbach-Straße 1, 65346 Eltville am Rhein",
"website_url": "https://www.kloster-eberbach.de/",
"drive_minutes": "10",
"walk_minutes": "69",
},
{
"id": "weinhaus_engel",
"name": "Weinhaus & Hotel Engel",
"address": "Hauptstraße 10, 65345 Eltville-Rauenthal",
"website_url": "https://weinhaus-engel.com/rauenthal/",
"drive_minutes": "10",
"walk_minutes": "49",
},
]
TEXTS = {
"de": {
"brand": "Svenja & Dominic",
"subtitle": "Willkommen zu unserer Hochzeits-App",
"subtitle": "Willkommen in unserer Hochzeits-App",
"login_note": "Passwortgeschützter Zugriff für unsere Gäste.",
"login_privacy_title": "Datenschutzhinweis",
"login_privacy_text": "Beim Anzeigen eingebetteter Karten werden Daten an Google übertragen. Details findet ihr in der Datenschutzerklärung.",
"login_privacy_accept": "Verstanden",
"login": "Login",
"group_name": "Benutzername",
"group_password": "Passwort",
@@ -247,17 +302,59 @@ TEXTS = {
"location": "Location",
"gifts": "Geschenke",
"visit_location": "Zur Location-Webseite",
"location_story_title": "Klostermühle in den Weinbergen",
"location_story_text": "Die Klostermühle liegt mitten in den Weinbergen und verbindet ein besonderes Weingut-Ambiente mit saisonaler Küche. Euch erwartet eine entspannte Atmosphäre zwischen Natur, gutem Essen und einem wunderschönen Rahmen für unseren Hochzeitstag.",
"route_from_current": "Route ab aktuellem Standort",
"maps_privacy_notice": "Zur Anzeige der Karte werden Daten an Google übertragen.",
"maps_load_button": "Google Maps anzeigen",
"privacy": "Datenschutz",
"imprint": "Impressum",
"hero_headline": "Willkommen zu unserer Hochzeit",
"hero_text": "Wir freuen uns riesig, diesen besonderen Tag mit euch zu feiern.",
"hero_text": "Wir freuen uns riesig, diesen besonderen Tag am 04.09.26 zu feiern.",
"hero_headline_with_group": "Willkommen zu unserer Hochzeit, {name}",
"hero_text_single": "Wir freuen uns riesig, diesen besonderen Tag am 04.09.26 mit dir zu feiern.",
"hero_text_group": "Wir freuen uns riesig, diesen besonderen Tag am 04.09.26 mit euch zu feiern.",
"hero_app_hint": "In dieser Webapp könnt ihr Zu- oder Absagen, Fotos hochladen, die Galerie ansehen und alle wichtigen Infos rund um den Tag finden. Bitte gebt eure Zu- oder Absage möglichst bald ab.",
"to_guest_area": "Zum Gästebereich",
"schedule_text": "15:00 Trauung, 17:00 Empfang, 19:00 Dinner.",
"schedule_intro": "Hier findet ihr den Ablauf für unseren Hochzeitstag.",
"schedule_arrival_title": "Ankunft",
"schedule_arrival_text": "Wir freuen uns auf eure Ankunft.",
"schedule_ceremony_title": "Trauzeremonie",
"schedule_ceremony_text": "Trauzeremonie im Rosengarten und Fotos.",
"schedule_reception_title": "Sektempfang & Häppchen",
"schedule_reception_text": "Sektempfang im Hof der Klostermühle.",
"schedule_buffet_title": "Buffet-Eröffnung",
"schedule_buffet_text": "Das Buffet wird eröffnet, danach folgen die Brautpaarspiele. Später gibt es zusätzlich Käseplatten.",
"schedule_cake_title": "Tortenanschnitt",
"schedule_cake_text": "Gemeinsamer Tortenmoment am Abend.",
"schedule_afterwards_time": "Anschließend",
"schedule_party_title": "Party",
"schedule_party_text": "Wir feiern gemeinsam bis 03:00 Uhr.",
"hotels_text": "Empfehlungen folgen. Bitte frühzeitig buchen.",
"taxi_text": "Taxi-Service: 01234 / 567890 (24/7).",
"gifts_text": "Eure Anwesenheit ist unser schönstes Geschenk. Falls ihr uns trotzdem etwas schenken möchtet, freuen wir uns über einen Beitrag zu unserer Reise nach der Hochzeit.",
"hotels_intro": "Hier findet ihr Hotels in der Nähe. Bitte bucht frühzeitig und schaut euch die aktuellen Preise direkt auf den Hotel-Webseiten an.",
"hotel_visit_website": "Zur Hotel-Webseite",
"hotel_route_drive": "Route zur Location (Auto)",
"hotel_route_walk": "Route zur Location (zu Fuß)",
"hotel_time_note": "Genaue Fahr- und Gehzeit seht ihr live in Google Maps.",
"hotel_route_modal_title": "Route zur Location",
"hotel_route_modal_close": "Karte schließen",
"hotel_route_open_maps": "In Google Maps öffnen",
"hotel_drive_badge": "Auto ca. {minutes} Min",
"hotel_walk_badge": "Zu Fuß ca. {minutes} Min",
"hotel_parkhotel_sonnenberg_desc": "Ruhiges Hotel mit Blick über Eltville.",
"hotel_hotel_frankenbach_desc": "Zentral in Eltville, gute Anbindung zur Location.",
"hotel_spritzenhaus_desc": "Historisches Haus am Rhein mit modernen Zimmern.",
"hotel_kronenschloesschen_desc": "Elegantes Hotel in Hattenheim, nah an den Weinbergen.",
"hotel_kloster_eberbach_desc": "Besonderes Ambiente am Kloster mit kurzer Fahrzeit.",
"hotel_weinhaus_engel_desc": "Gemütliches Weinhaus-Hotel in Rauenthal.",
"taxi_text": "An einem Taxiservice arbeiten wir noch.",
"taxi_sticker_alt": "Bauarbeiter-Sticker: Arbeit in Progress",
"gifts_teaser": "Wir würden uns über Geschenke sehr freuen...",
"gifts_reveal_button": "Wunsch aufdecken",
"gifts_image_alt": "Ein großer Haufen Geldscheine",
"gifts_caption": "Trommelwirbel... in unserer 70-Quadratmeter-Wohnung ist leider kein Platz mehr für Materielles.",
"gifts_text": "Also: Money! Wir freuen uns über einen finanziellen Beitrag zu unserer Reise nach der Hochzeit.",
"gallery_uploaded_by": "von {name}",
"gallery_empty": "Noch keine Bilder vorhanden.",
"gallery_image_alt": "Upload von {name}",
@@ -305,6 +402,9 @@ TEXTS = {
"brand": "Svenja & Dominic",
"subtitle": "Welcome to our wedding app",
"login_note": "Password-protected access for our guests.",
"login_privacy_title": "Privacy notice",
"login_privacy_text": "When embedded maps are displayed, data is transferred to Google. Details are available in the privacy policy.",
"login_privacy_accept": "Got it",
"login": "Login",
"group_name": "Username",
"group_password": "Password",
@@ -338,17 +438,59 @@ TEXTS = {
"location": "Location",
"gifts": "Gifts",
"visit_location": "Visit location website",
"location_story_title": "Klostermuehle in the vineyards",
"location_story_text": "Klostermuehle is surrounded by vineyards and combines a unique winery atmosphere with seasonal cuisine. Expect a relaxed setting with nature, great food, and a beautiful backdrop for our wedding day.",
"route_from_current": "Route from current location",
"maps_privacy_notice": "To display the map, data will be transferred to Google.",
"maps_load_button": "Show Google Maps",
"privacy": "Privacy",
"imprint": "Imprint",
"hero_headline": "Welcome to our wedding",
"hero_text": "We are so excited to celebrate this special day with you.",
"hero_text": "We are incredibly excited to celebrate this special day on 09/04/26.",
"hero_headline_with_group": "Welcome to our wedding, {name}",
"hero_text_single": "We are incredibly excited to celebrate this special day on 09/04/26 with you.",
"hero_text_group": "We are incredibly excited to celebrate this special day on 09/04/26 with all of you.",
"hero_app_hint": "In this web app, you can send your RSVP, upload photos, view the gallery, and find all important details for the day. Please submit your RSVP soon.",
"to_guest_area": "Open guest area",
"schedule_text": "3:00 PM ceremony, 5:00 PM reception, 7:00 PM dinner.",
"schedule_intro": "Here is the schedule for our wedding day.",
"schedule_arrival_title": "Arrival",
"schedule_arrival_text": "We look forward to welcoming you.",
"schedule_ceremony_title": "Wedding Ceremony",
"schedule_ceremony_text": "Wedding ceremony in the rose garden and photos.",
"schedule_reception_title": "Sparkling Reception & Snacks",
"schedule_reception_text": "Sparkling reception in the courtyard of the Klostermuehle.",
"schedule_buffet_title": "Buffet Opening",
"schedule_buffet_text": "The buffet opens, followed by wedding games. Cheese platters will be served later.",
"schedule_cake_title": "Cake Cutting",
"schedule_cake_text": "Cake cutting together in the evening.",
"schedule_afterwards_time": "Afterwards",
"schedule_party_title": "Party",
"schedule_party_text": "We celebrate together until 3:00 AM.",
"hotels_text": "Recommendations will follow. Please book early.",
"taxi_text": "Taxi service: 01234 / 567890 (24/7).",
"gifts_text": "Your presence is the greatest gift to us. If you would still like to give something, we would appreciate a contribution to our post-wedding trip.",
"hotels_intro": "Here are hotel options nearby. Please book early and check current rates directly on each hotel website.",
"hotel_visit_website": "Visit hotel website",
"hotel_route_drive": "Route to location (car)",
"hotel_route_walk": "Route to location (walk)",
"hotel_time_note": "Exact driving and walking times are shown live in Google Maps.",
"hotel_route_modal_title": "Route to location",
"hotel_route_modal_close": "Close map",
"hotel_route_open_maps": "Open in Google Maps",
"hotel_drive_badge": "By car about {minutes} min",
"hotel_walk_badge": "On foot about {minutes} min",
"hotel_parkhotel_sonnenberg_desc": "Quiet hotel with a view over Eltville.",
"hotel_hotel_frankenbach_desc": "Central in Eltville with good access to the venue.",
"hotel_spritzenhaus_desc": "Historic house by the Rhine with modern rooms.",
"hotel_kronenschloesschen_desc": "Elegant hotel in Hattenheim near the vineyards.",
"hotel_kloster_eberbach_desc": "Unique monastery setting with a short drive to the venue.",
"hotel_weinhaus_engel_desc": "Cozy winehouse hotel in Rauenthal.",
"taxi_text": "We are still working on a taxi service.",
"taxi_sticker_alt": "Construction worker sticker: Work in progress",
"gifts_teaser": "We would be very happy to receive gifts...",
"gifts_reveal_button": "Reveal wish",
"gifts_image_alt": "A big pile of cash",
"gifts_caption": "Drum roll... in our 70-square-meter apartment, we sadly have no room left for material things.",
"gifts_text": "So: Money! We would be very happy about a financial contribution to our trip after the wedding.",
"gallery_uploaded_by": "by {name}",
"gallery_empty": "No photos available yet.",
"gallery_image_alt": "Uploaded by {name}",
@@ -463,6 +605,62 @@ def get_wedding_countdown_iso() -> str:
return parsed_local.replace(tzinfo=tz).isoformat()
def build_google_maps_directions(origin: str, travel_mode: str) -> str:
query = urlencode(
{
"api": "1",
"origin": origin,
"destination": app.config["LOCATION_ADDRESS"],
"travelmode": travel_mode,
}
)
return f"https://www.google.com/maps/dir/?{query}"
def build_google_maps_destination(destination: str, travel_mode: str) -> str:
query = urlencode(
{
"api": "1",
"destination": destination,
"travelmode": travel_mode,
}
)
return f"https://www.google.com/maps/dir/?{query}"
def build_google_maps_route_embed(origin: str, travel_mode: str) -> str:
mode_flag = "d" if travel_mode == "driving" else "w"
query = urlencode(
{
"saddr": origin,
"daddr": app.config["LOCATION_ADDRESS"],
"dirflg": mode_flag,
"output": "embed",
}
)
return f"https://www.google.com/maps?{query}"
def get_hotels_for_page() -> list[dict]:
hotels = []
for entry in HOTEL_RECOMMENDATIONS:
hotels.append(
{
"name": entry["name"],
"address": entry["address"],
"website_url": entry["website_url"],
"description": t(f"hotel_{entry['id']}_desc"),
"drive_badge": t("hotel_drive_badge").format(minutes=entry["drive_minutes"]),
"walk_badge": t("hotel_walk_badge").format(minutes=entry["walk_minutes"]),
"drive_route_url": build_google_maps_directions(entry["address"], "driving"),
"walk_route_url": build_google_maps_directions(entry["address"], "walking"),
"drive_route_embed_url": build_google_maps_route_embed(entry["address"], "driving"),
"walk_route_embed_url": build_google_maps_route_embed(entry["address"], "walking"),
}
)
return hotels
@app.context_processor
def inject_common() -> dict:
role = session.get("role", "guest")
@@ -479,6 +677,7 @@ def inject_common() -> dict:
"location_name": app.config["LOCATION_NAME"],
"location_address": app.config["LOCATION_ADDRESS"],
"location_website_url": app.config["LOCATION_WEBSITE_URL"],
"location_route_url": build_google_maps_destination(app.config["LOCATION_ADDRESS"], "driving"),
"google_maps_embed_url": app.config["GOOGLE_MAPS_EMBED_URL"],
"wedding_date": app.config["WEDDING_DATE"],
"wedding_countdown_iso": get_wedding_countdown_iso(),
@@ -693,7 +892,21 @@ def set_lang(code: str):
@app.get("/welcome")
@login_required
def welcome():
return render_template("welcome.html")
db = get_db()
member_count_row = db.execute(
"SELECT COUNT(*) AS c FROM group_members WHERE group_id = ?",
(int(session["group_id"]),),
).fetchone()
member_count = int((member_count_row["c"] if member_count_row else 0) or 0)
is_single_guest = member_count <= 1
welcome_headline = t("hero_headline_with_group").format(name=session.get("group_name", ""))
welcome_text = t("hero_text_single") if is_single_guest else t("hero_text_group")
return render_template(
"welcome.html",
welcome_headline=welcome_headline,
welcome_text=welcome_text,
welcome_hint=t("hero_app_hint"),
)
@app.get("/gaestebereich")
@@ -918,7 +1131,8 @@ def info(page: str):
return redirect(url_for("guest_area"))
if page == "gifts" and session.get("role") == "admin":
return redirect(url_for("guest_area"))
return render_template("info.html", page=page)
hotels = get_hotels_for_page() if page == "hotels" else []
return render_template("info.html", page=page, hotels=hotels)
@app.get("/datenschutz")