diff --git a/assets/views/base.html b/assets/views/base.html index aa4ab4d..659c6c5 100644 --- a/assets/views/base.html +++ b/assets/views/base.html @@ -39,6 +39,14 @@ } document.addEventListener('DOMContentLoaded', markActiveNav); document.addEventListener('htmx:afterSwap', markActiveNav); + // Sum the quantities stored in the `cart` cookie for the header badge. + function cartCount() { + var m = document.cookie.split('; ').find(function (c) { return c.indexOf('cart=') === 0 }); + if (!m) return 0; + var v = decodeURIComponent(m.split('=')[1] || ''); + if (!v) return 0; + return v.split(',').reduce(function (s, e) { return s + (parseInt(e.split(':')[1]) || 0) }, 0); + } @@ -85,7 +93,7 @@ diff --git a/assets/views/shop/_cart_body.html b/assets/views/shop/_cart_body.html new file mode 100644 index 0000000..7580389 --- /dev/null +++ b/assets/views/shop/_cart_body.html @@ -0,0 +1,62 @@ +{# Cart contents, swapped in via htmx on quantity change / removal so the page + never does a full reload. Rendered inside
in cart.html + and returned on its own by /cart/update and /cart/remove. #} +{% if items | length > 0 %} +
+ + + + + + + + + + + + {% for item in items %} + + + + + + + + {% endfor %} + + + + + + + + +
{{ t(key="product", lang=lang | default(value='sk')) }}{{ t(key="price", lang=lang | default(value='sk')) }}{{ t(key="quantity", lang=lang | default(value='sk')) }}{{ t(key="cart-total", lang=lang | default(value='sk')) }}
+ {{ item.name }} + {{ item.price }} {{ item.currency }} + {# Changing the quantity posts via htmx and swaps only #cart-body. #} +
+ + +
+
{{ item.line_total }} {{ item.currency }} +
+ + +
+
{{ t(key="cart-total", lang=lang | default(value='sk')) }}{{ total }} {{ currency }}
+
+ +
+ {{ t(key="cart-continue", lang=lang | default(value='sk')) }} + {{ t(key="cart-checkout", lang=lang | default(value='sk')) }} +
+{% else %} +
+

{{ t(key="cart-empty", lang=lang | default(value='sk')) }}

+ {{ t(key="cart-continue", lang=lang | default(value='sk')) }} +
+{% endif %} diff --git a/assets/views/shop/cart.html b/assets/views/shop/cart.html index 6e4cc8c..07c8a06 100644 --- a/assets/views/shop/cart.html +++ b/assets/views/shop/cart.html @@ -6,62 +6,8 @@

{{ t(key="cart-title", lang=lang | default(value='sk')) }}

- {% if items | length > 0 %} -
- - - - - - - - - - - - {% for item in items %} - - - - - - - - {% endfor %} - - - - - - - - -
{{ t(key="product", lang=lang | default(value='sk')) }}{{ t(key="price", lang=lang | default(value='sk')) }}{{ t(key="quantity", lang=lang | default(value='sk')) }}{{ t(key="cart-total", lang=lang | default(value='sk')) }}
- {{ item.name }} - {{ item.price }} {{ item.currency }} -
- - - -
-
{{ item.line_total }} {{ item.currency }} -
- - -
-
{{ t(key="cart-total", lang=lang | default(value='sk')) }}{{ total }} {{ currency }}
+
+ {% include "shop/_cart_body.html" %}
- - - {% else %} -
-

{{ t(key="cart-empty", lang=lang | default(value='sk')) }}

- {{ t(key="cart-continue", lang=lang | default(value='sk')) }} -
- {% endif %}
{% endblock content %} diff --git a/assets/views/shop/checkout.html b/assets/views/shop/checkout.html index e38f9d5..265abfe 100644 --- a/assets/views/shop/checkout.html +++ b/assets/views/shop/checkout.html @@ -46,18 +46,32 @@
- + +
+ + +
    + +
+
@@ -85,15 +99,32 @@
- +
+ + +
    + +
+
diff --git a/src/controllers/cart.rs b/src/controllers/cart.rs index 5407f66..c3cdc62 100644 --- a/src/controllers/cart.rs +++ b/src/controllers/cart.rs @@ -1,4 +1,5 @@ use crate::{controllers::i18n::current_lang, shared::money::format_price, models::products}; +use axum::{http::HeaderMap, response::Redirect}; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; use loco_rs::prelude::*; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; @@ -96,6 +97,8 @@ async fn add( async fn update( jar: CookieJar, State(ctx): State, + ViewEngine(v): ViewEngine, + headers: HeaderMap, Form(form): Form, ) -> Result { let stock = published_product(&ctx, form.product_id) @@ -110,19 +113,57 @@ async fn update( } items.retain(|(_, qty)| *qty > 0); - format::render() - .cookies(&[cart_cookie(serialize_cart(&items))])? - .redirect("/cart") + let jar = jar.add(cart_cookie(serialize_cart(&items))); + cart_response(&ctx, &v, jar, &headers).await } #[debug_handler] -async fn remove(jar: CookieJar, Form(form): Form) -> Result { +async fn remove( + jar: CookieJar, + State(ctx): State, + ViewEngine(v): ViewEngine, + headers: HeaderMap, + Form(form): Form, +) -> Result { let mut items = parse_cart(&jar); items.retain(|(id, _)| *id != form.product_id); - format::render() - .cookies(&[cart_cookie(serialize_cart(&items))])? - .redirect("/cart") + let jar = jar.add(cart_cookie(serialize_cart(&items))); + cart_response(&ctx, &v, jar, &headers).await +} + +/// Response after a cart mutation: for an htmx request, just the `#cart-body` +/// fragment (so the page never fully reloads); otherwise a redirect back to +/// `/cart` for no-JS fallback. `jar` must already hold the updated cart cookie. +async fn cart_response( + ctx: &AppContext, + v: &TeraView, + jar: CookieJar, + headers: &HeaderMap, +) -> Result { + if !headers.contains_key("HX-Request") { + return Ok((jar, Redirect::to("/cart")).into_response()); + } + + let (lines, valid, total) = resolve_cart(ctx, &jar).await?; + let currency = lines + .first() + .and_then(|line| line["currency"].as_str()) + .unwrap_or("EUR") + .to_string(); + // Persist the re-validated cookie (drops now-invalid lines). + let jar = jar.add(cart_cookie(serialize_cart(&valid))); + let response = format::view( + v, + "shop/_cart_body.html", + json!({ + "items": lines, + "total": format_price(total), + "currency": currency, + "lang": current_lang(&jar), + }), + )?; + Ok((jar, response).into_response()) } /// Resolve the cart cookie into priced line items, dropping anything that is no