brewlog/templates/partials/image_upload.html
Jon Seager 4b3f03f5d3
feat: add entity image support with display on detail pages
Add image infrastructure, API, and detail page integration:

- EntityImage domain model with ImageRepository trait
- SQLite storage for images and thumbnails as BLOBs
- Image processing with data URL decoding and thumbnail generation
- REST API: upload, get, delete, thumbnail at /{entity_type}/{id}/image
- resolve_image_url helper for image fallback chains
- image-upload web component for direct upload on detail pages
- Image display with vignette overlay and lightbox viewer
- Template macros: image_thumbnail, readonly_image, lightbox_script
- All 7 detail pages updated with image thumbnails
- Brew images fall back to roast; cup images fall back to cafe then roast
- Delete handler macro extended with optional image cleanup
2026-02-10 17:50:13 +00:00

52 lines
2.2 KiB
HTML

<div id="entity-image">
{% if let Some(url) = image_url %}
<div class="relative rounded-lg border bg-surface overflow-hidden">
<div
class="h-48 w-full bg-cover bg-center"
style="background-image: url('{{ url }}')"
role="img"
aria-label="{{ entity_type }} image"
></div>
{% if is_authenticated %}
<div class="absolute top-2 right-2 flex gap-1">
<image-upload
entity-type="{{ entity_type }}"
entity-id="{{ entity_id }}"
mode="replace"
class="inline-flex items-center gap-1 rounded border bg-surface/90 px-2 py-1 text-xs font-medium text-accent hover:bg-surface cursor-pointer backdrop-blur-sm"
>
Replace
</image-upload>
<button
onclick="if(confirm('Remove this image?')){fetch('/api/v1/{{ entity_type }}/{{ entity_id }}/image',{method:'DELETE',headers:{'datastar-request':'true'}}).then(r=>r.text()).then(h=>{document.getElementById('entity-image').outerHTML=h})}"
class="inline-flex items-center gap-1 rounded border bg-surface/90 px-2 py-1 text-xs font-medium text-red-500 hover:bg-surface cursor-pointer backdrop-blur-sm"
>
Remove
</button>
</div>
{% endif %}
</div>
{% else if is_authenticated %}
<image-upload
entity-type="{{ entity_type }}"
entity-id="{{ entity_id }}"
mode="upload"
class="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-text-muted/30 bg-surface p-6 text-center text-text-muted cursor-pointer hover:border-accent/40 hover:text-text-secondary transition"
>
<svg
class="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"
/>
</svg>
<span class="text-sm">Drop image, browse, or take a photo</span>
</image-upload>
{% endif %}
</div>