Phase 4: Oberfläche mit Galerie, Erfassung und Reservierung
Jinja2 mit HTMX, Tailwind über die Standalone-Binärdatei im Docker-Build - kein Node.js, kein npm. Mobile-First, Tippflächen mindestens 44 px hoch. Seiten: Galerie mit Filterleiste, Detailseite, Erfassungsformular, Stapel-Import mit Entwurfsliste, Anmeldung. HTMX tauscht beim Filtern nur die Liste aus statt die ganze Seite neu zu laden. Aus den Entscheidungen im Plan folgt konkret: - Der Stapel-Import steht ZUERST im Formular: wer zwanzig Teile hat, will nicht zwanzigmal ein Formular ausfüllen. Bewusst ohne capture-Attribut, damit sich wahlweise fotografieren oder aus der Mediathek wählen lässt - mit capture ginge nur je ein Foto pro Vorgang. - Grösse, Kategorie, Geschlecht, Saison und Zustand werden vom zuletzt gespeicherten Teil übernommen. Beim Abarbeiten einer Kiste sind das fünf Felder weniger pro Stück. - Nach dem Nachtragen eines Entwurfs geht es direkt zum nächsten, nicht auf die Detailseite - das ist der eigentliche Arbeitsablauf. - Titel bleibt leer erlaubt; angezeigt wird dann Kategorie und Grösse. - reserved_by erscheint öffentlich nur als "reserviert". Den Namen sieht ausschliesslich der angemeldete Betreiber. - Nach dem Reservieren wird der Freigabe-Link angezeigt, als vollständige Adresse zum Aufbewahren. - Post/Redirect/Get überall, damit ein Neuladen nicht erneut abschickt. - CSRF-Token in jedem Formular, zusätzlich zu SameSite=Lax. HTMX liegt lokal im Repository, nicht vom CDN: die CSP erlaubt nur script-src 'self', und ein CDN-Aufruf gäbe die IP jedes Besuchers weiter - dieselbe Überlegung wie bei Swagger und bei den Schriften der Kantone-App. Ein Test prüft für alle Seiten, dass nichts von aussen geladen wird. Nebenbei TemplateResponse auf die aktuelle Signatur umgestellt (64 Deprecation-Warnungen weniger). 77 Tests, alle grün. Zusätzlich der ganze Weg gegen den Container: anmelden, drei Fotos als Stapel, Entwurf nachtragen, Werte werden für das nächste Teil übernommen, als Gast reservieren, Name bleibt verborgen, Selbst-Freigabe über den Link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
{# Die Eingabefelder eines Kleidungsstücks. Geteilt zwischen Erfassen und
|
||||
Bearbeiten, damit beide Formulare nicht auseinanderlaufen.
|
||||
|
||||
`vorgabe` ist beim Erfassen leer und beim Bearbeiten das Kleidungsstück;
|
||||
`zuletzt` sind die Werte des zuletzt gespeicherten Teils - beim
|
||||
Abarbeiten einer Kiste ändert sich meist nur der Titel. #}
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="col-span-2">
|
||||
<label class="etikett" for="title">Titel <span class="font-normal text-slate-400">(freiwillig)</span></label>
|
||||
<input class="feld" id="title" name="title" maxlength="100"
|
||||
value="{{ vorgabe.title or '' if vorgabe else '' }}"
|
||||
placeholder="{{ platzhalter_titel }}">
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Leer lassen genügt – dann steht dort automatisch Kategorie und Grösse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="etikett" for="size">Grösse</label>
|
||||
<select class="feld" id="size" name="size" required>
|
||||
<option value="">bitte wählen</option>
|
||||
{% for g in groessen %}
|
||||
<option value="{{ g.label }}"
|
||||
{{ 'selected' if (vorgabe.size if vorgabe else zuletzt.size) == g.label }}>
|
||||
{{ g.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="etikett" for="category_id">Kategorie</label>
|
||||
<select class="feld" id="category_id" name="category_id" required>
|
||||
<option value="">bitte wählen</option>
|
||||
{% for k in kategorien %}
|
||||
<option value="{{ k.id }}"
|
||||
{{ 'selected' if (vorgabe.category_id if vorgabe else zuletzt.category_id)|string == k.id|string }}>
|
||||
{{ k.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="etikett" for="gender">Für</label>
|
||||
<select class="feld" id="gender" name="gender">
|
||||
{% for wert, text in [('unisex','egal'), ('girl','Mädchen'), ('boy','Buben')] %}
|
||||
<option value="{{ wert }}"
|
||||
{{ 'selected' if (vorgabe.gender if vorgabe else zuletzt.gender) == wert }}>
|
||||
{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="etikett" for="season">Saison</label>
|
||||
<select class="feld" id="season" name="season">
|
||||
{% for wert, text in [('all_year','ganzjährig'), ('spring_summer','Frühling/Sommer'), ('autumn_winter','Herbst/Winter')] %}
|
||||
<option value="{{ wert }}"
|
||||
{{ 'selected' if (vorgabe.season if vorgabe else zuletzt.season) == wert }}>
|
||||
{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label class="etikett" for="condition">Zustand</label>
|
||||
<select class="feld" id="condition" name="condition">
|
||||
{% for wert, text in [('good','gut'), ('new','neu'), ('very_good','sehr gut'), ('worn','getragen')] %}
|
||||
<option value="{{ wert }}"
|
||||
{{ 'selected' if (vorgabe.condition if vorgabe else zuletzt.condition) == wert }}>
|
||||
{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label class="etikett" for="description">Bemerkung <span class="font-normal text-slate-400">(freiwillig)</span></label>
|
||||
<textarea class="feld py-2" id="description" name="description" rows="2"
|
||||
placeholder="z.B. kleiner Fleck am Ärmel">{{ vorgabe.description or '' if vorgabe else '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
{# Eine Kachel in der Galerie. Eigene Datei, weil HTMX beim Filtern nur die
|
||||
Liste austauscht und dieselbe Darstellung braucht. #}
|
||||
<a href="/kleid/{{ item.id }}" class="karte block group">
|
||||
<div class="aspect-[3/4] bg-slate-100">
|
||||
{% if item.hauptbild %}
|
||||
<img src="/uploads/{{ item.hauptbild.image_url }}"
|
||||
alt="{{ item.anzeige_titel }}" loading="lazy"
|
||||
class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-full h-full grid place-items-center text-slate-300 text-sm">
|
||||
kein Bild
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<p class="font-semibold leading-tight">{{ item.anzeige_titel }}</p>
|
||||
<p class="text-sm text-slate-500 mt-1">
|
||||
{{ groesse_text(item) }}
|
||||
</p>
|
||||
{% if item.status == 'reserved' %}
|
||||
{# Bewusst ohne Namen: die Galerie ist öffentlich, der Name steht
|
||||
nur in der Verwaltung. #}
|
||||
<span class="inline-block mt-2 text-xs font-semibold rounded-full
|
||||
px-2 py-0.5 bg-amber-100 text-amber-900">reserviert</span>
|
||||
{% elif item.status == 'given_away' %}
|
||||
<span class="inline-block mt-2 text-xs font-semibold rounded-full
|
||||
px-2 py-0.5 bg-slate-200 text-slate-600">abgegeben</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,25 @@
|
||||
{# Nur die Liste - HTMX ersetzt beim Filtern genau diesen Ausschnitt,
|
||||
statt die ganze Seite neu zu laden. #}
|
||||
<div id="liste">
|
||||
{% if items %}
|
||||
<p class="text-sm text-slate-500 mb-3">{{ gesamt }} Stück</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{% for item in items %}{% include "_kachel.html" %}{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if gesamt > items|length %}
|
||||
<div class="text-center mt-6">
|
||||
<button class="knopf-still"
|
||||
hx-get="{{ mehr_url }}"
|
||||
hx-target="#liste"
|
||||
hx-swap="outerHTML">
|
||||
Mehr anzeigen
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-center text-slate-500 py-12">
|
||||
Hier ist gerade nichts. Vielleicht mit einem anderen Filter?
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "basis.html" %}
|
||||
{% block titel %}Anmelden{% endblock %}
|
||||
{% block inhalt %}
|
||||
<form method="post" action="/anmelden" class="max-w-sm mx-auto karte p-6 space-y-4">
|
||||
<h1 class="text-xl font-bold">Anmelden</h1>
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<div>
|
||||
<label class="etikett" for="pw">Passwort</label>
|
||||
<input class="feld" id="pw" name="passwort" type="password" required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<button class="knopf-haupt w-full">Anmelden</button>
|
||||
<p class="text-xs text-slate-500">
|
||||
Nur zum Erfassen nötig. Stöbern und Reservieren geht ohne.
|
||||
</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
<!doctype html>
|
||||
<html lang="de-CH">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>{% block titel %}Kinderkleider-Börse{% endblock %}</title>
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
{# Lokal ausgeliefert, nicht vom CDN: die CSP erlaubt nur script-src
|
||||
'self', und ein CDN-Aufruf gäbe zudem die IP jedes Besuchers weiter. #}
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-900 min-h-screen flex flex-col">
|
||||
|
||||
<header class="bg-white border-b border-slate-200 sticky top-0 z-20">
|
||||
<div class="max-w-5xl mx-auto px-4 h-14 flex items-center gap-3">
|
||||
<a href="/" class="font-bold text-marke-700 text-lg">Kleider-Börse</a>
|
||||
<nav class="ml-auto flex items-center gap-2 text-sm">
|
||||
{% if betreiber %}
|
||||
<a href="/erfassen" class="knopf-still">Erfassen</a>
|
||||
{% if entwurf_anzahl %}
|
||||
<a href="/entwuerfe" class="knopf-still">
|
||||
Entwürfe
|
||||
<span class="bg-marke-700 text-white rounded-full px-2 text-xs">
|
||||
{{ entwurf_anzahl }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<form method="post" action="/abmelden" class="inline">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<button class="knopf-still">Abmelden</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="/anmelden" class="knopf-still">Anmelden</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 w-full max-w-5xl mx-auto px-4 py-5">
|
||||
{% if meldung %}
|
||||
<p class="mb-4 rounded-xl px-4 py-3 text-sm
|
||||
{{ 'bg-emerald-100 text-emerald-900' if meldung_art == 'ok'
|
||||
else 'bg-rose-100 text-rose-900' }}">{{ meldung }}</p>
|
||||
{% endif %}
|
||||
{% block inhalt %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="text-center text-xs text-slate-400 py-6">
|
||||
Kinderkleider-Börse
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "basis.html" %}
|
||||
{% block titel %}Bearbeiten{% endblock %}
|
||||
{% block inhalt %}
|
||||
|
||||
<h1 class="text-xl font-bold mb-4">
|
||||
{{ 'Entwurf nachtragen' if item.status == 'draft' else 'Bearbeiten' }}
|
||||
</h1>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-5">
|
||||
<div class="karte">
|
||||
{% if item.bilder %}
|
||||
<img src="/uploads/{{ item.hauptbild.image_url }}" alt=""
|
||||
class="w-full object-cover">
|
||||
{% if item.bilder|length > 1 %}
|
||||
<div class="grid grid-cols-4 gap-1 p-1">
|
||||
{% for b in item.bilder %}
|
||||
<img src="/uploads/{{ b.image_url }}" alt="" loading="lazy"
|
||||
class="aspect-square object-cover rounded-lg
|
||||
{{ 'ring-2 ring-marke-600' if b.is_primary }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="aspect-[3/4] grid place-items-center text-slate-300">kein Bild</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="/kleid/{{ item.id }}/bearbeiten"
|
||||
enctype="multipart/form-data" class="karte p-4 space-y-4">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
{% include "_felder.html" %}
|
||||
|
||||
<div>
|
||||
<label class="etikett" for="fotos">Weitere Fotos</label>
|
||||
<input class="feld py-2" id="fotos" type="file" name="dateien"
|
||||
accept="image/*" multiple>
|
||||
</div>
|
||||
|
||||
<button class="knopf-haupt w-full">
|
||||
{{ 'Veröffentlichen' if item.status == 'draft' else 'Speichern' }}
|
||||
</button>
|
||||
{% if item.status != 'draft' %}
|
||||
<a href="/kleid/{{ item.id }}" class="knopf-still w-full">Abbrechen</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,102 @@
|
||||
{% extends "basis.html" %}
|
||||
{% block titel %}{{ item.anzeige_titel }}{% endblock %}
|
||||
{% block inhalt %}
|
||||
|
||||
<a href="/" class="text-sm text-marke-700 font-semibold">← Übersicht</a>
|
||||
|
||||
<div class="mt-3 grid md:grid-cols-2 gap-5">
|
||||
<div class="karte">
|
||||
{% if item.bilder %}
|
||||
<img src="/uploads/{{ item.hauptbild.image_url }}"
|
||||
alt="{{ item.anzeige_titel }}" class="w-full object-cover">
|
||||
{% if item.bilder|length > 1 %}
|
||||
<div class="grid grid-cols-4 gap-1 p-1">
|
||||
{% for b in item.bilder %}
|
||||
<img src="/uploads/{{ b.image_url }}" alt="" loading="lazy"
|
||||
class="aspect-square object-cover rounded-lg">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="aspect-[3/4] grid place-items-center text-slate-300">
|
||||
kein Bild
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ item.anzeige_titel }}</h1>
|
||||
<dl class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||
<div><dt class="text-slate-500">Grösse</dt>
|
||||
<dd class="font-semibold">{{ item.size or '–' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Kategorie</dt>
|
||||
<dd class="font-semibold">{{ item.kategorie.name if item.kategorie else '–' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Für</dt>
|
||||
<dd class="font-semibold">{{ geschlecht_text(item.gender) }}</dd></div>
|
||||
<div><dt class="text-slate-500">Zustand</dt>
|
||||
<dd class="font-semibold">{{ zustand_text(item.condition) }}</dd></div>
|
||||
<div><dt class="text-slate-500">Saison</dt>
|
||||
<dd class="font-semibold">{{ saison_text(item.season) }}</dd></div>
|
||||
</dl>
|
||||
|
||||
{% if item.description %}
|
||||
<p class="mt-4 text-slate-700 whitespace-pre-line">{{ item.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6">
|
||||
{% if item.status == 'available' %}
|
||||
<form method="post" action="/kleid/{{ item.id }}/reservieren"
|
||||
class="karte p-4 space-y-3">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<label class="etikett" for="wer">Dein Name</label>
|
||||
<input class="feld" id="wer" name="reserved_by" required
|
||||
maxlength="100" autocomplete="name"
|
||||
placeholder="z.B. Familie Meier">
|
||||
<button class="knopf-haupt w-full">Reservieren</button>
|
||||
<p class="text-xs text-slate-500">
|
||||
Du bekommst danach einen Link, mit dem du die Reservierung
|
||||
selbst wieder aufheben kannst.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{% elif item.status == 'reserved' %}
|
||||
<p class="rounded-xl bg-amber-100 text-amber-900 px-4 py-3">
|
||||
Ist bereits reserviert.
|
||||
</p>
|
||||
{% if betreiber %}
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
Reserviert von: <strong>{{ item.reserved_by }}</strong>
|
||||
</p>
|
||||
<form method="post" action="/kleid/{{ item.id }}/freigeben" class="mt-2">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<button class="knopf-still">Reservierung aufheben</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% elif item.status == 'given_away' %}
|
||||
<p class="rounded-xl bg-slate-200 text-slate-700 px-4 py-3">
|
||||
Schon abgegeben.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if betreiber %}
|
||||
<div class="mt-6 border-t border-slate-200 pt-4 flex flex-wrap gap-2">
|
||||
<a href="/kleid/{{ item.id }}/bearbeiten" class="knopf-still">Bearbeiten</a>
|
||||
{% if item.status == 'reserved' %}
|
||||
<form method="post" action="/kleid/{{ item.id }}/abgegeben">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<button class="knopf-still">Als abgegeben markieren</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/kleid/{{ item.id }}/loeschen"
|
||||
onsubmit="return confirm('Wirklich löschen? Auch die Fotos werden entfernt.')">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<button class="knopf-still text-rose-700">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "basis.html" %}
|
||||
{% block titel %}Entwürfe{% endblock %}
|
||||
{% block inhalt %}
|
||||
|
||||
<h1 class="text-xl font-bold">Unfertige Einträge ({{ gesamt }})</h1>
|
||||
<p class="text-sm text-slate-600 mt-1 mb-5">
|
||||
Aus dem Stapel-Import. Erst wenn Grösse und Kategorie eingetragen sind,
|
||||
erscheint ein Teil in der Galerie.
|
||||
</p>
|
||||
|
||||
{% if items %}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{% for item in items %}
|
||||
<a href="/kleid/{{ item.id }}/bearbeiten" class="karte block">
|
||||
<div class="aspect-[3/4] bg-slate-100">
|
||||
{% if item.hauptbild %}
|
||||
<img src="/uploads/{{ item.hauptbild.image_url }}" alt=""
|
||||
loading="lazy" class="w-full h-full object-cover">
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="p-3 text-sm font-semibold text-marke-700">Nachtragen →</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-slate-500 py-12">
|
||||
Nichts offen. <a href="/erfassen" class="text-marke-700 font-semibold">Neue Fotos hochladen</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "basis.html" %}
|
||||
{% block titel %}Erfassen{% endblock %}
|
||||
{% block inhalt %}
|
||||
|
||||
<h1 class="text-xl font-bold mb-4">Kleider erfassen</h1>
|
||||
|
||||
{# Der schnelle Weg für den Erstbestand. Bewusst zuerst: wer zwanzig Teile
|
||||
hat, will nicht zwanzigmal ein Formular ausfüllen. #}
|
||||
<form method="post" action="/stapel" enctype="multipart/form-data"
|
||||
class="karte p-4 space-y-3">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<h2 class="font-semibold">Viele auf einmal</h2>
|
||||
<p class="text-sm text-slate-600">
|
||||
Erst alles mit der Kamera fotografieren, dann hier alle Fotos
|
||||
zusammen auswählen. Pro Foto entsteht ein Entwurf, die Angaben
|
||||
trägst du danach in Ruhe nach.
|
||||
</p>
|
||||
{# Ohne "capture": so lässt sich wahlweise fotografieren ODER aus der
|
||||
Mediathek wählen. Mit capture ginge nur je ein Foto pro Vorgang. #}
|
||||
<input class="feld py-2" type="file" name="dateien" accept="image/*" multiple required>
|
||||
<button class="knopf-haupt w-full">Fotos hochladen</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-sm text-slate-500 my-5">oder einzeln erfassen</p>
|
||||
|
||||
<form method="post" action="/erfassen" enctype="multipart/form-data"
|
||||
class="karte p-4 space-y-4">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
|
||||
<div>
|
||||
<label class="etikett" for="fotos">Fotos</label>
|
||||
<input class="feld py-2" id="fotos" type="file" name="dateien"
|
||||
accept="image/*" multiple>
|
||||
</div>
|
||||
|
||||
{% include "_felder.html" %}
|
||||
|
||||
<button class="knopf-haupt w-full">Speichern</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "basis.html" %}
|
||||
{% block inhalt %}
|
||||
|
||||
<form class="mb-5 grid grid-cols-2 sm:grid-cols-4 gap-2"
|
||||
hx-get="/teil/liste" hx-target="#liste" hx-swap="outerHTML"
|
||||
hx-trigger="change">
|
||||
<div>
|
||||
<label class="etikett" for="f-size">Grösse</label>
|
||||
<select class="feld" id="f-size" name="size">
|
||||
<option value="">alle</option>
|
||||
{% for g in groessen %}
|
||||
<option value="{{ g.label }}" {{ 'selected' if g.label == filter.size }}>
|
||||
{{ g.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="etikett" for="f-kat">Kategorie</label>
|
||||
<select class="feld" id="f-kat" name="category_id">
|
||||
<option value="">alle</option>
|
||||
{% for k in kategorien %}
|
||||
<option value="{{ k.id }}"
|
||||
{{ 'selected' if k.id|string == filter.category_id }}>
|
||||
{{ k.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="etikett" for="f-gender">Für</label>
|
||||
<select class="feld" id="f-gender" name="gender">
|
||||
<option value="">alle</option>
|
||||
<option value="girl" {{ 'selected' if filter.gender == 'girl' }}>Mädchen</option>
|
||||
<option value="boy" {{ 'selected' if filter.gender == 'boy' }}>Buben</option>
|
||||
<option value="unisex" {{ 'selected' if filter.gender == 'unisex' }}>egal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="etikett" for="f-status">Zustand</label>
|
||||
<select class="feld" id="f-status" name="status">
|
||||
<option value="available" {{ 'selected' if filter.status == 'available' }}>
|
||||
nur verfügbare</option>
|
||||
<option value="" {{ 'selected' if not filter.status }}>alle</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% include "_liste.html" %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "basis.html" %}
|
||||
{% block titel %}Reserviert{% endblock %}
|
||||
{% block inhalt %}
|
||||
<div class="max-w-lg mx-auto karte p-6 text-center">
|
||||
<h1 class="text-xl font-bold">Reserviert für dich</h1>
|
||||
<p class="mt-2 text-slate-600">
|
||||
<strong>{{ item.anzeige_titel }}</strong> ist jetzt für dich zurückgelegt.
|
||||
</p>
|
||||
|
||||
<div class="mt-5 text-left">
|
||||
<p class="etikett">Link zum Aufheben – bitte aufbewahren</p>
|
||||
{# Nur hier sichtbar: mit diesem Link kann die Reservierung
|
||||
zurückgenommen werden, ohne dass jemand ein Konto braucht. #}
|
||||
<input class="feld text-xs" readonly value="{{ freigabe_url }}"
|
||||
onclick="this.select()">
|
||||
<p class="text-xs text-slate-500 mt-2">
|
||||
Ohne diesen Link kannst du die Reservierung nicht selbst aufheben –
|
||||
dann hilft nur eine kurze Nachricht.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-2 justify-center">
|
||||
<a href="/" class="knopf-haupt">Weiterstöbern</a>
|
||||
<a href="{{ freigabe_url }}" class="knopf-still">Doch nicht</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user