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 })
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
||||
pub(crate) async fn create_bag(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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(
|
||||
State(state): State<AppState>,
|
||||
_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; \
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
|
||||
font-src 'self' https://fonts.gstatic.com; \
|
||||
img-src 'self' data:; \
|
||||
img-src 'self' data: blob:; \
|
||||
frame-ancestors 'none'",
|
||||
),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub struct CafeDetailView {
|
|||
pub country: String,
|
||||
pub country_flag: String,
|
||||
pub website: Option<String>,
|
||||
pub map_url: String,
|
||||
pub map_countries: String,
|
||||
pub map_max: u32,
|
||||
pub legend_entries: Vec<LegendEntry>,
|
||||
|
|
@ -24,6 +25,10 @@ impl CafeDetailView {
|
|||
.map(iso_to_flag_emoji)
|
||||
.unwrap_or_default();
|
||||
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 {
|
||||
id: cafe.id.to_string(),
|
||||
|
|
@ -32,6 +37,7 @@ impl CafeDetailView {
|
|||
country_flag,
|
||||
country: cafe.country,
|
||||
website: cafe.website,
|
||||
map_url,
|
||||
map_countries,
|
||||
map_max,
|
||||
legend_entries: vec![LegendEntry {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ pub struct CupDetailView {
|
|||
pub cafe_country: String,
|
||||
pub cafe_country_flag: String,
|
||||
pub cafe_website: Option<String>,
|
||||
pub cafe_map_url: String,
|
||||
// Map
|
||||
pub map_countries: String,
|
||||
pub map_max: u32,
|
||||
|
|
@ -111,6 +112,10 @@ impl CupDetailView {
|
|||
cafe_country: cafe.country.clone(),
|
||||
cafe_country_flag,
|
||||
cafe_website: cafe.website.clone(),
|
||||
cafe_map_url: format!(
|
||||
"https://www.google.com/maps?q={},{}",
|
||||
cafe.latitude, cafe.longitude
|
||||
),
|
||||
roaster_slug: roaster.slug.clone(),
|
||||
roast_slug: roast.slug.clone(),
|
||||
cafe_slug: cafe.slug.clone(),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,28 @@ customElements.define(
|
|||
// Deferred mode: store data URL in a target hidden input
|
||||
const targetId = this.getAttribute("target-input");
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,21 @@
|
|||
<div>
|
||||
<dt class="text-text-muted">Country</dt>
|
||||
<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>
|
||||
</div>
|
||||
{% if let Some(url) = cafe.website %}
|
||||
|
|
|
|||
|
|
@ -62,13 +62,27 @@
|
|||
<div>
|
||||
<dt class="text-text-muted">Country</dt>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-text-muted">City</dt>
|
||||
<dd class="font-medium text-text">{{ cup.cafe_city }}</dd>
|
||||
</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 %}
|
||||
<div>
|
||||
<dt class="text-text-muted">Website</dt>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
data-signals:_amount="{{ amount }}"
|
||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}', {contentType: 'form'})"
|
||||
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.' }"
|
||||
>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
|
|
@ -82,17 +82,25 @@
|
|||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span data-show="$_submitting" style="display:none"
|
||||
>{{ icons::spinner("h-4 w-4") }}</span
|
||||
>
|
||||
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||
Save Changes
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
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: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.' }"
|
||||
>
|
||||
<!-- Coffee -->
|
||||
|
|
@ -273,28 +273,31 @@
|
|||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span
|
||||
class="text-xs font-semibold text-text-muted uppercase tracking-wide"
|
||||
>Time (s)</span
|
||||
>Time</span
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-adjust"
|
||||
data-on:click="$_brewTime = Math.max(5, Number($_brewTime) - 5)"
|
||||
data-on:click="$_brewTime = Math.max(5, $_brewTime - 5)"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
type="hidden"
|
||||
name="brew_time"
|
||||
step="1"
|
||||
min="0"
|
||||
data-attr:value="$_brewTime"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
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
|
||||
type="button"
|
||||
class="btn-adjust"
|
||||
data-on:click="$_brewTime = Number($_brewTime) + 5"
|
||||
data-on:click="$_brewTime = $_brewTime + 5"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
|
|
@ -357,7 +360,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
{{ img::deferred_upload("edit-brew-image", "Brew Image") }}
|
||||
{{ img::deferred_upload_with_preview("edit-brew-image", "Brew Image", "brew", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
|
|
@ -365,17 +368,25 @@
|
|||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span data-show="$_submitting" style="display:none"
|
||||
>{{ icons::spinner("h-4 w-4") }}</span
|
||||
>
|
||||
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||
Save Changes
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
data-signals:_website="'{{ website }}'"
|
||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}', {contentType: 'form'})"
|
||||
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.' }"
|
||||
>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload("edit-cafe-image", "Cafe Image") }}
|
||||
{{ img::deferred_upload_with_preview("edit-cafe-image", "Cafe Image", "cafe", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
|
|
@ -125,17 +125,25 @@
|
|||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span data-show="$_submitting" style="display:none"
|
||||
>{{ icons::spinner("h-4 w-4") }}</span
|
||||
>
|
||||
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||
Save Changes
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
data-signals:_submit-error="''"
|
||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}', {contentType: 'form'})"
|
||||
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.' }"
|
||||
>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
</searchable-select>
|
||||
</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
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
|
|
@ -79,17 +79,25 @@
|
|||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span data-show="$_submitting" style="display:none"
|
||||
>{{ icons::spinner("h-4 w-4") }}</span
|
||||
>
|
||||
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||
Save Changes
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
data-signals:_model="'{{ model }}'"
|
||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}', {contentType: 'form'})"
|
||||
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.' }"
|
||||
>
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload("edit-gear-image", "Gear Image") }}
|
||||
{{ img::deferred_upload_with_preview("edit-gear-image", "Gear Image", "gear", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
|
|
@ -71,17 +71,25 @@
|
|||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span data-show="$_submitting" style="display:none"
|
||||
>{{ icons::spinner("h-4 w-4") }}</span
|
||||
>
|
||||
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||
Save Changes
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
data-signals:_tasting-notes="'{{ tasting_notes }}'"
|
||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}', {contentType: 'form'})"
|
||||
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.' }"
|
||||
>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
|
|
@ -139,7 +139,7 @@
|
|||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload("edit-roast-image", "Roast Image") }}
|
||||
{{ img::deferred_upload_with_preview("edit-roast-image", "Roast Image", "roast", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
|
|
@ -147,17 +147,25 @@
|
|||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span data-show="$_submitting" style="display:none"
|
||||
>{{ icons::spinner("h-4 w-4") }}</span
|
||||
>
|
||||
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||
Save Changes
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
data-signals:_homepage="'{{ homepage }}'"
|
||||
data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}', {contentType: 'form'})"
|
||||
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.' }"
|
||||
>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
{{ img::deferred_upload("edit-roaster-image", "Roaster Image") }}
|
||||
{{ img::deferred_upload_with_preview("edit-roaster-image", "Roaster Image", "roaster", id, image_url) }}
|
||||
<p
|
||||
data-show="$_submitError"
|
||||
data-text="$_submitError"
|
||||
|
|
@ -89,17 +89,25 @@
|
|||
class="text-sm text-error"
|
||||
role="alert"
|
||||
></p>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span data-show="$_submitting" style="display:none"
|
||||
>{{ icons::spinner("h-4 w-4") }}</span
|
||||
>
|
||||
<span data-show="!$_submitting">{{ icons::check("h-4 w-4") }}</span>
|
||||
Save Changes
|
||||
</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>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,60 @@
|
|||
</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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue