Files
kleiderboerse/app/templates/detail.html
T
StefanandClaude Opus 5 75b95b38e6 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
2026-08-29 23:29:56 +02:00

103 lines
4.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "basis.html" %}
{% block titel %}{{ item.anzeige_titel }}{% endblock %}
{% block inhalt %}
<a href="/" class="text-sm text-marke-700 font-semibold">&larr; Ü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 %}