new feat übersetzer und faq
This commit is contained in:
Binary file not shown.
155
backend/app.py
155
backend/app.py
@@ -53,6 +53,7 @@ ALLOWED_MIME_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOCATION_VIDEO_EXTENSIONS = {".mp4", ".webm", ".mov", ".m4v"}
|
LOCATION_VIDEO_EXTENSIONS = {".mp4", ".webm", ".mov", ".m4v"}
|
||||||
|
ONBOARDING_VERSION = 2
|
||||||
|
|
||||||
DEFAULT_INVITATION_GROUPS = [
|
DEFAULT_INVITATION_GROUPS = [
|
||||||
{
|
{
|
||||||
@@ -330,8 +331,8 @@ TEXTS = {
|
|||||||
"hero_headline_with_group": "Willkommen zu unserer Hochzeit, {name}",
|
"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_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_text_group": "Wir freuen uns riesig, diesen besonderen Tag am 04.09.26 mit euch zu feiern.",
|
||||||
"hero_app_hint_single": "In dieser Webapp kannst du zu- oder absagen, Fotos hochladen, die Galerie ansehen und alle wichtigen Infos rund um den Tag finden. Bitte gib deine Zu- oder Absage möglichst bald ab.",
|
"hero_app_hint_single": "In dieser Webapp kannst du zu- oder absagen, Fotos hochladen, die Galerie ansehen und alle wichtigen Infos rund um den Tag finden. Im Header findest du außerdem unsere FAQ und eine Deutsch-Englisch-Übersetzungshilfe. Bitte gib deine Zu- oder Absage möglichst bald ab.",
|
||||||
"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.",
|
"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. Im Header findet ihr außerdem unsere FAQ und eine Deutsch-Englisch-Übersetzungshilfe. Bitte gebt eure Zu- oder Absage möglichst bald ab.",
|
||||||
"to_guest_area": "Zum Gästebereich",
|
"to_guest_area": "Zum Gästebereich",
|
||||||
"schedule_text": "15:00 Trauung, 17:00 Empfang, 19:00 Dinner.",
|
"schedule_text": "15:00 Trauung, 17:00 Empfang, 19:00 Dinner.",
|
||||||
"schedule_intro": "Hier findet ihr den Ablauf für unseren Hochzeitstag.",
|
"schedule_intro": "Hier findet ihr den Ablauf für unseren Hochzeitstag.",
|
||||||
@@ -373,6 +374,7 @@ TEXTS = {
|
|||||||
"gifts_teaser": "Wir würden uns über diese Geschenke sehr freuen...",
|
"gifts_teaser": "Wir würden uns über diese Geschenke sehr freuen...",
|
||||||
"gifts_reveal_button": "Wunsch aufdecken",
|
"gifts_reveal_button": "Wunsch aufdecken",
|
||||||
"gifts_image_alt": "Ein großer Haufen Geldscheine",
|
"gifts_image_alt": "Ein großer Haufen Geldscheine",
|
||||||
|
"gifts_money_label": "Moneten",
|
||||||
"gifts_caption": "Trommelwirbel... in unserer 70-Quadratmeter-Wohnung ist leider kein Platz mehr für Materielles.",
|
"gifts_caption": "Trommelwirbel... in unserer 70-Quadratmeter-Wohnung ist leider kein Platz mehr für Materielles.",
|
||||||
"gifts_text": "Also: Money! Ein finanzieller Beitrag zu unserer Reise nach der Hochzeit ist herzlich willkommen.",
|
"gifts_text": "Also: Money! Ein finanzieller Beitrag zu unserer Reise nach der Hochzeit ist herzlich willkommen.",
|
||||||
"gallery_uploaded_by": "von {name}",
|
"gallery_uploaded_by": "von {name}",
|
||||||
@@ -409,6 +411,57 @@ TEXTS = {
|
|||||||
"flash_admin_only": "Dieser Bereich ist nur für Admins verfügbar.",
|
"flash_admin_only": "Dieser Bereich ist nur für Admins verfügbar.",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
|
"faq": "Häufig gestellte Fragen",
|
||||||
|
"faq_intro": "Hier findet ihr Antworten auf die wichtigsten Fragen rund um unsere Hochzeit.",
|
||||||
|
"faq_deadline_question": "Bis wann sollen wir zu- oder absagen?",
|
||||||
|
"faq_deadline_answer": "Bitte gebt uns bis spätestens 4. August 2026 über den Bereich „Zu-/Absage“ Bescheid.",
|
||||||
|
"faq_change_question": "Kann ich meine Antwort später noch ändern?",
|
||||||
|
"faq_change_answer": "Ja, ihr könnt eure Zu- oder Absage jederzeit bis zum Ende der Rückmeldefrist anpassen.",
|
||||||
|
"faq_when_where_question": "Wann und wo findet die Hochzeit statt?",
|
||||||
|
"faq_when_where_answer": "Unsere Hochzeit beginnt um 15:30 Uhr auf der Hochheimer Terrasse. Weitere Informationen findet ihr auf der Location-Seite.",
|
||||||
|
"faq_parking_question": "Gibt es Parkmöglichkeiten?",
|
||||||
|
"faq_parking_answer": "In der Hochheimer Altstadt und in der näheren Umgebung gibt es viele Parkmöglichkeiten. Plant bitte etwas Zeit für die Parkplatzsuche und den anschließenden Fußweg ein.",
|
||||||
|
"faq_hotel_question": "Wo können wir übernachten?",
|
||||||
|
"faq_hotel_answer": "Für eine möglichst kurze Anreise empfehlen wir euch eine Unterkunft direkt in Hochheim oder in der näheren Umgebung. Eine Auswahl findet ihr auf unserer Hotel-Seite.",
|
||||||
|
"faq_upload_question": "Wie funktioniert der Foto-Upload?",
|
||||||
|
"faq_upload_answer": "Über den Bereich „Upload“ könnt ihr Fotos direkt von eurem Smartphone oder Computer hochladen.",
|
||||||
|
"faq_visibility_question": "Wer kann die hochgeladenen Bilder sehen?",
|
||||||
|
"faq_visibility_answer": "Die Bilder sind ausschließlich für eingeloggte Hochzeitsgäste sichtbar. Eure eigenen Uploads könnt ihr selbst wieder löschen.",
|
||||||
|
"translator": "Übersetzungshilfe",
|
||||||
|
"translator_kicker": "Deutsch & Englisch",
|
||||||
|
"translator_close": "Übersetzungshilfe schließen",
|
||||||
|
"translator_direction": "Übersetzungsrichtung",
|
||||||
|
"translator_swap": "Übersetzungsrichtung wechseln",
|
||||||
|
"translator_input_label": "Was möchtest du übersetzen?",
|
||||||
|
"translator_placeholder": "Kurzen Text eingeben …",
|
||||||
|
"translator_quick_phrases": "Schnelle Sätze",
|
||||||
|
"translator_phrase_meet": "Kennenlernen",
|
||||||
|
"translator_phrase_drink": "Getränk anbieten",
|
||||||
|
"translator_phrase_restroom": "Toilette finden",
|
||||||
|
"translator_privacy": "Der Text wird erst nach dem Klick an DeepL übertragen. Bitte keine Namen, Kontaktdaten oder vertraulichen Inhalte eingeben.",
|
||||||
|
"translator_submit": "Mit DeepL übersetzen",
|
||||||
|
"translator_policy": "Datenschutz bei DeepL",
|
||||||
|
"language_german": "Deutsch",
|
||||||
|
"language_english": "Englisch",
|
||||||
|
"tour_title": "Willkommen in unserer Hochzeits-App",
|
||||||
|
"tour_progress": "Schritt {current} von {total}",
|
||||||
|
"tour_dashboard_title": "Alles auf einen Blick",
|
||||||
|
"tour_dashboard_text": "Im Dashboard findet ihr alle wichtigen Bereiche und Informationen für unseren Hochzeitstag.",
|
||||||
|
"tour_rsvp_title": "Zu- oder Absage",
|
||||||
|
"tour_rsvp_text": "Gebt die Rückmeldung für jedes Mitglied eurer Einladung bequem über den Bereich „Zu-/Absage“ ab.",
|
||||||
|
"tour_photos_title": "Fotos & Galerie",
|
||||||
|
"tour_photos_text": "Ladet eure Fotos direkt vom Smartphone hoch, entdeckt die gemeinsame Galerie und ladet Erinnerungen herunter.",
|
||||||
|
"tour_routes_title": "Hotels, Location & Routen",
|
||||||
|
"tour_routes_text": "Bei den Hotels und der Location könnt ihr Routen mit Google Maps öffnen. Karten werden erst nach eurem Klick geladen.",
|
||||||
|
"tour_translator_title": "Übersetzungshilfe",
|
||||||
|
"tour_translator_text": "Über das Buch im Header könnt ihr kurze deutsche oder englische Texte für die Übersetzung mit DeepL vorbereiten.",
|
||||||
|
"tour_faq_title": "Häufig gestellte Fragen",
|
||||||
|
"tour_faq_text": "Das Fragezeichen im Header führt euch jederzeit zu den wichtigsten Antworten rund um die Hochzeit.",
|
||||||
|
"tour_back": "Zurück",
|
||||||
|
"tour_next": "Weiter",
|
||||||
|
"tour_skip": "Überspringen",
|
||||||
|
"tour_finish": "Los geht’s",
|
||||||
|
"tour_restart": "Einführung erneut ansehen",
|
||||||
"countdown_button_label": "Countdown bis zur Hochzeit",
|
"countdown_button_label": "Countdown bis zur Hochzeit",
|
||||||
"countdown_until": "Noch",
|
"countdown_until": "Noch",
|
||||||
"countdown_started": "Die Feier hat begonnen",
|
"countdown_started": "Die Feier hat begonnen",
|
||||||
@@ -474,8 +527,8 @@ TEXTS = {
|
|||||||
"hero_headline_with_group": "Welcome to our wedding, {name}",
|
"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_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_text_group": "We are incredibly excited to celebrate this special day on 09/04/26 with all of you.",
|
||||||
"hero_app_hint_single": "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.",
|
"hero_app_hint_single": "In this web app, you can send your RSVP, upload photos, view the gallery, and find all important details for the day. The header also includes our FAQ and a German-English translation helper. Please submit your RSVP soon.",
|
||||||
"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.",
|
"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. The header also includes our FAQ and a German-English translation helper. Please submit your RSVP soon.",
|
||||||
"to_guest_area": "Open guest area",
|
"to_guest_area": "Open guest area",
|
||||||
"schedule_text": "3:00 PM ceremony, 5:00 PM reception, 7:00 PM dinner.",
|
"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_intro": "Here is the schedule for our wedding day.",
|
||||||
@@ -517,6 +570,7 @@ TEXTS = {
|
|||||||
"gifts_teaser": "We would be very happy to receive these gifts...",
|
"gifts_teaser": "We would be very happy to receive these gifts...",
|
||||||
"gifts_reveal_button": "Reveal wish",
|
"gifts_reveal_button": "Reveal wish",
|
||||||
"gifts_image_alt": "A big pile of cash",
|
"gifts_image_alt": "A big pile of cash",
|
||||||
|
"gifts_money_label": "Moolah",
|
||||||
"gifts_caption": "Drum roll... in our 70-square-meter apartment, we sadly have no room left for material things.",
|
"gifts_caption": "Drum roll... in our 70-square-meter apartment, we sadly have no room left for material things.",
|
||||||
"gifts_text": "So: Money! A financial contribution to our trip after the wedding is very welcome.",
|
"gifts_text": "So: Money! A financial contribution to our trip after the wedding is very welcome.",
|
||||||
"gallery_uploaded_by": "by {name}",
|
"gallery_uploaded_by": "by {name}",
|
||||||
@@ -553,6 +607,57 @@ TEXTS = {
|
|||||||
"flash_admin_only": "This area is available to admins only.",
|
"flash_admin_only": "This area is available to admins only.",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
|
"faq": "Frequently Asked Questions",
|
||||||
|
"faq_intro": "Here you will find answers to the most important questions about our wedding.",
|
||||||
|
"faq_deadline_question": "When do we need to RSVP by?",
|
||||||
|
"faq_deadline_answer": "Please let us know whether you will be attending by 4 August 2026 in the “RSVP” section.",
|
||||||
|
"faq_change_question": "Can I change my response later?",
|
||||||
|
"faq_change_answer": "Yes, you can update your RSVP at any time before the response deadline.",
|
||||||
|
"faq_when_where_question": "When and where is the wedding taking place?",
|
||||||
|
"faq_when_where_answer": "Our wedding begins at 3:30 PM at Hochheimer Terrasse. You can find more information on the location page.",
|
||||||
|
"faq_parking_question": "Is parking available?",
|
||||||
|
"faq_parking_answer": "There are plenty of parking options in Hochheim's old town and the surrounding area. Please allow some extra time to find a space and walk to the venue.",
|
||||||
|
"faq_hotel_question": "Where can we stay overnight?",
|
||||||
|
"faq_hotel_answer": "For the shortest possible journey, we recommend staying in Hochheim or nearby. You can find a selection on our hotels page.",
|
||||||
|
"faq_upload_question": "How does the photo upload work?",
|
||||||
|
"faq_upload_answer": "You can upload photos directly from your smartphone or computer in the “Upload” section.",
|
||||||
|
"faq_visibility_question": "Who can see the uploaded photos?",
|
||||||
|
"faq_visibility_answer": "The photos are visible only to logged-in wedding guests. You can delete your own uploads at any time.",
|
||||||
|
"translator": "Translation helper",
|
||||||
|
"translator_kicker": "English & German",
|
||||||
|
"translator_close": "Close translation helper",
|
||||||
|
"translator_direction": "Translation direction",
|
||||||
|
"translator_swap": "Switch translation direction",
|
||||||
|
"translator_input_label": "What would you like to translate?",
|
||||||
|
"translator_placeholder": "Enter a short text …",
|
||||||
|
"translator_quick_phrases": "Quick phrases",
|
||||||
|
"translator_phrase_meet": "Meeting someone",
|
||||||
|
"translator_phrase_drink": "Offering a drink",
|
||||||
|
"translator_phrase_restroom": "Finding the restroom",
|
||||||
|
"translator_privacy": "Your text is sent to DeepL only after you click the button. Please do not enter names, contact details, or confidential information.",
|
||||||
|
"translator_submit": "Translate with DeepL",
|
||||||
|
"translator_policy": "DeepL privacy policy",
|
||||||
|
"language_german": "German",
|
||||||
|
"language_english": "English",
|
||||||
|
"tour_title": "Welcome to our wedding app",
|
||||||
|
"tour_progress": "Step {current} of {total}",
|
||||||
|
"tour_dashboard_title": "Everything at a glance",
|
||||||
|
"tour_dashboard_text": "The dashboard brings together all the important sections and information for our wedding day.",
|
||||||
|
"tour_rsvp_title": "RSVP",
|
||||||
|
"tour_rsvp_text": "Submit a response for every member of your invitation conveniently in the “RSVP” section.",
|
||||||
|
"tour_photos_title": "Photos & gallery",
|
||||||
|
"tour_photos_text": "Upload photos directly from your phone, explore the shared gallery, and download your favourite memories.",
|
||||||
|
"tour_routes_title": "Hotels, location & routes",
|
||||||
|
"tour_routes_text": "You can open Google Maps routes from the hotel and location pages. Maps load only after you click.",
|
||||||
|
"tour_translator_title": "Translation helper",
|
||||||
|
"tour_translator_text": "Use the book in the header to prepare short German or English texts for translation with DeepL.",
|
||||||
|
"tour_faq_title": "Frequently asked questions",
|
||||||
|
"tour_faq_text": "The question mark in the header takes you to the most important answers about the wedding at any time.",
|
||||||
|
"tour_back": "Back",
|
||||||
|
"tour_next": "Next",
|
||||||
|
"tour_skip": "Skip",
|
||||||
|
"tour_finish": "Let’s go",
|
||||||
|
"tour_restart": "View introduction again",
|
||||||
"countdown_button_label": "Wedding countdown",
|
"countdown_button_label": "Wedding countdown",
|
||||||
"countdown_until": "Countdown",
|
"countdown_until": "Countdown",
|
||||||
"countdown_started": "The celebration has started",
|
"countdown_started": "The celebration has started",
|
||||||
@@ -767,6 +872,7 @@ def init_db() -> None:
|
|||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
role TEXT NOT NULL DEFAULT 'guest',
|
role TEXT NOT NULL DEFAULT 'guest',
|
||||||
|
onboarding_completed INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT NOT NULL
|
created_at TEXT NOT NULL
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
@@ -811,6 +917,8 @@ def init_db() -> None:
|
|||||||
group_cols = table_columns(conn, "groups")
|
group_cols = table_columns(conn, "groups")
|
||||||
if "role" not in group_cols:
|
if "role" not in group_cols:
|
||||||
conn.execute("ALTER TABLE groups ADD COLUMN role TEXT NOT NULL DEFAULT 'guest'")
|
conn.execute("ALTER TABLE groups ADD COLUMN role TEXT NOT NULL DEFAULT 'guest'")
|
||||||
|
if "onboarding_completed" not in group_cols:
|
||||||
|
conn.execute("ALTER TABLE groups ADD COLUMN onboarding_completed INTEGER NOT NULL DEFAULT 0")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -947,14 +1055,38 @@ def welcome():
|
|||||||
is_single_guest = member_count <= 1
|
is_single_guest = member_count <= 1
|
||||||
welcome_headline = t("hero_headline_with_group").format(name=session.get("group_name", ""))
|
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")
|
welcome_text = t("hero_text_single") if is_single_guest else t("hero_text_group")
|
||||||
|
onboarding_row = db.execute(
|
||||||
|
"SELECT onboarding_completed FROM groups WHERE id = ?",
|
||||||
|
(int(session["group_id"]),),
|
||||||
|
).fetchone()
|
||||||
|
completed_onboarding_version = (
|
||||||
|
int(onboarding_row["onboarding_completed"] or 0) if onboarding_row else 0
|
||||||
|
)
|
||||||
|
show_onboarding = (
|
||||||
|
request.args.get("tour") == "1"
|
||||||
|
or completed_onboarding_version < ONBOARDING_VERSION
|
||||||
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"welcome.html",
|
"welcome.html",
|
||||||
welcome_headline=welcome_headline,
|
welcome_headline=welcome_headline,
|
||||||
welcome_text=welcome_text,
|
welcome_text=welcome_text,
|
||||||
welcome_hint=t("hero_app_hint_single") if is_single_guest else t("hero_app_hint"),
|
welcome_hint=t("hero_app_hint_single") if is_single_guest else t("hero_app_hint"),
|
||||||
|
show_onboarding=show_onboarding,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/onboarding/complete")
|
||||||
|
@login_required
|
||||||
|
def complete_onboarding():
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"UPDATE groups SET onboarding_completed = ? WHERE id = ?",
|
||||||
|
(ONBOARDING_VERSION, int(session["group_id"])),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return ("", 204)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/gaestebereich")
|
@app.get("/gaestebereich")
|
||||||
@app.get("/gästebereich")
|
@app.get("/gästebereich")
|
||||||
@login_required
|
@login_required
|
||||||
@@ -1002,6 +1134,21 @@ def dashboard():
|
|||||||
return redirect(url_for("guest_area"))
|
return redirect(url_for("guest_area"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/faq")
|
||||||
|
@login_required
|
||||||
|
def faq():
|
||||||
|
faq_items = [
|
||||||
|
("faq_deadline_question", "faq_deadline_answer"),
|
||||||
|
("faq_change_question", "faq_change_answer"),
|
||||||
|
("faq_when_where_question", "faq_when_where_answer"),
|
||||||
|
("faq_parking_question", "faq_parking_answer"),
|
||||||
|
("faq_hotel_question", "faq_hotel_answer"),
|
||||||
|
("faq_upload_question", "faq_upload_answer"),
|
||||||
|
("faq_visibility_question", "faq_visibility_answer"),
|
||||||
|
]
|
||||||
|
return render_template("faq.html", faq_items=faq_items)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/rsvp", methods=["GET", "POST"])
|
@app.route("/rsvp", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def rsvp():
|
def rsvp():
|
||||||
|
|||||||
Binary file not shown.
@@ -36,5 +36,4 @@
|
|||||||
<text x="280" y="69" text-anchor="middle" font-size="28" font-family="Arial, sans-serif" fill="#2f5e35">$</text>
|
<text x="280" y="69" text-anchor="middle" font-size="28" font-family="Arial, sans-serif" fill="#2f5e35">$</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<text x="480" y="90" text-anchor="middle" font-size="54" font-family="Arial, sans-serif" fill="#f0db8e">Gift Fund</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
53
backend/static/faq.js
Normal file
53
backend/static/faq.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
(() => {
|
||||||
|
function initFaqItem(item) {
|
||||||
|
const summary = item.querySelector("summary");
|
||||||
|
const answer = item.querySelector(".faq-answer");
|
||||||
|
if (!summary || !answer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAnimating = false;
|
||||||
|
|
||||||
|
summary.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.open) {
|
||||||
|
item.open = true;
|
||||||
|
item.classList.remove("is-closing");
|
||||||
|
item.classList.add("is-opening");
|
||||||
|
const finishOpen = () => {
|
||||||
|
item.classList.remove("is-opening");
|
||||||
|
isAnimating = false;
|
||||||
|
};
|
||||||
|
isAnimating = true;
|
||||||
|
answer.addEventListener("animationend", finishOpen, { once: true });
|
||||||
|
window.setTimeout(
|
||||||
|
finishOpen,
|
||||||
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 360,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnimating = true;
|
||||||
|
item.classList.remove("is-opening");
|
||||||
|
item.classList.add("is-closing");
|
||||||
|
const finishClose = () => {
|
||||||
|
item.classList.remove("is-closing");
|
||||||
|
item.open = false;
|
||||||
|
isAnimating = false;
|
||||||
|
};
|
||||||
|
answer.addEventListener("animationend", finishClose, { once: true });
|
||||||
|
window.setTimeout(
|
||||||
|
finishClose,
|
||||||
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 360,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll(".faq-item").forEach(initFaqItem);
|
||||||
|
});
|
||||||
|
})();
|
||||||
81
backend/static/onboarding.js
Normal file
81
backend/static/onboarding.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
(() => {
|
||||||
|
function initOnboarding(dialog) {
|
||||||
|
const slides = Array.from(dialog.querySelectorAll("[data-onboarding-slide]"));
|
||||||
|
const dots = Array.from(dialog.querySelectorAll("[data-onboarding-dot]"));
|
||||||
|
const progress = dialog.querySelector("[data-onboarding-progress]");
|
||||||
|
const backButton = dialog.querySelector("[data-onboarding-back]");
|
||||||
|
const nextButton = dialog.querySelector("[data-onboarding-next]");
|
||||||
|
const completeUrl = dialog.dataset.completeUrl;
|
||||||
|
const progressTemplate = dialog.dataset.progressTemplate || "Step {current} of {total}";
|
||||||
|
|
||||||
|
if (!slides.length || !progress || !backButton || !nextButton || !completeUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentIndex = 0;
|
||||||
|
let completionSent = false;
|
||||||
|
|
||||||
|
const markComplete = () => {
|
||||||
|
if (completionSent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completionSent = true;
|
||||||
|
window.fetch(completeUrl, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
|
keepalive: true,
|
||||||
|
}).catch(() => {
|
||||||
|
completionSent = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
markComplete();
|
||||||
|
dialog.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
slides.forEach((slide, index) => {
|
||||||
|
slide.hidden = index !== currentIndex;
|
||||||
|
});
|
||||||
|
dots.forEach((dot, index) => {
|
||||||
|
dot.classList.toggle("is-active", index <= currentIndex);
|
||||||
|
});
|
||||||
|
progress.textContent = progressTemplate
|
||||||
|
.replace("{current}", String(currentIndex + 1))
|
||||||
|
.replace("{total}", String(slides.length));
|
||||||
|
backButton.hidden = currentIndex === 0;
|
||||||
|
const isLast = currentIndex === slides.length - 1;
|
||||||
|
nextButton.textContent = isLast ? nextButton.dataset.finishLabel : nextButton.dataset.nextLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
backButton.addEventListener("click", () => {
|
||||||
|
currentIndex = Math.max(0, currentIndex - 1);
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton.addEventListener("click", () => {
|
||||||
|
if (currentIndex === slides.length - 1) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentIndex += 1;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.addEventListener("cancel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
render();
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const dialog = document.querySelector("[data-onboarding-dialog]");
|
||||||
|
if (dialog) {
|
||||||
|
initOnboarding(dialog);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -697,7 +697,17 @@ input[type="file"]:focus {
|
|||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-session-actions {
|
.toolbar-faq-btn svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-translate-btn svg {
|
||||||
|
width: 1.02rem;
|
||||||
|
height: 1.02rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-row {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.38rem;
|
gap: 0.38rem;
|
||||||
@@ -816,6 +826,437 @@ input[type="file"]:focus {
|
|||||||
0 1px 0 rgba(255, 255, 255, 0.5) inset;
|
0 1px 0 rgba(255, 255, 255, 0.5) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.faq-card {
|
||||||
|
width: min(760px, 100%);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: clamp(1.2rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-card h1 {
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-intro {
|
||||||
|
width: min(56ch, 100%);
|
||||||
|
margin: 0 auto 1.4rem;
|
||||||
|
color: rgba(31, 26, 23, 0.76);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.7rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.14);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #fffdf9 0%, #fbf3e6 100%);
|
||||||
|
box-shadow: 0 5px 14px rgba(31, 58, 47, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--forest);
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item summary:focus-visible {
|
||||||
|
outline: 2px solid var(--gold);
|
||||||
|
outline-offset: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
flex: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-toggle::before,
|
||||||
|
.faq-toggle::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0.85rem;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--gold);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-toggle::after {
|
||||||
|
transform: translate(-50%, -50%) rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item[open] .faq-toggle::after {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-answer {
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
color: rgba(31, 26, 23, 0.8);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-answer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item.is-opening .faq-answer {
|
||||||
|
animation: faq-slide-in 300ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item.is-closing .faq-answer {
|
||||||
|
animation: faq-slide-out 260ms cubic-bezier(0.55, 0.06, 0.68, 0.19) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes faq-slide-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(2.5rem);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes faq-slide-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(2.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-dialog {
|
||||||
|
width: min(92vw, 520px);
|
||||||
|
max-height: min(88vh, 720px);
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--ink);
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.16);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: var(--card);
|
||||||
|
box-shadow: 0 24px 70px rgba(18, 31, 24, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-dialog::backdrop {
|
||||||
|
background: rgba(18, 31, 24, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-dialog.is-opening {
|
||||||
|
animation: translator-slide-up 320ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-dialog.is-opening::backdrop {
|
||||||
|
animation: translator-backdrop-in 320ms ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-dialog.is-closing {
|
||||||
|
animation: translator-slide-down 280ms cubic-bezier(0.55, 0.06, 0.68, 0.19) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-dialog.is-closing::backdrop {
|
||||||
|
animation: translator-backdrop-out 280ms ease-in both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes translator-slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(3.5rem);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes translator-slide-down {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(3.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes translator-backdrop-in {
|
||||||
|
from {
|
||||||
|
background: rgba(18, 31, 24, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background: rgba(18, 31, 24, 0.22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes translator-backdrop-out {
|
||||||
|
from {
|
||||||
|
background: rgba(18, 31, 24, 0.22);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background: rgba(18, 31, 24, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-sheet {
|
||||||
|
max-height: min(88vh, 720px);
|
||||||
|
padding: clamp(1.1rem, 4vw, 1.6rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-head h2 {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-direction {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--forest);
|
||||||
|
font-weight: 650;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-direction > span {
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.14);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(178, 137, 70, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-swap {
|
||||||
|
width: 2.35rem;
|
||||||
|
height: 2.35rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--forest);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 7.5rem;
|
||||||
|
resize: vertical;
|
||||||
|
padding: 0.85rem;
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.22);
|
||||||
|
border-radius: 14px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: #fff;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.45;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-input:focus {
|
||||||
|
outline: 2px solid rgba(178, 137, 70, 0.55);
|
||||||
|
border-color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-quick-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-quick-list button {
|
||||||
|
padding: 0.38rem 0.65rem;
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.16);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--forest);
|
||||||
|
background: rgba(255, 253, 248, 0.92);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-privacy {
|
||||||
|
margin: 1rem 0 0.8rem;
|
||||||
|
color: rgba(31, 26, 23, 0.7);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-submit {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-dialog {
|
||||||
|
width: min(92vw, 560px);
|
||||||
|
max-height: min(88vh, 720px);
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--ink);
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.16);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: var(--card);
|
||||||
|
box-shadow: 0 26px 72px rgba(18, 31, 24, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-dialog::backdrop {
|
||||||
|
background: rgba(18, 31, 24, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-shell {
|
||||||
|
max-height: min(88vh, 720px);
|
||||||
|
padding: clamp(1.25rem, 4vw, 1.8rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-progress {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-progress span {
|
||||||
|
width: 1.4rem;
|
||||||
|
height: 0.28rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(31, 58, 47, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-progress span.is-active {
|
||||||
|
background: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-step-label {
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
color: var(--gold-text);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-shell > h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--forest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-slide {
|
||||||
|
min-height: 14rem;
|
||||||
|
padding: 1.15rem;
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.12);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(180deg, #fffdf8 0%, #fdf5e8 100%);
|
||||||
|
box-shadow: 0 8px 20px rgba(31, 58, 47, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-slide[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 3.4rem;
|
||||||
|
height: 3.4rem;
|
||||||
|
margin: 0 auto 0.8rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(180deg, #2a4a3b 0%, var(--forest) 100%);
|
||||||
|
box-shadow: 0 7px 16px rgba(31, 58, 47, 0.2);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-icon svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-icon-route svg {
|
||||||
|
width: 1.65rem;
|
||||||
|
height: 1.65rem;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-slide h3 {
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
color: var(--forest);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-slide p {
|
||||||
|
max-width: 44ch;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--gold-text);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.65rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-actions > div {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-actions .btn {
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-tour-restart {
|
||||||
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 1.2rem auto 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
.gallery-grid {
|
.gallery-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
@@ -1172,6 +1613,20 @@ input[type="file"]:focus {
|
|||||||
padding: 0.74rem 1.2rem;
|
padding: 0.74rem 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-location-btn.btn-ghost {
|
||||||
|
color: #174ea6;
|
||||||
|
border-color: #a8c7fa;
|
||||||
|
background: linear-gradient(180deg, #f2f7ff 0%, #e8f0fe 100%);
|
||||||
|
box-shadow: 0 5px 12px rgba(66, 133, 244, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-location-btn.btn-ghost:hover {
|
||||||
|
color: #123d82;
|
||||||
|
border-color: #7eaff8;
|
||||||
|
background: linear-gradient(180deg, #edf5ff 0%, #dce9fd 100%);
|
||||||
|
box-shadow: 0 7px 16px rgba(66, 133, 244, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
body.has-route-modal {
|
body.has-route-modal {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -1296,6 +1751,7 @@ body.has-route-modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gift-image-wrap {
|
.gift-image-wrap {
|
||||||
|
position: relative;
|
||||||
max-width: min(640px, 100%);
|
max-width: min(640px, 100%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
@@ -1309,6 +1765,18 @@ body.has-route-modal {
|
|||||||
box-shadow: 0 12px 28px rgba(31, 58, 47, 0.18);
|
box-shadow: 0 12px 28px rgba(31, 58, 47, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gift-money-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 7%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #f0db8e;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: clamp(1.35rem, 5.6vw, 2.25rem);
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.gift-caption {
|
.gift-caption {
|
||||||
margin: 0.8rem 0 0;
|
margin: 0.8rem 0 0;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1528,6 +1996,33 @@ body.has-route-modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.28rem;
|
||||||
|
width: 13.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-login {
|
||||||
|
width: auto;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-login .toolbar-row {
|
||||||
|
width: auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.38rem;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin: 1.15rem auto 1.25rem;
|
margin: 1.15rem auto 1.25rem;
|
||||||
}
|
}
|
||||||
@@ -1609,9 +2104,62 @@ body.has-route-modal {
|
|||||||
.route-open-btn {
|
.route-open-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.translator-dialog {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
max-height: 88vh;
|
||||||
|
margin: auto 0 0;
|
||||||
|
border-radius: 22px 22px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-sheet {
|
||||||
|
max-height: 88vh;
|
||||||
|
padding-bottom: max(1.2rem, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-dialog {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
max-height: 90vh;
|
||||||
|
margin: auto 0 0;
|
||||||
|
border-radius: 24px 24px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-shell {
|
||||||
|
max-height: 90vh;
|
||||||
|
padding-bottom: max(1.25rem, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-slide {
|
||||||
|
min-height: 12.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-actions {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-actions > div,
|
||||||
|
.onboarding-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.translator-dialog.is-opening,
|
||||||
|
.translator-dialog.is-closing,
|
||||||
|
.faq-item.is-opening .faq-answer,
|
||||||
|
.faq-item.is-closing .faq-answer {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-dialog.is-opening::backdrop,
|
||||||
|
.translator-dialog.is-closing::backdrop {
|
||||||
|
animation: none;
|
||||||
|
background: rgba(18, 31, 24, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.gift-reveal.is-active {
|
.gift-reveal.is-active {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|||||||
117
backend/static/translator.js
Normal file
117
backend/static/translator.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
(() => {
|
||||||
|
function initTranslator(dialog) {
|
||||||
|
const openButton = document.querySelector("[data-translator-open]");
|
||||||
|
const swapButton = dialog.querySelector("[data-translator-swap]");
|
||||||
|
const submitButton = dialog.querySelector("[data-translator-submit]");
|
||||||
|
const input = dialog.querySelector("[data-translator-input]");
|
||||||
|
const sourceLabel = dialog.querySelector("[data-translator-source]");
|
||||||
|
const targetLabel = dialog.querySelector("[data-translator-target]");
|
||||||
|
const phraseButtons = dialog.querySelectorAll("[data-translator-phrase]");
|
||||||
|
|
||||||
|
if (!openButton || !swapButton || !submitButton || !input || !sourceLabel || !targetLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceLang = dialog.dataset.sourceLang === "en" ? "en" : "de";
|
||||||
|
const labels = {
|
||||||
|
de: sourceLang === "de" ? sourceLabel.textContent : targetLabel.textContent,
|
||||||
|
en: sourceLang === "en" ? sourceLabel.textContent : targetLabel.textContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDirection = () => {
|
||||||
|
const targetLang = sourceLang === "de" ? "en" : "de";
|
||||||
|
sourceLabel.textContent = labels[sourceLang];
|
||||||
|
targetLabel.textContent = labels[targetLang];
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSubmit = () => {
|
||||||
|
submitButton.disabled = input.value.trim().length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let isClosing = false;
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (!dialog.open || isClosing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isClosing = true;
|
||||||
|
dialog.classList.remove("is-opening");
|
||||||
|
dialog.classList.add("is-closing");
|
||||||
|
|
||||||
|
const finishClose = () => {
|
||||||
|
if (!dialog.open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialog.classList.remove("is-closing");
|
||||||
|
dialog.close();
|
||||||
|
isClosing = false;
|
||||||
|
openButton.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.addEventListener("animationend", finishClose, { once: true });
|
||||||
|
window.setTimeout(
|
||||||
|
finishClose,
|
||||||
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 360,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
openButton.addEventListener("click", () => {
|
||||||
|
dialog.showModal();
|
||||||
|
dialog.classList.remove("is-closing");
|
||||||
|
dialog.classList.add("is-opening");
|
||||||
|
window.requestAnimationFrame(() => input.focus());
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.addEventListener("click", (event) => {
|
||||||
|
if (event.target === dialog) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.addEventListener("cancel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
swapButton.addEventListener("click", () => {
|
||||||
|
sourceLang = sourceLang === "de" ? "en" : "de";
|
||||||
|
renderDirection();
|
||||||
|
phraseButtons.forEach((button) => {
|
||||||
|
input.value = input.value === button.dataset.de || input.value === button.dataset.en
|
||||||
|
? button.dataset[sourceLang]
|
||||||
|
: input.value;
|
||||||
|
});
|
||||||
|
updateSubmit();
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
phraseButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
input.value = button.dataset[sourceLang] || "";
|
||||||
|
updateSubmit();
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("input", updateSubmit);
|
||||||
|
submitButton.addEventListener("click", () => {
|
||||||
|
const text = input.value.trim();
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetLang = sourceLang === "de" ? "en" : "de";
|
||||||
|
const url = `https://www.deepl.com/translator#${sourceLang}/${targetLang}/${encodeURIComponent(text)}`;
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderDirection();
|
||||||
|
updateSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const dialog = document.querySelector("[data-translator-dialog]");
|
||||||
|
if (dialog) {
|
||||||
|
initTranslator(dialog);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -7,14 +7,15 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Source+Sans+3:wght@400;500;600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Source+Sans+3:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css', v='20260321a') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css', v='20260724v') }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div>
|
<div>
|
||||||
<a class="brand" href="{{ url_for('welcome') if guest_name else url_for('landing') }}">{{ t('brand') }}</a>
|
<a class="brand" href="{{ url_for('welcome') if guest_name else url_for('landing') }}">{{ t('brand') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar {{ 'toolbar-authenticated' if guest_name else 'toolbar-login' }}">
|
||||||
|
<div class="toolbar-row toolbar-row-top">
|
||||||
<form method="post" action="{{ url_for('set_lang', code='de') }}">
|
<form method="post" action="{{ url_for('set_lang', code='de') }}">
|
||||||
<button class="btn btn-ghost" type="submit">DE</button>
|
<button class="btn btn-ghost" type="submit">DE</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -22,6 +23,26 @@
|
|||||||
<button class="btn btn-ghost" type="submit">EN</button>
|
<button class="btn btn-ghost" type="submit">EN</button>
|
||||||
</form>
|
</form>
|
||||||
{% if guest_name %}
|
{% if guest_name %}
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost toolbar-nav-btn toolbar-translate-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="{{ t('translator') }}"
|
||||||
|
title="{{ t('translator') }}"
|
||||||
|
data-translator-open
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M12 5.5A8.4 8.4 0 0 0 6.5 3.4 8.6 8.6 0 0 0 2 4.7v14.1a.75.75 0 0 0 1.02.7A8.1 8.1 0 0 1 6.5 18.6a7.1 7.1 0 0 1 4.98 2 .75.75 0 0 0 1.04 0 7.1 7.1 0 0 1 4.98-2 8.1 8.1 0 0 1 3.48.9.75.75 0 0 0 1.02-.7V4.7a8.6 8.6 0 0 0-4.5-1.3A8.4 8.4 0 0 0 12 5.5zm-1 12.9a8.7 8.7 0 0 0-4.5-1.3 9.6 9.6 0 0 0-3 .5V5.7a7.4 7.4 0 0 1 3-.8A6.9 6.9 0 0 1 11 6.7zm9.5-.8a9.6 9.6 0 0 0-3-.5 8.7 8.7 0 0 0-4.5 1.3V6.7a6.9 6.9 0 0 1 4.5-1.8 7.4 7.4 0 0 1 3 .8z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-ghost toolbar-nav-btn toolbar-faq-btn" href="{{ url_for('faq') }}" aria-label="{{ t('faq') }}" title="{{ t('faq') }}">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M12 2a10 10 0 1 0 10 10A10.01 10.01 0 0 0 12 2zm.15 16.25a1.25 1.25 0 1 1 1.25-1.25 1.25 1.25 0 0 1-1.25 1.25zm1.75-7.05-.75.48a1.36 1.36 0 0 0-.65 1.17v.4h-2v-.4a3.34 3.34 0 0 1 1.58-2.85l.75-.48a1.75 1.75 0 1 0-2.68-1.48h-2a3.75 3.75 0 1 1 5.75 3.16z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if guest_name %}
|
||||||
|
<div class="toolbar-row toolbar-row-bottom">
|
||||||
<div
|
<div
|
||||||
class="toolbar-timer"
|
class="toolbar-timer"
|
||||||
data-countdown-target="{{ wedding_countdown_iso }}"
|
data-countdown-target="{{ wedding_countdown_iso }}"
|
||||||
@@ -62,7 +83,6 @@
|
|||||||
<p class="toolbar-timer-subline" data-countdown-subline>{{ t('countdown_subline') }}</p>
|
<p class="toolbar-timer-subline" data-countdown-subline>{{ t('countdown_subline') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-session-actions">
|
|
||||||
<a class="btn btn-ghost toolbar-nav-btn" href="{{ url_for('guest_area') }}" aria-label="{{ t('dashboard') }}" title="{{ t('dashboard') }}">
|
<a class="btn btn-ghost toolbar-nav-btn" href="{{ url_for('guest_area') }}" aria-label="{{ t('dashboard') }}" title="{{ t('dashboard') }}">
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
<path d="M3 3h8v8H3V3zm10 0h8v5h-8V3zM3 13h5v8H3v-8zm7 0h11v8H10v-8z" />
|
<path d="M3 3h8v8H3V3zm10 0h8v5h-8V3zM3 13h5v8H3v-8zm7 0h11v8H10v-8z" />
|
||||||
@@ -76,6 +96,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% if guest_name %}
|
||||||
|
<dialog
|
||||||
|
class="translator-dialog"
|
||||||
|
data-translator-dialog
|
||||||
|
data-source-lang="{{ 'de' if lang == 'de' else 'en' }}"
|
||||||
|
aria-labelledby="translator-title"
|
||||||
|
>
|
||||||
|
<div class="translator-sheet">
|
||||||
|
<div class="translator-head">
|
||||||
|
<div>
|
||||||
|
<h2 id="translator-title">{{ t('translator') }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="translator-direction" aria-label="{{ t('translator_direction') }}">
|
||||||
|
<span data-translator-source>{{ t('language_german') if lang == 'de' else t('language_english') }}</span>
|
||||||
|
<button class="translator-swap" type="button" data-translator-swap aria-label="{{ t('translator_swap') }}">⇄</button>
|
||||||
|
<span data-translator-target>{{ t('language_english') if lang == 'de' else t('language_german') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="translator-label" for="translator-text">{{ t('translator_input_label') }}</label>
|
||||||
|
<textarea
|
||||||
|
id="translator-text"
|
||||||
|
class="translator-input"
|
||||||
|
rows="4"
|
||||||
|
maxlength="500"
|
||||||
|
placeholder="{{ t('translator_placeholder') }}"
|
||||||
|
data-translator-input
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="translator-quick-list" aria-label="{{ t('translator_quick_phrases') }}">
|
||||||
|
<button type="button" data-translator-phrase data-de="Schön, dich kennenzulernen." data-en="Nice to meet you.">{{ t('translator_phrase_meet') }}</button>
|
||||||
|
<button type="button" data-translator-phrase data-de="Möchtest du etwas trinken?" data-en="Would you like something to drink?">{{ t('translator_phrase_drink') }}</button>
|
||||||
|
<button type="button" data-translator-phrase data-de="Wo ist die Toilette?" data-en="Where is the restroom?">{{ t('translator_phrase_restroom') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="translator-privacy">{{ t('translator_privacy') }}</p>
|
||||||
|
<button class="btn translator-submit" type="button" data-translator-submit disabled>
|
||||||
|
{{ t('translator_submit') }} <span aria-hidden="true">↗</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
|
||||||
{% with messages = get_flashed_messages() %}
|
{% with messages = get_flashed_messages() %}
|
||||||
@@ -95,5 +159,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
<script src="{{ url_for('static', filename='dashboard-transition.js', v='20260303a') }}" defer></script>
|
<script src="{{ url_for('static', filename='dashboard-transition.js', v='20260303a') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='countdown.js', v='20260303c') }}" defer></script>
|
<script src="{{ url_for('static', filename='countdown.js', v='20260303c') }}" defer></script>
|
||||||
|
<script src="{{ url_for('static', filename='translator.js', v='20260724b') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -169,6 +169,23 @@
|
|||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article class="legal-section" id="deepl-translation">
|
||||||
|
<h2>Translation with DeepL</h2>
|
||||||
|
<p>
|
||||||
|
The protected guest area provides an optional link to the external translation service DeepL, operated by DeepL
|
||||||
|
SE, Maarweg 165, 50825 Cologne, Germany. No connection to DeepL is established merely by opening this website.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Only after you click “Translate with DeepL” will the text you entered and technical connection data, such as your
|
||||||
|
IP address, be transmitted directly from your browser to DeepL. The text is not sent to or stored by our web
|
||||||
|
server. Please do not enter names, contact details, confidential information, or other personal data.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
After the link is opened, DeepL's privacy policy and terms apply:
|
||||||
|
<a href="https://www.deepl.com/privacy" target="_blank" rel="noopener noreferrer">DeepL privacy policy</a>.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
<article class="legal-section" id="photo-upload">
|
<article class="legal-section" id="photo-upload">
|
||||||
<h2>Photo Uploads</h2>
|
<h2>Photo Uploads</h2>
|
||||||
<p>
|
<p>
|
||||||
@@ -384,6 +401,25 @@
|
|||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article class="legal-section" id="deepl-uebersetzung">
|
||||||
|
<h2>Übersetzung mit DeepL</h2>
|
||||||
|
<p>
|
||||||
|
Im geschützten Gästebereich wird eine optionale Weiterleitung zum externen Übersetzungsdienst DeepL angeboten.
|
||||||
|
Anbieter ist die DeepL SE, Maarweg 165, 50825 Köln. Allein durch das Öffnen dieser Website wird noch keine
|
||||||
|
Verbindung zu DeepL hergestellt.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Erst wenn Sie „Mit DeepL übersetzen“ anklicken, werden der von Ihnen eingegebene Text und technische
|
||||||
|
Verbindungsdaten, beispielsweise Ihre IP-Adresse, direkt durch Ihren Browser an DeepL übermittelt. Der Text wird
|
||||||
|
nicht an unseren Webserver gesendet oder dort gespeichert. Bitte geben Sie keine Namen, Kontaktdaten,
|
||||||
|
vertraulichen Informationen oder sonstigen personenbezogenen Daten ein.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Nach dem Öffnen des Links gelten die Datenschutzbestimmungen und Nutzungsbedingungen von DeepL:
|
||||||
|
<a href="https://www.deepl.com/privacy" target="_blank" rel="noopener noreferrer">Datenschutz bei DeepL</a>.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
<article class="legal-section" id="foto-upload">
|
<article class="legal-section" id="foto-upload">
|
||||||
<h2>Upload von Fotos durch Nutzer</h2>
|
<h2>Upload von Fotos durch Nutzer</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
24
backend/templates/faq.html
Normal file
24
backend/templates/faq.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="card faq-card">
|
||||||
|
<h1>{{ t('faq') }}</h1>
|
||||||
|
<p class="faq-intro">{{ t('faq_intro') }}</p>
|
||||||
|
|
||||||
|
<div class="faq-list">
|
||||||
|
{% for question_key, answer_key in faq_items %}
|
||||||
|
<details class="faq-item">
|
||||||
|
<summary>
|
||||||
|
<span>{{ t(question_key) }}</span>
|
||||||
|
<span class="faq-toggle" aria-hidden="true"></span>
|
||||||
|
</summary>
|
||||||
|
<div class="faq-answer">
|
||||||
|
<p>{{ t(answer_key) }}</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-ghost faq-tour-restart" href="{{ url_for('welcome', tour=1) }}">{{ t('tour_restart') }}</a>
|
||||||
|
</section>
|
||||||
|
<script src="{{ url_for('static', filename='faq.js', v='20260724a') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<div class="hotel-actions">
|
<div class="hotel-actions">
|
||||||
<a class="btn" href="{{ hotel.website_url }}" target="_blank" rel="noopener">{{ t('hotel_visit_website') }}</a>
|
<a class="btn" href="{{ hotel.website_url }}" target="_blank" rel="noopener">{{ t('hotel_visit_website') }}</a>
|
||||||
<a
|
<a
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost route-location-btn"
|
||||||
href="{{ hotel.current_route_url }}"
|
href="{{ hotel.current_route_url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
@@ -199,6 +199,7 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
>
|
>
|
||||||
|
<span class="gift-money-label">{{ t('gifts_money_label') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="gift-caption">{{ t('gifts_caption') }}</p>
|
<p class="gift-caption">{{ t('gifts_caption') }}</p>
|
||||||
<p>{{ t('gifts_text') }}</p>
|
<p>{{ t('gifts_text') }}</p>
|
||||||
@@ -318,7 +319,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="map-embed-target" data-map-embed-target></div>
|
<div class="map-embed-target" data-map-embed-target></div>
|
||||||
<div class="location-actions">
|
<div class="location-actions">
|
||||||
<a class="btn btn-ghost" href="{{ location_route_url }}" target="_blank" rel="noopener" data-location-route-live>{{ t('route_from_current') }}</a>
|
<a class="btn btn-ghost route-location-btn" href="{{ location_route_url }}" target="_blank" rel="noopener" data-location-route-live>{{ t('route_from_current') }}</a>
|
||||||
<a class="btn" href="{{ location_website_url }}" target="_blank" rel="noopener">{{ t('visit_location') }}</a>
|
<a class="btn" href="{{ location_website_url }}" target="_blank" rel="noopener">{{ t('visit_location') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,4 +11,87 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<p class="hero-hint-below">{{ welcome_hint }}</p>
|
<p class="hero-hint-below">{{ welcome_hint }}</p>
|
||||||
|
|
||||||
|
{% if show_onboarding %}
|
||||||
|
<dialog
|
||||||
|
class="onboarding-dialog"
|
||||||
|
data-onboarding-dialog
|
||||||
|
data-complete-url="{{ url_for('complete_onboarding') }}"
|
||||||
|
data-progress-template="{{ t('tour_progress') }}"
|
||||||
|
aria-labelledby="onboarding-title"
|
||||||
|
>
|
||||||
|
<div class="onboarding-shell">
|
||||||
|
<div class="onboarding-progress" aria-hidden="true">
|
||||||
|
{% for _ in range(6) %}<span data-onboarding-dot></span>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<p class="onboarding-step-label" data-onboarding-progress></p>
|
||||||
|
<h2 id="onboarding-title">{{ t('tour_title') }}</h2>
|
||||||
|
|
||||||
|
<div class="onboarding-slides">
|
||||||
|
<section class="onboarding-slide" data-onboarding-slide>
|
||||||
|
<span class="onboarding-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" focusable="false">
|
||||||
|
<path d="M3 3h8v8H3V3zm10 0h8v5h-8V3zM3 13h5v8H3v-8zm7 0h11v8H10v-8z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h3>{{ t('tour_dashboard_title') }}</h3>
|
||||||
|
<p>{{ t('tour_dashboard_text') }}</p>
|
||||||
|
</section>
|
||||||
|
<section class="onboarding-slide" data-onboarding-slide hidden>
|
||||||
|
<span class="onboarding-icon" aria-hidden="true">✓</span>
|
||||||
|
<h3>{{ t('tour_rsvp_title') }}</h3>
|
||||||
|
<p>{{ t('tour_rsvp_text') }}</p>
|
||||||
|
</section>
|
||||||
|
<section class="onboarding-slide" data-onboarding-slide hidden>
|
||||||
|
<span class="onboarding-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" focusable="false">
|
||||||
|
<path d="M9 4 7.6 6H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-3.6L15 4H9zm3 4.2A4.8 4.8 0 1 1 7.2 13 4.8 4.8 0 0 1 12 8.2zm0 2A2.8 2.8 0 1 0 14.8 13 2.8 2.8 0 0 0 12 10.2z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h3>{{ t('tour_photos_title') }}</h3>
|
||||||
|
<p>{{ t('tour_photos_text') }}</p>
|
||||||
|
</section>
|
||||||
|
<section class="onboarding-slide" data-onboarding-slide hidden>
|
||||||
|
<span class="onboarding-icon onboarding-icon-route" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" focusable="false">
|
||||||
|
<circle cx="11" cy="13" r="7.5" />
|
||||||
|
<circle cx="11" cy="13" r="3.5" />
|
||||||
|
<path d="M20.5 3.5 11 13M17 7l.2-3.6M17 7l3.6-.2" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h3>{{ t('tour_routes_title') }}</h3>
|
||||||
|
<p>{{ t('tour_routes_text') }}</p>
|
||||||
|
</section>
|
||||||
|
<section class="onboarding-slide" data-onboarding-slide hidden>
|
||||||
|
<span class="onboarding-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" focusable="false">
|
||||||
|
<path d="M12 5.5A8.4 8.4 0 0 0 6.5 3.4 8.6 8.6 0 0 0 2 4.7v14.1a.75.75 0 0 0 1.02.7A8.1 8.1 0 0 1 6.5 18.6a7.1 7.1 0 0 1 4.98 2 .75.75 0 0 0 1.04 0 7.1 7.1 0 0 1 4.98-2 8.1 8.1 0 0 1 3.48.9.75.75 0 0 0 1.02-.7V4.7a8.6 8.6 0 0 0-4.5-1.3A8.4 8.4 0 0 0 12 5.5zm-1 12.9a8.7 8.7 0 0 0-4.5-1.3 9.6 9.6 0 0 0-3 .5V5.7a7.4 7.4 0 0 1 3-.8A6.9 6.9 0 0 1 11 6.7zm9.5-.8a9.6 9.6 0 0 0-3-.5 8.7 8.7 0 0 0-4.5 1.3V6.7a6.9 6.9 0 0 1 4.5-1.8 7.4 7.4 0 0 1 3 .8z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h3>{{ t('tour_translator_title') }}</h3>
|
||||||
|
<p>{{ t('tour_translator_text') }}</p>
|
||||||
|
</section>
|
||||||
|
<section class="onboarding-slide" data-onboarding-slide hidden>
|
||||||
|
<span class="onboarding-icon" aria-hidden="true">?</span>
|
||||||
|
<h3>{{ t('tour_faq_title') }}</h3>
|
||||||
|
<p>{{ t('tour_faq_text') }}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="onboarding-actions">
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-ghost" type="button" data-onboarding-back hidden>{{ t('tour_back') }}</button>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
type="button"
|
||||||
|
data-onboarding-next
|
||||||
|
data-next-label="{{ t('tour_next') }}"
|
||||||
|
data-finish-label="{{ t('tour_finish') }}"
|
||||||
|
>{{ t('tour_next') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
{% endif %}
|
||||||
|
<script src="{{ url_for('static', filename='onboarding.js', v='20260724a') }}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user