4 Commits

Author SHA1 Message Date
Priec
42f30261d0 proper spacing and bascket icon
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-19 23:07:39 +02:00
Priec
ffda718a46 fixed menus now 2026-06-19 22:54:15 +02:00
Priec
673b28c361 working profile pic, but its trash, redoing the navbar icons now 2026-06-19 22:34:11 +02:00
Priec
454d5cb349 navbar profile 2026-06-19 13:59:31 +02:00
12 changed files with 192 additions and 58 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -117,8 +117,8 @@
{% block crumb %}{{ t(key="admin-title", lang=lang | default(value='sk')) }}{% endblock crumb %}
</span>
<!-- settings (language + theme) dropdown -->
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative ml-auto">
<!-- settings (language + theme) dropdown (self-contained Alpine state) -->
<div class="ml-auto">
{% include "partials/settings_dropdown.html" %}
</div>
</header>

View File

@@ -57,6 +57,9 @@
</script>
<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>
<!-- Alpine Focus plugin (x-trap / $focus) — must load before Alpine core;
required by the Penguin UI keyboard-accessible dropdowns. -->
<script defer src="/static/vendor/alpine/alpine-focus-3.14.9.min.js"></script>
<script defer src="/static/vendor/alpine/alpinejs-3.14.9.min.js"></script>
</head>
<body hx-boost="true"
@@ -86,12 +89,7 @@
</form>
</li>
{% elif logged_in_customer %}
<li>{{ ui::nav_link(label=t(key="nav-profile", lang=lang | default(value='sk')), href="/account/profile", data_nav="/account") }}</li>
<li>
<form method="post" action="/logout" hx-boost="false">
<button type="submit" class="text-sm font-medium text-danger underline-offset-2 transition hover:opacity-75 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form>
</li>
{# customer account links live in the profile dropdown next to the cart #}
{% else %}
<li>{{ ui::nav_link(label=t(key="nav-login", lang=lang | default(value='sk')), href="/login", data_nav="/login") }}</li>
<li>{{ ui::nav_link(label=t(key="nav-register", lang=lang | default(value='sk')), href="/register", data_nav="/register") }}</li>
@@ -99,9 +97,14 @@
</ul>
<!-- right side: cart + settings + mobile toggle -->
<div class="ml-auto flex items-center gap-1">
<!-- cart with live item-count badge read from the `cart` cookie -->
<a href="/cart" data-nav="/cart"
<div class="ml-auto flex items-center gap-2">
<!-- customer profile dropdown (avatar + name + account type) -->
{% if logged_in_customer %}
{% include "partials/profile_menu.html" %}
{% endif %}
<!-- cart with live item-count badge read from the `cart` cookie.
hx-boost=false: a plain full-page navigation to /cart, no SPA swap. -->
<a href="/cart" data-nav="/cart" hx-boost="false"
x-data="{ count: 0 }"
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')) }}"
@@ -111,10 +114,9 @@
<span x-show="count > 0" x-cloak x-text="count"
class="absolute -right-1 -top-1 inline-flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-4 text-on-primary dark:bg-primary-dark dark:text-on-primary-dark"></span>
</a>
<!-- settings (language + theme) dropdown -->
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative">
{% include "partials/settings_dropdown.html" %}
</div>
<!-- settings (language + theme) dropdown (self-contained Alpine state) -->
{% include "partials/settings_dropdown.html" %}
<!-- mobile hamburger — Penguin animated icon swap (bars ↔ X), kept in
our ghost-square icon-button shell for consistency with cart/gear -->

View File

@@ -0,0 +1,73 @@
{# Customer profile dropdown in the storefront navbar.
Proper Penguin UI dropdown: behaviour is the vendored
dropdowns/dropdown-with-icons.html verbatim (isOpen / openedWithKeyboard,
x-trap + $focus keyboard nav, x-cloak x-show, @click.outside). Trigger is the
round initials avatar (avatar-with-initials.html, primary variant). Menu items
are our account links.
Needs the Alpine Focus plugin (loaded before Alpine core in base.html) for
x-trap / $focus. Self-contained Alpine state; the host only needs to place it
in the navbar flex row. The panel has NO id on purpose — an id would make htmx
hx-boost "settle" it across boosted navigations and reappear; id-less Penguin
dropdowns are unaffected. #}
{# initials from the full name, e.g. "Filip Priec" -> "FP" #}
{% set _name = customer_name | default(value='') | trim %}
{% set _parts = _name | split(pat=' ') %}
{% set _initials = _parts.0 | truncate(length=1, end='') | upper %}
{% if _parts | length > 1 %}{% set _second = _parts | last | truncate(length=1, end='') | upper %}{% set _initials = _initials ~ _second %}{% endif %}
{% if customer_account_type == "company" %}{% set _type_label = t(key="account-company", lang=lang | default(value='sk')) %}{% else %}{% set _type_label = t(key="account-personal", lang=lang | default(value='sk')) %}{% endif %}
{% set _person_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-5"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/></svg>' %}
<div x-data="{ isOpen: false, openedWithKeyboard: false }"
x-on:keydown.esc.window="isOpen = false, openedWithKeyboard = false"
class="relative">
<!-- Toggle Button: round initials avatar -->
<button type="button" x-on:click="isOpen = ! isOpen"
x-on:keydown.space.prevent="openedWithKeyboard = true" x-on:keydown.enter.prevent="openedWithKeyboard = true" x-on:keydown.down.prevent="openedWithKeyboard = true"
x-bind:aria-expanded="isOpen || openedWithKeyboard" aria-haspopup="true"
aria-label="{{ t(key='nav-account', lang=lang | default(value='sk')) }}"
class="flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-full border border-primary bg-primary text-sm font-bold tracking-wider text-on-primary/90 transition hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark/90 dark:focus-visible:outline-primary-dark">
{%- if _initials %}{{ _initials }}{% else %}{{ _person_icon | safe }}{% endif -%}
</button>
<!-- Dropdown Menu (positioned like the settings cog: right-0 mt-2) -->
<div x-cloak x-show="isOpen || openedWithKeyboard" x-transition x-trap="openedWithKeyboard"
x-on:click.outside="isOpen = false, openedWithKeyboard = false"
x-on:keydown.down.prevent="$focus.wrap().next()" x-on:keydown.up.prevent="$focus.wrap().previous()"
class="absolute right-0 mt-2 flex w-60 min-w-48 flex-col divide-y divide-outline overflow-hidden rounded-radius border border-outline bg-surface-alt shadow-lg dark:divide-outline-dark dark:border-outline-dark dark:bg-surface-dark-alt" role="menu">
<!-- header: avatar + name + account type -->
<div class="flex items-center gap-3 px-4 py-2.5">
<span class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-full border border-primary bg-primary text-base font-bold tracking-wider text-on-primary/90 dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark/90">
{%- if _initials %}{{ _initials }}{% else %}{{ _person_icon | safe }}{% endif -%}
</span>
<div class="flex min-w-0 flex-col">
<span class="truncate text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ _name }}</span>
<p class="truncate text-xs text-on-surface dark:text-on-surface-dark">{{ _type_label }}</p>
</div>
</div>
<!-- account links (with icons) -->
<div class="flex flex-col py-1.5">
<a href="/account/orders" data-nav="/account/orders" role="menuitem" class="flex items-center gap-2 bg-surface-alt px-4 py-2 text-sm text-on-surface hover:bg-surface-dark-alt/5 hover:text-on-surface-strong focus-visible:bg-surface-dark-alt/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:bg-surface-dark-alt dark:text-on-surface-dark dark:hover:bg-surface-alt/5 dark:hover:text-on-surface-dark-strong dark:focus-visible:bg-surface-alt/10 dark:focus-visible:text-on-surface-dark-strong">
{{ ui::icon(name="cart", size="size-4", extra="shrink-0") }}
{{ t(key="account-orders", lang=lang | default(value='sk')) }}
</a>
<a href="/account/profile" data-nav="/account/profile" role="menuitem" class="flex items-center gap-2 bg-surface-alt px-4 py-2 text-sm text-on-surface hover:bg-surface-dark-alt/5 hover:text-on-surface-strong focus-visible:bg-surface-dark-alt/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:bg-surface-dark-alt dark:text-on-surface-dark dark:hover:bg-surface-alt/5 dark:hover:text-on-surface-dark-strong dark:focus-visible:bg-surface-alt/10 dark:focus-visible:text-on-surface-dark-strong">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-4 shrink-0"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/></svg>
{{ t(key="profile-title", lang=lang | default(value='sk')) }}
</a>
<a href="/account/password" data-nav="/account/password" role="menuitem" class="flex items-center gap-2 bg-surface-alt px-4 py-2 text-sm text-on-surface hover:bg-surface-dark-alt/5 hover:text-on-surface-strong focus-visible:bg-surface-dark-alt/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:bg-surface-dark-alt dark:text-on-surface-dark dark:hover:bg-surface-alt/5 dark:hover:text-on-surface-dark-strong dark:focus-visible:bg-surface-alt/10 dark:focus-visible:text-on-surface-dark-strong">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-4 shrink-0"><path fill-rule="evenodd" d="M15.75 1.5a6.75 6.75 0 00-6.651 7.906c.067.39-.032.717-.221.906l-6.5 6.499a3 3 0 00-.878 2.121v2.818c0 .414.336.75.75.75H6a.75.75 0 00.75-.75v-1.5h1.5A.75.75 0 009 21v-1.5h1.5a.75.75 0 00.53-.22l2.658-2.658c.19-.189.517-.288.906-.22A6.75 6.75 0 1015.75 1.5zm0 3a.75.75 0 000 1.5A2.25 2.25 0 0118 8.25a.75.75 0 001.5 0 3.75 3.75 0 00-3.75-3.75z" clip-rule="evenodd"/></svg>
{{ t(key="account-change-password", lang=lang | default(value='sk')) }}
</a>
</div>
<!-- logout -->
<div class="flex flex-col py-1.5">
<form method="post" action="/logout" hx-boost="false"><button type="submit" role="menuitem" class="flex w-full items-center gap-2 bg-surface-alt px-4 py-2 text-left text-sm text-on-surface hover:bg-surface-dark-alt/5 hover:text-on-surface-strong focus-visible:bg-surface-dark-alt/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:bg-surface-dark-alt dark:text-on-surface-dark dark:hover:bg-surface-alt/5 dark:hover:text-on-surface-dark-strong dark:focus-visible:bg-surface-alt/10 dark:focus-visible:text-on-surface-dark-strong">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-4 shrink-0"><path fill-rule="evenodd" d="M7.5 3.75A1.5 1.5 0 006 5.25v13.5a1.5 1.5 0 001.5 1.5h6a1.5 1.5 0 001.5-1.5V15a.75.75 0 011.5 0v3.75a3 3 0 01-3 3h-6a3 3 0 01-3-3V5.25a3 3 0 013-3h6a3 3 0 013 3V9A.75.75 0 0115 9V5.25a1.5 1.5 0 00-1.5-1.5h-6zm10.72 4.72a.75.75 0 011.06 0l3 3a.75.75 0 010 1.06l-3 3a.75.75 0 11-1.06-1.06l1.72-1.72H9a.75.75 0 010-1.5h10.94l-1.72-1.72a.75.75 0 010-1.06z" clip-rule="evenodd"/></svg>
{{ t(key="logout", lang=lang | default(value='sk')) }}
</button></form>
</div>
</div>
</div>

View File

@@ -1,18 +1,22 @@
{# Settings dropdown (language + theme). Shared by base.html and admin/base.html
to kill the former ~100-line copy-paste duplication.
Adapted from the vendored Penguin UI component
penguinui-components/dropdowns/dropdown-with-click.html: Penguin's dropdown
menu container + item treatment. Deviations: kept our gear icon-only trigger
and our core-Alpine open / @click.outside toggle (upstream's x-trap / $focus
need the Alpine Focus plugin, which we don't bundle); item hover uses
bg-primary/5 to stay consistent with the rest of our Penguin-ified UI.
Proper Penguin UI dropdown: behaviour is the vendored
dropdowns/dropdown-with-icons.html verbatim (isOpen / openedWithKeyboard,
x-trap + $focus keyboard nav, x-cloak x-show, @click.outside). Trigger is our
gear icon-only button; content is the language form + theme toggle. Needs the
Alpine Focus plugin (loaded in base.html) for x-trap / $focus.
The host template provides the wrapper
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative ...">
so it controls its own positioning (e.g. ml-auto in admin). #}
{{ ui::icon_button(aria_label=t(key='settings', lang=lang | default(value='sk')), attrs='@click="open = !open" :aria-expanded="open"', icon='<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>') }}
<div x-show="open" x-cloak @click.outside="open = false" x-transition.origin.top.right
Self-contained Alpine state + relative positioning; the host only places it
(e.g. ml-auto in admin). The panel has NO id on purpose (see profile_menu.html
for why — htmx hx-boost settles by id). #}
<div x-data="{ isOpen: false, openedWithKeyboard: false }"
x-on:keydown.esc.window="isOpen = false, openedWithKeyboard = false"
class="relative">
{{ ui::icon_button(aria_label=t(key='settings', lang=lang | default(value='sk')), attrs='x-on:click="isOpen = ! isOpen" x-on:keydown.space.prevent="openedWithKeyboard = true" x-on:keydown.enter.prevent="openedWithKeyboard = true" x-on:keydown.down.prevent="openedWithKeyboard = true" x-bind:aria-expanded="isOpen || openedWithKeyboard" aria-haspopup="true"', icon='<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>') }}
<div x-cloak x-show="isOpen || openedWithKeyboard" x-transition x-trap="openedWithKeyboard"
x-on:click.outside="isOpen = false, openedWithKeyboard = false"
x-on:keydown.down.prevent="$focus.wrap().next()" x-on:keydown.up.prevent="$focus.wrap().previous()"
class="absolute right-0 mt-2 flex w-56 flex-col overflow-hidden rounded-radius border border-outline bg-surface-alt py-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt"
role="menu">
<form method="post" action="/lang" hx-boost="false">
@@ -54,3 +58,4 @@
</label>
</div>
</div>
</div>

View File

@@ -126,6 +126,8 @@ fn profile_view(
"logged_in_admin": false,
"logged_in_customer": true,
"account_nav": true,
"customer_name": user.name,
"customer_account_type": user.account_type,
"saved": saved,
"error": error,
"name": user.name,
@@ -232,6 +234,8 @@ async fn orders_page(
"logged_in_admin": false,
"logged_in_customer": true,
"account_nav": true,
"customer_name": user.name,
"customer_account_type": user.account_type,
"active_orders": shape(active),
"past_orders": shape(past),
"lang": current_lang(&jar),
@@ -272,6 +276,8 @@ async fn order_detail_page(
"logged_in_admin": false,
"logged_in_customer": true,
"account_nav": true,
"customer_name": user.name,
"customer_account_type": user.account_type,
"order": order_view::detail(
&order,
settings::get(&ctx, "bank_iban").unwrap_or(""),
@@ -293,6 +299,7 @@ struct ChangePasswordForm {
fn password_view(
v: &TeraView,
jar: &CookieJar,
user: &users::Model,
changed: bool,
error: Option<&str>,
) -> Result<Response> {
@@ -303,6 +310,8 @@ fn password_view(
"logged_in_admin": false,
"logged_in_customer": true,
"account_nav": true,
"customer_name": user.name,
"customer_account_type": user.account_type,
"changed": changed,
"error": error,
"lang": current_lang(jar),
@@ -322,7 +331,7 @@ async fn change_password_page(
if guard::is_admin(&ctx, &user) {
return format::redirect("/admin/dashboard");
}
password_view(&v, &jar, false, None)
password_view(&v, &jar, &user, false, None)
}
#[debug_handler]
@@ -339,18 +348,19 @@ async fn change_password(
return format::redirect("/admin/dashboard");
}
if !user.verify_password(&form.current_password) {
return password_view(&v, &jar, false, Some("current"));
return password_view(&v, &jar, &user, false, Some("current"));
}
if form.password != form.password_confirm {
return password_view(&v, &jar, false, Some("mismatch"));
return password_view(&v, &jar, &user, false, Some("mismatch"));
}
if form.password.len() < 8 {
return password_view(&v, &jar, false, Some("weak"));
return password_view(&v, &jar, &user, false, Some("weak"));
}
user.into_active_model()
let user = user
.into_active_model()
.reset_password(&ctx.db, &form.password)
.await?;
password_view(&v, &jar, true, None)
password_view(&v, &jar, &user, true, None)
}
pub fn routes() -> Routes {

View File

@@ -234,7 +234,7 @@ async fn show(
// Drop any now-invalid lines from the cookie so the badge stays accurate.
let rebuilt = serialize_cart(&valid);
let (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
let c = guard::chrome(&ctx, &jar).await;
let response = format::view(
&v,
"shop/cart.html",
@@ -242,8 +242,10 @@ async fn show(
"items": lines,
"total": format_price(total),
"currency": currency,
"logged_in_admin": logged_in_admin,
"logged_in_customer": logged_in_customer,
"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,
"lang": current_lang(&jar),
}),
)?;

View File

@@ -357,7 +357,7 @@ async fn order_confirmation(
.filter(order_items::Column::OrderId.eq(order.id))
.all(&ctx.db)
.await?;
let (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
let c = guard::chrome(&ctx, &jar).await;
let account_created = params.contains_key("account_created");
format::view(
@@ -370,8 +370,10 @@ async fn order_confirmation(
settings::get(&ctx, "bank_account_name").unwrap_or(""),
),
"items": view::items(&items),
"logged_in_admin": logged_in_admin,
"logged_in_customer": logged_in_customer,
"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,
"account_created": account_created,
"lang": current_lang(&jar),
}),

View File

@@ -13,15 +13,17 @@ async fn index(
State(ctx): State<AppContext>,
) -> Result<Response> {
let products = shop::featured_products(&ctx, 8).await?;
let (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
let c = guard::chrome(&ctx, &jar).await;
format::view(
&v,
"home/index.html",
json!({
"products": products,
"logged_in_admin": logged_in_admin,
"logged_in_customer": logged_in_customer,
"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,
"lang": current_lang(&jar),
}),
)

View File

@@ -69,14 +69,16 @@ async fn index(
.all(&ctx.db)
.await?;
let (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
let c = guard::chrome(&ctx, &jar).await;
format::view(
&v,
"shop/index.html",
json!({
"products": product_rows(&ctx, list).await?,
"logged_in_admin": logged_in_admin,
"logged_in_customer": logged_in_customer,
"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,
"lang": current_lang(&jar),
}),
)
@@ -110,7 +112,7 @@ async fn show(
None => None,
};
let (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
let c = guard::chrome(&ctx, &jar).await;
format::view(
&v,
"shop/show.html",
@@ -118,8 +120,10 @@ async fn show(
"product": view::product_card(&product, None, category.as_ref().map(|c| c.name.clone())),
"images": images.iter().map(|i| i.image_id.clone()).collect::<Vec<_>>(),
"category": category,
"logged_in_admin": logged_in_admin,
"logged_in_customer": logged_in_customer,
"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,
"lang": current_lang(&jar),
}),
)
@@ -155,7 +159,7 @@ async fn category(
.all(&ctx.db)
.await?;
let (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
let c = guard::chrome(&ctx, &jar).await;
format::view(
&v,
"shop/category.html",
@@ -164,8 +168,10 @@ async fn category(
"breadcrumbs": breadcrumbs,
"children": children,
"products": product_rows(&ctx, list).await?,
"logged_in_admin": logged_in_admin,
"logged_in_customer": logged_in_customer,
"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,
"lang": current_lang(&jar),
}),
)

View File

@@ -46,13 +46,30 @@ pub async fn logged_in(ctx: &AppContext, jar: &CookieJar) -> bool {
}
}
/// Nav chrome flags for storefront pages, in one DB lookup: returns
/// `(logged_in_admin, logged_in_customer)`. A customer is any authenticated
/// non-admin user. Both are `false` for anonymous visitors.
pub async fn chrome(ctx: &AppContext, jar: &CookieJar) -> (bool, bool) {
/// Nav chrome for storefront pages, resolved in one DB lookup. A customer is any
/// authenticated non-admin user; `customer_name`/`customer_account_type` are set
/// only for such a customer (used by the navbar profile menu). Everything is the
/// zero value for anonymous visitors and the name/type stay `None` for admins.
#[derive(Debug, Default)]
pub struct Chrome {
pub logged_in_admin: bool,
pub logged_in_customer: bool,
pub customer_name: Option<String>,
pub customer_account_type: Option<String>,
}
pub async fn chrome(ctx: &AppContext, jar: &CookieJar) -> Chrome {
match current_user(ctx, jar).await {
Some(user) if is_admin(ctx, &user) => (true, false),
Some(_) => (false, true),
None => (false, false),
Some(user) if is_admin(ctx, &user) => Chrome {
logged_in_admin: true,
..Default::default()
},
Some(user) => Chrome {
logged_in_customer: true,
customer_name: Some(user.name),
customer_account_type: Some(user.account_type),
..Default::default()
},
None => Chrome::default(),
}
}