From 3cd7b7899512b07b333bca86a128fc13a0486432 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 1 Mar 2026 20:51:26 +0000 Subject: [PATCH] Massiv + --- README.md | 38 -- backend/__pycache__/app.cpython-312.pyc | Bin 30057 -> 38152 bytes backend/app.py | 496 +++++++++++++----- backend/app.sqlite3 | Bin 20480 -> 45056 bytes .../static/assets/location-map-preview.svg | 42 ++ backend/static/styles.css | 226 +++++++- backend/templates/base.html | 5 + backend/templates/guest_area.html | 2 + backend/templates/host_area.html | 41 +- backend/templates/info.html | 55 +- backend/templates/login.html | 8 +- backend/templates/rsvp.html | 84 ++- backend/templates/upload.html | 116 ++-- data/db/app.sqlite3 | Bin 24576 -> 49152 bytes docker-compose.yml | 4 + 15 files changed, 859 insertions(+), 258 deletions(-) create mode 100644 backend/static/assets/location-map-preview.svg diff --git a/README.md b/README.md index c88a9aa..d58eed9 100644 --- a/README.md +++ b/README.md @@ -511,154 +511,116 @@ Adminnutzer: Bubus (Admingruppe) Mitglieder: Svenja, Dominic -Passwort: Bubu!Herz24# Standardnutzer: Remi Mitglieder: Remi -Passwort: Remi#Ring24! Chantal Mitglieder: Chantal -Passwort: Chan!Tanz24# Madeleine Mitglieder: Madeleine -Passwort: Madi$Rose24! Julie & Daniel Mitglieder: Julie, Daniel -Passwort: Juli&Dan24!# Tim & Sophie Mitglieder: Tim, Sophie -Passwort: Tim+Sofi24!# Marcel & Kathrin Mitglieder: Marcel, Kathrin -Passwort: Marc&Kath24# Familie Olsem Mitglieder: Laura, Sven, Lena, Finn -Passwort: Olse!Fam24#? Maxime Mitglieder: Maxime, Freund -Passwort: Maxi#Love24! Familie Löster Mitglieder: Claudia, Mario, Mélodie -Passwort: Loes@Ring24# Familie Thiels Mitglieder: Matthias, Opa Bernd, Oma Heidi -Passwort: Thie$Fest24! Familie Gollor Mitglieder: Michael, Christin, Bruno -Passwort: Goll%Herz24! Monika Mitglieder: Monika -Passwort: Moni!Rose24# Familie Konrad Mitglieder: Michael, Sandra, Christoph, Alexander -Passwort: Konr#Fest24! Mark Mitglieder: Mark -Passwort: Mark!Gold24# Elias Mitglieder: Elias -Passwort: Elia$Ring24! Milan Mitglieder: Milan -Passwort: Mila#Tanz24! Familie Wolff Mitglieder: Anja, Bodo -Passwort: Wolf!Herz24# Anna & Leon Mitglieder: Anna, Leon -Passwort: Anna&Leo24!# Aryan Mitglieder: Aryan -Passwort: Arya!Fest24# Sebastian Mitglieder: Sebastian, Olivia -Passwort: Seba$Ring24! Leander & Heni Mitglieder: Leander, Heni -Passwort: Lea&Heni24!# Flo Mitglieder: Flo -Passwort: Flo!Liebe24# Nico & Pia Mitglieder: Nico, Pia -Passwort: Nico&Pia24!# Kiki Mitglieder: Kiki -Passwort: Kiki!Rose24# Lana & Eric Mitglieder: Lana, Eric -Passwort: Lan&Eric24!# Britta Mitglieder: Britta -Passwort: Brit!Tanz24# Holzi Mitglieder: Holzi -Passwort: Holz!Ring24# Eirene Mitglieder: Eirene -Passwort: Eire$Fest24! Family Hynes Mitglieder: Steven, Martha, William, Tim, Steven Jr. -Passwort: Hyne#Love24! Timbo Mitglieder: Timbo -Passwort: Timb!Rose24# Karen & Jay Mitglieder: Karen, Jay -Passwort: Kare&Jay24!# Alina Mitglieder: Alina -Passwort: Alin!Gold24# Max Mitglieder: Max -Passwort: Max!Liebe24# Paul & Alix Mitglieder: Paul, Alix -Passwort: Paul&Alx24!# Alfred & Nadia Mitglieder: Alfred, Nadia -Passwort: Alfr&Nad24!# Anne-Marie & Erny Mitglieder: Anne-Marie, Erny -Passwort: Anne&Ern24!# Familie Kieffer Mitglieder: Anny, John, Jana -Passwort: Kief!Fest24# diff --git a/backend/__pycache__/app.cpython-312.pyc b/backend/__pycache__/app.cpython-312.pyc index ea7ac04defe78c7597c1620cab451d4eb8c36644..f4b4dc30ea3213635f7ed78ee66f0f24f4c0cd74 100644 GIT binary patch literal 38152 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 delta 13983 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 str: @app.context_processor def inject_common() -> dict: + role = session.get("role", "guest") + group_id = session.get("group_id") return { "t": t, "lang": get_lang(), - "guest_name": session.get("guest_name"), - "guest_id": session.get("guest_id"), - "is_host": bool(session.get("is_host")), + "guest_name": session.get("group_name"), + "guest_id": group_id, + "group_name": session.get("group_name"), + "group_id": group_id, + "is_host": role == "admin", + "is_admin": role == "admin", "location_name": app.config["LOCATION_NAME"], "location_address": app.config["LOCATION_ADDRESS"], "location_website_url": app.config["LOCATION_WEBSITE_URL"], @@ -268,6 +429,11 @@ def close_db(_error) -> None: db.close() +def table_columns(conn: sqlite3.Connection, table_name: str) -> set[str]: + rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall() + return {str(row[1]) for row in rows} + + def init_db() -> None: db_path = app.config["DB_PATH"] db_dir = os.path.dirname(db_path) @@ -277,15 +443,32 @@ def init_db() -> None: with sqlite3.connect(db_path) as conn: conn.execute( """ - CREATE TABLE IF NOT EXISTS guests ( + CREATE TABLE IF NOT EXISTS groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, - attending INTEGER, - plus_one INTEGER NOT NULL DEFAULT 0, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'guest', created_at TEXT NOT NULL ) """ ) + + conn.execute( + """ + CREATE TABLE IF NOT EXISTS group_members ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL, + name TEXT NOT NULL, + attending INTEGER, + child_age INTEGER, + requires_age INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + FOREIGN KEY(group_id) REFERENCES groups(id), + UNIQUE(group_id, name) + ) + """ + ) + conn.execute( """ CREATE TABLE IF NOT EXISTS uploads ( @@ -293,23 +476,83 @@ def init_db() -> None: filename TEXT NOT NULL, uploaded_by INTEGER NOT NULL, uploaded_at TEXT NOT NULL, - FOREIGN KEY(uploaded_by) REFERENCES guests(id) + FOREIGN KEY(uploaded_by) REFERENCES groups(id) ) """ ) + + member_cols = table_columns(conn, "group_members") + if "child_age" not in member_cols: + conn.execute("ALTER TABLE group_members ADD COLUMN child_age INTEGER") + if "requires_age" not in member_cols: + conn.execute("ALTER TABLE group_members ADD COLUMN requires_age INTEGER NOT NULL DEFAULT 0") + if "created_at" not in member_cols: + conn.execute("ALTER TABLE group_members ADD COLUMN created_at TEXT NOT NULL DEFAULT ''") + + group_cols = table_columns(conn, "groups") + if "role" not in group_cols: + conn.execute("ALTER TABLE groups ADD COLUMN role TEXT NOT NULL DEFAULT 'guest'") + conn.commit() +def seed_default_groups() -> None: + db = get_db() + existing = db.execute("SELECT COUNT(*) AS c FROM groups").fetchone() + if int(existing["c"] or 0) > 0: + return + + now = datetime.utcnow().isoformat() + for entry in DEFAULT_INVITATION_GROUPS: + password_hash = generate_password_hash(entry["password"]) + cursor = db.execute( + "INSERT INTO groups (name, password_hash, role, created_at) VALUES (?, ?, ?, ?)", + (entry["name"], password_hash, entry["role"], now), + ) + group_id = int(cursor.lastrowid) + member_rows = [] + for member_name in entry["members"]: + member_rows.append( + ( + group_id, + member_name, + 1 if member_name in AGE_REQUIRED_NAMES else 0, + now, + ) + ) + + db.executemany( + """ + INSERT INTO group_members (group_id, name, requires_age, created_at) + VALUES (?, ?, ?, ?) + """, + member_rows, + ) + + db.commit() + + def login_required(view): @wraps(view) def wrapped(*args, **kwargs): - if "guest_id" not in session: + if "group_id" not in session: return redirect(url_for("landing")) return view(*args, **kwargs) return wrapped +def admin_required(view): + @wraps(view) + def wrapped(*args, **kwargs): + if session.get("role") != "admin": + flash(t("flash_admin_only")) + return redirect(url_for("guest_area")) + return view(*args, **kwargs) + + return wrapped + + @app.errorhandler(RequestEntityTooLarge) def handle_request_too_large(_error): max_mb = max(1, int(app.config.get("MAX_CONTENT_LENGTH", 0)) // (1024 * 1024)) @@ -323,21 +566,6 @@ def is_allowed_file(filename: str) -> bool: return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS -def upsert_guest(name: str) -> int: - now = datetime.utcnow().isoformat() - db = get_db() - row = db.execute("SELECT id FROM guests WHERE name = ?", (name,)).fetchone() - if row: - return int(row["id"]) - - cursor = db.execute( - "INSERT INTO guests (name, created_at) VALUES (?, ?)", - (name, now), - ) - db.commit() - return int(cursor.lastrowid) - - @app.get("/health") def health(): return {"status": "ok"} @@ -345,28 +573,33 @@ def health(): @app.get("/") def landing(): - if "guest_id" in session: + if "group_id" in session: return redirect(url_for("welcome")) return render_template("login.html") @app.post("/login") def login(): - name = (request.form.get("name") or "").strip() - event_password = request.form.get("event_password") or "" + group_name = (request.form.get("group_name") or "").strip() + group_password = request.form.get("group_password") or "" - if not name: - flash(t("flash_enter_name")) + if not group_name: + flash(t("flash_enter_group_name")) return redirect(url_for("landing")) - if event_password != app.config["EVENT_PASSWORD"]: - flash(t("flash_invalid_password")) + db = get_db() + group = db.execute( + "SELECT id, name, password_hash, role FROM groups WHERE LOWER(name) = LOWER(?)", + (group_name,), + ).fetchone() + + if group is None or not check_password_hash(str(group["password_hash"]), group_password): + flash(t("flash_invalid_group_login")) return redirect(url_for("landing")) - guest_id = upsert_guest(name) - session["guest_id"] = guest_id - session["guest_name"] = name - session.pop("is_host", None) + session["group_id"] = int(group["id"]) + session["group_name"] = str(group["name"]) + session["role"] = str(group["role"] or "guest") return redirect(url_for("welcome")) @@ -396,21 +629,10 @@ def guest_area(): return render_template("guest_area.html") -@app.route("/gastgeberbereich", methods=["GET", "POST"]) +@app.route("/gastgeberbereich", methods=["GET"]) @login_required +@admin_required def host_area(): - if request.method == "POST": - host_password = request.form.get("host_password") or "" - expected = app.config.get("HOST_PASSWORD", "") - if not expected or not compare_digest(host_password, expected): - flash(t("flash_invalid_host_password")) - return redirect(url_for("host_area")) - session["is_host"] = True - return redirect(url_for("host_area")) - - if not session.get("is_host"): - return render_template("host_area.html", unlocked=False) - db = get_db() stats_row = db.execute( """ @@ -418,16 +640,17 @@ def host_area(): COUNT(*) AS total_guests, SUM(CASE WHEN attending = 1 THEN 1 ELSE 0 END) AS attending_yes, SUM(CASE WHEN attending = 0 THEN 1 ELSE 0 END) AS attending_no, - SUM(CASE WHEN attending IS NULL THEN 1 ELSE 0 END) AS attending_open, - SUM(CASE WHEN attending = 1 AND plus_one = 1 THEN 1 ELSE 0 END) AS plus_one_total - FROM guests + SUM(CASE WHEN attending IS NULL THEN 1 ELSE 0 END) AS attending_open + FROM group_members """ ).fetchone() - guests = db.execute( + + members = db.execute( """ - SELECT name, attending, plus_one - FROM guests - ORDER BY name COLLATE NOCASE ASC + SELECT groups.name AS group_name, group_members.name, group_members.attending, group_members.child_age, group_members.requires_age + FROM group_members + JOIN groups ON groups.id = group_members.group_id + ORDER BY groups.name COLLATE NOCASE ASC, group_members.name COLLATE NOCASE ASC """ ).fetchall() @@ -436,9 +659,8 @@ def host_area(): "attending_yes": int(stats_row["attending_yes"] or 0), "attending_no": int(stats_row["attending_no"] or 0), "attending_open": int(stats_row["attending_open"] or 0), - "plus_one_total": int(stats_row["plus_one_total"] or 0), } - return render_template("host_area.html", unlocked=True, stats=stats, guests=guests) + return render_template("host_area.html", stats=stats, members=members) @app.get("/dashboard") @@ -451,33 +673,59 @@ def dashboard(): @login_required def rsvp(): db = get_db() + current_group_id = int(session["group_id"]) + + members = db.execute( + """ + SELECT id, name, attending, child_age, requires_age + FROM group_members + WHERE group_id = ? + ORDER BY id ASC + """, + (current_group_id,), + ).fetchall() if request.method == "POST": - attending_raw = request.form.get("attending") - plus_one = 1 if request.form.get("plus_one") == "on" else 0 + updates = [] + for member in members: + member_id = int(member["id"]) + attending_raw = request.form.get(f"attending_{member_id}") - if attending_raw not in {"yes", "no"}: - flash(t("flash_rsvp_select")) - return redirect(url_for("rsvp")) + if attending_raw not in {"yes", "no"}: + flash(t("flash_rsvp_select")) + return redirect(url_for("rsvp")) - attending = 1 if attending_raw == "yes" else 0 - if not attending: - plus_one = 0 + attending = 1 if attending_raw == "yes" else 0 + child_age = None + if attending == 1 and int(member["requires_age"] or 0) == 1: + age_raw = (request.form.get(f"age_{member_id}") or "").strip() + if not age_raw: + flash(t("flash_rsvp_age_missing").format(name=member["name"])) + return redirect(url_for("rsvp")) + if not age_raw.isdigit(): + flash(t("flash_rsvp_age_invalid").format(name=member["name"])) + return redirect(url_for("rsvp")) + age_value = int(age_raw) + if age_value < 0 or age_value > 17: + flash(t("flash_rsvp_age_invalid").format(name=member["name"])) + return redirect(url_for("rsvp")) + child_age = age_value - db.execute( - "UPDATE guests SET attending = ?, plus_one = ? WHERE id = ?", - (attending, plus_one, session["guest_id"]), + updates.append((attending, child_age, member_id, current_group_id)) + + db.executemany( + """ + UPDATE group_members + SET attending = ?, child_age = ? + WHERE id = ? AND group_id = ? + """, + updates, ) db.commit() flash(t("flash_rsvp_saved")) return redirect(url_for("rsvp")) - guest = db.execute( - "SELECT attending, plus_one FROM guests WHERE id = ?", - (session["guest_id"],), - ).fetchone() - - return render_template("rsvp.html", guest=guest) + return render_template("rsvp.html", members=members) @app.route("/upload", methods=["GET", "POST"]) @@ -495,7 +743,6 @@ def upload(): flash(t("flash_allowed_types")) return redirect(url_for("upload")) mime_type = (file.mimetype or "").lower() - # Some mobile browsers may omit or vary MIME types for valid images. if mime_type and mime_type not in ALLOWED_MIME_TYPES: flash(t("flash_allowed_types")) return redirect(url_for("upload")) @@ -513,7 +760,7 @@ def upload(): ext = safe_name.rsplit(".", 1)[1].lower() stored_name = f"{uuid.uuid4().hex}.{ext}" file.save(os.path.join(upload_dir, stored_name)) - upload_rows.append((stored_name, session["guest_id"], now)) + upload_rows.append((stored_name, int(session["group_id"]), now)) db.executemany( "INSERT INTO uploads (filename, uploaded_by, uploaded_at) VALUES (?, ?, ?)", @@ -539,9 +786,10 @@ def gallery(): db = get_db() images = db.execute( """ - SELECT uploads.id, uploads.filename, uploads.uploaded_by, uploads.uploaded_at, guests.name AS uploaded_by_name + SELECT uploads.id, uploads.filename, uploads.uploaded_by, uploads.uploaded_at, + COALESCE(groups.name, 'Unbekannt') AS uploaded_by_name FROM uploads - JOIN guests ON guests.id = uploads.uploaded_by + LEFT JOIN groups ON groups.id = uploads.uploaded_by ORDER BY uploads.id DESC """ ).fetchall() @@ -559,9 +807,9 @@ def delete_image(image_id: int): if image is None: return redirect(url_for("gallery")) - current_guest_id = int(session["guest_id"]) - is_host = bool(session.get("is_host")) - if not is_host and int(image["uploaded_by"]) != current_guest_id: + current_group_id = int(session["group_id"]) + is_admin = session.get("role") == "admin" + if not is_admin and int(image["uploaded_by"]) != current_group_id: flash(t("flash_delete_not_allowed")) return redirect(url_for("gallery")) @@ -608,6 +856,8 @@ def impressum(): init_db() +with app.app_context(): + seed_default_groups() if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) diff --git a/backend/app.sqlite3 b/backend/app.sqlite3 index 711cf4181eddb79d3804551cd2b15f749db045da..62f8ed5dbf9864d94a86807807f8c002b4bfaa95 100644 GIT binary patch literal 45056 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 Qt^^MkA>M{g*iZEQ7Yn?k@Bjb+ delta 71 zcmZp8z|^pSae}lU3j+fKD-go~(?lI(Q5FWhvQA$99}FzK3JiR!_)qgi@G5K;6j;Nv US%LSV(q=}5FZ`Ro+H(s40Os8gGXMYp diff --git a/backend/static/assets/location-map-preview.svg b/backend/static/assets/location-map-preview.svg new file mode 100644 index 0000000..8f2acb6 --- /dev/null +++ b/backend/static/assets/location-map-preview.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Klostermühle Kiedrich + An d. Klostermühle 3, 65399 Kiedrich + Kartenvorschau + + + + + + + + diff --git a/backend/static/styles.css b/backend/static/styles.css index 5bd1d33..62d8664 100644 --- a/backend/static/styles.css +++ b/backend/static/styles.css @@ -55,6 +55,8 @@ h3 { .toolbar { display: flex; gap: 0.4rem; + flex-wrap: wrap; + justify-content: flex-end; } .container { @@ -185,6 +187,89 @@ input[type="file"]:focus { gap: 0.45rem; } +.member-card { + border: 1px solid rgba(39, 66, 53, 0.14); + border-radius: 14px; + padding: 0.9rem; + background: #fff; +} + +.member-name { + margin: 0 0 0.45rem; + font-size: 1.15rem; +} + +.member-choice-row { + display: grid; + gap: 0.5rem; +} + +.member-age-wrap { + display: none; + margin-top: 0.5rem; +} + +.member-age-wrap.is-visible { + display: grid; +} + +.member-age-wrap small { + color: rgba(31, 31, 31, 0.68); +} + +.upload-card { + max-width: 760px; + margin-inline: auto; +} + +.upload-intro { + margin-top: -0.15rem; + margin-bottom: 0.3rem; + color: rgba(31, 31, 31, 0.82); +} + +.upload-picker { + display: grid; + gap: 0.25rem; + border: 1px dashed rgba(39, 66, 53, 0.35); + border-radius: 14px; + padding: 0.9rem 1rem; + background: rgba(255, 255, 255, 0.88); + cursor: pointer; + transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; +} + +.upload-picker:hover { + border-color: rgba(39, 66, 53, 0.55); + background: #fff; +} + +.upload-picker:focus-within { + border-color: rgba(184, 145, 76, 0.88); + box-shadow: 0 0 0 3px rgba(184, 145, 76, 0.18); +} + +.upload-picker-title { + font-weight: 700; +} + +.upload-picker-subtitle { + color: rgba(31, 31, 31, 0.66); + font-size: 0.92rem; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .upload-hint { margin: -0.2rem 0 0.1rem; color: rgba(31, 31, 31, 0.72); @@ -193,6 +278,7 @@ input[type="file"]:focus { #extra-file-inputs { display: grid; + grid-template-columns: 1fr; gap: 0.65rem; } @@ -200,17 +286,61 @@ input[type="file"]:focus { display: grid; } +.upload-add-wrap { + display: grid; +} + +.upload-add-wrap.is-hidden { + display: none; +} + .upload-count { margin: 0.2rem 0 0; font-weight: 600; } +.upload-ready { + margin: -0.2rem 0 0; + color: rgba(39, 66, 53, 0.88); + font-weight: 600; + display: none; +} + +.upload-ready.is-visible { + display: block; +} + .upload-file-list { margin: 0; - padding-left: 1.1rem; + padding: 0; + list-style: none; color: rgba(31, 31, 31, 0.82); - max-height: 9rem; + max-height: 10rem; overflow: auto; + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.upload-file-list li { + display: inline-flex; + align-items: center; + gap: 0.42rem; + border: 1px solid rgba(39, 66, 53, 0.2); + border-radius: 999px; + padding: 0.32rem 0.68rem; + background: rgba(255, 255, 255, 0.92); + font-size: 0.9rem; +} + +.upload-file-remove { + border: 0; + background: transparent; + color: rgba(39, 66, 53, 0.82); + font-size: 1rem; + line-height: 1; + cursor: pointer; + padding: 0; } .btn { @@ -233,6 +363,14 @@ input[type="file"]:focus { box-shadow: 0 10px 24px rgba(39, 66, 53, 0.2); } +.btn:disabled { + cursor: not-allowed; + background: rgba(39, 66, 53, 0.45); + box-shadow: none; + transform: none; + filter: none; +} + .btn-ghost { color: var(--forest); background: transparent; @@ -250,6 +388,16 @@ input[type="file"]:focus { font-weight: 600; } +.toolbar-nav-btn { + padding: 0.42rem 0.62rem; +} + +.toolbar-nav-btn svg { + width: 0.9rem; + height: 0.9rem; + fill: currentColor; +} + .flash { padding: 0.7rem 0.9rem; border-radius: 10px; @@ -501,6 +649,80 @@ input[type="file"]:focus { margin: 0.8rem 0; } +.map-consent { + display: grid; + gap: 0.7rem; + padding: 0.9rem; + border: 1px solid rgba(39, 66, 53, 0.14); + border-radius: 12px; + background: rgba(255, 255, 255, 0.7); +} + +.map-consent p { + margin: 0; +} + +.map-preview { + width: 100%; + aspect-ratio: 16 / 7; + min-height: 0; + 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)), + var(--map-preview-image); + background-size: contain; + background-position: top left; + background-repeat: no-repeat; + display: grid; + place-items: center; + padding: 1rem; + transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease; +} + +.map-preview:hover { + transform: translateY(-1px); + box-shadow: 0 10px 24px rgba(39, 66, 53, 0.14); + filter: brightness(1.02); +} + +.map-preview-overlay { + font-weight: 700; + color: #fff; + background: rgba(39, 66, 53, 0.9); + border-radius: 999px; + padding: 0.48rem 0.82rem; +} + +.map-embed-target:not(:empty) { + margin-top: 0.2rem; +} + +.location-actions { + display: flex; + justify-content: flex-start; + margin-top: 0.55rem; + gap: 0.55rem; + flex-wrap: wrap; +} + +.location-actions .btn { + width: auto; + min-width: 0; + white-space: nowrap; + justify-content: center; + padding: 0.58rem 0.88rem; + font-size: 0.98rem; +} + +@media (max-width: 640px) { + .location-actions { + justify-content: flex-start; + } +} + .site-footer { border-top: 1px solid rgba(39, 66, 53, 0.12); background: rgba(255, 255, 255, 0.64); diff --git a/backend/templates/base.html b/backend/templates/base.html index 42b7337..bb2c5a9 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -22,6 +22,11 @@ {% if guest_name %} + + +
diff --git a/backend/templates/guest_area.html b/backend/templates/guest_area.html index c5bf85e..b26431f 100644 --- a/backend/templates/guest_area.html +++ b/backend/templates/guest_area.html @@ -13,6 +13,8 @@ {{ t('hotels') }} {{ t('taxi') }} {{ t('location') }} + {% if is_admin %} {{ t('host_area') }} + {% endif %} {% endblock %} diff --git a/backend/templates/host_area.html b/backend/templates/host_area.html index 865cf9f..f3bc664 100644 --- a/backend/templates/host_area.html +++ b/backend/templates/host_area.html @@ -1,21 +1,5 @@ {% extends 'base.html' %} {% block content %} -
-

{{ t('host_access_title') }}

-

{{ t('host_access_note') }}

-
- -{% if not unlocked %} -
-
- - -
-
-{% else %}

{{ t('total_guests') }}

@@ -33,10 +17,6 @@

{{ t('attending_open') }}

{{ stats.attending_open }}

-
-

{{ t('plus_one_total') }}

-

{{ stats.plus_one_total }}

-
@@ -45,29 +25,31 @@ - + + - + - {% for guest in guests %} + {% for member in members %} - + + @@ -76,5 +58,4 @@
{{ t('host_table_name') }}{{ t('host_table_group') }}{{ t('host_table_member') }} {{ t('host_table_status') }}{{ t('host_table_plus_one') }}{{ t('host_table_age') }}
{{ guest["name"] }}{{ member["group_name"] }}{{ member["name"] }} - {% if guest["attending"] == 1 %} + {% if member["attending"] == 1 %} {{ t('status_yes') }} - {% elif guest["attending"] == 0 %} + {% elif member["attending"] == 0 %} {{ t('status_no') }} {% else %} {{ t('status_open') }} {% endif %} - {% if guest["attending"] == 1 and guest["plus_one"] == 1 %} - {{ t('yes') }} + {% if member["child_age"] is not none %} + {{ member["child_age"] }} {% else %} - {{ t('no') }} + - {% endif %}
-{% endif %} {% endblock %} diff --git a/backend/templates/info.html b/backend/templates/info.html index a28a0c0..223f32b 100644 --- a/backend/templates/info.html +++ b/backend/templates/info.html @@ -17,15 +17,54 @@ {% elif page == 'location' %}

{{ location_name }}

{{ location_address }}

-
- + - {{ t('visit_location') }} + + {% endif %} {% endblock %} diff --git a/backend/templates/login.html b/backend/templates/login.html index dded186..a02c4a5 100644 --- a/backend/templates/login.html +++ b/backend/templates/login.html @@ -10,13 +10,13 @@

{{ t('login') }}

diff --git a/backend/templates/rsvp.html b/backend/templates/rsvp.html index efa8acd..9dda343 100644 --- a/backend/templates/rsvp.html +++ b/backend/templates/rsvp.html @@ -1,24 +1,82 @@ {% extends 'base.html' %} {% block content %} -
+

{{ t('rsvp') }}

+

{{ t('rsvp_members_intro') }}

- + {% for member in members %} +
+

{{ member['name'] }}

+
+ - + +
- + {% if member['requires_age'] == 1 %} + + {% endif %} +
+ {% endfor %}
+ + {% endblock %} diff --git a/backend/templates/upload.html b/backend/templates/upload.html index 003e3e0..d0c3684 100644 --- a/backend/templates/upload.html +++ b/backend/templates/upload.html @@ -1,77 +1,113 @@ {% extends 'base.html' %} {% block content %} -
+

{{ t('upload') }}

-
-
{% endblock %} diff --git a/data/db/app.sqlite3 b/data/db/app.sqlite3 index aa2d81d1ef7b2bfdfae7eb55b508a5d571e4894d..edc7c167b4b05fbaef149d17b907206469cf2b99 100644 GIT binary patch literal 49152 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 delta 268 zcmXwxK}y3w7)B=v5oE|rTMB|%=(?D6=ATJsl9h!Xq2j`glSxw$#g@>G8*>DE2v3lm zD|fv?Hy$A1x(6^Ke(*LQkJmI_^Xa^W!wEv@r00L$?n<0ZQ2eq9$P@B$hsd66g9~zv zyLLF(;I_l(W8CX5?GQcE-}j3K*!4nZyMBB&Wj$(-)4}D~rL{vpXicZ&o4k{I5(bA} zzN4@6k&kXqXC?58XG$`tN~x4JoFxUY%4jP<>nwg)FIT`IjhKiSUvS71l|vdUt+`0i x-0{(kGc(pGZfyoKNo3zEB$H+-%0gHv0C=BSd4Cy*c(wq-a|t=