This commit is contained in:
Priec
2026-06-25 12:12:50 +02:00
parent d3d1c0d157
commit f51875d5f4
14 changed files with 225 additions and 17 deletions

View File

@@ -521,3 +521,6 @@ footer-orders = My orders
footer-email = info@kompress.sk
footer-hours = MonFri 8:0016:00
footer-rights = © 2026 Kompress · Medical supplies
page-coming-soon = This page is coming soon. In the meantime, feel free to contact us by phone or e-mail.
page-contact-intro = We're happy to help you choose. Get in touch:
page-sitemap-intro = An overview of the shop's main sections.

View File

@@ -521,3 +521,6 @@ footer-orders = Moje objednávky
footer-email = info@kompress.sk
footer-hours = PoPia 8:0016:00
footer-rights = © 2026 Kompress · Zdravotnícke potreby
page-coming-soon = Túto stránku práve pripravujeme. Medzitým nás môžete kontaktovať telefonicky alebo e-mailom.
page-contact-intro = Radi vám poradíme s výberom. Ozvite sa nám:
page-sitemap-intro = Prehľad hlavných sekcií obchodu.

File diff suppressed because one or more lines are too long

View File

@@ -83,9 +83,9 @@
<a href="/shop" data-nav="/shop" class="transition hover:text-primary aria-[current=page]:font-semibold aria-[current=page]:text-primary dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a>
</div>
<div class="flex items-center gap-4">
<a href="#" class="transition hover:text-primary dark:hover:text-primary-dark">{{ t(key="top-contact", lang=lang | default(value='sk')) }}</a>
<a href="/kontakt" class="transition hover:text-primary dark:hover:text-primary-dark">{{ t(key="top-contact", lang=lang | default(value='sk')) }}</a>
<span class="h-3 w-px bg-outline dark:bg-outline-dark"></span>
<a href="#" class="transition hover:text-primary dark:hover:text-primary-dark">{{ t(key="top-sitemap", lang=lang | default(value='sk')) }}</a>
<a href="/mapa-stranky" class="transition hover:text-primary dark:hover:text-primary-dark">{{ t(key="top-sitemap", lang=lang | default(value='sk')) }}</a>
</div>
</div>
</div>
@@ -298,10 +298,10 @@
</div>
<div class="flex flex-col gap-2.5">
<div class="text-xs font-bold uppercase tracking-wider text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="footer-info", lang=lang | default(value='sk')) }}</div>
<a href="#" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-terms", lang=lang | default(value='sk')) }}</a>
<a href="#" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-about", lang=lang | default(value='sk')) }}</a>
<a href="#" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-stores", lang=lang | default(value='sk')) }}</a>
<a href="#" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-shipping", lang=lang | default(value='sk')) }}</a>
<a href="/obchodne-podmienky" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-terms", lang=lang | default(value='sk')) }}</a>
<a href="/o-nas" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-about", lang=lang | default(value='sk')) }}</a>
<a href="/predajne" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-stores", lang=lang | default(value='sk')) }}</a>
<a href="/doprava-a-platba" class="text-sm text-on-surface/70 transition hover:text-primary dark:text-on-surface-dark/70 dark:hover:text-primary-dark">{{ t(key="footer-shipping", lang=lang | default(value='sk')) }}</a>
</div>
<div class="flex flex-col gap-2.5">
<div class="text-xs font-bold uppercase tracking-wider text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="footer-account", lang=lang | default(value='sk')) }}</div>

View File

@@ -82,7 +82,7 @@
<h2 class="border-b border-outline px-4 py-3.5 text-xs font-bold uppercase tracking-wider text-on-surface-strong dark:border-outline-dark dark:text-on-surface-dark-strong">Naše obchody</h2>
<div class="p-3.5">
<div class="flex h-28 items-center justify-center rounded-radius border border-outline bg-surface text-sm text-on-surface/50 dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark/40">Foto predajne</div>
<a href="#" class="mt-3 inline-block text-sm font-bold text-primary transition hover:underline dark:text-primary-dark">Objaviť naše obchody </a>
<a href="/predajne" class="mt-3 inline-block text-sm font-bold text-primary transition hover:underline dark:text-primary-dark">Objaviť naše obchody </a>
</div>
</section>

View File

@@ -291,3 +291,18 @@ border-t border-outline dark:border-outline-dark
{% macro crumb_current(label) -%}
<li class="font-semibold text-on-surface-strong dark:text-on-surface-dark-strong" aria-current="page">{{ label }}</li>
{%- endmacro crumb_current %}
{# Title for the static info pages (controllers/pages.rs → pages/info.html),
resolved from the `page` slug. Lives in a macro because a child template's
top-level {% set %} isn't visible inside its {% block %}s under `extends`;
the macro can be called from both the title and content blocks. #}
{% macro page_title(page, lang) -%}
{%- if page == "contact" -%}{{ t(key="top-contact", lang=lang) }}
{%- elif page == "sitemap" -%}{{ t(key="top-sitemap", lang=lang) }}
{%- elif page == "terms" -%}{{ t(key="footer-terms", lang=lang) }}
{%- elif page == "about" -%}{{ t(key="footer-about", lang=lang) }}
{%- elif page == "stores" -%}{{ t(key="footer-stores", lang=lang) }}
{%- elif page == "shipping" -%}{{ t(key="footer-shipping", lang=lang) }}
{%- else -%}{{ t(key="brand", lang=lang) }}
{%- endif -%}
{%- endmacro page_title %}

View File

@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{# Static info pages (contact / sitemap / terms / about / stores / shipping).
One template switches title + body on the `page` slug passed by
controllers/pages.rs. Titles reuse the existing top-/footer- i18n keys. #}
{% block title %}{{ ui::page_title(page=page, lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
{% set L = lang | default(value='sk') %}
{% set title = ui::page_title(page=page, lang=L) %}
<div class="mx-auto max-w-3xl space-y-6">
<nav aria-label="breadcrumb" class="text-sm">
<ol class="flex flex-wrap items-center gap-1.5 text-on-surface/60 dark:text-on-surface-dark/60">
{{ ui::crumb(label=t(key="nav-home", lang=L), href="/") }}
{{ ui::crumb_current(label=title) }}
</ol>
</nav>
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ title }}</h1>
{% if page == "contact" %}
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="page-contact-intro", lang=L) }}</p>
<div class="grid gap-3 sm:grid-cols-3">
<div class="rounded-radius border border-outline bg-surface-alt p-4 dark:border-outline-dark dark:bg-surface-dark-alt">
<div class="text-xs font-semibold uppercase tracking-wide text-on-surface/50 dark:text-on-surface-dark/50">{{ t(key="top-contact", lang=L) }}</div>
<a href="tel:+421903410476" class="mt-1 block text-lg font-bold text-primary dark:text-primary-dark">{{ t(key="hotline", lang=L) }}</a>
</div>
<div class="rounded-radius border border-outline bg-surface-alt p-4 dark:border-outline-dark dark:bg-surface-dark-alt">
<div class="text-xs font-semibold uppercase tracking-wide text-on-surface/50 dark:text-on-surface-dark/50">E-mail</div>
<a href="mailto:info@kompress.sk" class="mt-1 block font-semibold text-primary dark:text-primary-dark">{{ t(key="footer-email", lang=L) }}</a>
</div>
<div class="rounded-radius border border-outline bg-surface-alt p-4 dark:border-outline-dark dark:bg-surface-dark-alt">
<div class="text-xs font-semibold uppercase tracking-wide text-on-surface/50 dark:text-on-surface-dark/50">{{ t(key="footer-hours", lang=L) }}</div>
<div class="mt-1 font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="footer-hours", lang=L) }}</div>
</div>
</div>
{% elif page == "sitemap" %}
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="page-sitemap-intro", lang=L) }}</p>
<ul class="grid gap-2 sm:grid-cols-2">
<li><a href="/" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="nav-home", lang=L) }}</a></li>
<li><a href="/shop" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="nav-shop", lang=L) }}</a></li>
<li><a href="/cart" hx-boost="false" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="cart-title", lang=L) }}</a></li>
<li><a href="/kontakt" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="top-contact", lang=L) }}</a></li>
<li><a href="/o-nas" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="footer-about", lang=L) }}</a></li>
<li><a href="/predajne" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="footer-stores", lang=L) }}</a></li>
<li><a href="/doprava-a-platba" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="footer-shipping", lang=L) }}</a></li>
<li><a href="/obchodne-podmienky" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="footer-terms", lang=L) }}</a></li>
{% if logged_in_customer %}
<li><a href="/account/orders" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="footer-orders", lang=L) }}</a></li>
{% else %}
<li><a href="/login" class="text-primary transition hover:underline dark:text-primary-dark">{{ t(key="nav-login", lang=L) }}</a></li>
{% endif %}
</ul>
{% else %}
<div class="rounded-radius border border-outline bg-surface-alt p-6 text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
<p>{{ t(key="page-coming-soon", lang=L) }}</p>
<div class="mt-4 flex flex-wrap gap-4 text-sm">
<a href="tel:+421903410476" class="font-semibold text-primary dark:text-primary-dark">{{ t(key="hotline", lang=L) }}</a>
<a href="mailto:info@kompress.sk" class="font-semibold text-primary dark:text-primary-dark">{{ t(key="footer-email", lang=L) }}</a>
</div>
</div>
{% endif %}
</div>
{% endblock content %}

View File

@@ -16,8 +16,11 @@
<a href="/shop/{{ product.slug }}" class="flex min-w-0 flex-1"
:class="view === 'list' ? 'flex-row' : 'flex-col'">
<!-- Image -->
<div class="overflow-hidden bg-surface-alt dark:bg-surface-dark"
<div class="relative overflow-hidden bg-surface-alt dark:bg-surface-dark"
:class="view === 'list' ? 'size-28 shrink-0 sm:size-40' : 'h-44 md:h-64'">
{% if product.on_sale and product.percent_off > 0 %}
<span class="absolute left-2 top-2 z-10 rounded-full bg-danger px-2 py-0.5 text-[11px] font-bold text-on-danger shadow-sm">{{ product.percent_off }} %</span>
{% endif %}
{% if product.image %}
<img src="/images/{{ product.image }}" alt="{{ product.name }}" class="size-full object-cover transition duration-700 ease-out group-hover:scale-105">
{% endif %}
@@ -44,6 +47,18 @@
{% else %}
<span class="break-words text-xl"><span class="sr-only">Price</span>{% if product.has_options %}{{ t(key="from-price", price=product.price, lang=lang | default(value='sk')) }}{% else %}{{ product.price }}{% endif %} {{ currency_symbol }}</span>
{% endif %}
<!-- stock pill (Kompress design): green "in stock" / red "sold out" -->
<div class="mt-0.5">
{% if product.in_stock %}
<span class="inline-flex items-center gap-1.5 rounded-full bg-success/10 px-2 py-0.5 text-xs font-semibold text-success">
<span class="size-1.5 rounded-full bg-success" aria-hidden="true"></span>{{ t(key="in-stock", lang=lang | default(value='sk')) }}
</span>
{% else %}
<span class="inline-flex items-center gap-1.5 rounded-full bg-danger/10 px-2 py-0.5 text-xs font-semibold text-danger">
<span class="size-1.5 rounded-full bg-danger" aria-hidden="true"></span>{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}
</span>
{% endif %}
</div>
</div>
</a>
<div class="flex flex-col gap-2"

View File

@@ -70,11 +70,9 @@
</p>
{% set L = lang | default(value='sk') %}
<div class="flex flex-col gap-0.5">
{% for key in ["footer-terms", "footer-about", "footer-stores", "footer-shipping"] %}
<a href="#"
class="flex items-center gap-2 truncate rounded-radius px-2 py-1.5 text-sm text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">
{{ t(key=key, lang=L) }}
</a>
{% endfor %}
<a href="/obchodne-podmienky" class="flex items-center gap-2 truncate rounded-radius px-2 py-1.5 text-sm text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">{{ t(key="footer-terms", lang=L) }}</a>
<a href="/o-nas" class="flex items-center gap-2 truncate rounded-radius px-2 py-1.5 text-sm text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">{{ t(key="footer-about", lang=L) }}</a>
<a href="/predajne" class="flex items-center gap-2 truncate rounded-radius px-2 py-1.5 text-sm text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">{{ t(key="footer-stores", lang=L) }}</a>
<a href="/doprava-a-platba" class="flex items-center gap-2 truncate rounded-radius px-2 py-1.5 text-sm text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">{{ t(key="footer-shipping", lang=L) }}</a>
</div>
</div>

View File

@@ -124,7 +124,7 @@ Mockup: a *Porovnať* button in the listing toolbar.
- **No compare feature exists.** Netnew: selecttocompare state (Alpine + a cookie
or server list) and a compare view. Largest item here — defer unless wanted.
### B6. Listview product card — 🟡
### B6. Listview product card — ✅ done
Mockup list rows are richer (left thumbnail, description, stock pill, price block,
add/options/outofstock button on the right).
- `shop/_card.html` already has a `view === 'list'` horizontal layout. Close already;

View File

@@ -20,7 +20,7 @@ use crate::{
account, admin_categories, admin_currencies, admin_customers, admin_dashboard,
admin_discount_profiles, admin_form, admin_orders, admin_products, admin_shipping,
auth, auth_pages, cart, checkout, currency, home, i18n, media, oauth2,
shop,
pages, shop,
},
initializers,
models::_entities::users,
@@ -97,6 +97,7 @@ impl Hooks for App {
.add_route(cart::routes())
.add_route(checkout::routes())
.add_route(currency::routes())
.add_route(pages::routes())
// cross-cutting
.add_route(auth::routes())
.add_route(auth_pages::routes())

View File

@@ -17,4 +17,5 @@ pub mod currency;
pub mod home;
pub mod i18n;
pub mod media;
pub mod pages;
pub mod shop;

96
src/controllers/pages.rs Normal file
View File

@@ -0,0 +1,96 @@
//! Static informational pages (contact, sitemap, terms, about, stores,
//! shipping). These back the top-bar / footer / sidebar links so none of them
//! is a dead `#`. Content is static; the same chrome context as the home page
//! is supplied so `base.html` (header, cart badge, currencies) renders.
use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*;
use serde_json::json;
use crate::{controllers::i18n::current_lang, shared::currency, shared::guard};
/// Render one static page through `pages/info.html`, which switches its title +
/// body on the `page` slug. Mirrors `home::index`'s chrome wiring.
async fn render(v: &TeraView, jar: &CookieJar, ctx: &AppContext, page: &str) -> Result<Response> {
let user = guard::current_user(ctx, jar).await;
let cur = currency::resolve(ctx, jar).await;
let c = guard::chrome_from(ctx, user.as_ref());
format::view(
v,
"pages/info.html",
json!({
"page": page,
"logged_in_admin": c.logged_in_admin,
"logged_in_customer": c.logged_in_customer,
"customer_name": c.customer_name,
"customer_account_type": c.customer_account_type,
"currency_symbol": cur.symbol,
"lang": current_lang(jar),
}),
)
}
#[debug_handler]
async fn contact(
jar: CookieJar,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<Response> {
render(&v, &jar, &ctx, "contact").await
}
#[debug_handler]
async fn sitemap(
jar: CookieJar,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<Response> {
render(&v, &jar, &ctx, "sitemap").await
}
#[debug_handler]
async fn terms(
jar: CookieJar,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<Response> {
render(&v, &jar, &ctx, "terms").await
}
#[debug_handler]
async fn about(
jar: CookieJar,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<Response> {
render(&v, &jar, &ctx, "about").await
}
#[debug_handler]
async fn stores(
jar: CookieJar,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<Response> {
render(&v, &jar, &ctx, "stores").await
}
#[debug_handler]
async fn shipping(
jar: CookieJar,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<Response> {
render(&v, &jar, &ctx, "shipping").await
}
pub fn routes() -> Routes {
Routes::new()
.add("/kontakt", get(contact))
.add("/mapa-stranky", get(sitemap))
.add("/obchodne-podmienky", get(terms))
.add("/o-nas", get(about))
.add("/predajne", get(stores))
.add("/doprava-a-platba", get(shipping))
}

View File

@@ -22,6 +22,14 @@ pub fn product_card(
category_name: Option<String>,
cur: &Currency,
) -> Value {
// Whole-percent discount for the card's sale badge (e.g. "15 %"). Only
// meaningful when the resolved price is actually reduced below the regular.
let percent_off = if priced.is_reduced() && priced.regular_cents > priced.price_cents {
(((priced.regular_cents - priced.price_cents) as f64 / priced.regular_cents as f64) * 100.0)
.round() as i64
} else {
0
};
json!({
"id": product.id,
"variant_id": representative.id,
@@ -31,6 +39,7 @@ pub fn product_card(
"short_description": product.short_description,
"price": cur.format(priced.price_cents),
"on_sale": priced.is_reduced(),
"percent_off": percent_off,
"is_business": priced.is_business,
"regular_price": cur.format(priced.regular_cents),
"sku": representative.sku,