Massiv +
This commit is contained in:
@@ -22,6 +22,11 @@
|
||||
<button class="btn btn-ghost" type="submit">EN</button>
|
||||
</form>
|
||||
{% if guest_name %}
|
||||
<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">
|
||||
<path d="M3 3h8v8H3V3zm10 0h8v5h-8V3zM3 13h5v8H3v-8zm7 0h11v8H10v-8z" />
|
||||
</svg>
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('logout') }}">
|
||||
<button class="btn btn-ghost" type="submit">{{ t('logout') }}</button>
|
||||
</form>
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
<a class="card link-card" href="{{ url_for('info', page='hotels') }}">{{ t('hotels') }}</a>
|
||||
<a class="card link-card" href="{{ url_for('info', page='taxi') }}">{{ t('taxi') }}</a>
|
||||
<a class="card link-card" href="{{ url_for('info', page='location') }}">{{ t('location') }}</a>
|
||||
{% if is_admin %}
|
||||
<a class="card link-card" href="{{ url_for('host_area') }}">{{ t('host_area') }}</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<section class="card">
|
||||
<h1>{{ t('host_access_title') }}</h1>
|
||||
<p>{{ t('host_access_note') }}</p>
|
||||
</section>
|
||||
|
||||
{% if not unlocked %}
|
||||
<section class="card form-card">
|
||||
<form method="post" action="{{ url_for('host_area') }}" class="form-grid">
|
||||
<label>
|
||||
{{ t('host_password') }}
|
||||
<input type="password" name="host_password" required />
|
||||
</label>
|
||||
<button class="btn" type="submit">{{ t('host_access_submit') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="stats-grid">
|
||||
<article class="card stat-card">
|
||||
<h2>{{ t('total_guests') }}</h2>
|
||||
@@ -33,10 +17,6 @@
|
||||
<h2>{{ t('attending_open') }}</h2>
|
||||
<p>{{ stats.attending_open }}</p>
|
||||
</article>
|
||||
<article class="card stat-card">
|
||||
<h2>{{ t('plus_one_total') }}</h2>
|
||||
<p>{{ stats.plus_one_total }}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -45,29 +25,31 @@
|
||||
<table class="guest-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('host_table_name') }}</th>
|
||||
<th>{{ t('host_table_group') }}</th>
|
||||
<th>{{ t('host_table_member') }}</th>
|
||||
<th>{{ t('host_table_status') }}</th>
|
||||
<th>{{ t('host_table_plus_one') }}</th>
|
||||
<th>{{ t('host_table_age') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for guest in guests %}
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<td>{{ guest["name"] }}</td>
|
||||
<td>{{ member["group_name"] }}</td>
|
||||
<td>{{ member["name"] }}</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -76,5 +58,4 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,15 +17,54 @@
|
||||
{% elif page == 'location' %}
|
||||
<p><strong>{{ location_name }}</strong></p>
|
||||
<p>{{ location_address }}</p>
|
||||
<div class="map-wrap">
|
||||
<iframe
|
||||
src="{{ google_maps_embed_url }}"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
<div class="map-wrap map-consent" data-map-consent>
|
||||
<p>{{ t('maps_privacy_notice') }}</p>
|
||||
<button
|
||||
class="map-preview"
|
||||
type="button"
|
||||
data-map-load
|
||||
aria-label="{{ t('maps_load_button') }}"
|
||||
title="{{ t('maps_load_button') }}"
|
||||
style="--map-preview-image: url('{{ url_for('static', filename='assets/location-map-preview.svg') }}');"
|
||||
>
|
||||
<span class="map-preview-overlay">{{ t('maps_load_button') }}</span>
|
||||
</button>
|
||||
<div class="map-embed-target" data-map-embed-target></div>
|
||||
<div class="location-actions">
|
||||
<a class="btn" href="{{ location_website_url }}" target="_blank" rel="noopener">{{ t('visit_location') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn" href="{{ location_website_url }}" target="_blank" rel="noopener">{{ t('visit_location') }}</a>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const wrapper = document.querySelector("[data-map-consent]");
|
||||
if (!wrapper) return;
|
||||
const loadButtons = Array.from(wrapper.querySelectorAll("[data-map-load]"));
|
||||
const previewButton = wrapper.querySelector(".map-preview");
|
||||
const target = wrapper.querySelector("[data-map-embed-target]");
|
||||
const src = {{ google_maps_embed_url|tojson }};
|
||||
let loaded = false;
|
||||
|
||||
const loadMap = () => {
|
||||
if (loaded) return;
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = src;
|
||||
iframe.loading = "lazy";
|
||||
iframe.referrerPolicy = "no-referrer-when-downgrade";
|
||||
iframe.allowFullscreen = true;
|
||||
target.appendChild(iframe);
|
||||
if (previewButton) {
|
||||
previewButton.remove();
|
||||
}
|
||||
loadButtons.forEach((button) => button.remove());
|
||||
loaded = true;
|
||||
};
|
||||
|
||||
loadButtons.forEach((button) => {
|
||||
button.addEventListener("click", loadMap);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<h2>{{ t('login') }}</h2>
|
||||
<form method="post" action="{{ url_for('login') }}" class="form-grid">
|
||||
<label>
|
||||
{{ t('name') }}
|
||||
<input type="text" name="name" required />
|
||||
{{ t('group_name') }}
|
||||
<input type="text" name="group_name" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
{{ t('event_password') }}
|
||||
<input type="password" name="event_password" required />
|
||||
{{ t('group_password') }}
|
||||
<input type="password" name="group_password" required />
|
||||
</label>
|
||||
|
||||
<button class="btn" type="submit">{{ t('login_submit') }}</button>
|
||||
|
||||
@@ -1,24 +1,82 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<section class="card form-card">
|
||||
<section class="card">
|
||||
<h1>{{ t('rsvp') }}</h1>
|
||||
<p>{{ t('rsvp_members_intro') }}</p>
|
||||
<form method="post" class="form-grid">
|
||||
<label class="radio-row">
|
||||
<input type="radio" name="attending" value="yes" {% if guest and guest['attending'] == 1 %}checked{% endif %} />
|
||||
{{ t('attending') }}
|
||||
</label>
|
||||
{% for member in members %}
|
||||
<article class="member-card" data-member-card>
|
||||
<h2 class="member-name">{{ member['name'] }}</h2>
|
||||
<div class="member-choice-row">
|
||||
<label class="radio-row">
|
||||
<input
|
||||
type="radio"
|
||||
name="attending_{{ member['id'] }}"
|
||||
value="yes"
|
||||
{% if member['attending'] == 1 %}checked{% endif %}
|
||||
data-attendance-input
|
||||
data-member-id="{{ member['id'] }}"
|
||||
/>
|
||||
{{ t('attending') }}
|
||||
</label>
|
||||
|
||||
<label class="radio-row">
|
||||
<input type="radio" name="attending" value="no" {% if guest and guest['attending'] == 0 %}checked{% endif %} />
|
||||
{{ t('not_attending') }}
|
||||
</label>
|
||||
<label class="radio-row">
|
||||
<input
|
||||
type="radio"
|
||||
name="attending_{{ member['id'] }}"
|
||||
value="no"
|
||||
{% if member['attending'] == 0 %}checked{% endif %}
|
||||
data-attendance-input
|
||||
data-member-id="{{ member['id'] }}"
|
||||
/>
|
||||
{{ t('not_attending') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="plus_one" {% if guest and guest['plus_one'] == 1 %}checked{% endif %} />
|
||||
{{ t('plus_one') }}
|
||||
</label>
|
||||
{% if member['requires_age'] == 1 %}
|
||||
<label class="member-age-wrap" data-age-wrap data-member-id="{{ member['id'] }}">
|
||||
{{ t('member_age') }}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="17"
|
||||
step="1"
|
||||
name="age_{{ member['id'] }}"
|
||||
value="{{ member['child_age'] if member['child_age'] is not none else '' }}"
|
||||
/>
|
||||
<small>{{ t('member_age_hint') }}</small>
|
||||
</label>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
<button class="btn" type="submit">{{ t('save') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const inputs = Array.from(document.querySelectorAll("[data-attendance-input]"));
|
||||
const ageWraps = Array.from(document.querySelectorAll("[data-age-wrap]"));
|
||||
|
||||
const updateAgeVisibility = () => {
|
||||
ageWraps.forEach((wrap) => {
|
||||
const memberId = wrap.getAttribute("data-member-id");
|
||||
const yesRadio = document.querySelector(`input[name="attending_${memberId}"][value="yes"]`);
|
||||
const ageInput = wrap.querySelector("input[type='number']");
|
||||
const shouldShow = Boolean(yesRadio && yesRadio.checked);
|
||||
wrap.classList.toggle("is-visible", shouldShow);
|
||||
if (ageInput) {
|
||||
ageInput.required = shouldShow;
|
||||
if (!shouldShow) {
|
||||
ageInput.value = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
inputs.forEach((input) => input.addEventListener("change", updateAgeVisibility));
|
||||
updateAgeVisibility();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,77 +1,113 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<section class="card form-card">
|
||||
<section class="card upload-card">
|
||||
<h1>{{ t('upload') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" class="form-grid">
|
||||
<label>
|
||||
{{ t('file') }}
|
||||
<input id="photo-input" type="file" name="photo" accept="image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif" multiple />
|
||||
<p class="upload-intro">{{ t('upload_intro') }}</p>
|
||||
<form id="upload-form" method="post" enctype="multipart/form-data" class="form-grid">
|
||||
<label class="upload-picker" for="photo-input">
|
||||
<span class="upload-picker-title">{{ t('file') }}</span>
|
||||
<span class="upload-picker-subtitle">{{ t('upload_picker_hint') }}</span>
|
||||
<input id="photo-input" class="sr-only" type="file" name="photo" accept="image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif" multiple />
|
||||
</label>
|
||||
<p class="upload-hint">{{ t('upload_multi_hint') }}</p>
|
||||
<div id="extra-file-inputs"></div>
|
||||
<button id="add-file-input" class="btn btn-ghost" type="button">{{ t('add_more_files') }}</button>
|
||||
<p id="upload-selected-count" class="upload-count"></p>
|
||||
<p id="upload-ready-hint" class="upload-ready">{{ t('upload_ready') }}</p>
|
||||
<ul id="upload-file-list" class="upload-file-list"></ul>
|
||||
<button class="btn" type="submit">{{ t('upload_submit') }}</button>
|
||||
<button id="upload-submit-btn" class="btn" type="submit" disabled>{{ t('upload_submit') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const addBtn = document.getElementById("add-file-input");
|
||||
const extraInputs = document.getElementById("extra-file-inputs");
|
||||
const form = document.getElementById("upload-form");
|
||||
const fileInput = document.getElementById("photo-input");
|
||||
const countEl = document.getElementById("upload-selected-count");
|
||||
const readyEl = document.getElementById("upload-ready-hint");
|
||||
const listEl = document.getElementById("upload-file-list");
|
||||
const submitBtn = document.getElementById("upload-submit-btn");
|
||||
const countTpl = {{ t('upload_selected_count')|tojson }};
|
||||
const selectedFiles = [];
|
||||
|
||||
const allInputs = () => Array.from(document.querySelectorAll('input[name="photo"]'));
|
||||
const fileKey = (file) => `${file.name}__${file.size}__${file.lastModified}`;
|
||||
|
||||
const createExtraInput = () => {
|
||||
const wrapper = document.createElement("label");
|
||||
wrapper.className = "extra-file-input";
|
||||
wrapper.textContent = {{ t('file')|tojson }};
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.name = "photo";
|
||||
input.accept = "image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif";
|
||||
input.required = false;
|
||||
input.addEventListener("change", renderSelection);
|
||||
|
||||
wrapper.appendChild(input);
|
||||
extraInputs.appendChild(wrapper);
|
||||
const syncInputFiles = () => {
|
||||
const transfer = new DataTransfer();
|
||||
selectedFiles.forEach((file) => transfer.items.add(file));
|
||||
fileInput.files = transfer.files;
|
||||
};
|
||||
|
||||
const renderSelection = () => {
|
||||
const names = [];
|
||||
allInputs().forEach((input) => {
|
||||
Array.from(input.files || []).forEach((file) => names.push(file.name));
|
||||
});
|
||||
|
||||
if (!names.length) {
|
||||
if (!selectedFiles.length) {
|
||||
countEl.textContent = "";
|
||||
listEl.innerHTML = "";
|
||||
if (readyEl) {
|
||||
readyEl.classList.remove("is-visible");
|
||||
}
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
countEl.textContent = countTpl.replace("{count}", String(names.length));
|
||||
countEl.textContent = countTpl.replace("{count}", String(selectedFiles.length));
|
||||
listEl.innerHTML = "";
|
||||
names.slice(0, 20).forEach((name) => {
|
||||
if (readyEl) {
|
||||
readyEl.classList.add("is-visible");
|
||||
}
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
selectedFiles.slice(0, 20).forEach((file, index) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = name;
|
||||
const label = document.createElement("span");
|
||||
label.textContent = file.name;
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.type = "button";
|
||||
removeBtn.className = "upload-file-remove";
|
||||
removeBtn.setAttribute("aria-label", "remove file");
|
||||
removeBtn.textContent = "×";
|
||||
removeBtn.addEventListener("click", () => {
|
||||
selectedFiles.splice(index, 1);
|
||||
syncInputFiles();
|
||||
renderSelection();
|
||||
});
|
||||
|
||||
item.appendChild(label);
|
||||
item.appendChild(removeBtn);
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
if (names.length > 20) {
|
||||
if (selectedFiles.length > 20) {
|
||||
const more = document.createElement("li");
|
||||
more.textContent = `+ ${names.length - 20} weitere`;
|
||||
more.textContent = `+ ${selectedFiles.length - 20} weitere`;
|
||||
listEl.appendChild(more);
|
||||
}
|
||||
};
|
||||
|
||||
allInputs().forEach((input) => input.addEventListener("change", renderSelection));
|
||||
addBtn.addEventListener("click", () => {
|
||||
createExtraInput();
|
||||
fileInput.addEventListener("change", () => {
|
||||
const existingKeys = new Set(selectedFiles.map(fileKey));
|
||||
Array.from(fileInput.files || []).forEach((file) => {
|
||||
const key = fileKey(file);
|
||||
if (!existingKeys.has(key)) {
|
||||
selectedFiles.push(file);
|
||||
existingKeys.add(key);
|
||||
}
|
||||
});
|
||||
syncInputFiles();
|
||||
fileInput.value = "";
|
||||
renderSelection();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
if (!selectedFiles.length) {
|
||||
event.preventDefault();
|
||||
renderSelection();
|
||||
return;
|
||||
}
|
||||
syncInputFiles();
|
||||
});
|
||||
|
||||
renderSelection();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user