- Skip payload in tracing::instrument to avoid logging base64 image data - Add blob: to CSP img-src for image preview support - Add deferred_upload_with_preview macro for edit form image previews with Replace/Remove buttons and proper DOM cleanup on replacement - Fix datastar-fetch finished handler (evt.detail.response is undefined for redirect scripts) - Display brew time in M:SS format on edit form - Add full-width Save Changes button with check icon and Cancel button to all edit forms - Fix country flag emoji spacing on cafe and cup detail pages - Add "View on Map" Google Maps link to cafe and cup detail pages
199 lines
7.5 KiB
HTML
199 lines
7.5 KiB
HTML
{% macro readonly_image(url, alt_text) %}
|
|
<div
|
|
class="relative shrink-0 h-14 w-14 md:h-20 md:w-20 rounded-lg overflow-hidden cursor-pointer"
|
|
onclick="openImageModal('{{ url }}')"
|
|
>
|
|
<img
|
|
src="{{ url }}"
|
|
class="h-full w-full object-cover"
|
|
alt="{{ alt_text }}"
|
|
/>
|
|
<div
|
|
class="absolute inset-0 pointer-events-none"
|
|
style="box-shadow: inset 0 0 20px rgba(0,0,0,0.4)"
|
|
></div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro image_thumbnail(entity_type, entity_id, image_url, is_authenticated) %}
|
|
{% if let Some(url) = image_url %}
|
|
<div
|
|
class="relative shrink-0 h-14 w-14 md:h-20 md:w-20 rounded-lg overflow-hidden cursor-pointer"
|
|
onclick="openImageModal('{{ url }}')"
|
|
>
|
|
<div
|
|
class="absolute inset-0 flex items-center justify-center border-2 border-dashed border-text-muted/30 rounded-lg text-text-muted"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
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>
|
|
</div>
|
|
<img
|
|
src="{{ url }}"
|
|
class="relative h-full w-full object-cover"
|
|
alt="{{ entity_type }} image"
|
|
onload="this.previousElementSibling.hidden = true"
|
|
/>
|
|
<div
|
|
class="absolute inset-0 pointer-events-none"
|
|
style="box-shadow: inset 0 0 20px rgba(0,0,0,0.4)"
|
|
></div>
|
|
</div>
|
|
{% else if is_authenticated %}
|
|
<image-upload
|
|
entity-type="{{ entity_type }}"
|
|
entity-id="{{ entity_id }}"
|
|
mode="upload"
|
|
class="flex shrink-0 items-center justify-center h-14 w-14 md:h-20 md:w-20 rounded-lg border-2 border-dashed border-text-muted/30 text-text-muted cursor-pointer hover:border-accent/40 hover:text-text-secondary transition"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"
|
|
/>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z"
|
|
/>
|
|
</svg>
|
|
</image-upload>
|
|
{% else %}
|
|
<div
|
|
class="flex shrink-0 items-center justify-center h-14 w-14 md:h-20 md:w-20 rounded-lg border-2 border-dashed border-text-muted/30 text-text-muted"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
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>
|
|
</div>
|
|
{% endif %}
|
|
{% endmacro %}
|
|
|
|
{% macro deferred_upload(input_id, label) %}
|
|
<input type="hidden" name="image" id="{{ input_id }}" />
|
|
<image-upload
|
|
mode="deferred"
|
|
target-input="{{ input_id }}"
|
|
class="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-text-muted/30 bg-surface p-4 text-center text-text-muted cursor-pointer hover:border-accent/40 hover:text-text-secondary transition"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
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-xs">{{ label }}</span>
|
|
</image-upload>
|
|
{% endmacro %}
|
|
|
|
{% macro deferred_upload_with_preview(input_id, label, entity_type, entity_id, image_url) %}
|
|
<input type="hidden" name="image" id="{{ input_id }}" />
|
|
{% if let Some(url) = image_url %}
|
|
<div
|
|
id="{{ input_id }}-existing"
|
|
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="{{ label }}"
|
|
></div>
|
|
<div class="absolute top-2 right-2 flex gap-1">
|
|
<image-upload
|
|
mode="deferred"
|
|
target-input="{{ input_id }}"
|
|
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
|
|
type="button"
|
|
onclick="if(confirm('Remove this image?')){fetch('/api/v1/{{ entity_type }}/{{ entity_id }}/image',{method:'DELETE'}).then(()=>{this.closest('[id$=-existing]').remove()})}"
|
|
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>
|
|
</div>
|
|
{% else %}
|
|
<image-upload
|
|
mode="deferred"
|
|
target-input="{{ input_id }}"
|
|
class="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-text-muted/30 bg-surface p-4 text-center text-text-muted cursor-pointer hover:border-accent/40 hover:text-text-secondary transition"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
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-xs">{{ label }}</span>
|
|
</image-upload>
|
|
{% endif %}
|
|
{% endmacro %}
|
|
|
|
{% macro lightbox_script() %}
|
|
<script>
|
|
const openImageModal = (src) => {
|
|
const overlay = document.createElement("div");
|
|
overlay.className =
|
|
"fixed inset-0 z-50 flex items-center justify-center bg-black/80 cursor-pointer";
|
|
overlay.onclick = () => overlay.remove();
|
|
const img = document.createElement("img");
|
|
img.src = src;
|
|
img.className = "max-h-[90vh] max-w-[90vw] rounded-lg object-contain";
|
|
img.onclick = (e) => e.stopPropagation();
|
|
overlay.appendChild(img);
|
|
document.body.appendChild(overlay);
|
|
const onKey = (e) => {
|
|
if (e.key === "Escape") {
|
|
overlay.remove();
|
|
document.removeEventListener("keydown", onKey);
|
|
}
|
|
};
|
|
document.addEventListener("keydown", onKey);
|
|
};
|
|
</script>
|
|
{% endmacro %}
|