hardcoded toast
This commit is contained in:
@@ -202,6 +202,7 @@ parent-category = Parent category
|
|||||||
no-parent = — None (top level) —
|
no-parent = — None (top level) —
|
||||||
quantity = Quantity
|
quantity = Quantity
|
||||||
add-to-cart = Add to cart
|
add-to-cart = Add to cart
|
||||||
|
cart-added = Added to cart
|
||||||
in-stock = In stock
|
in-stock = In stock
|
||||||
out-of-stock = Out of stock
|
out-of-stock = Out of stock
|
||||||
confirm-delete = Delete this for good?
|
confirm-delete = Delete this for good?
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ parent-category = Nadradená kategória
|
|||||||
no-parent = — Žiadna (najvyššia úroveň) —
|
no-parent = — Žiadna (najvyššia úroveň) —
|
||||||
quantity = Množstvo
|
quantity = Množstvo
|
||||||
add-to-cart = Pridať do košíka
|
add-to-cart = Pridať do košíka
|
||||||
|
cart-added = Pridané do košíka
|
||||||
in-stock = Na sklade
|
in-stock = Na sklade
|
||||||
out-of-stock = Vypredané
|
out-of-stock = Vypredané
|
||||||
confirm-delete = Naozaj zmazať?
|
confirm-delete = Naozaj zmazať?
|
||||||
|
|||||||
@@ -47,6 +47,10 @@
|
|||||||
if (!v) return 0;
|
if (!v) return 0;
|
||||||
return v.split(',').reduce(function (s, e) { return s + (parseInt(e.split(':')[1]) || 0) }, 0);
|
return v.split(',').reduce(function (s, e) { return s + (parseInt(e.split(':')[1]) || 0) }, 0);
|
||||||
}
|
}
|
||||||
|
// Show a floating toast notification. Usage: toast('Saved').
|
||||||
|
function toast(message) {
|
||||||
|
window.dispatchEvent(new CustomEvent('toast', { detail: message }));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<link href="/static/css/app.css?v=2026-06-16" rel="stylesheet" type="text/css">
|
<link href="/static/css/app.css?v=2026-06-16" rel="stylesheet" type="text/css">
|
||||||
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
|
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
|
||||||
@@ -93,7 +97,7 @@
|
|||||||
<!-- cart with live item-count badge read from the `cart` cookie -->
|
<!-- cart with live item-count badge read from the `cart` cookie -->
|
||||||
<a href="/cart" data-nav="/cart"
|
<a href="/cart" data-nav="/cart"
|
||||||
x-data="{ count: 0 }"
|
x-data="{ count: 0 }"
|
||||||
x-init="count = cartCount(); window.addEventListener('htmx:afterSwap', function () { count = cartCount() })"
|
x-init="count = cartCount(); ['htmx:afterSwap', 'htmx:afterRequest'].forEach(function (e) { window.addEventListener(e, function () { count = cartCount() }) })"
|
||||||
aria-label="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
aria-label="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
||||||
title="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
title="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
||||||
class="relative inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
class="relative inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
||||||
@@ -205,5 +209,20 @@
|
|||||||
{% block content %}{% endblock content %}
|
{% block content %}{% endblock content %}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- toast notifications: fire from anywhere with toast('message') -->
|
||||||
|
<div x-data="{ toasts: [] }"
|
||||||
|
@toast.window="const id = Date.now() + Math.random(); toasts.push({ id, msg: $event.detail }); setTimeout(() => { toasts = toasts.filter(t => t.id !== id) }, 3000)"
|
||||||
|
class="pointer-events-none fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||||
|
<template x-for="t in toasts" :key="t.id">
|
||||||
|
<div x-transition.opacity.duration.300ms
|
||||||
|
class="pointer-events-auto flex items-center gap-2 rounded-radius border border-outline bg-surface px-4 py-3 text-sm font-medium text-on-surface shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-5 shrink-0 text-success">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
<span x-text="t.msg"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
<div class="flex flex-col gap-2 px-4 pb-4">
|
<div class="flex flex-col gap-2 px-4 pb-4">
|
||||||
{% if product.stock > 0 %}
|
{% if product.stock > 0 %}
|
||||||
<p class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}</p>
|
<p class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}</p>
|
||||||
<form method="post" action="/cart/add" hx-boost="false">
|
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none"
|
||||||
|
hx-on::after-request="if (event.detail.successful) toast('{{ t(key='cart-added', lang=lang | default(value='sk')) }}')">
|
||||||
<input type="hidden" name="product_id" value="{{ product.id }}">
|
<input type="hidden" name="product_id" value="{{ product.id }}">
|
||||||
<input type="hidden" name="quantity" value="1">
|
<input type="hidden" name="quantity" value="1">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
|
|||||||
@@ -39,7 +39,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if product.stock > 0 %}
|
{% if product.stock > 0 %}
|
||||||
<form method="post" action="/cart/add" hx-boost="false" class="flex flex-wrap items-end gap-3">
|
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none" class="flex flex-wrap items-end gap-3"
|
||||||
|
hx-on::after-request="if (event.detail.successful) toast('{{ t(key='cart-added', lang=lang | default(value='sk')) }}')">
|
||||||
<input type="hidden" name="product_id" value="{{ product.id }}">
|
<input type="hidden" name="product_id" value="{{ product.id }}">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="quantity" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="quantity", lang=lang | default(value='sk')) }}</label>
|
<label for="quantity" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="quantity", lang=lang | default(value='sk')) }}</label>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::{controllers::i18n::current_lang, shared::money::format_price, models::products};
|
use crate::{controllers::i18n::current_lang, shared::money::format_price, models::products};
|
||||||
use axum::{http::HeaderMap, response::Redirect};
|
use axum::{
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
response::Redirect,
|
||||||
|
};
|
||||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
@@ -73,6 +76,7 @@ async fn published_product(ctx: &AppContext, id: i32) -> Result<Option<products:
|
|||||||
async fn add(
|
async fn add(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
|
headers: HeaderMap,
|
||||||
Form(form): Form<AddForm>,
|
Form(form): Form<AddForm>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
let Some(product) = published_product(&ctx, form.product_id).await? else {
|
let Some(product) = published_product(&ctx, form.product_id).await? else {
|
||||||
@@ -88,9 +92,21 @@ async fn add(
|
|||||||
}
|
}
|
||||||
items.retain(|(_, qty)| *qty > 0);
|
items.retain(|(_, qty)| *qty > 0);
|
||||||
|
|
||||||
format::render()
|
let jar = jar.add(cart_cookie(serialize_cart(&items)));
|
||||||
.cookies(&[cart_cookie(serialize_cart(&items))])?
|
|
||||||
.redirect("/cart")
|
// Adding to the cart should never navigate away: htmx requests get an empty
|
||||||
|
// 204 (the header cart badge updates client-side), and a no-JS submit goes
|
||||||
|
// back to the page the customer was on rather than to the basket.
|
||||||
|
if headers.contains_key("HX-Request") {
|
||||||
|
Ok((jar, StatusCode::NO_CONTENT).into_response())
|
||||||
|
} else {
|
||||||
|
let back = headers
|
||||||
|
.get("referer")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("/shop")
|
||||||
|
.to_string();
|
||||||
|
Ok((jar, Redirect::to(&back)).into_response())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
|
|||||||
Reference in New Issue
Block a user