Feat: Fotofilter + Fotobox/graf user
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Aktueller Stand der Login-Gruppen aus `backend/app.py` (`DEFAULT_INVITATION_GROUPS`).
|
Aktueller Stand der Login-Gruppen aus `backend/app.py` (`DEFAULT_INVITATION_GROUPS`).
|
||||||
|
|
||||||
Anzahl Gruppen: **41**
|
Anzahl Gruppen: **43**
|
||||||
|
|
||||||
| Username | Rolle | Passwort | Members |
|
| Username | Rolle | Passwort | Members |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -47,5 +47,7 @@ Anzahl Gruppen: **41**
|
|||||||
| Familie Kieffer | guest | `Kief!Fest24#` | Anny, John |
|
| Familie Kieffer | guest | `Kief!Fest24#` | Anny, John |
|
||||||
| Peter | guest | `Peter!Fest26#` | Peter |
|
| Peter | guest | `Peter!Fest26#` | Peter |
|
||||||
| Johanna | guest | `Joha!Fest26#` | Johanna |
|
| Johanna | guest | `Joha!Fest26#` | Johanna |
|
||||||
|
| Fotograf | guest | `Foto!Graf26#` | – (nur Foto-Upload) |
|
||||||
|
| Fotobox | guest | `Foto!Box26#` | – (nur Foto-Upload) |
|
||||||
|
|
||||||
Hinweis: Diese Datei enthält Klartext-Passwörter und sollte nicht öffentlich geteilt werden.
|
Hinweis: Diese Datei enthält Klartext-Passwörter und sollte nicht öffentlich geteilt werden.
|
||||||
|
|||||||
+57
-13
@@ -212,6 +212,18 @@ DEFAULT_INVITATION_GROUPS = [
|
|||||||
},
|
},
|
||||||
{"name": "Peter", "password": "Peter!Fest26#", "role": "guest", "members": ["Peter"]},
|
{"name": "Peter", "password": "Peter!Fest26#", "role": "guest", "members": ["Peter"]},
|
||||||
{"name": "Johanna", "password": "Joha!Fest26#", "role": "guest", "members": ["Johanna"]},
|
{"name": "Johanna", "password": "Joha!Fest26#", "role": "guest", "members": ["Johanna"]},
|
||||||
|
{
|
||||||
|
"name": "Fotograf",
|
||||||
|
"password": os.environ.get("PHOTOGRAPHER_PASSWORD", "Foto!Graf26#"),
|
||||||
|
"role": "guest",
|
||||||
|
"members": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fotobox",
|
||||||
|
"password": os.environ.get("PHOTOBOOTH_PASSWORD", "Foto!Box26#"),
|
||||||
|
"role": "guest",
|
||||||
|
"members": [],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
AGE_REQUIRED_NAMES = {"Lena", "Finn", "Fin", "Bruno"}
|
AGE_REQUIRED_NAMES = {"Lena", "Finn", "Fin", "Bruno"}
|
||||||
|
|
||||||
@@ -453,7 +465,15 @@ TEXTS = {
|
|||||||
"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}",
|
||||||
|
"gallery_uploaded_by_prefix": "von",
|
||||||
"gallery_empty": "Noch keine Bilder vorhanden.",
|
"gallery_empty": "Noch keine Bilder vorhanden.",
|
||||||
|
"gallery_filter_label": "Galerie nach Uploader filtern",
|
||||||
|
"gallery_filter_trigger": "Nach Uploader filtern:",
|
||||||
|
"gallery_filter_no_results": "Keine Bilder entsprechen den ausgewählten Filtern.",
|
||||||
|
"gallery_uploader_filter_label": "Uploader auswählen",
|
||||||
|
"gallery_uploader_all": "Alle",
|
||||||
|
"gallery_uploader_none": "Keine",
|
||||||
|
"gallery_uploader_selected": "{count} ausgewählt",
|
||||||
"gallery_image_alt": "Upload von {name}",
|
"gallery_image_alt": "Upload von {name}",
|
||||||
"gallery_save_hint": "Tipp: Halte das Bild länger gedrückt, um es in deiner Mediathek zu speichern.",
|
"gallery_save_hint": "Tipp: Halte das Bild länger gedrückt, um es in deiner Mediathek zu speichern.",
|
||||||
"flash_enter_group_name": "Bitte Gruppenname eingeben.",
|
"flash_enter_group_name": "Bitte Gruppenname eingeben.",
|
||||||
@@ -727,7 +747,15 @@ TEXTS = {
|
|||||||
"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}",
|
||||||
|
"gallery_uploaded_by_prefix": "by",
|
||||||
"gallery_empty": "No photos available yet.",
|
"gallery_empty": "No photos available yet.",
|
||||||
|
"gallery_filter_label": "Filter gallery by uploader",
|
||||||
|
"gallery_filter_trigger": "Filter by uploader:",
|
||||||
|
"gallery_filter_no_results": "No photos match the selected filters.",
|
||||||
|
"gallery_uploader_filter_label": "Select uploaders",
|
||||||
|
"gallery_uploader_all": "All",
|
||||||
|
"gallery_uploader_none": "None",
|
||||||
|
"gallery_uploader_selected": "{count} selected",
|
||||||
"gallery_image_alt": "Uploaded by {name}",
|
"gallery_image_alt": "Uploaded by {name}",
|
||||||
"gallery_save_hint": "Tip: Touch and hold the photo to save it to your photo library.",
|
"gallery_save_hint": "Tip: Touch and hold the photo to save it to your photo library.",
|
||||||
"flash_enter_group_name": "Please enter group name.",
|
"flash_enter_group_name": "Please enter group name.",
|
||||||
@@ -1081,12 +1109,15 @@ def init_db() -> None:
|
|||||||
|
|
||||||
def seed_default_groups() -> None:
|
def seed_default_groups() -> None:
|
||||||
db = get_db()
|
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()
|
now = datetime.utcnow().isoformat()
|
||||||
for entry in DEFAULT_INVITATION_GROUPS:
|
for entry in DEFAULT_INVITATION_GROUPS:
|
||||||
|
existing = db.execute(
|
||||||
|
"SELECT id FROM groups WHERE LOWER(name) = LOWER(?)",
|
||||||
|
(entry["name"],),
|
||||||
|
).fetchone()
|
||||||
|
if existing is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
password_hash = generate_password_hash(entry["password"])
|
password_hash = generate_password_hash(entry["password"])
|
||||||
cursor = db.execute(
|
cursor = db.execute(
|
||||||
"INSERT INTO groups (name, password_hash, role, created_at) VALUES (?, ?, ?, ?)",
|
"INSERT INTO groups (name, password_hash, role, created_at) VALUES (?, ?, ?, ?)",
|
||||||
@@ -1104,13 +1135,14 @@ def seed_default_groups() -> None:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.executemany(
|
if member_rows:
|
||||||
"""
|
db.executemany(
|
||||||
INSERT INTO group_members (group_id, name, requires_age, created_at)
|
"""
|
||||||
VALUES (?, ?, ?, ?)
|
INSERT INTO group_members (group_id, name, requires_age, created_at)
|
||||||
""",
|
VALUES (?, ?, ?, ?)
|
||||||
member_rows,
|
""",
|
||||||
)
|
member_rows,
|
||||||
|
)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -1247,7 +1279,12 @@ def complete_onboarding():
|
|||||||
@app.get("/gästebereich")
|
@app.get("/gästebereich")
|
||||||
@login_required
|
@login_required
|
||||||
def guest_area():
|
def guest_area():
|
||||||
return render_template("guest_area.html")
|
db = get_db()
|
||||||
|
member_row = db.execute(
|
||||||
|
"SELECT 1 FROM group_members WHERE group_id = ? LIMIT 1",
|
||||||
|
(int(session["group_id"]),),
|
||||||
|
).fetchone()
|
||||||
|
return render_template("guest_area.html", has_rsvp_members=member_row is not None)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/gastgeberbereich", methods=["GET"])
|
@app.route("/gastgeberbereich", methods=["GET"])
|
||||||
@@ -1322,6 +1359,9 @@ def rsvp():
|
|||||||
(current_group_id,),
|
(current_group_id,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
|
if not members:
|
||||||
|
return redirect(url_for("guest_area"))
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
updates = []
|
updates = []
|
||||||
for member in members:
|
for member in members:
|
||||||
@@ -1430,7 +1470,11 @@ def gallery():
|
|||||||
ORDER BY uploads.id DESC
|
ORDER BY uploads.id DESC
|
||||||
"""
|
"""
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return render_template("gallery.html", images=images)
|
uploaders = sorted(
|
||||||
|
{str(image["uploaded_by_name"]) for image in images},
|
||||||
|
key=str.casefold,
|
||||||
|
)
|
||||||
|
return render_template("gallery.html", images=images, uploaders=uploaders)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/gallery/delete/<int:image_id>")
|
@app.post("/gallery/delete/<int:image_id>")
|
||||||
|
|||||||
@@ -1269,6 +1269,129 @@ input[type="file"]:focus {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gallery-filter-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.65rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item.is-filtered-out {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-filter-empty {
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
color: rgba(31, 26, 23, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-filter {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 22rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-trigger {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding: 0 0.9rem 0 1rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.2);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--forest);
|
||||||
|
background: rgba(255, 253, 248, 0.92);
|
||||||
|
box-shadow: 0 5px 15px rgba(31, 58, 47, 0.08);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-trigger:hover,
|
||||||
|
.gallery-uploader-trigger:focus-visible,
|
||||||
|
.gallery-uploader-trigger[aria-expanded="true"] {
|
||||||
|
border-color: var(--gold);
|
||||||
|
box-shadow: 0 0 0 3px rgba(178, 137, 70, 0.14);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-trigger svg {
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-chevron {
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-trigger[aria-expanded="true"] .gallery-uploader-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-search {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
top: calc(100% + 0.45rem);
|
||||||
|
left: 50%;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 14rem;
|
||||||
|
max-width: min(20rem, calc(100vw - 2rem));
|
||||||
|
max-height: min(22rem, 60vh);
|
||||||
|
padding: 0.45rem;
|
||||||
|
overflow: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 1px solid rgba(31, 58, 47, 0.16);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fffdf8;
|
||||||
|
box-shadow: 0 14px 32px rgba(31, 58, 47, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-menu[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-options {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
border-top: 1px solid rgba(31, 58, 47, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-option {
|
||||||
|
min-height: 2.4rem;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-option:hover {
|
||||||
|
background: rgba(232, 219, 196, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-option input {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
accent-color: var(--forest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-uploader-option-all {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.gallery-item {
|
.gallery-item {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -1298,6 +1421,14 @@ input[type="file"]:focus {
|
|||||||
.gallery-item figcaption {
|
.gallery-item figcaption {
|
||||||
margin-top: 0.55rem;
|
margin-top: 0.55rem;
|
||||||
margin-bottom: 0.55rem;
|
margin-bottom: 0.55rem;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(31, 58, 47, 0.68);
|
||||||
|
font-size: 0.94rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item figcaption strong {
|
||||||
|
color: var(--forest);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-delete-form {
|
.gallery-delete-form {
|
||||||
|
|||||||
@@ -3,9 +3,44 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<h1>{{ t('gallery') }}</h1>
|
<h1>{{ t('gallery') }}</h1>
|
||||||
{% if images %}
|
{% if images %}
|
||||||
|
<div class="gallery-filter-controls">
|
||||||
|
<div class="gallery-uploader-filter" id="gallery-uploader-filter">
|
||||||
|
<button
|
||||||
|
class="gallery-uploader-trigger"
|
||||||
|
id="gallery-uploader-trigger"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="gallery-uploader-menu"
|
||||||
|
aria-label="{{ t('gallery_filter_label') }}"
|
||||||
|
>
|
||||||
|
<svg class="gallery-uploader-search" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="m21 20-4.35-4.35a8 8 0 1 0-1.41 1.41L19.59 21 21 20ZM5 11a6 6 0 1 1 12 0 6 6 0 0 1-12 0Z" />
|
||||||
|
</svg>
|
||||||
|
<span>{{ t('gallery_filter_trigger') }}</span>
|
||||||
|
<strong id="gallery-uploader-summary">{{ t('gallery_uploader_all') }}</strong>
|
||||||
|
<svg class="gallery-uploader-chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="m7 9.5 5 5 5-5 1.4 1.4-6.4 6.4-6.4-6.4L7 9.5Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="gallery-uploader-menu" id="gallery-uploader-menu" aria-label="{{ t('gallery_uploader_filter_label') }}" hidden>
|
||||||
|
<label class="gallery-uploader-option gallery-uploader-option-all">
|
||||||
|
<input id="gallery-uploader-all" type="checkbox" checked />
|
||||||
|
<span>{{ t('gallery_uploader_all') }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="gallery-uploader-options">
|
||||||
|
{% for uploader in uploaders %}
|
||||||
|
<label class="gallery-uploader-option">
|
||||||
|
<input type="checkbox" value="{{ uploader }}" data-uploader-option checked />
|
||||||
|
<span>{{ uploader }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="gallery-grid">
|
<div class="gallery-grid">
|
||||||
{% for image in images %}
|
{% for image in images %}
|
||||||
<figure class="gallery-item gallery-card">
|
<figure class="gallery-item gallery-card" data-uploader="{{ image['uploaded_by_name'] }}">
|
||||||
<div class="gallery-media">
|
<div class="gallery-media">
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('serve_upload', filename=image['filename']) }}"
|
href="{{ url_for('serve_upload', filename=image['filename']) }}"
|
||||||
@@ -24,10 +59,14 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<figcaption>{{ t('gallery_uploaded_by').format(name=image['uploaded_by_name']) }}</figcaption>
|
<figcaption>
|
||||||
|
<span>{{ t('gallery_uploaded_by_prefix') }}</span>
|
||||||
|
<strong>{{ image['uploaded_by_name'] }}</strong>
|
||||||
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="gallery-filter-empty" id="gallery-filter-empty" hidden>{{ t('gallery_filter_no_results') }}</p>
|
||||||
|
|
||||||
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
|
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
|
||||||
<button class="lightbox-close" type="button" aria-label="Close">×</button>
|
<button class="lightbox-close" type="button" aria-label="Close">×</button>
|
||||||
@@ -54,9 +93,22 @@
|
|||||||
const prevBtn = lightbox.querySelector(".lightbox-prev");
|
const prevBtn = lightbox.querySelector(".lightbox-prev");
|
||||||
const nextBtn = lightbox.querySelector(".lightbox-next");
|
const nextBtn = lightbox.querySelector(".lightbox-next");
|
||||||
const openButtons = Array.from(document.querySelectorAll(".gallery-open"));
|
const openButtons = Array.from(document.querySelectorAll(".gallery-open"));
|
||||||
|
const galleryItems = Array.from(document.querySelectorAll(".gallery-item"));
|
||||||
|
const filterEmpty = document.getElementById("gallery-filter-empty");
|
||||||
|
const uploaderFilter = document.getElementById("gallery-uploader-filter");
|
||||||
|
const uploaderTrigger = document.getElementById("gallery-uploader-trigger");
|
||||||
|
const uploaderMenu = document.getElementById("gallery-uploader-menu");
|
||||||
|
const uploaderSummary = document.getElementById("gallery-uploader-summary");
|
||||||
|
const uploaderAll = document.getElementById("gallery-uploader-all");
|
||||||
|
const uploaderOptions = Array.from(document.querySelectorAll("[data-uploader-option]"));
|
||||||
|
const uploaderLabelAll = {{ t('gallery_uploader_all')|tojson }};
|
||||||
|
const uploaderLabelNone = {{ t('gallery_uploader_none')|tojson }};
|
||||||
|
const uploaderLabelSelected = {{ t('gallery_uploader_selected')|tojson }};
|
||||||
const images = [
|
const images = [
|
||||||
{% for image in images %}
|
{% for image in images %}
|
||||||
{
|
{
|
||||||
|
index: {{ loop.index0 }},
|
||||||
|
uploader: {{ image['uploaded_by_name']|tojson }},
|
||||||
src: {{ url_for('serve_upload', filename=image['filename'])|tojson }},
|
src: {{ url_for('serve_upload', filename=image['filename'])|tojson }},
|
||||||
download: {{ url_for('serve_upload', filename=image['filename'], download=1)|tojson }},
|
download: {{ url_for('serve_upload', filename=image['filename'], download=1)|tojson }},
|
||||||
alt: {{ t('gallery_image_alt').format(name=image['uploaded_by_name'])|tojson }}
|
alt: {{ t('gallery_image_alt').format(name=image['uploaded_by_name'])|tojson }}
|
||||||
@@ -64,6 +116,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let visibleImages = [...images];
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
let touchStartX = 0;
|
let touchStartX = 0;
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
@@ -85,18 +138,18 @@
|
|||||||
|
|
||||||
const setImage = (index, options = {}) => {
|
const setImage = (index, options = {}) => {
|
||||||
const { revealControls = true } = options;
|
const { revealControls = true } = options;
|
||||||
if (!images.length) {
|
if (!visibleImages.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentIndex = (index + images.length) % images.length;
|
currentIndex = (index + visibleImages.length) % visibleImages.length;
|
||||||
const item = images[currentIndex];
|
const item = visibleImages[currentIndex];
|
||||||
lightboxImage.classList.remove("is-fading");
|
lightboxImage.classList.remove("is-fading");
|
||||||
void lightboxImage.offsetWidth;
|
void lightboxImage.offsetWidth;
|
||||||
lightboxImage.classList.add("is-fading");
|
lightboxImage.classList.add("is-fading");
|
||||||
lightboxImage.src = item.src;
|
lightboxImage.src = item.src;
|
||||||
lightboxImage.alt = item.alt;
|
lightboxImage.alt = item.alt;
|
||||||
lightboxDownload.href = item.download;
|
lightboxDownload.href = item.download;
|
||||||
lightboxCounter.textContent = `${currentIndex + 1} / ${images.length}`;
|
lightboxCounter.textContent = `${currentIndex + 1} / ${visibleImages.length}`;
|
||||||
if (revealControls) {
|
if (revealControls) {
|
||||||
showControlsTemporarily();
|
showControlsTemporarily();
|
||||||
}
|
}
|
||||||
@@ -126,10 +179,71 @@
|
|||||||
openButtons.forEach((link) => {
|
openButtons.forEach((link) => {
|
||||||
link.addEventListener("click", (event) => {
|
link.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openLightbox(Number(link.dataset.imageIndex || 0));
|
const imageIndex = Number(link.dataset.imageIndex || 0);
|
||||||
|
const visibleIndex = visibleImages.findIndex((item) => item.index === imageIndex);
|
||||||
|
if (visibleIndex >= 0) {
|
||||||
|
openLightbox(visibleIndex);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
const selectedUploaders = new Set(
|
||||||
|
uploaderOptions.filter((option) => option.checked).map((option) => option.value)
|
||||||
|
);
|
||||||
|
galleryItems.forEach((item) => {
|
||||||
|
const matchesUploader = selectedUploaders.has(item.dataset.uploader || "");
|
||||||
|
item.classList.toggle("is-filtered-out", !matchesUploader);
|
||||||
|
});
|
||||||
|
visibleImages = images.filter((item) => selectedUploaders.has(item.uploader));
|
||||||
|
filterEmpty.hidden = visibleImages.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncUploaderSummary = () => {
|
||||||
|
const selectedCount = uploaderOptions.filter((option) => option.checked).length;
|
||||||
|
uploaderAll.checked = selectedCount === uploaderOptions.length;
|
||||||
|
uploaderAll.indeterminate = selectedCount > 0 && selectedCount < uploaderOptions.length;
|
||||||
|
if (selectedCount === uploaderOptions.length) {
|
||||||
|
uploaderSummary.textContent = uploaderLabelAll;
|
||||||
|
} else if (selectedCount === 0) {
|
||||||
|
uploaderSummary.textContent = uploaderLabelNone;
|
||||||
|
} else {
|
||||||
|
uploaderSummary.textContent = uploaderLabelSelected.replace("{count}", String(selectedCount));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeUploaderMenu = () => {
|
||||||
|
uploaderMenu.hidden = true;
|
||||||
|
uploaderTrigger.setAttribute("aria-expanded", "false");
|
||||||
|
};
|
||||||
|
|
||||||
|
uploaderTrigger.addEventListener("click", () => {
|
||||||
|
const shouldOpen = uploaderMenu.hidden;
|
||||||
|
uploaderMenu.hidden = !shouldOpen;
|
||||||
|
uploaderTrigger.setAttribute("aria-expanded", String(shouldOpen));
|
||||||
|
});
|
||||||
|
|
||||||
|
uploaderAll.addEventListener("change", () => {
|
||||||
|
uploaderOptions.forEach((option) => {
|
||||||
|
option.checked = uploaderAll.checked;
|
||||||
|
});
|
||||||
|
syncUploaderSummary();
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
uploaderOptions.forEach((option) => {
|
||||||
|
option.addEventListener("change", () => {
|
||||||
|
syncUploaderSummary();
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!uploaderFilter.contains(event.target)) {
|
||||||
|
closeUploaderMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
prevBtn.addEventListener("click", () => setImage(currentIndex - 1, { revealControls: true }));
|
prevBtn.addEventListener("click", () => setImage(currentIndex - 1, { revealControls: true }));
|
||||||
nextBtn.addEventListener("click", () => setImage(currentIndex + 1, { revealControls: true }));
|
nextBtn.addEventListener("click", () => setImage(currentIndex + 1, { revealControls: true }));
|
||||||
closeBtn.addEventListener("click", closeLightbox);
|
closeBtn.addEventListener("click", closeLightbox);
|
||||||
@@ -184,6 +298,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape" && !uploaderMenu.hidden) {
|
||||||
|
closeUploaderMenu();
|
||||||
|
uploaderTrigger.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!lightbox.classList.contains("is-open")) {
|
if (!lightbox.classList.contains("is-open")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="card-grid dashboard-grid">
|
<section class="card-grid dashboard-grid">
|
||||||
|
{% if has_rsvp_members %}
|
||||||
<a class="card link-card dashboard-link-card" href="{{ url_for('rsvp') }}">{{ t('rsvp') }}</a>
|
<a class="card link-card dashboard-link-card" href="{{ url_for('rsvp') }}">{{ t('rsvp') }}</a>
|
||||||
|
{% endif %}
|
||||||
<a class="card link-card dashboard-link-card" href="{{ url_for('upload') }}">{{ t('upload') }}</a>
|
<a class="card link-card dashboard-link-card" href="{{ url_for('upload') }}">{{ t('upload') }}</a>
|
||||||
<a class="card link-card dashboard-link-card" href="{{ url_for('gallery') }}">{{ t('gallery') }}</a>
|
<a class="card link-card dashboard-link-card" href="{{ url_for('gallery') }}">{{ t('gallery') }}</a>
|
||||||
<a class="card link-card dashboard-link-card" href="{{ url_for('info', page='schedule') }}">{{ t('schedule') }}</a>
|
<a class="card link-card dashboard-link-card" href="{{ url_for('info', page='schedule') }}">{{ t('schedule') }}</a>
|
||||||
|
|||||||
Binary file not shown.
@@ -25,6 +25,8 @@ services:
|
|||||||
- UPLOAD_FOLDER=/app/uploads
|
- UPLOAD_FOLDER=/app/uploads
|
||||||
- EVENT_PASSWORD=${EVENT_PASSWORD:-wedding2026}
|
- EVENT_PASSWORD=${EVENT_PASSWORD:-wedding2026}
|
||||||
- HOST_PASSWORD=${HOST_PASSWORD:-gastgeber2026}
|
- HOST_PASSWORD=${HOST_PASSWORD:-gastgeber2026}
|
||||||
|
- PHOTOGRAPHER_PASSWORD=${PHOTOGRAPHER_PASSWORD:-Foto!Graf26#}
|
||||||
|
- PHOTOBOOTH_PASSWORD=${PHOTOBOOTH_PASSWORD:-Foto!Box26#}
|
||||||
- WEDDING_DATE=${WEDDING_DATE:-}
|
- WEDDING_DATE=${WEDDING_DATE:-}
|
||||||
- WEDDING_COUNTDOWN_LOCAL=${WEDDING_COUNTDOWN_LOCAL:-2026-09-04 16:00}
|
- WEDDING_COUNTDOWN_LOCAL=${WEDDING_COUNTDOWN_LOCAL:-2026-09-04 16:00}
|
||||||
- WEDDING_TIMEZONE=${WEDDING_TIMEZONE:-Europe/Berlin}
|
- WEDDING_TIMEZONE=${WEDDING_TIMEZONE:-Europe/Berlin}
|
||||||
|
|||||||
Reference in New Issue
Block a user