feat: new Tehrani brand logo, custom button audience, trim dashboard

- Replace the crown logo with logo.svg / logo_bk.svg across both frontends;
  regenerate favicons and PWA icons via scripts/generate-brand-icons.cjs
- Custom button: admin chooses whether it shows before login or only after
- Dashboard: drop the survey-majority banner, hide the custom-order row

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mahmood
2026-11-14 16:53:25 +03:30
co-authored by Cursor
parent 0f6ad79ae7
commit de13445109
37 changed files with 428 additions and 105 deletions
+2 -2
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png?v=2" />
<link rel="shortcut icon" href="/favicon.ico?v=2" />
<link rel="icon" type="image/png" href="/favicon.png?v=3" />
<link rel="shortcut icon" href="/favicon.ico?v=3" />
<title>%BRAND_TITLE%</title>
</head>
<body>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

@@ -19,7 +19,7 @@
>
<div class="nav-header" :class="{ 'is-collapsed': collapsed }">
<span class="nav-logo">
<img src="../../assets/images/logo/brand-icon.png" alt="" />
<img src="../../assets/images/logo/brand-logo.svg" alt="" />
</span>
<span v-if="!collapsed" class="nav-brand">
<span class="nav-title">{{ title }}</span>
@@ -207,8 +207,9 @@ function onClick({ key }: { key: string | number }) {
padding: 0;
}
.nav-logo {
width: 38px;
height: 38px;
/* wide: the mark is about 1.45:1 */
width: 54px;
height: 40px;
flex-shrink: 0;
display: grid;
place-items: center;
@@ -218,7 +219,7 @@ function onClick({ key }: { key: string | number }) {
box-shadow: 0 4px 12px -6px rgba(28, 45, 86, 0.35);
}
.nav-logo img {
width: 64%;
width: 86%;
}
.nav-brand {
display: flex;
+6 -5
View File
@@ -20,7 +20,7 @@
<section class="hero" aria-hidden="true">
<div class="brand-row">
<div class="logo-chip">
<img src="../../assets/images/logo/brand-icon.png" alt="" />
<img src="../../assets/images/logo/brand-logo.svg" alt="" />
</div>
<span class="brand-name">{{ brand.nameEn }}</span>
</div>
@@ -83,7 +83,7 @@
<div class="glass-card">
<div class="card-head">
<div class="logo-chip logo-chip--sm">
<img src="../../assets/images/logo/brand-icon.png" :alt="brand.nameEn" />
<img src="../../assets/images/logo/brand-logo.svg" :alt="brand.nameEn" />
</div>
<span class="eyebrow"><span class="dot"></span>پنل مدیریت</span>
</div>
@@ -347,7 +347,8 @@ async function loginJWT() {
gap: 12px;
}
.logo-chip {
width: 52px;
/* wide: the mark is about 1.45:1 */
width: 72px;
height: 52px;
display: grid;
place-items: center;
@@ -358,7 +359,7 @@ async function loginJWT() {
0 10px 30px -8px rgba(242, 201, 92, 0.45);
}
.logo-chip img {
width: 64%;
width: 82%;
}
.brand-name {
font-size: 16px;
@@ -492,7 +493,7 @@ async function loginJWT() {
justify-content: space-between;
}
.logo-chip--sm {
width: 44px;
width: 60px;
height: 44px;
border-radius: 14px;
}
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="flex gap-6 w-full justify-center h-screen branch-login-page">
<div class="flex flex-col h-screen items-center justify-center">
<img
src="../../assets/images/logo/brand-icon.png"
src="../../assets/images/logo/brand-logo.svg"
width="150"
alt="login"
class="mx-auto mb-5"
@@ -121,7 +121,7 @@
<ul>
<li class="comment-item">
<img src="../../../assets/images/logo/brand-icon.png" alt="comment author" class="img border" />
<img src="../../../assets/images/logo/brand-logo.svg" alt="comment author" class="img border" />
<div class="comment-body">
<h5>ادمین</h5>
<span v-if="tempRow.reply && tempRow.reply[0]" class="time">
@@ -271,6 +271,12 @@ async function submitChanges(row: Record<string, any>) {
border: 1px solid grey;
border-radius: 50%;
}
img.img {
object-fit: contain;
padding: 10px;
box-sizing: border-box;
background: #fff;
}
@media (max-width: 575px) {
.comment-item {
+14 -1
View File
@@ -142,6 +142,12 @@
un-checked-children="غیرفعال"
/>
</a-form-item>
<a-form-item label="نمایش به">
<a-radio-group v-model:value="customButton.audience">
<a-radio value="all">همه (قبل و بعد از ورود)</a-radio>
<a-radio value="loggedIn">فقط بعد از ورود</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="عنوان دکمه">
<a-input
v-model:value="customButton.title"
@@ -248,7 +254,12 @@ const tagsInputValue = ref('');
const sliders = ref<{ image: string; link: string }[]>([]);
// ── custom button (Setting key: custom_button)
const customButton = ref({ enabled: false, title: '', link: '' });
const customButton = ref<{ enabled: boolean; title: string; link: string; audience: 'all' | 'loggedIn' }>({
enabled: false,
title: '',
link: '',
audience: 'all',
});
// ── collections
const collections = ref<{ id: string; title: string }[]>([]);
@@ -360,6 +371,7 @@ async function saveCustomButton() {
enabled: customButton.value.enabled,
title,
link,
audience: customButton.value.audience,
},
});
message.success('دکمه سفارشی ذخیره شد');
@@ -428,6 +440,7 @@ onMounted(async () => {
enabled: Boolean(btn.data.enabled),
title: String(btn.data.title ?? ''),
link: String(btn.data.link ?? ''),
audience: btn.data.audience === 'loggedIn' ? 'loggedIn' : 'all',
};
}
} catch (e) {
@@ -24,18 +24,23 @@ export class PublicSliderController {
return Array.isArray(value) ? value : [];
}
/** Custom button on the profile dashboard (ثابت‌ها → دکمه سفارشی). */
/**
* Custom button on the profile dashboard (ثابت‌ها → دکمه سفارشی).
* `audience`: 'all' = signed out too, 'loggedIn' = only after login. Rows
* saved before the field existed were shown to everyone, so they read 'all'.
*/
@Get('custom-button')
async getCustomButton() {
const value = await this.misc.getConst('custom_button');
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return { enabled: false, title: '', link: '' };
return { enabled: false, title: '', link: '', audience: 'all' };
}
const row = value as Record<string, unknown>;
return {
enabled: Boolean(row.enabled),
title: String(row.title ?? ''),
link: String(row.link ?? ''),
audience: row.audience === 'loggedIn' ? 'loggedIn' : 'all',
};
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/favicon.ico?v=3" />
<meta charset="UTF-8" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+2
View File
@@ -332,6 +332,8 @@ export interface CustomButton {
enabled: boolean;
title: string;
link: string;
/** 'all' = also before login; 'loggedIn' = only signed-in customers. Absent on older APIs = 'all'. */
audience?: 'all' | 'loggedIn';
}
/** One admin-uploaded home banner slide — raw shape as saved by the admin
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

@@ -732,6 +732,11 @@ html.theme-dark body {
color: @ink;
}
// The brand mark is black line art; on the dark canvas it is drawn light.
html.theme-dark .brand-mark {
filter: invert(1);
}
html.theme-dark {
.ant-input,
.ant-input-affix-wrapper,
+1 -1
View File
@@ -2,7 +2,7 @@
<template>
<Spin :spinning="spinning" :tip="tip" :size="(size as 'small' | 'default' | 'large')" :delay="delay" :class="fixed ? '!max-h-screen !fixed' : ''">
<template #indicator>
<img v-if="!indicator" src="/img/brand-logo.png" :alt="brand.nameEn" class="spinner-brand" width="48" height="48" />
<img v-if="!indicator" src="/img/brand-logo.svg" :alt="brand.nameEn" class="spinner-brand brand-mark" width="48" height="48" />
<!-- keep-motion: a working spinner, exempt from reduced motion. -->
<div class="loading keep-motion" v-else>
<div class="effect-1 effects"></div>
@@ -22,10 +22,10 @@
>
<img
class="rfx__logo"
src="/img/brand-logo.png"
src="/img/brand-logo-card.svg"
:alt="brand.name"
width="140"
height="140"
width="168"
height="117"
/>
<div class="rfx__spin" aria-hidden="true">
<Loading type="spinner" size="28px" color="#fff" />
@@ -88,8 +88,8 @@ const blocking = computed(() => props.intensity >= 0.99)
}
.rfx__logo {
width: 140px;
height: 140px;
width: 168px;
height: 117px;
object-fit: contain;
filter: drop-shadow(0 22px 44px rgba(0, 0, 0, 0.5))
drop-shadow(0 0 36px rgba(124, 0, 53, 0.4));
+1 -1
View File
@@ -25,7 +25,7 @@ import { defineStore } from 'pinia'
import { api } from '../api/client'
import shippingArt from '../assets/img/shipping-and-delivery.png'
import competitionArt from '../assets/img/sports-and-competition.png'
import brandArt from '../assets/img/brand-logo.png'
import brandArt from '../assets/img/brand-logo.svg'
import { brand } from '../brand'
export interface Slide {
+1 -1
View File
@@ -22,7 +22,7 @@
<div class="home">
<header class="home__bar">
<div class="home__barInner">
<img src="/img/brand-logo.png" :alt="brand.name" class="home__logo" />
<img src="/img/brand-logo.svg" :alt="brand.name" class="home__logo brand-mark" />
<RateStrip :quote="goldPrice" compact class="home__rate" />
</div>
</header>
@@ -116,7 +116,7 @@
<!-- Signed out: same card shell as the logged-in hero; logo + CTA only. -->
<section v-else class="dash__hero dash__invite dash__reveal" :style="revealDelay(0)">
<img class="dash__inviteLogo" src="/img/brand-logo.png" :alt="brand.name" />
<img class="dash__inviteLogo" src="/img/brand-logo.svg" :alt="brand.name" />
<button type="button" class="dash__inviteCta" @click="router.push({ name: 'login' })">
ورود / ثبت‌نام
</button>
@@ -141,25 +141,6 @@
</span>
</button>
<button
v-if="session.isLoggedIn && surveyMajorityHits.length"
type="button"
class="dash__banner dash__banner--survey dash__reveal"
:style="revealDelay(1)"
@click="goToDetails(8)"
>
<span class="dash__bannerIcon" aria-hidden="true">
<Icon icon="vuesax-linear:like-1" size="18" />
</span>
<span class="dash__bannerText">
<b>{{ surveyMajorityHits.length }} طرح به حد اکثریت رسید</b>
<small>{{ surveyMajorityHits[0]?.title || 'بدون عنوان' }}{{ surveyMajorityHits.length > 1 ? ' و …' : '' }}</small>
</span>
<span class="dash__bannerChevron" aria-hidden="true">
<Icon icon="vuesax-linear:arrow-left-1" size="16" />
</span>
</button>
<!-- Settings-style rows: every destination the hero no longer carries
as its own quick-action row. -->
<section class="dash__rows dash__reveal" :style="revealDelay(1)">
@@ -216,6 +197,7 @@
mcode, so every signed-in customer sees it and the server scopes it
to their token.
-->
<!-- «سفارش ساخت» hidden for now.
<button type="button" class="dash__row" @click="goToDetails(7)">
<span class="dash__rowIcon" aria-hidden="true">
<Icon icon="vuesax-linear:3d-cube-scan" size="18" />
@@ -227,6 +209,7 @@
<Icon icon="vuesax-linear:arrow-left-1" size="16" />
</span>
</button>
-->
<button type="button" class="dash__row" @click="goToDetails(8)">
<span class="dash__rowIcon" aria-hidden="true">
@@ -354,7 +337,7 @@ import RateStrip from '../../components/home/RateStrip.vue'
import IslandSheet from '../../components/ui/IslandSheet.vue'
import { price } from '../../utils/filters'
import { useSessionStore } from '../../stores/session'
import { api, type CustomButton, type CustomerDetail, type GoldPriceQuote, type ProductSurveyRow } from '../../api/client'
import { api, type CustomButton, type CustomerDetail, type GoldPriceQuote } from '../../api/client'
import { useCachedReads } from '../../api/cachedReads'
import { KEYS } from '../../stores/queryCache'
@@ -365,12 +348,12 @@ const reads = useCachedReads()
const customer = ref<CustomerDetail | null>(reads.peek<CustomerDetail>(KEYS.profile) ?? null)
const goldPrice = ref<GoldPriceQuote | null>(reads.peek<GoldPriceQuote>(KEYS.goldPrice) ?? null)
const customButton = ref<CustomButton | null>(reads.peek<CustomButton>(KEYS.customButton) ?? null)
const surveyMajorityHits = ref<ProductSurveyRow[]>([])
const refreshing = ref(false)
const profileError = ref(false)
const showCustomButton = computed(() => {
const btn = customButton.value
if (btn?.audience === 'loggedIn' && !session.isLoggedIn) return false
return Boolean(btn?.enabled && btn.title.trim() && btn.link.trim())
})
@@ -481,17 +464,6 @@ async function loadDashboard() {
}
goldPrice.value = price.status === 'fulfilled' ? price.value : null
if (btn.status === 'fulfilled') customButton.value = btn.value
if (session.isLoggedIn) {
try {
const surveys = await api.listOpenProductSurveys()
surveyMajorityHits.value = surveys.majorityHits ?? surveys.rows.filter((r) => r.reachedMajority)
} catch {
surveyMajorityHits.value = []
}
} else {
surveyMajorityHits.value = []
}
}
async function onRefresh() {
@@ -984,11 +956,6 @@ onMounted(async () => {
overflow: hidden;
text-overflow: ellipsis;
}
.dash__banner--survey {
background: linear-gradient(115deg, @green 0%, #1f7a4d 100%);
box-shadow: 0 10px 24px -16px alpha(@green, 70%);
}
.dash__bannerChevron {
flex: 0 0 auto;
width: 28px;
@@ -23,7 +23,7 @@
<container class="auth__body">
<section class="auth__intro">
<img class="auth__logo" src="/img/brand-logo.png" :alt="brand.name" />
<img class="auth__logo brand-mark" src="/img/brand-logo.svg" :alt="brand.name" />
<h2 class="auth__title">ورود به {{ brand.name }}</h2>
<p class="auth__sub">
{{ body.step === 1 ? 'شماره موبایل خود را وارد کنید' : 'رمز یکبارمصرف را وارد کنید' }}