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 ? 'شماره موبایل خود را وارد کنید' : 'رمز یکبارمصرف را وارد کنید' }}
+15 -7
View File
@@ -9,19 +9,27 @@
| نام فارسی، نام لاتین، عنوان تب مرورگر، آدرس سایت | [`brand.ts`](../brand.ts) در ریشهٔ پروژه | پنل ادمین، اپ مشتری و اپ مشتری v2 همه از همین یک فایل می‌خوانند: عنوان صفحه، نام نصب PWA، عنوان صفحهٔ ورود، پیام خوش‌آمد، متن‌های راهنما، لینک محصول. |
| نام برند در **همهٔ پیامک‌ها** | متغیر محیطی `BRAND_NAME` روی سرور API | خالی باشد = «تهران تاج» (پیش‌فرض در [`apps/api/src/common/brand.ts`](../apps/api/src/common/brand.ts)). بعد از تغییر فقط API را ری‌استارت کنید. |
| لینک سایت در پیامک خوش‌آمد | متغیر محیطی `BRAND_SITE_URL` روی سرور API | پیش‌فرض فعلاً `https://bnk.montzar.ir` است (دامنهٔ زنده). |
| لوگو | فایل‌های تصویر با نام ثابت `brand-*` (پایین) | نام فایل‌ها عمومی است؛ فقط خود تصویر را با همان نام جایگزین کنید. |
| لوگو | دو فایل SVG در ریشهٔ پروژه (پایین) | فقط همین دو فایل را عوض کنید و اسکریپت ساخت را اجرا کنید؛ بقیه از روی آن‌ها ساخته می‌شوند. |
### فایل‌های لوگو
- `apps/user-app-v2/public/img/brand-logo.png` و `.webp`
- `apps/user-app-v2/src/assets/img/brand-logo.png`
- `apps/admin-panel/src/assets/images/logo/brand-icon.png` و `.webp`، `brand-horizontal.png`
- آیکون‌های PWA و favicon: `apps/*/public/icon-192.png`، `icon-512.png`، `favicon.*`
- اسکریپت ساخت همهٔ این‌ها از یک تصویر: [`scripts/generate-brand-icons.py`](../scripts/generate-brand-icons.py) (مسیر تصویر ورودی را در خط اول آن عوض کنید)
منبع (ریشهٔ پروژه):
- `logo.svg` — نقش مشکی روی زمینهٔ شفاف؛ در صفحه‌ها، favicon و آیکون PWA
- `logo_bk.svg` — همان نقش روی کارت سفید؛ برای زمینه‌های تیره (پردهٔ تازه‌سازی اپ)
ساخت همهٔ خروجی‌ها: `node scripts/generate-brand-icons.cjs` ([`scripts/generate-brand-icons.cjs`](../scripts/generate-brand-icons.cjs)). خروجی‌ها:
- `apps/user-app-v2/public/img/brand-logo.svg`، `brand-logo-card.svg`، و نسخهٔ `brand-logo.png`/`.webp` برای لینک‌های مستقیم قدیمی
- `apps/user-app-v2/src/assets/img/brand-logo.svg`
- `apps/admin-panel/src/assets/images/logo/brand-logo.svg`
- آیکون‌های PWA و favicon: `apps/user-app-v2/public/icon-192.png`، `icon-512.png`، `apps/*/public/favicon.*`
نقش مشکی است؛ در حالت تیرهٔ اپ مشتری، کلاس `brand-mark` آن را روشن نشان می‌دهد (`style.less`).
### بعد از تغییر
1. `dist` هر سه فرانت و `apps/api/release` را دوباره بسازید (طبق روال انتشار).
1. `dist` هر دو فرانت و `apps/api/release` را دوباره بسازید (طبق روال انتشار).
2. اگر در دیتابیس فعلی برندی به نام قدیمی در «برندهای محصول» هست، از پنل ادمین ویرایشش کنید (seed فقط دیتابیس خالی را پر می‌کند).
## ۲. عمداً عوض نشده‌اند (نام داخلی، دیده نمی‌شوند)
+42
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

+45
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

+95
View File
@@ -0,0 +1,95 @@
// Builds every brand image of the two frontends from the two source SVGs in
// the repo root (see docs/BRANDING.md):
// logo.svg black mark on transparent — pages, favicons, PWA icons
// logo_bk.svg the same mark on a white rounded card — dark backdrops
//
// node scripts/generate-brand-icons.cjs
//
// sharp is resolved from apps/api, which already depends on it.
const { createRequire } = require('node:module');
const { readFileSync, writeFileSync } = require('node:fs');
const { join } = require('node:path');
const root = join(__dirname, '..');
const sharp = createRequire(join(root, 'apps/api/package.json'))('sharp');
const userApp = join(root, 'apps/user-app-v2');
const admin = join(root, 'apps/admin-panel');
// Illustrator's "<title>Asset 1</title>" is noise in an <img>.
const clean = (file) => readFileSync(join(root, file), 'utf8').replace(/\s*<title>[^<]*<\/title>/, '');
const logo = clean('logo.svg');
const card = clean('logo_bk.svg');
/** The mark rendered `width` px wide, transparent. */
const mark = (width) => sharp(Buffer.from(logo), { density: 600 }).resize({ width }).png().toBuffer();
/**
* Square icon: the mark centred on white. `fill` is the mark's width as a
* share of the side; `radius` rounds the white square (0 = full bleed, as the
* OS masks PWA icons itself).
*/
async function icon(size, { fill, radius }) {
const r = Math.round(size * radius);
const plate = Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">` +
`<rect width="${size}" height="${size}" rx="${r}" ry="${r}" fill="#fff"/></svg>`,
);
return sharp(plate)
.composite([{ input: await mark(Math.round(size * fill)), gravity: 'center' }])
.png()
.toBuffer();
}
/** .ico holding PNG entries (supported by every current browser). */
function ico(pngs) {
const header = Buffer.alloc(6);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(pngs.length, 4);
const entries = [];
let offset = 6 + 16 * pngs.length;
for (const { size, data } of pngs) {
const e = Buffer.alloc(16);
e.writeUInt8(size >= 256 ? 0 : size, 0);
e.writeUInt8(size >= 256 ? 0 : size, 1);
e.writeUInt16LE(1, 4);
e.writeUInt16LE(32, 6);
e.writeUInt32LE(data.length, 8);
e.writeUInt32LE(offset, 12);
offset += data.length;
entries.push(e);
}
return Buffer.concat([header, ...entries, ...pngs.map((p) => p.data)]);
}
function write(path, data) {
writeFileSync(path, data);
console.log('wrote', path.slice(root.length + 1));
}
(async () => {
write(join(userApp, 'public/img/brand-logo.svg'), logo);
write(join(userApp, 'public/img/brand-logo-card.svg'), card);
write(join(userApp, 'src/assets/img/brand-logo.svg'), logo);
write(join(admin, 'src/assets/images/logo/brand-logo.svg'), logo);
// Raster copies at the old public URLs, for anything linking them directly.
const big = await mark(1024);
write(join(userApp, 'public/img/brand-logo.png'), big);
write(join(userApp, 'public/img/brand-logo.webp'), await sharp(big).webp({ quality: 90 }).toBuffer());
// PWA: one file serves "any" and "maskable", so the mark stays inside the
// maskable safe circle (80% of the side): a 1.45:1 mark at 64% width does.
write(join(userApp, 'public/icon-192.png'), await icon(192, { fill: 0.64, radius: 0 }));
write(join(userApp, 'public/icon-512.png'), await icon(512, { fill: 0.64, radius: 0 }));
// Tab icons: white rounded chip, so the black mark shows on dark tab strips too.
const chip = { fill: 0.86, radius: 0.2 };
const favicon = ico(await Promise.all([16, 32, 48, 64].map(async (size) => ({ size, data: await icon(size, chip) }))));
write(join(userApp, 'public/favicon.ico'), favicon);
write(join(admin, 'public/favicon.ico'), favicon);
write(join(admin, 'public/favicon.png'), await icon(192, chip));
})().catch((e) => {
console.error(e);
process.exit(1);
});
-34
View File
@@ -1,34 +0,0 @@
from PIL import Image
from pathlib import Path
src = Path(r"C:\Users\mahmo\.cursor\projects\c-App-gold-bank\assets\c__Users_mahmo_AppData_Roaming_Cursor_User_workspaceStorage_e98278b20f306ff9e248e1d576e8bb3f_images_mzLogoDarkmode-079331ec-aa35-4a0f-bc63-1b0a0c4215eb.webp")
img = Image.open(src).convert("RGBA")
print("source size", img.size)
root = Path(r"C:\App\gold-bank")
(root / "apps/user-app/public/img").mkdir(parents=True, exist_ok=True)
(root / "apps/user-app/src/assets/img").mkdir(parents=True, exist_ok=True)
(root / "apps/admin-panel/src/assets/images/logo").mkdir(parents=True, exist_ok=True)
img.save(root / "apps/user-app/public/img/brand-logo.webp", "WEBP")
img.save(root / "apps/user-app/public/img/brand-logo.png", "PNG")
img.save(root / "apps/user-app/src/assets/img/brand-logo.png", "PNG")
img.save(root / "apps/admin-panel/src/assets/images/logo/brand-icon.webp", "WEBP")
img.save(root / "apps/admin-panel/src/assets/images/logo/brand-icon.png", "PNG")
img.save(root / "apps/admin-panel/src/assets/images/logo/brand-horizontal.png", "PNG")
for size, path in [
(192, root / "apps/user-app/public/icon-192.png"),
(512, root / "apps/user-app/public/icon-512.png"),
]:
resized = img.resize((size, size), Image.Resampling.LANCZOS)
resized.save(path, "PNG")
print("wrote", path, size)
favicon = img.resize((256, 256), Image.Resampling.LANCZOS)
favicon.save(root / "apps/user-app/public/favicon.ico", sizes=[(16, 16), (32, 32), (48, 48), (64, 64)])
favicon.save(
root / "apps/admin-panel/src/assets/images/logo/favicon.ico",
sizes=[(16, 16), (32, 32), (48, 48), (64, 64)],
)
print("done")