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
82 lines
3.6 KiB
HTML
82 lines
3.6 KiB
HTML
{# 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>
|