Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2023b24d92 | ||
|
|
aea4782e68 |
@@ -387,6 +387,11 @@ profile-last-name = Surname
|
||||
profile-edit = Edit profile
|
||||
profile-cancel = Cancel
|
||||
profile-not-set = Not set
|
||||
profile-avatar = Profile picture
|
||||
profile-avatar-hint = PNG, JPG, WEBP or GIF, up to 10 MB.
|
||||
profile-avatar-choose = Choose a picture
|
||||
profile-avatar-upload = Upload
|
||||
profile-avatar-remove = Remove picture
|
||||
nav-account = My account
|
||||
account-orders = My orders
|
||||
account-change-password = Change password
|
||||
@@ -507,6 +512,8 @@ brand-subtitle = medical supplies
|
||||
top-contact = Contact
|
||||
top-sitemap = Sitemap
|
||||
search-button = Search
|
||||
search-scope-in = Searching in:
|
||||
search-scope-all = Search the whole shop
|
||||
welcome = Welcome
|
||||
cart-units = items
|
||||
hotline = +421 903 410 476
|
||||
|
||||
@@ -387,6 +387,11 @@ profile-last-name = Priezvisko
|
||||
profile-edit = Upraviť profil
|
||||
profile-cancel = Zrušiť
|
||||
profile-not-set = Neuvedené
|
||||
profile-avatar = Profilová fotka
|
||||
profile-avatar-hint = PNG, JPG, WEBP alebo GIF, max. 10 MB.
|
||||
profile-avatar-choose = Vybrať fotku
|
||||
profile-avatar-upload = Nahrať
|
||||
profile-avatar-remove = Odstrániť fotku
|
||||
nav-account = Môj účet
|
||||
account-orders = Moje objednávky
|
||||
account-change-password = Zmeniť heslo
|
||||
@@ -507,6 +512,8 @@ brand-subtitle = zdravotnícke potreby
|
||||
top-contact = Kontakt
|
||||
top-sitemap = Mapa stránky
|
||||
search-button = Hľadať
|
||||
search-scope-in = Hľadáte v kategórii:
|
||||
search-scope-all = Hľadať v celom obchode
|
||||
welcome = Vitajte
|
||||
cart-units = ks
|
||||
hotline = +421 903 410 476
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,6 +28,45 @@
|
||||
{{ ui::alert_danger(message=t(key="profile-company-required", lang=lang | default(value='sk')), extra="mt-4") }}
|
||||
{% endif %}
|
||||
|
||||
{# initials fallback when no avatar is set, e.g. "Filip Priec" -> "FP" #}
|
||||
{% set _name = 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 %}
|
||||
|
||||
<!-- avatar: upload / replace / remove. Own multipart form, independent of the
|
||||
profile edit toggle below, so it works in both view and edit modes. -->
|
||||
<fieldset class="mt-6 space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"
|
||||
x-data="{ name: '' }">
|
||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-avatar", lang=lang | default(value='sk')) }}</legend>
|
||||
<div class="flex items-center gap-5">
|
||||
<span class="flex size-20 shrink-0 items-center justify-center overflow-hidden rounded-full border border-primary bg-primary text-2xl font-bold tracking-wider text-on-primary/90 dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark/90">
|
||||
{%- if avatar_id %}<img src="/images/{{ avatar_id }}" alt="{{ _name }}" class="size-full object-cover">{% elif _initials %}{{ _initials }}{% endif -%}
|
||||
</span>
|
||||
<div class="min-w-0 space-y-3">
|
||||
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="profile-avatar-hint", lang=lang | default(value='sk')) }}</p>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<form method="post" action="/account/profile/avatar" enctype="multipart/form-data" hx-boost="false" class="flex flex-wrap items-center gap-3">
|
||||
{{ ui::csrf_field() }}
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 rounded-radius border border-outline bg-surface-alt px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-primary/5 hover:text-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark dark:hover:text-primary-dark">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 shrink-0" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" /></svg>
|
||||
<span class="truncate max-w-[12rem]" x-text="name || '{{ t(key='profile-avatar-choose', lang=lang | default(value='sk')) }}'">{{ t(key="profile-avatar-choose", lang=lang | default(value='sk')) }}</span>
|
||||
<input type="file" name="image" accept="image/png,image/jpeg,image/webp,image/gif" class="sr-only"
|
||||
@change="name = $event.target.files.length ? $event.target.files[0].name : ''">
|
||||
</label>
|
||||
{{ ui::button(label=t(key="profile-avatar-upload", lang=lang | default(value='sk')), type="submit", size="px-4 py-2 text-sm", attrs='x-show="name" x-cloak') }}
|
||||
</form>
|
||||
{% if avatar_id %}
|
||||
<form method="post" action="/account/profile/avatar/remove" hx-boost="false">
|
||||
{{ ui::csrf_field() }}
|
||||
{{ ui::button(label=t(key="profile-avatar-remove", lang=lang | default(value='sk')), type="submit", variant="outline-secondary", size="px-4 py-2 text-sm") }}
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- read-only view (default) -->
|
||||
<div x-show="!editing" class="mt-6 space-y-6">
|
||||
<fieldset class="space-y-2 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
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 -%}
|
||||
{%- if customer_avatar %}<img src="/images/{{ customer_avatar }}" alt="{{ _name }}" class="size-full object-cover">{% elif _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"
|
||||
@@ -40,7 +40,7 @@
|
||||
<!-- 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 -%}
|
||||
{%- if customer_avatar %}<img src="/images/{{ customer_avatar }}" alt="{{ _name }}" class="size-full object-cover">{% elif _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>
|
||||
|
||||
@@ -47,6 +47,29 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Scope indicator: when a category is active, make clear the search is
|
||||
limited to it (not the whole shop), with a one-click escape to search
|
||||
everything. Category only changes via full navigation (the sidebar), so
|
||||
this stays accurate across the toolbar's results-only htmx swaps. #}
|
||||
{% if selected_category and selected_category != "all" %}
|
||||
{# set_global so the value survives the nested if (a plain `set` inside a
|
||||
block is scoped to that block in Tera and wouldn't be visible below). #}
|
||||
{% set_global _scope = selected_category_name | default(value="") %}
|
||||
{% if selected_category == "none" %}{% set_global _scope = t(key="uncategorized", lang=L) %}{% endif %}
|
||||
{% if _scope %}
|
||||
<div class="flex max-w-xl flex-wrap items-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1 font-medium text-primary dark:bg-primary-dark/15 dark:text-primary-dark">
|
||||
{{ ui::icon(name="search", size="size-3.5", extra="shrink-0") }}
|
||||
{{ t(key="search-scope-in", lang=L) }} <span class="font-semibold">{{ _scope }}</span>
|
||||
</span>
|
||||
<a href="/search{% if query %}?q={{ query | urlencode }}{% endif %}"
|
||||
class="font-medium text-on-surface/60 underline-offset-2 hover:text-primary hover:underline dark:text-on-surface-dark/60 dark:hover:text-primary-dark">
|
||||
{{ t(key="search-scope-all", lang=L) }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- sort + product card style switch -->
|
||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||
<label class="flex items-center gap-2 text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">
|
||||
|
||||
@@ -49,6 +49,7 @@ mod m20260623_000001_add_short_description_to_products;
|
||||
mod m20260623_000002_strip_html_from_product_search;
|
||||
mod m20260623_000003_drop_currency;
|
||||
mod m20260623_000004_currencies;
|
||||
mod m20260625_000001_add_avatar_to_users;
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -102,6 +103,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260623_000002_strip_html_from_product_search::Migration),
|
||||
Box::new(m20260623_000003_drop_currency::Migration),
|
||||
Box::new(m20260623_000004_currencies::Migration),
|
||||
Box::new(m20260625_000001_add_avatar_to_users::Migration),
|
||||
// inject-above (do not remove this comment)
|
||||
]
|
||||
}
|
||||
|
||||
20
migration/src/m20260625_000001_add_avatar_to_users.rs
Normal file
20
migration/src/m20260625_000001_add_avatar_to_users.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use loco_rs::schema::*;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
// Optional profile avatar. `avatar_id` holds the stored image's filename (the
|
||||
// same `<uuid>.<ext>` scheme as product/category images), served through the
|
||||
// shared `/images/{filename}` route. NULL = no avatar, fall back to initials.
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||
add_column(m, "users", "avatar_id", ColType::StringNull).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||
remove_column(m, "users", "avatar_id").await
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
//! on the user — it is shown here read-only and can never be changed. The
|
||||
//! profile only edits the type-specific details (company identity + address).
|
||||
|
||||
use axum::extract::{DefaultBodyLimit, Multipart};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::QueryOrder;
|
||||
@@ -14,7 +15,11 @@ use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
controllers::i18n::current_lang,
|
||||
controllers::{
|
||||
admin_form::{read_multipart_form, store_image},
|
||||
i18n::current_lang,
|
||||
media::IMAGE_MAX_BYTES,
|
||||
},
|
||||
models::{
|
||||
customer_profiles::{self, ProfileFields},
|
||||
order_items, orders, users,
|
||||
@@ -128,6 +133,8 @@ fn profile_view(
|
||||
"account_nav": true,
|
||||
"customer_name": user.name,
|
||||
"customer_account_type": user.account_type,
|
||||
"customer_avatar": user.avatar_id,
|
||||
"avatar_id": user.avatar_id,
|
||||
"saved": saved,
|
||||
"error": error,
|
||||
"name": user.name,
|
||||
@@ -202,6 +209,64 @@ async fn save_profile(
|
||||
profile_view(&v, &jar, &user, &fields, true, false)
|
||||
}
|
||||
|
||||
/// Persist `avatar_id` (a stored image filename, or `None` to clear) on the
|
||||
/// signed-in customer and re-render the profile page with the success banner.
|
||||
async fn set_avatar(
|
||||
v: &TeraView,
|
||||
jar: &CookieJar,
|
||||
ctx: &AppContext,
|
||||
user: users::Model,
|
||||
avatar_id: Option<String>,
|
||||
) -> Result<Response> {
|
||||
let mut active = user.clone().into_active_model();
|
||||
active.avatar_id = ActiveValue::set(avatar_id.clone());
|
||||
let user = active.update(&ctx.db).await?;
|
||||
let profile = customer_profiles::Model::find_for_user(&ctx.db, user.id).await?;
|
||||
profile_view(v, jar, &user, &fields_of(profile.as_ref()), true, false)
|
||||
}
|
||||
|
||||
/// Upload (or replace) the signed-in customer's avatar picture. The single
|
||||
/// `image` file part is validated and stored through the shared image storage,
|
||||
/// then its generated filename is saved as the user's `avatar_id`.
|
||||
#[debug_handler]
|
||||
async fn upload_avatar(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
multipart: Multipart,
|
||||
) -> Result<Response> {
|
||||
let Some(user) = guard::current_user(&ctx, &jar).await else {
|
||||
return format::redirect("/login");
|
||||
};
|
||||
if guard::is_admin(&ctx, &user) {
|
||||
return format::redirect("/admin/dashboard");
|
||||
}
|
||||
let form = read_multipart_form(multipart).await?;
|
||||
let Some(image) = form.single_image() else {
|
||||
// No file chosen — nothing to do, just re-show the profile.
|
||||
let profile = customer_profiles::Model::find_for_user(&ctx.db, user.id).await?;
|
||||
return profile_view(&v, &jar, &user, &fields_of(profile.as_ref()), false, false);
|
||||
};
|
||||
let filename = store_image(&ctx, image).await?;
|
||||
set_avatar(&v, &jar, &ctx, user, Some(filename)).await
|
||||
}
|
||||
|
||||
/// Remove the signed-in customer's avatar, reverting to the initials fallback.
|
||||
#[debug_handler]
|
||||
async fn remove_avatar(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let Some(user) = guard::current_user(&ctx, &jar).await else {
|
||||
return format::redirect("/login");
|
||||
};
|
||||
if guard::is_admin(&ctx, &user) {
|
||||
return format::redirect("/admin/dashboard");
|
||||
}
|
||||
set_avatar(&v, &jar, &ctx, user, None).await
|
||||
}
|
||||
|
||||
/// Lists the signed-in customer's orders, split into still-active and past.
|
||||
#[debug_handler]
|
||||
async fn orders_page(
|
||||
@@ -236,6 +301,7 @@ async fn orders_page(
|
||||
"account_nav": true,
|
||||
"customer_name": user.name,
|
||||
"customer_account_type": user.account_type,
|
||||
"customer_avatar": user.avatar_id,
|
||||
"active_orders": shape(active),
|
||||
"past_orders": shape(past),
|
||||
"lang": current_lang(&jar),
|
||||
@@ -278,6 +344,7 @@ async fn order_detail_page(
|
||||
"account_nav": true,
|
||||
"customer_name": user.name,
|
||||
"customer_account_type": user.account_type,
|
||||
"customer_avatar": user.avatar_id,
|
||||
"order": order_view::detail(
|
||||
&order,
|
||||
settings::get(&ctx, "bank_iban").unwrap_or(""),
|
||||
@@ -312,6 +379,7 @@ fn password_view(
|
||||
"account_nav": true,
|
||||
"customer_name": user.name,
|
||||
"customer_account_type": user.account_type,
|
||||
"customer_avatar": user.avatar_id,
|
||||
"changed": changed,
|
||||
"error": error,
|
||||
"lang": current_lang(jar),
|
||||
@@ -406,6 +474,7 @@ fn security_view(
|
||||
"account_nav": true,
|
||||
"customer_name": user.name,
|
||||
"customer_account_type": user.account_type,
|
||||
"customer_avatar": user.avatar_id,
|
||||
"totp_enabled": user.totp_enabled(),
|
||||
"enrolling": enrolling,
|
||||
"qr": qr,
|
||||
@@ -538,6 +607,11 @@ pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
.add("/account/profile", get(profile_page))
|
||||
.add("/account/profile", post(save_profile))
|
||||
.add(
|
||||
"/account/profile/avatar",
|
||||
post(upload_avatar).layer(DefaultBodyLimit::max(IMAGE_MAX_BYTES + 1024 * 1024)),
|
||||
)
|
||||
.add("/account/profile/avatar/remove", post(remove_avatar))
|
||||
.add("/account/orders", get(orders_page))
|
||||
.add("/account/orders/{order_number}", get(order_detail_page))
|
||||
.add("/account/password", get(change_password_page))
|
||||
|
||||
@@ -264,6 +264,7 @@ async fn show(
|
||||
"logged_in_customer": c.logged_in_customer,
|
||||
"customer_name": c.customer_name,
|
||||
"customer_account_type": c.customer_account_type,
|
||||
"customer_avatar": c.customer_avatar,
|
||||
"lang": current_lang(&jar),
|
||||
}),
|
||||
)?;
|
||||
|
||||
@@ -132,6 +132,7 @@ async fn checkout_page(
|
||||
// logged_in_customer is true); None for admins/guests.
|
||||
"customer_name": user.as_ref().filter(|_| is_customer).map(|u| u.name.clone()),
|
||||
"customer_account_type": user.as_ref().filter(|_| is_customer).map(|u| u.account_type.clone()),
|
||||
"customer_avatar": user.as_ref().filter(|_| is_customer).and_then(|u| u.avatar_id.clone()),
|
||||
"profile_filled": profile_filled,
|
||||
// A logged-in customer's account type is fixed; only guests pick it
|
||||
// and may opt to create an account from the order.
|
||||
@@ -375,6 +376,7 @@ async fn order_confirmation(
|
||||
"logged_in_customer": c.logged_in_customer,
|
||||
"customer_name": c.customer_name,
|
||||
"customer_account_type": c.customer_account_type,
|
||||
"customer_avatar": c.customer_avatar,
|
||||
"account_created": account_created,
|
||||
"lang": current_lang(&jar),
|
||||
}),
|
||||
|
||||
@@ -28,6 +28,7 @@ async fn index(
|
||||
"logged_in_customer": c.logged_in_customer,
|
||||
"customer_name": c.customer_name,
|
||||
"customer_account_type": c.customer_account_type,
|
||||
"customer_avatar": c.customer_avatar,
|
||||
"currency_symbol": cur.symbol,
|
||||
"lang": current_lang(&jar),
|
||||
// The header search bar only appears on the landing page.
|
||||
|
||||
@@ -25,6 +25,7 @@ async fn render(v: &TeraView, jar: &CookieJar, ctx: &AppContext, page: &str) ->
|
||||
"logged_in_customer": c.logged_in_customer,
|
||||
"customer_name": c.customer_name,
|
||||
"customer_account_type": c.customer_account_type,
|
||||
"customer_avatar": c.customer_avatar,
|
||||
"currency_symbol": cur.symbol,
|
||||
"lang": current_lang(jar),
|
||||
}),
|
||||
|
||||
@@ -250,6 +250,13 @@ async fn run_search(
|
||||
// Numeric form so the <select> can mark the active option (Tera can't
|
||||
// compare a string param against a numeric category id).
|
||||
"selected_category_id": selected_category.parse::<i32>().unwrap_or(-1),
|
||||
// Display name of the active category, so the search bar can show that
|
||||
// the query is scoped to it. `None` for "all"/"none" (the template maps
|
||||
// "none" to the localized "uncategorized" label itself).
|
||||
"selected_category_name": selected_category
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.and_then(|id| category_name.get(&id).cloned()),
|
||||
"uncategorized_count": uncategorized_count,
|
||||
"sort": sort,
|
||||
"per_page": per_page,
|
||||
@@ -350,6 +357,7 @@ fn add_chrome(ctx_value: &mut serde_json::Value, c: &guard::Chrome, lang: &str)
|
||||
map.insert("logged_in_customer".into(), json!(c.logged_in_customer));
|
||||
map.insert("customer_name".into(), json!(c.customer_name));
|
||||
map.insert("customer_account_type".into(), json!(c.customer_account_type));
|
||||
map.insert("customer_avatar".into(), json!(c.customer_avatar));
|
||||
map.insert("lang".into(), json!(lang));
|
||||
}
|
||||
}
|
||||
@@ -482,6 +490,7 @@ async fn show(
|
||||
"logged_in_customer": c.logged_in_customer,
|
||||
"customer_name": c.customer_name,
|
||||
"customer_account_type": c.customer_account_type,
|
||||
"customer_avatar": c.customer_avatar,
|
||||
"currency_symbol": cur.symbol,
|
||||
"lang": current_lang(&jar),
|
||||
}),
|
||||
|
||||
@@ -31,6 +31,7 @@ pub struct Model {
|
||||
pub totp_enabled_at: Option<DateTimeWithTimeZone>,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub totp_backup_codes: Option<String>,
|
||||
pub avatar_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -56,6 +56,9 @@ pub struct Chrome {
|
||||
pub logged_in_customer: bool,
|
||||
pub customer_name: Option<String>,
|
||||
pub customer_account_type: Option<String>,
|
||||
/// Stored avatar image filename (served via `/images/{filename}`), set only
|
||||
/// for a logged-in customer who uploaded one. `None` -> initials fallback.
|
||||
pub customer_avatar: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn chrome(ctx: &AppContext, jar: &CookieJar) -> Chrome {
|
||||
@@ -74,6 +77,7 @@ pub fn chrome_from(ctx: &AppContext, user: Option<&users::Model>) -> Chrome {
|
||||
logged_in_customer: true,
|
||||
customer_name: Some(user.name.clone()),
|
||||
customer_account_type: Some(user.account_type.clone()),
|
||||
customer_avatar: user.avatar_id.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
None => Chrome::default(),
|
||||
|
||||
Reference in New Issue
Block a user