diff --git a/backend/app.py b/backend/app.py index a75001c..a7e0c3f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -28,12 +28,12 @@ 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["MAX_CONTENT_LENGTH"] = int(os.environ.get("MAX_UPLOAD_BYTES", str(64 * 1024 * 1024))) -app.config["LOCATION_NAME"] = os.environ.get("LOCATION_NAME", "Schlossgarten Venue") -app.config["LOCATION_ADDRESS"] = os.environ.get("LOCATION_ADDRESS", "Musterstraße 12, 12345 Musterstadt") -app.config["LOCATION_WEBSITE_URL"] = os.environ.get("LOCATION_WEBSITE_URL", "https://example.com/location") +app.config["LOCATION_NAME"] = os.environ.get("LOCATION_NAME", "Hochheimer Terrasse") +app.config["LOCATION_ADDRESS"] = os.environ.get("LOCATION_ADDRESS", "Mainzer Str. 22, 65239 Hochheim am Main") +app.config["LOCATION_WEBSITE_URL"] = os.environ.get("LOCATION_WEBSITE_URL", "https://hochheimerterrasse.de/") app.config["GOOGLE_MAPS_EMBED_URL"] = os.environ.get( "GOOGLE_MAPS_EMBED_URL", - "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d0", + "https://www.google.com/maps?q=Hochheimer+Terrasse+Mainzer+Str.+22+65239+Hochheim+am+Main&output=embed", ) app.config["WEDDING_DATE"] = os.environ.get("WEDDING_DATE", "") app.config["WEDDING_COUNTDOWN_ISO"] = os.environ.get("WEDDING_COUNTDOWN_ISO", "") @@ -52,6 +52,8 @@ ALLOWED_MIME_TYPES = { "image/heif-sequence", } +LOCATION_VIDEO_EXTENSIONS = {".mp4", ".webm", ".mov", ".m4v"} + DEFAULT_INVITATION_GROUPS = [ { "name": "Bubus", @@ -218,48 +220,48 @@ HOTEL_RECOMMENDATIONS = [ "name": "Parkhotel Sonnenberg", "address": "Friedrichstraße 65, 65343 Eltville am Rhein", "website_url": "https://parkhotel-sonnenberg.com/", - "drive_minutes": "5", - "walk_minutes": "27", + "drive_minutes": "25", + "walk_minutes": None, }, { "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", + "drive_minutes": "25", + "walk_minutes": None, }, { "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", + "drive_minutes": "25", + "walk_minutes": None, }, { "id": "kronenschloesschen", "name": "Hotel Kronenschlösschen", "address": "Rheinallee, 65347 Eltville-Hattenheim", "website_url": "https://www.kronenschloesschen.de/", - "drive_minutes": "6", - "walk_minutes": "63", + "drive_minutes": "30", + "walk_minutes": None, }, { "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", + "drive_minutes": "30", + "walk_minutes": None, }, { "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", + "drive_minutes": "25", + "walk_minutes": None, }, ] @@ -306,12 +308,12 @@ 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.", + "location_story_title": "Rheingauer Flair auf der Hochheimer Terrasse", + "location_story_text": "Die Hochheimer Terrasse liegt in der historischen Altstadt von Hochheim und verbindet Rheingauer Atmosphäre mit einem eleganten Rahmen für unseren Hochzeitstag. Euch erwartet ein besonderer Ort zwischen Mainz, Wiesbaden und den Weinbergen, an dem wir gemeinsam feiern möchten.", + "location_map_intro": "Damit ihr problemlos zur Location findet, könnt ihr direkt von hier zu Google Maps wechseln oder eine Route von eurem aktuellen Standort aus starten.", "route_from_current": "Route ab aktuellem Standort", "route_location_denied": "Standortfreigabe abgelehnt. Es wurde keine Route mit deinem Standort erstellt.", "route_location_unavailable": "Standort ist in diesem Browser derzeit nicht verfügbar.", - "maps_privacy_notice": "Zur Anzeige der Karte werden Daten an Google übertragen.", "maps_load_button": "Google Maps anzeigen", "privacy": "Datenschutz", "imprint": "Impressum", @@ -328,16 +330,17 @@ TEXTS = { "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_ceremony_text": "Trauzeremonie im Hof und Fotos.", "schedule_reception_title": "Sektempfang & Häppchen", - "schedule_reception_text": "Sektempfang im Hof der Klostermühle.", + "schedule_reception_text": "Sektempfang im Hof der Hochheimer Terrasse.", "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_time": "Am Abend", "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.", + "schedule_party_text": "Wir feiern gemeinsam bis 03:30 Uhr.", "hotels_text": "Empfehlungen folgen. Bitte frühzeitig buchen.", "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", @@ -349,6 +352,7 @@ TEXTS = { "hotel_route_open_maps": "In Google Maps öffnen", "hotel_drive_badge": "Auto ca. {minutes} Min", "hotel_walk_badge": "Zu Fuß ca. {minutes} Min", + "hotel_walk_not_recommended": "Zu Fuß nicht empfohlen", "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.", @@ -447,12 +451,12 @@ 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.", + "location_story_title": "Rheingau flair at Hochheimer Terrasse", + "location_story_text": "Hochheimer Terrasse is located in Hochheim's historic old town and combines Rheingau atmosphere with an elegant setting for our wedding day. It is a special place between Mainz, Wiesbaden, and the vineyards where we cannot wait to celebrate with you.", + "location_map_intro": "To help you find the location easily, you can open Google Maps directly from here or start a route from your current location.", "route_from_current": "Route from current location", "route_location_denied": "Location access was denied. No route with your location was created.", "route_location_unavailable": "Location is currently unavailable in this browser.", - "maps_privacy_notice": "To display the map, data will be transferred to Google.", "maps_load_button": "Show Google Maps", "privacy": "Privacy", "imprint": "Imprint", @@ -469,16 +473,17 @@ TEXTS = { "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_ceremony_text": "Wedding ceremony in the courtyard and photos.", "schedule_reception_title": "Sparkling Reception & Snacks", - "schedule_reception_text": "Sparkling reception in the courtyard of the Klostermuehle.", + "schedule_reception_text": "Sparkling reception in the courtyard of Hochheimer Terrasse.", "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_time": "In the evening", "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.", + "schedule_party_text": "We celebrate together until 3:30 AM.", "hotels_text": "Recommendations will follow. Please book early.", "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", @@ -490,6 +495,7 @@ TEXTS = { "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_walk_not_recommended": "Walking not recommended", "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.", @@ -663,7 +669,11 @@ def get_hotels_for_page() -> list[dict]: "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"]), + "walk_badge": ( + t("hotel_walk_badge").format(minutes=entry["walk_minutes"]) + if entry["walk_minutes"] + else t("hotel_walk_not_recommended") + ), "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"), @@ -673,10 +683,22 @@ def get_hotels_for_page() -> list[dict]: return hotels +def get_location_video_asset() -> str | None: + location_dir = base_dir / "static" / "assets" / "location" + if not location_dir.exists(): + return None + + for asset in sorted(location_dir.iterdir()): + if asset.is_file() and asset.suffix.lower() in LOCATION_VIDEO_EXTENSIONS: + return f"assets/location/{asset.name}" + return None + + @app.context_processor def inject_common() -> dict: role = session.get("role", "guest") group_id = session.get("group_id") + location_video_asset = get_location_video_asset() return { "t": t, "lang": get_lang(), @@ -691,6 +713,7 @@ def inject_common() -> dict: "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"], + "location_video_url": url_for("static", filename=location_video_asset) if location_video_asset else None, "wedding_date": app.config["WEDDING_DATE"], "wedding_countdown_iso": get_wedding_countdown_iso(), "hero_image_url": url_for("static", filename=get_hero_image_asset()), diff --git a/backend/static/assets/location-map-preview.jpeg b/backend/static/assets/location-map-preview.jpeg new file mode 100644 index 0000000..2bfd5d7 Binary files /dev/null and b/backend/static/assets/location-map-preview.jpeg differ diff --git a/backend/static/assets/location-map-preview.svg b/backend/static/assets/location-map-preview.svg deleted file mode 100644 index 8f2acb6..0000000 --- a/backend/static/assets/location-map-preview.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Klostermühle Kiedrich - An d. Klostermühle 3, 65399 Kiedrich - Kartenvorschau - - - - - - - - diff --git a/backend/static/assets/location/draußen.png b/backend/static/assets/location/draußen.png new file mode 100644 index 0000000..ce3febd Binary files /dev/null and b/backend/static/assets/location/draußen.png differ diff --git a/backend/static/assets/location/essen1.png b/backend/static/assets/location/essen1.png new file mode 100644 index 0000000..a8e6a92 Binary files /dev/null and b/backend/static/assets/location/essen1.png differ diff --git a/backend/static/assets/location/gewölbe.png b/backend/static/assets/location/gewölbe.png new file mode 100644 index 0000000..d19e97d Binary files /dev/null and b/backend/static/assets/location/gewölbe.png differ diff --git a/backend/static/assets/location/hochheim.png b/backend/static/assets/location/hochheim.png new file mode 100644 index 0000000..7f8a698 Binary files /dev/null and b/backend/static/assets/location/hochheim.png differ diff --git a/backend/static/assets/location/thresen.png b/backend/static/assets/location/thresen.png new file mode 100644 index 0000000..ddc2bcb Binary files /dev/null and b/backend/static/assets/location/thresen.png differ diff --git a/backend/static/assets/location/trausaal.png b/backend/static/assets/location/trausaal.png new file mode 100644 index 0000000..259375a Binary files /dev/null and b/backend/static/assets/location/trausaal.png differ diff --git a/backend/static/styles.css b/backend/static/styles.css index aa9b741..2a55a1d 100644 --- a/backend/static/styles.css +++ b/backend/static/styles.css @@ -1395,9 +1395,94 @@ body.has-route-modal { } } +.location-video-card { + width: min(100%, 920px); + margin: 0 auto 1.1rem; + overflow: hidden; + border: 1px solid rgba(178, 137, 70, 0.24); + border-radius: 16px; + background: #1f3a2f; + box-shadow: + 0 14px 30px rgba(31, 58, 47, 0.16), + 0 1px 0 rgba(255, 255, 255, 0.22) inset; +} + +.location-video { + display: block; + width: 100%; + aspect-ratio: 16 / 9; + background: #1f3a2f; + object-fit: cover; +} + +.location-slideshow { + position: relative; + isolation: isolate; + width: min(100%, 920px); + aspect-ratio: 16 / 9; + margin: 1rem auto 1.1rem; + overflow: hidden; + border: 1px solid rgba(178, 137, 70, 0.24); + border-radius: 16px; + background: #eadfce; + box-shadow: + 0 14px 30px rgba(31, 58, 47, 0.14), + 0 1px 0 rgba(255, 255, 255, 0.64) inset; +} + +.location-slideshow::after { + content: ""; + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + background: + linear-gradient(180deg, rgba(22, 34, 28, 0.06), transparent 44%), + linear-gradient(0deg, rgba(22, 34, 28, 0.12), transparent 34%); +} + +.location-slide { + position: absolute; + inset: 0; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; + transform: scale(1.03); + animation: location-slide-fade 36s ease-in-out infinite; + animation-delay: calc(var(--slide-index) * 6s); +} + +@keyframes location-slide-fade { + 0% { + opacity: 0; + transform: scale(1.03); + } + 4%, + 16% { + opacity: 1; + } + 22%, + 100% { + opacity: 0; + transform: scale(1.08); + } +} + +.location-map-intro { + max-width: 58ch; + margin: 1rem auto 0.75rem; + color: rgba(31, 26, 23, 0.82); + line-height: 1.5; + text-align: center; +} + .map-consent { display: grid; gap: 0.7rem; + width: min(100%, 920px); + margin: 0 auto; padding: 0.9rem; border: 1px solid rgba(31, 58, 47, 0.12); border-radius: 12px; @@ -1413,17 +1498,18 @@ body.has-route-modal { .map-preview { width: 100%; - aspect-ratio: 16 / 7; + aspect-ratio: 4 / 3; min-height: 0; + overflow: hidden; border: 1px solid rgba(39, 66, 53, 0.16); border-radius: 12px; cursor: pointer; background-color: rgba(221, 230, 225, 0.95); background-image: - linear-gradient(180deg, rgba(28, 45, 37, 0.22), rgba(28, 45, 37, 0.22)), + linear-gradient(180deg, rgba(22, 34, 28, 0.12), rgba(22, 34, 28, 0.2)), var(--map-preview-image); - background-size: contain; - background-position: top left; + background-size: cover; + background-position: center; background-repeat: no-repeat; display: grid; place-items: center; @@ -1440,9 +1526,11 @@ body.has-route-modal { .map-preview-overlay { font-weight: 700; color: #fff; - background: rgba(39, 66, 53, 0.9); + background: rgba(31, 58, 47, 0.9); + border: 1px solid rgba(255, 255, 255, 0.26); border-radius: 999px; - padding: 0.48rem 0.82rem; + box-shadow: 0 8px 18px rgba(18, 31, 24, 0.2); + padding: 0.58rem 0.95rem; } .map-embed-target:not(:empty) { @@ -1497,6 +1585,11 @@ body.has-route-modal { justify-content: center; } + .location-slideshow { + aspect-ratio: 4 / 3; + border-radius: 13px; + } + .dashboard-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1555,6 +1648,15 @@ body.has-route-modal { animation: none; opacity: 0; } + + .location-slide { + animation: none; + } + + .location-slide:first-child { + opacity: 1; + transform: none; + } } @media (max-width: 380px) { diff --git a/backend/templates/info.html b/backend/templates/info.html index 6cc375f..d081a6e 100644 --- a/backend/templates/info.html +++ b/backend/templates/info.html @@ -33,7 +33,7 @@

{{ t('schedule_buffet_text') }}

-

22:00

+

{{ t('schedule_cake_time') }}

{{ t('schedule_cake_title') }}

{{ t('schedule_cake_text') }}

@@ -190,8 +190,72 @@ })(); {% elif page == 'location' %} + {% if location_video_url %} +
+ +
+ {% endif %}

{{ t('location_story_title') }}

{{ t('location_story_text') }}

+
+ {{ location_name }} + {{ location_name }} + {{ location_name }} + {{ location_name }} + {{ location_name }} + {{ location_name }} +
+

{{ t('location_map_intro') }}