fix: polish edit forms and detail pages
- 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
This commit is contained in:
parent
9650735057
commit
4ee8cf7964
21 changed files with 225 additions and 52 deletions
|
|
@ -52,7 +52,7 @@ pub(crate) async fn load_bag_page(
|
||||||
Ok(BagPageData { bags, navigator })
|
Ok(BagPageData { bags, navigator })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn create_bag(
|
pub(crate) async fn create_bag(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -156,7 +156,7 @@ impl UpdateBagSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn update_bag(
|
pub(crate) async fn update_bag(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ impl NewBrewSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn create_brew(
|
pub(crate) async fn create_brew(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -375,7 +375,7 @@ impl UpdateBrewSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||||
pub(crate) async fn update_brew(
|
pub(crate) async fn update_brew(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ impl NewCafeSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn create_cafe(
|
pub(crate) async fn create_cafe(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -175,7 +175,7 @@ impl UpdateCafeSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||||
pub(crate) async fn update_cafe(
|
pub(crate) async fn update_cafe(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ pub(crate) async fn load_cup_page(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn create_cup(
|
pub(crate) async fn create_cup(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -116,7 +116,7 @@ impl UpdateCupSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||||
pub(crate) async fn update_cup(
|
pub(crate) async fn update_cup(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ pub(crate) async fn load_gear_page(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn create_gear(
|
pub(crate) async fn create_gear(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -148,7 +148,7 @@ impl UpdateGearSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||||
pub(crate) async fn update_gear(
|
pub(crate) async fn update_gear(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ impl NewRoasterSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn create_roaster(
|
pub(crate) async fn create_roaster(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -168,7 +168,7 @@ impl UpdateRoasterSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||||
pub(crate) async fn update_roaster(
|
pub(crate) async fn update_roaster(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ pub(crate) async fn load_roast_page(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||||
pub(crate) async fn create_roast(
|
pub(crate) async fn create_roast(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -214,7 +214,7 @@ impl UpdateRoastSubmission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||||
pub(crate) async fn update_roast(
|
pub(crate) async fn update_roast(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ pub fn app_router(state: AppState) -> axum::Router {
|
||||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; \
|
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; \
|
||||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
|
||||||
font-src 'self' https://fonts.gstatic.com; \
|
font-src 'self' https://fonts.gstatic.com; \
|
||||||
img-src 'self' data:; \
|
img-src 'self' data: blob:; \
|
||||||
frame-ancestors 'none'",
|
frame-ancestors 'none'",
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ pub struct CafeDetailView {
|
||||||
pub country: String,
|
pub country: String,
|
||||||
pub country_flag: String,
|
pub country_flag: String,
|
||||||
pub website: Option<String>,
|
pub website: Option<String>,
|
||||||
|
pub map_url: String,
|
||||||
pub map_countries: String,
|
pub map_countries: String,
|
||||||
pub map_max: u32,
|
pub map_max: u32,
|
||||||
pub legend_entries: Vec<LegendEntry>,
|
pub legend_entries: Vec<LegendEntry>,
|
||||||
|
|
@ -24,6 +25,10 @@ impl CafeDetailView {
|
||||||
.map(iso_to_flag_emoji)
|
.map(iso_to_flag_emoji)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let (map_countries, map_max) = build_map_data(&[(&cafe.country, 1)]);
|
let (map_countries, map_max) = build_map_data(&[(&cafe.country, 1)]);
|
||||||
|
let map_url = format!(
|
||||||
|
"https://www.google.com/maps?q={},{}",
|
||||||
|
cafe.latitude, cafe.longitude
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: cafe.id.to_string(),
|
id: cafe.id.to_string(),
|
||||||
|
|
@ -32,6 +37,7 @@ impl CafeDetailView {
|
||||||
country_flag,
|
country_flag,
|
||||||
country: cafe.country,
|
country: cafe.country,
|
||||||
website: cafe.website,
|
website: cafe.website,
|
||||||
|
map_url,
|
||||||
map_countries,
|
map_countries,
|
||||||
map_max,
|
map_max,
|
||||||
legend_entries: vec![LegendEntry {
|
legend_entries: vec![LegendEntry {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ pub struct CupDetailView {
|
||||||
pub cafe_country: String,
|
pub cafe_country: String,
|
||||||
pub cafe_country_flag: String,
|
pub cafe_country_flag: String,
|
||||||
pub cafe_website: Option<String>,
|
pub cafe_website: Option<String>,
|
||||||
|
pub cafe_map_url: String,
|
||||||
// Map
|
// Map
|
||||||
pub map_countries: String,
|
pub map_countries: String,
|
||||||
pub map_max: u32,
|
pub map_max: u32,
|
||||||
|
|
@ -111,6 +112,10 @@ impl CupDetailView {
|
||||||
cafe_country: cafe.country.clone(),
|
cafe_country: cafe.country.clone(),
|
||||||
cafe_country_flag,
|
cafe_country_flag,
|
||||||
cafe_website: cafe.website.clone(),
|
cafe_website: cafe.website.clone(),
|
||||||
|
cafe_map_url: format!(
|
||||||
|
"https://www.google.com/maps?q={},{}",
|
||||||
|
cafe.latitude, cafe.longitude
|
||||||
|
),
|
||||||
roaster_slug: roaster.slug.clone(),
|
roaster_slug: roaster.slug.clone(),
|
||||||
roast_slug: roast.slug.clone(),
|
roast_slug: roast.slug.clone(),
|
||||||
cafe_slug: cafe.slug.clone(),
|
cafe_slug: cafe.slug.clone(),
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,28 @@ customElements.define(
|
||||||
// Deferred mode: store data URL in a target hidden input
|
// Deferred mode: store data URL in a target hidden input
|
||||||
const targetId = this.getAttribute("target-input");
|
const targetId = this.getAttribute("target-input");
|
||||||
if (targetId) {
|
if (targetId) {
|
||||||
document.getElementById(targetId).value = dataUrl;
|
const hiddenInput = document.getElementById(targetId);
|
||||||
|
hiddenInput.value = dataUrl;
|
||||||
|
|
||||||
|
// Remove existing server image preview (edit forms)
|
||||||
|
const existing = document.getElementById(`${targetId}-existing`);
|
||||||
|
if (existing) {
|
||||||
|
existing.remove();
|
||||||
|
// Create standalone preview after the hidden input since
|
||||||
|
// the Replace button (this element) was inside the removed container
|
||||||
|
let preview = hiddenInput.nextElementSibling;
|
||||||
|
if (
|
||||||
|
!preview ||
|
||||||
|
!preview.classList.contains("image-upload-preview")
|
||||||
|
) {
|
||||||
|
preview = document.createElement("div");
|
||||||
|
preview.className =
|
||||||
|
"image-upload-preview h-48 w-full bg-cover bg-center rounded-lg border";
|
||||||
|
hiddenInput.insertAdjacentElement("afterend", preview);
|
||||||
|
}
|
||||||
|
preview.style.backgroundImage = `url('${dataUrl}')`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._showPreview(dataUrl);
|
this._showPreview(dataUrl);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,21 @@
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Country</dt>
|
<dt class="text-text-muted">Country</dt>
|
||||||
<dd class="font-medium text-text">
|
<dd class="font-medium text-text">
|
||||||
{% if !cafe.country_flag.is_empty() %}{{ cafe.country_flag }}{% endif %}{{ cafe.country }}
|
{% if !cafe.country_flag.is_empty() %}
|
||||||
|
<span class="mr-1">{{ cafe.country_flag }}</span>
|
||||||
|
{% endif %}{{ cafe.country }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-text-muted">Location</dt>
|
||||||
|
<dd class="font-medium">
|
||||||
|
<a
|
||||||
|
href="{{ cafe.map_url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="text-accent hover:text-accent-hover transition"
|
||||||
|
>View on Map</a
|
||||||
|
>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{% if let Some(url) = cafe.website %}
|
{% if let Some(url) = cafe.website %}
|
||||||
|
|
|
||||||
|
|
@ -62,13 +62,27 @@
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Country</dt>
|
<dt class="text-text-muted">Country</dt>
|
||||||
<dd class="font-medium text-text">
|
<dd class="font-medium text-text">
|
||||||
{% if !cup.cafe_country_flag.is_empty() %}{{ cup.cafe_country_flag }}{% endif %}{{ cup.cafe_country }}
|
{% if !cup.cafe_country_flag.is_empty() %}
|
||||||
|
<span class="mr-1">{{ cup.cafe_country_flag }}</span>
|
||||||
|
{% endif %}{{ cup.cafe_country }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">City</dt>
|
<dt class="text-text-muted">City</dt>
|
||||||
<dd class="font-medium text-text">{{ cup.cafe_city }}</dd>
|
<dd class="font-medium text-text">{{ cup.cafe_city }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-text-muted">Location</dt>
|
||||||
|
<dd class="font-medium">
|
||||||
|
<a
|
||||||
|
href="{{ cup.cafe_map_url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="text-accent hover:text-accent-hover transition"
|
||||||
|
>View on Map</a
|
||||||
|
>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
{% if let Some(url) = cup.cafe_website %}
|
{% if let Some(url) = cup.cafe_website %}
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Website</dt>
|
<dt class="text-text-muted">Website</dt>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
data-signals:_amount="{{ amount }}"
|
data-signals:_amount="{{ amount }}"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}', {contentType: 'form'})"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Bag updated'); window.location.href = evt.detail.response.headers.get('location') || '/bags/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Bag updated') }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-1 text-sm">
|
<div class="flex flex-col gap-1 text-sm">
|
||||||
|
|
@ -82,17 +82,25 @@
|
||||||
class="text-sm text-error"
|
class="text-sm text-error"
|
||||||
role="alert"
|
role="alert"
|
||||||
></p>
|
></p>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||||
data-attr:disabled="$_submitting"
|
data-attr:disabled="$_submitting"
|
||||||
>
|
>
|
||||||
<span data-show="$_submitting" style="display:none"
|
<span data-show="$_submitting" style="display:none"
|
||||||
>{{ icons::spinner("h-4 w-4") }}</span
|
>{{ icons::spinner("h-4 w-4") }}</span
|
||||||
>
|
>
|
||||||
|
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="history.back()"
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
data-signals:_qn-over-extracted="{% if quick_notes.contains("over-extracted") %}true{% else %}false{% endif %}"
|
data-signals:_qn-over-extracted="{% if quick_notes.contains("over-extracted") %}true{% else %}false{% endif %}"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/brews/{{ id }}', {contentType: 'form'})"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/brews/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Brew updated'); window.location.href = evt.detail.response.headers.get('location') || '/brews/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Brew updated') }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
>
|
>
|
||||||
<!-- Coffee -->
|
<!-- Coffee -->
|
||||||
|
|
@ -273,28 +273,31 @@
|
||||||
<div class="flex flex-col gap-1 text-sm">
|
<div class="flex flex-col gap-1 text-sm">
|
||||||
<span
|
<span
|
||||||
class="text-xs font-semibold text-text-muted uppercase tracking-wide"
|
class="text-xs font-semibold text-text-muted uppercase tracking-wide"
|
||||||
>Time (s)</span
|
>Time</span
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-adjust"
|
class="btn-adjust"
|
||||||
data-on:click="$_brewTime = Math.max(5, Number($_brewTime) - 5)"
|
data-on:click="$_brewTime = Math.max(5, $_brewTime - 5)"
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="hidden"
|
||||||
name="brew_time"
|
name="brew_time"
|
||||||
step="1"
|
data-attr:value="$_brewTime"
|
||||||
min="0"
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
class="input-field flex-1 text-center"
|
class="input-field flex-1 text-center"
|
||||||
data-bind:_brew-time
|
data-attr:value="Math.floor($_brewTime / 60) + ':' + String($_brewTime % 60).padStart(2, '0')"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-adjust"
|
class="btn-adjust"
|
||||||
data-on:click="$_brewTime = Number($_brewTime) + 5"
|
data-on:click="$_brewTime = $_brewTime + 5"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -357,7 +360,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ img::deferred_upload("edit-brew-image", "Brew Image") }}
|
{{ img::deferred_upload_with_preview("edit-brew-image", "Brew Image", "brew", id, image_url) }}
|
||||||
<p
|
<p
|
||||||
data-show="$_submitError"
|
data-show="$_submitError"
|
||||||
data-text="$_submitError"
|
data-text="$_submitError"
|
||||||
|
|
@ -365,17 +368,25 @@
|
||||||
class="text-sm text-error"
|
class="text-sm text-error"
|
||||||
role="alert"
|
role="alert"
|
||||||
></p>
|
></p>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||||
data-attr:disabled="$_submitting"
|
data-attr:disabled="$_submitting"
|
||||||
>
|
>
|
||||||
<span data-show="$_submitting" style="display:none"
|
<span data-show="$_submitting" style="display:none"
|
||||||
>{{ icons::spinner("h-4 w-4") }}</span
|
>{{ icons::spinner("h-4 w-4") }}</span
|
||||||
>
|
>
|
||||||
|
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="history.back()"
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
data-signals:_website="'{{ website }}'"
|
data-signals:_website="'{{ website }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}', {contentType: 'form'})"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cafe updated'); window.location.href = evt.detail.response.headers.get('location') || '/cafes/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cafe updated') }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{ img::deferred_upload("edit-cafe-image", "Cafe Image") }}
|
{{ img::deferred_upload_with_preview("edit-cafe-image", "Cafe Image", "cafe", id, image_url) }}
|
||||||
<p
|
<p
|
||||||
data-show="$_submitError"
|
data-show="$_submitError"
|
||||||
data-text="$_submitError"
|
data-text="$_submitError"
|
||||||
|
|
@ -125,17 +125,25 @@
|
||||||
class="text-sm text-error"
|
class="text-sm text-error"
|
||||||
role="alert"
|
role="alert"
|
||||||
></p>
|
></p>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||||
data-attr:disabled="$_submitting"
|
data-attr:disabled="$_submitting"
|
||||||
>
|
>
|
||||||
<span data-show="$_submitting" style="display:none"
|
<span data-show="$_submitting" style="display:none"
|
||||||
>{{ icons::spinner("h-4 w-4") }}</span
|
>{{ icons::spinner("h-4 w-4") }}</span
|
||||||
>
|
>
|
||||||
|
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="history.back()"
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
data-signals:_submit-error="''"
|
data-signals:_submit-error="''"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}', {contentType: 'form'})"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cup updated'); window.location.href = evt.detail.response.headers.get('location') || '/cups/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cup updated') }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
</searchable-select>
|
</searchable-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ img::deferred_upload("edit-cup-image", "Cup Image") }}
|
{{ img::deferred_upload_with_preview("edit-cup-image", "Cup Image", "cup", id, image_url) }}
|
||||||
<p
|
<p
|
||||||
data-show="$_submitError"
|
data-show="$_submitError"
|
||||||
data-text="$_submitError"
|
data-text="$_submitError"
|
||||||
|
|
@ -79,17 +79,25 @@
|
||||||
class="text-sm text-error"
|
class="text-sm text-error"
|
||||||
role="alert"
|
role="alert"
|
||||||
></p>
|
></p>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||||
data-attr:disabled="$_submitting"
|
data-attr:disabled="$_submitting"
|
||||||
>
|
>
|
||||||
<span data-show="$_submitting" style="display:none"
|
<span data-show="$_submitting" style="display:none"
|
||||||
>{{ icons::spinner("h-4 w-4") }}</span
|
>{{ icons::spinner("h-4 w-4") }}</span
|
||||||
>
|
>
|
||||||
|
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="history.back()"
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
data-signals:_model="'{{ model }}'"
|
data-signals:_model="'{{ model }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}', {contentType: 'form'})"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Gear updated'); window.location.href = evt.detail.response.headers.get('location') || '/gear/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Gear updated') }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{ img::deferred_upload("edit-gear-image", "Gear Image") }}
|
{{ img::deferred_upload_with_preview("edit-gear-image", "Gear Image", "gear", id, image_url) }}
|
||||||
<p
|
<p
|
||||||
data-show="$_submitError"
|
data-show="$_submitError"
|
||||||
data-text="$_submitError"
|
data-text="$_submitError"
|
||||||
|
|
@ -71,17 +71,25 @@
|
||||||
class="text-sm text-error"
|
class="text-sm text-error"
|
||||||
role="alert"
|
role="alert"
|
||||||
></p>
|
></p>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||||
data-attr:disabled="$_submitting"
|
data-attr:disabled="$_submitting"
|
||||||
>
|
>
|
||||||
<span data-show="$_submitting" style="display:none"
|
<span data-show="$_submitting" style="display:none"
|
||||||
>{{ icons::spinner("h-4 w-4") }}</span
|
>{{ icons::spinner("h-4 w-4") }}</span
|
||||||
>
|
>
|
||||||
|
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="history.back()"
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
data-signals:_tasting-notes="'{{ tasting_notes }}'"
|
data-signals:_tasting-notes="'{{ tasting_notes }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}', {contentType: 'form'})"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roast updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasts/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roast updated') }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-1 text-sm">
|
<div class="flex flex-col gap-1 text-sm">
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{ img::deferred_upload("edit-roast-image", "Roast Image") }}
|
{{ img::deferred_upload_with_preview("edit-roast-image", "Roast Image", "roast", id, image_url) }}
|
||||||
<p
|
<p
|
||||||
data-show="$_submitError"
|
data-show="$_submitError"
|
||||||
data-text="$_submitError"
|
data-text="$_submitError"
|
||||||
|
|
@ -147,17 +147,25 @@
|
||||||
class="text-sm text-error"
|
class="text-sm text-error"
|
||||||
role="alert"
|
role="alert"
|
||||||
></p>
|
></p>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||||
data-attr:disabled="$_submitting"
|
data-attr:disabled="$_submitting"
|
||||||
>
|
>
|
||||||
<span data-show="$_submitting" style="display:none"
|
<span data-show="$_submitting" style="display:none"
|
||||||
>{{ icons::spinner("h-4 w-4") }}</span
|
>{{ icons::spinner("h-4 w-4") }}</span
|
||||||
>
|
>
|
||||||
|
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="history.back()"
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
data-signals:_homepage="'{{ homepage }}'"
|
data-signals:_homepage="'{{ homepage }}'"
|
||||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}', {contentType: 'form'})"
|
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}', {contentType: 'form'})"
|
||||||
data-on:datastar-fetch="if (!$_submitting) return;
|
data-on:datastar-fetch="if (!$_submitting) return;
|
||||||
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roaster updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasters/{{ id }}' }
|
if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roaster updated') }
|
||||||
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{ img::deferred_upload("edit-roaster-image", "Roaster Image") }}
|
{{ img::deferred_upload_with_preview("edit-roaster-image", "Roaster Image", "roaster", id, image_url) }}
|
||||||
<p
|
<p
|
||||||
data-show="$_submitError"
|
data-show="$_submitError"
|
||||||
data-text="$_submitError"
|
data-text="$_submitError"
|
||||||
|
|
@ -89,17 +89,25 @@
|
||||||
class="text-sm text-error"
|
class="text-sm text-error"
|
||||||
role="alert"
|
role="alert"
|
||||||
></p>
|
></p>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
|
||||||
data-attr:disabled="$_submitting"
|
data-attr:disabled="$_submitting"
|
||||||
>
|
>
|
||||||
<span data-show="$_submitting" style="display:none"
|
<span data-show="$_submitting" style="display:none"
|
||||||
>{{ icons::spinner("h-4 w-4") }}</span
|
>{{ icons::spinner("h-4 w-4") }}</span
|
||||||
>
|
>
|
||||||
|
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="history.back()"
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-md border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,60 @@
|
||||||
</image-upload>
|
</image-upload>
|
||||||
{% endmacro %}
|
{% 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() %}
|
{% macro lightbox_script() %}
|
||||||
<script>
|
<script>
|
||||||
const openImageModal = (src) => {
|
const openImageModal = (src) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue