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
52 lines
2.2 KiB
HTML
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>
|