15:00
+{{ t('schedule_arrival_title') }}
+{{ t('schedule_arrival_text') }}
+diff --git a/backend/Dockerfile b/backend/Dockerfile index 11b8103..4381c8a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,4 +10,4 @@ RUN uv sync --frozen --no-dev COPY . . EXPOSE 8000 -CMD ["uv", "run", "gunicorn", "-b", "0.0.0.0:8000", "--timeout", "300", "--graceful-timeout", "30", "--keep-alive", "5", "app:app"] +CMD ["uv", "run", "gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--timeout", "300", "--graceful-timeout", "30", "--keep-alive", "5", "--error-logfile", "-", "--access-logfile", "-", "app:app"] diff --git a/backend/__pycache__/app.cpython-312.pyc b/backend/__pycache__/app.cpython-312.pyc index 25b9c2e..2afa598 100644 Binary files a/backend/__pycache__/app.cpython-312.pyc and b/backend/__pycache__/app.cpython-312.pyc differ diff --git a/backend/app.py b/backend/app.py index 277e7a7..881eb46 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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") diff --git a/backend/static/assets/ankunft.png b/backend/static/assets/ankunft.png new file mode 100644 index 0000000..b38b886 Binary files /dev/null and b/backend/static/assets/ankunft.png differ diff --git a/backend/static/assets/bauarbeiter-sticker.png b/backend/static/assets/bauarbeiter-sticker.png new file mode 100644 index 0000000..2b08ba3 Binary files /dev/null and b/backend/static/assets/bauarbeiter-sticker.png differ diff --git a/backend/static/assets/buffet.png b/backend/static/assets/buffet.png new file mode 100644 index 0000000..2aeda02 Binary files /dev/null and b/backend/static/assets/buffet.png differ diff --git a/backend/static/assets/kuchen.png b/backend/static/assets/kuchen.png new file mode 100644 index 0000000..76f95de Binary files /dev/null and b/backend/static/assets/kuchen.png differ diff --git a/backend/static/assets/money-pile.svg b/backend/static/assets/money-pile.svg new file mode 100644 index 0000000..7848c48 --- /dev/null +++ b/backend/static/assets/money-pile.svg @@ -0,0 +1,40 @@ + diff --git a/backend/static/assets/party.png b/backend/static/assets/party.png new file mode 100644 index 0000000..b851a45 Binary files /dev/null and b/backend/static/assets/party.png differ diff --git a/backend/static/assets/sektempfang.png b/backend/static/assets/sektempfang.png new file mode 100644 index 0000000..1a828cf Binary files /dev/null and b/backend/static/assets/sektempfang.png differ diff --git a/backend/static/assets/trauzermonie.png b/backend/static/assets/trauzermonie.png new file mode 100644 index 0000000..66a1d3f Binary files /dev/null and b/backend/static/assets/trauzermonie.png differ diff --git a/backend/static/styles.css b/backend/static/styles.css index 51e6d68..cb64da8 100644 --- a/backend/static/styles.css +++ b/backend/static/styles.css @@ -101,6 +101,7 @@ h3 { width: 100%; padding: 1.1rem; color: #fff; + text-align: center; } .hero-overlay h1, @@ -120,6 +121,22 @@ h3 { margin-top: 0.6rem; } +.hero-hint { + margin-top: 0.8rem; + max-width: 58ch; + font-size: clamp(0.94rem, 1.55vw, 1.05rem); + color: rgba(255, 255, 255, 0.93); +} + +.hero-hint-below { + margin: 0.2rem 0 1rem; + max-width: 72ch; + text-align: center; + color: rgba(31, 26, 23, 0.92); + font-size: clamp(0.97rem, 1.45vw, 1.05rem); + line-height: 1.45; +} + .card { background: var(--card); border: 1px solid rgba(31, 58, 47, 0.11); @@ -131,6 +148,13 @@ h3 { margin-bottom: 1rem; } +/* Center standalone copy blocks while keeping forms and tables unaffected */ +section.card > h1, +section.card > h2, +section.card > p { + text-align: center; +} + .hero { background: radial-gradient(circle at 12% 16%, rgba(184, 145, 76, 0.16), transparent 44%), @@ -224,6 +248,10 @@ h3 { padding: clamp(1.2rem, 2.6vw, 1.7rem); } +.login-intro { + text-align: center; +} + .login-intro h1 { margin-bottom: 0.75rem; font-size: clamp(2rem, 4.2vw, 3.3rem); @@ -294,6 +322,48 @@ h3 { 0 1px 0 rgba(178, 137, 70, 0.4) inset; } +.login-privacy-banner { + position: fixed; + left: 50%; + bottom: 0.9rem; + transform: translateX(-50%); + width: min(760px, calc(100vw - 1.2rem)); + z-index: 1250; + border: 1px solid rgba(31, 58, 47, 0.16); + border-radius: 14px; + padding: 0.8rem 0.85rem; + background: rgba(255, 252, 247, 0.98); + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22); + backdrop-filter: blur(4px); +} + +.login-privacy-title { + margin: 0; + color: var(--forest); + font-weight: 700; + font-size: 0.95rem; +} + +.login-privacy-text { + margin: 0.32rem 0 0; + font-size: 0.9rem; + line-height: 1.35; + color: rgba(31, 26, 23, 0.92); +} + +.login-privacy-text a { + color: var(--forest); + font-weight: 700; +} + +.login-privacy-btn { + margin-top: 0.6rem; + width: auto; + min-width: 10rem; + border-radius: 999px; + padding: 0.6rem 1.05rem; +} + .form-grid { display: grid; gap: 0.8rem; @@ -902,6 +972,322 @@ input[type="file"]:focus { margin: 0.8rem 0; } +.schedule-timeline { + display: grid; + gap: 0.85rem; + margin-top: 0.9rem; +} + +.schedule-item { + position: relative; + overflow: hidden; + border-radius: 14px; + padding: 1rem; + border: 1px solid rgba(31, 58, 47, 0.12); + background-color: #f5ece0; + background-image: + linear-gradient(180deg, rgba(22, 34, 28, 0.5), rgba(22, 34, 28, 0.28)), + var(--schedule-card-image); + background-size: cover; + background-position: var(--schedule-card-position, center); + color: #fff; + box-shadow: + 0 10px 24px rgba(31, 58, 47, 0.15), + 0 1px 0 rgba(255, 255, 255, 0.25) inset; +} + +.schedule-time { + margin: 0 0 0.2rem; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(255, 247, 230, 0.95); +} + +.schedule-item h2 { + margin: 0; + font-size: 1.28rem; + line-height: 1.15; + color: #fff; +} + +.schedule-item p { + margin: 0.45rem 0 0; + max-width: 42ch; + color: rgba(255, 255, 255, 0.95); +} + +.hotel-list { + display: grid; + gap: 0.9rem; + margin-top: 0.8rem; +} + +.hotel-card { + border-radius: 14px; + border: 1px solid rgba(31, 58, 47, 0.12); + padding: 1rem; + text-align: center; + background: linear-gradient(180deg, #fffdf8 0%, #fdf5e8 100%); + box-shadow: + 0 10px 22px rgba(31, 58, 47, 0.12), + 0 1px 0 rgba(178, 137, 70, 0.2) inset; +} + +.hotel-card h2 { + margin: 0; + font-size: 1.25rem; + color: var(--forest); +} + +.hotel-address { + margin: 0.32rem 0 0; + color: rgba(31, 58, 47, 0.84); + font-size: 0.95rem; +} + +.hotel-card p { + margin: 0.52rem 0 0; +} + +.hotel-badges { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-top: 0.62rem; + justify-content: center; +} + +.hotel-badges span { + border-radius: 999px; + border: 1px solid rgba(31, 58, 47, 0.18); + background: rgba(255, 255, 255, 0.9); + padding: 0.26rem 0.62rem; + font-size: 0.86rem; + color: rgba(31, 58, 47, 0.9); +} + +.hotel-actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 0.55rem; + margin-top: 0.8rem; + justify-items: center; +} + +.hotel-actions .btn { + width: min(100%, 320px); + border-radius: 999px; + padding: 0.74rem 1.2rem; +} + +body.has-route-modal { + overflow: hidden; +} + +.route-modal[hidden] { + display: none; +} + +.route-modal { + position: fixed; + inset: 0; + z-index: 1300; + display: grid; + place-items: center; + padding: 1rem; +} + +.route-modal-backdrop { + position: absolute; + inset: 0; + background: rgba(18, 28, 23, 0.62); + backdrop-filter: blur(2px); +} + +.route-modal-panel { + position: relative; + width: min(980px, 96vw); + border-radius: 16px; + overflow: hidden; + background: #fffdf8; + border: 1px solid rgba(31, 58, 47, 0.18); + box-shadow: 0 20px 44px rgba(0, 0, 0, 0.28); +} + +.route-modal-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + padding: 0.75rem 0.85rem; + border-bottom: 1px solid rgba(31, 58, 47, 0.12); + background: linear-gradient(180deg, #fff, #fdf4e6); +} + +.route-modal-head h2 { + margin: 0; + font-size: 1.04rem; + color: var(--forest); +} + +.route-modal-close { + white-space: nowrap; + font-size: 0.85rem; + padding: 0.42rem 0.76rem; + border-radius: 999px; +} + +.route-modal-close.btn-ghost { + background: rgba(255, 255, 255, 0.56); + border-color: rgba(31, 58, 47, 0.2); + box-shadow: none; +} + +.route-modal-body { + padding: 0.65rem; + position: relative; +} + +.route-modal-body iframe { + display: block; + width: 100%; + min-height: min(72vh, 640px); + border: 0; + border-radius: 12px; +} + +.route-map-actions { + position: absolute; + right: 1.15rem; + bottom: 1.15rem; +} + +.route-open-btn { + min-width: clamp(220px, 38vw, 320px); + border-radius: 999px; + padding: 0.78rem 1.25rem; + box-shadow: 0 10px 22px rgba(31, 58, 47, 0.24); +} + +.taxi-coming-soon { + margin: 0.3rem auto 0.9rem; + max-width: min(460px, 100%); +} + +.taxi-coming-soon img { + display: block; + width: 100%; + height: auto; + object-fit: contain; + filter: drop-shadow(0 10px 22px rgba(31, 58, 47, 0.18)); +} + +.gift-fun { + position: relative; + overflow: hidden; + border-radius: 14px; + border: 1px solid rgba(31, 58, 47, 0.12); + padding: 1rem; + background: linear-gradient(155deg, #fff8eb 0%, #f3e6cc 100%); +} + +.gift-lead { + margin: 0; + font-size: clamp(1.02rem, 2.1vw, 1.2rem); + font-weight: 700; +} + +.gift-reveal-btn { + margin-top: 0.85rem; + width: auto; +} + +.gift-reveal { + margin-top: 0.95rem; + position: relative; + padding: 0.9rem; + border-radius: 12px; + border: 1px solid rgba(31, 58, 47, 0.15); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.94)); +} + +.gift-reveal.is-active { + animation: gift-pop-in 420ms cubic-bezier(0.2, 0.75, 0.2, 1) both; +} + +.gift-image-wrap { + max-width: min(640px, 100%); + margin: 0 auto; +} + +.gift-image-wrap img { + display: block; + width: 100%; + height: auto; + border-radius: 12px; + border: 1px solid rgba(31, 58, 47, 0.2); + box-shadow: 0 12px 28px rgba(31, 58, 47, 0.18); +} + +.gift-caption { + margin: 0.8rem 0 0; + font-weight: 700; + color: var(--forest); +} + +.money-rain { + pointer-events: none; + position: absolute; + inset: -6px; + overflow: hidden; +} + +.bill { + position: absolute; + width: 26px; + height: 12px; + top: -20px; + left: calc((var(--idx) * 8.2%) - 2%); + border-radius: 2px; + border: 1px solid rgba(43, 107, 49, 0.46); + background: + radial-gradient(circle at 50% 50%, rgba(247, 255, 240, 0.92) 0 28%, transparent 30%), + linear-gradient(180deg, #d7f7bd 0%, #9fd58f 100%); + opacity: 0; +} + +.gift-reveal.is-active .bill { + animation: money-fall 4200ms linear forwards; + animation-delay: calc(var(--idx) * 120ms); +} + +@keyframes gift-pop-in { + 0% { + opacity: 0; + transform: translateY(8px) scale(0.98); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes money-fall { + 0% { + opacity: 0; + transform: translate3d(0, -12px, 0) rotate(0deg); + } + 10% { + opacity: 0.95; + } + 100% { + opacity: 0; + transform: translate3d(-22px, 240px, 0) rotate(30deg); + } +} + .map-consent { display: grid; gap: 0.7rem; @@ -953,23 +1339,24 @@ input[type="file"]:focus { } .map-embed-target:not(:empty) { - margin-top: 0.2rem; + margin-top: 0.35rem; } .location-actions { display: flex; - justify-content: flex-start; + justify-content: center; margin-top: 0.55rem; gap: 0.55rem; flex-wrap: wrap; } .location-actions .btn { - width: auto; - min-width: 0; + width: min(100%, 320px); + min-width: 240px; white-space: nowrap; justify-content: center; - padding: 0.58rem 0.88rem; + padding: 0.72rem 1.2rem; + border-radius: 999px; font-size: 0.98rem; } @@ -988,8 +1375,19 @@ input[type="file"]:focus { font-size: 0.9rem; } + .login-privacy-banner { + width: calc(100vw - 0.8rem); + bottom: 0.35rem; + border-radius: 12px; + padding: 0.7rem; + } + + .login-privacy-btn { + width: 100%; + } + .location-actions { - justify-content: flex-start; + justify-content: center; } .dashboard-grid { @@ -1000,6 +1398,56 @@ input[type="file"]:focus { min-height: 0; font-size: 1.08rem; } + + .schedule-item { + padding: 0.9rem; + } + + .schedule-item h2 { + font-size: 1.18rem; + } + + .gift-reveal { + padding: 0.75rem; + } + + .route-modal { + padding: 0.6rem; + } + + .route-modal-head { + align-items: center; + flex-direction: row; + } + + .route-modal-close { + width: auto; + align-self: center; + } + + .route-modal-body iframe { + min-height: 58vh; + } + + .route-map-actions { + left: 1.15rem; + right: 1.15rem; + } + + .route-open-btn { + width: 100%; + } +} + +@media (prefers-reduced-motion: reduce) { + .gift-reveal.is-active { + animation: none; + } + + .gift-reveal.is-active .bill { + animation: none; + opacity: 0; + } } @media (max-width: 380px) { diff --git a/backend/templates/info.html b/backend/templates/info.html index 6953b0d..6613115 100644 --- a/backend/templates/info.html +++ b/backend/templates/info.html @@ -10,18 +10,188 @@ {% if page == 'schedule' %} -
{{ t('schedule_text') }}
+{{ t('schedule_intro') }}
+15:00
+{{ t('schedule_arrival_text') }}
+15:30
+{{ t('schedule_ceremony_text') }}
+16:00
+{{ t('schedule_reception_text') }}
+18:00
+{{ t('schedule_buffet_text') }}
+22:00
+{{ t('schedule_cake_text') }}
+{{ t('schedule_afterwards_time') }}
+{{ t('schedule_party_text') }}
+{{ t('hotels_text') }}
+{{ t('hotels_intro') }}
+{{ hotel.address }}
+{{ hotel.description }}
+
+ {{ t('taxi_text') }}
{% elif page == 'gifts' %} -{{ t('gifts_text') }}
+{{ t('gifts_teaser') }}
+ +{{ t('gifts_caption') }}
+{{ t('gifts_text') }}
+ +{{ location_name }}
-{{ location_address }}
+{{ t('location_story_text') }}
@@ -46,6 +217,8 @@ const previewButton = wrapper.querySelector(".map-preview"); const target = wrapper.querySelector("[data-map-embed-target]"); const src = {{ google_maps_embed_url|tojson }}; + const destination = {{ location_address|tojson }}; + const liveRouteLink = wrapper.querySelector("[data-location-route-live]"); let loaded = false; const loadMap = () => { @@ -66,6 +239,38 @@ loadButtons.forEach((button) => { button.addEventListener("click", loadMap); }); + + if (liveRouteLink) { + liveRouteLink.addEventListener("click", (event) => { + event.preventDefault(); + const fallback = liveRouteLink.getAttribute("href"); + const openFallback = () => { + if (fallback) { + window.open(fallback, "_blank", "noopener"); + } + }; + + if (!navigator.geolocation) { + openFallback(); + return; + } + + navigator.geolocation.getCurrentPosition( + ({ coords }) => { + const origin = `${coords.latitude},${coords.longitude}`; + const query = new URLSearchParams({ + api: "1", + origin, + destination, + travelmode: "driving", + }); + window.open(`https://www.google.com/maps/dir/?${query.toString()}`, "_blank", "noopener"); + }, + () => openFallback(), + { enableHighAccuracy: true, timeout: 4500, maximumAge: 120000 } + ); + }); + } })(); {% endif %} diff --git a/backend/templates/login.html b/backend/templates/login.html index ade2942..4e7e932 100644 --- a/backend/templates/login.html +++ b/backend/templates/login.html @@ -23,4 +23,28 @@ + + {% endblock %} diff --git a/backend/templates/welcome.html b/backend/templates/welcome.html index cf426ab..d1881fe 100644 --- a/backend/templates/welcome.html +++ b/backend/templates/welcome.html @@ -5,9 +5,10 @@ {% if wedding_date %}{{ wedding_date }}
{% endif %} -{{ t('hero_text') }}
+{{ welcome_text or t('hero_text') }}
{{ t('to_guest_area') }} +{{ welcome_hint }}
{% endblock %} diff --git a/data/db/app.sqlite3 b/data/db/app.sqlite3 index 48169b8..52c0d04 100644 Binary files a/data/db/app.sqlite3 and b/data/db/app.sqlite3 differ