From 2d594c5d69269e44fe5aa8113fc1fe18aeface17 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Mon, 17 Aug 2026 09:26:05 +0330 Subject: [PATCH] feat(ui): dynamic logo customization, centralized contact links, hero slider, banner placements, reorganized admin sidebar & ui-texts save fix --- backend/src/settings/default-ui-texts.ts | 14 + backend/src/settings/settings.controller.ts | 37 +- backend/src/settings/settings.service.ts | 24 +- .../admin-panel/src/components/Sidebar.tsx | 329 +- .../admin-panel/src/pages/BannersManager.tsx | 17 +- frontend/admin-panel/src/pages/Settings.tsx | 80 +- frontend/admin-panel/src/pages/UITexts.tsx | 18 +- frontend/application/app/HomeClient.tsx | 5 + .../application/components/ArchivePage.tsx | 20 +- .../components/BannerPlacement.tsx | 202 + frontend/application/components/BrandLogo.tsx | 100 + .../application/components/CheckoutPage.tsx | 4 +- frontend/application/components/Footer.tsx | 38 +- frontend/application/components/Header.tsx | 26 +- frontend/application/components/Hero.tsx | 155 +- .../components/MaintenancePage.tsx | 16 +- .../application/components/ProductPage.tsx | 6 +- .../lib/services/productService.ts | 10 + .../application/lib/store/settingsStore.ts | 27 +- graphify-out/.graphify_labels.json | 94 +- graphify-out/.graphify_labels.json.sig | 2 +- .../2026-08-17/.graphify_analysis.json | 2883 + graphify-out/2026-08-17/.graphify_labels.json | 291 + .../2026-08-17/.graphify_semantic_marker | 1 + graphify-out/2026-08-17/GRAPH_REPORT.md | 994 + graphify-out/2026-08-17/graph.json | 99336 ++++++++++++++++ graphify-out/2026-08-17/manifest.json | 3038 + graphify-out/GRAPH_REPORT.md | 441 +- ...d2f7a2316fe25bd1cd328421ffa823b36f40f.json | 1 + ...f179c0a37dd473b3b433507957fbd964e28ce.json | 1 + graphify-out/cache/stat-index.json | 2 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 26504 +++-- graphify-out/manifest.json | 1786 +- 34 files changed, 126746 insertions(+), 9764 deletions(-) create mode 100644 frontend/application/components/BannerPlacement.tsx create mode 100644 frontend/application/components/BrandLogo.tsx create mode 100644 graphify-out/2026-08-17/.graphify_analysis.json create mode 100644 graphify-out/2026-08-17/.graphify_labels.json create mode 100644 graphify-out/2026-08-17/.graphify_semantic_marker create mode 100644 graphify-out/2026-08-17/GRAPH_REPORT.md create mode 100644 graphify-out/2026-08-17/graph.json create mode 100644 graphify-out/2026-08-17/manifest.json create mode 100644 graphify-out/cache/ast/v0.9.44/1b7ffde4b290e742802fc554f2dd2f7a2316fe25bd1cd328421ffa823b36f40f.json create mode 100644 graphify-out/cache/ast/v0.9.44/f545dfd832cb16f1a3cfc4f43f2f179c0a37dd473b3b433507957fbd964e28ce.json diff --git a/backend/src/settings/default-ui-texts.ts b/backend/src/settings/default-ui-texts.ts index 9cbb77e..1d36861 100644 --- a/backend/src/settings/default-ui-texts.ts +++ b/backend/src/settings/default-ui-texts.ts @@ -5,12 +5,26 @@ export const DEFAULT_UI_TEXTS: Record = { "maintenance_desc": "وب‌سایت رسمی کنینا ایران جهت ارتقای زیرساخت‌ها و بهبود عملکرد به صورت موقت در دست به‌روزرسانی است. از شکیبایی شما سپاسگزاریم.", "maintenance_eta": "زمان تقریبی بازگشایی: کمتر از ۲ ساعت آینده", "maintenance_contact_phone": "۰۲۱-۸۸۸۸۸۸۸۸", + "maintenance_contact_phone_link": "tel:02188888888", "maintenance_badge": "سامانه در حال ارتقا", "catalog_mode": "false", "catalog_hide_prices": "false", "catalog_disable_cart": "false", "catalog_disable_checkout": "false", + // === Brand & Logo === + "site_logo": "", + "site_logo_text_en": "Canina", + "site_logo_text_fa": "ایران", + "site_logo_subtitle": "نماینده رسمی CANINA PHARMA GMBH GERMANY", + "contact_phone": "۰۲۱-۸۸۸۸۴۴۴۴", + "contact_phone_link": "tel:02188884444", + "contact_email": "info@canina.ir", + "contact_address": "تهران، جردن، خیابان سعیدی، ساختمان کنینا، طبقه ۵، واحد ۱۹", + "contact_whatsapp": "09120000000", + "contact_telegram": "https://t.me/canina_iran", + "contact_instagram": "https://instagram.com/canina_iran", + // === Header / Navigation === "shipping_notice": "ارسال رایگان برای سفارش‌های بالای ۱۵۰ هزار تومان — گارانتی اصالت کنینا آلمان", "brand_name_fa": "کنینا ایران", diff --git a/backend/src/settings/settings.controller.ts b/backend/src/settings/settings.controller.ts index 4d69b04..7d14f63 100644 --- a/backend/src/settings/settings.controller.ts +++ b/backend/src/settings/settings.controller.ts @@ -41,11 +41,40 @@ export class SettingsController { @UseGuards(JwtAuthGuard, RolesGuard) @Roles('Admin') @ApiBearerAuth() - @Patch('ui-texts/:key') @Put('ui-texts/:key') - @ApiOperation({ summary: 'ویرایش یا ثبت متن یک کلید در رابط کاربری' }) - updateUiText(@Param('key') key: string, @Body('value') value: string) { - return this.settingsService.updateUiText(key, value); + @ApiOperation({ summary: 'ویرایش یا ثبت متن یک کلید در رابط کاربری با متد PUT' }) + putUiText(@Param('key') key: string, @Body() body: any) { + const val = + typeof body === 'object' && body !== null && 'value' in body + ? body.value + : typeof body === 'string' + ? body + : JSON.stringify(body); + return this.settingsService.updateUiText(key, String(val ?? '')); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('Admin') + @ApiBearerAuth() + @Patch('ui-texts/:key') + @ApiOperation({ summary: 'ویرایش یا ثبت متن یک کلید در رابط کاربری با متد PATCH' }) + patchUiText(@Param('key') key: string, @Body() body: any) { + const val = + typeof body === 'object' && body !== null && 'value' in body + ? body.value + : typeof body === 'string' + ? body + : JSON.stringify(body); + return this.settingsService.updateUiText(key, String(val ?? '')); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('Admin') + @ApiBearerAuth() + @Put('ui-texts') + @ApiOperation({ summary: 'ویرایش گروهی متون رابط کاربری' }) + putBulkUiTexts(@Body() body: Record) { + return this.settingsService.updateBulkUiTexts(body); } @Get('scientific-terms') diff --git a/backend/src/settings/settings.service.ts b/backend/src/settings/settings.service.ts index 25738fb..7ceac25 100644 --- a/backend/src/settings/settings.service.ts +++ b/backend/src/settings/settings.service.ts @@ -90,13 +90,33 @@ export class SettingsService implements OnModuleInit { } async updateUiText(key: string, value: string) { + const strVal = String(value ?? ''); + try { + await this.prisma.setting.upsert({ + where: { key }, + update: { value: strVal }, + create: { key, value: strVal, category: 'general' }, + }); + } catch (e) { + this.logger.warn(`Failed to sync setting key ${key}: ${e}`); + } + return this.prisma.uiText.upsert({ where: { key }, - update: { value }, - create: { key, value }, + update: { value: strVal }, + create: { key, value: strVal }, }); } + async updateBulkUiTexts(texts: Record) { + if (!texts || typeof texts !== 'object') return { success: true }; + const entries = Object.entries(texts); + for (const [key, value] of entries) { + await this.updateUiText(key, String(value ?? '')); + } + return { success: true, count: entries.length }; + } + async getScientificTerms() { return this.prisma.scientificTerm.findMany(); } diff --git a/frontend/admin-panel/src/components/Sidebar.tsx b/frontend/admin-panel/src/components/Sidebar.tsx index 3138947..7c1b547 100644 --- a/frontend/admin-panel/src/components/Sidebar.tsx +++ b/frontend/admin-panel/src/components/Sidebar.tsx @@ -1,63 +1,50 @@ import { useState, useEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Users, ShoppingCart, Tag, Settings, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video, PhoneCall, Sparkles, MessageSquareQuote, FlaskConical, Building2, Globe, DollarSign, Sliders, MessageSquare, Receipt, CreditCard } from 'lucide-react'; +import { + LayoutDashboard, + TrendingUp, + ShoppingCart, + Receipt, + Package, + FolderTree, + Tag, + Sparkles, + FileText, + Users, + Heart, + MessageSquare, + Building2, + PhoneCall, + Image, + FlaskConical, + MessageSquareQuote, + BookOpen, + Video, + Settings, + ShieldCheck, + Languages, + DollarSign, + Sliders, + Globe, + ChevronDown, + Star, + Layers, +} from 'lucide-react'; import api from '../services/api'; -const menuGroups = [ - { - title: 'اصلی', - items: [ - { icon: LayoutDashboard, label: 'داشبورد', path: '/' }, - { icon: TrendingUp, label: 'گزارشات', path: '/reports' }, - ] - }, - { - title: 'فروشگاه و تخصصی', - items: [ - { icon: ShoppingCart, label: 'سفارشات', path: '/orders' }, - { icon: Receipt, label: 'تراکنش‌ها و لاگ پرداخت', path: '/transactions' }, - { icon: Package, label: 'محصولات', path: '/products' }, - { icon: MessageSquare, label: 'نظرات محصولات', path: '/reviews' }, - { icon: FolderTree, label: 'دسته‌بندی‌ها', path: '/categories' }, - { icon: Tag, label: 'کدهای تخفیف', path: '/coupons' }, - { icon: Sparkles, label: 'مشاور هوشمند', path: '/smart-advisor' }, - { icon: FileText, label: 'نسخه‌های پزشکی', path: '/prescriptions' }, - ] - }, - { - title: 'مدیریت و همکاران B2B', - items: [ - { icon: Users, label: 'کاربران', path: '/users' }, - { icon: Heart, label: 'حیوانات (Pets)', path: '/pets' }, - { icon: MessageSquare, label: 'تیکت‌ها & مشاوره دامپزشک', path: '/tickets' }, - { icon: Building2, label: 'مدیریت B2B & عمده', path: '/b2b' }, - { icon: PhoneCall, label: 'تماس با ما & اطلاعات', path: '/contact' }, - ] - }, - { - title: 'محتوا و دانشنامه', - items: [ - { icon: Image, label: 'بنرها و اسلایدرها', path: '/banners' }, - { icon: FlaskConical, label: 'دانشنامه ترکیبات', path: '/ingredients' }, - { icon: MessageSquareQuote, label: 'نظرات و گواهی‌ها', path: '/testimonials' }, - { icon: FileText, label: 'وبلاگ', path: '/blogs' }, - { icon: BookOpen, label: 'دانشنامه عمومی', path: '/wiki' }, - { icon: Video, label: 'مدیریت ویدئوها', path: '/videos' }, - ] - }, - { - title: 'سیستم و تنظیمات', - items: [ - { icon: Settings, label: 'تنظیمات کلی', path: '/settings' }, - { icon: MessageSquare, label: 'تنظیمات پیامک', path: '/settings/sms' }, - { icon: Globe, label: 'تنظیمات سئو', path: '/settings/seo' }, - { icon: DollarSign, label: 'تنظیمات مالی & ارسال', path: '/settings/financial' }, - { icon: Sliders, label: 'تنظیمات سیستمی', path: '/settings/system' }, - { icon: Languages, label: 'متون رابط کاربری', path: '/ui-texts' }, - { icon: Image, label: 'مدیریت رسانه', path: '/media' }, - ] - } -]; +interface SubMenuItem { + icon: React.ComponentType<{ className?: string }>; + label: string; + path: string; + badge?: string | number; +} + +interface MenuGroup { + id: string; + title: string; + icon: React.ComponentType<{ className?: string }>; + items: SubMenuItem[]; +} interface SidebarProps { isOpen: boolean; @@ -69,12 +56,17 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) { const [newOrdersCount, setNewOrdersCount] = useState(0); const intervalRef = useRef | null>(null); + // Auto-close sidebar on mobile navigation + useEffect(() => { + setIsOpen(false); + }, [location.pathname, setIsOpen]); + useEffect(() => { const fetchOrdersCount = async () => { try { const response = await api.get('/admin/dashboard/stats'); if (response.data?.success) { - setNewOrdersCount(response.data.data.newOrders); + setNewOrdersCount(response.data.data.newOrders || 0); } } catch (err) { console.error('Failed to fetch stats in sidebar', err); @@ -87,6 +79,106 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) { }; }, []); + const menuGroups: MenuGroup[] = [ + { + id: 'main', + title: 'داشبورد و تحلیل داده', + icon: LayoutDashboard, + items: [ + { icon: LayoutDashboard, label: 'نمای کلی داشبورد', path: '/' }, + { icon: TrendingUp, label: 'گزارشات و تحلیل فروش', path: '/reports' }, + ], + }, + { + id: 'orders', + title: 'سفارشات و مالی', + icon: ShoppingCart, + items: [ + { + icon: ShoppingCart, + label: 'مدیریت سفارشات', + path: '/orders', + badge: newOrdersCount > 0 ? newOrdersCount : undefined, + }, + { icon: Receipt, label: 'تراکنش‌ها و لاگ پرداخت', path: '/transactions' }, + { icon: DollarSign, label: 'تنظیمات مالی و ارسال', path: '/settings/financial' }, + ], + }, + { + id: 'products', + title: 'محصولات و کاتالوگ', + icon: Package, + items: [ + { icon: Package, label: 'لیست محصولات', path: '/products' }, + { icon: Star, label: 'نظرات و امتیازات', path: '/reviews' }, + { icon: FolderTree, label: 'دسته‌بندی‌های تخصصی', path: '/categories' }, + { icon: Tag, label: 'کدهای تخفیف', path: '/coupons' }, + { icon: Sparkles, label: 'مشاور هوشمند بالینی', path: '/smart-advisor' }, + { icon: FileText, label: 'نسخه‌های دریافتی', path: '/prescriptions' }, + ], + }, + { + id: 'crm', + title: 'کاربران و همکاران', + icon: Users, + items: [ + { icon: Users, label: 'مدیریت کاربران', path: '/users' }, + { icon: Heart, label: 'پرونده پت‌ها (همدم‌ها)', path: '/pets' }, + { icon: MessageSquare, label: 'تیکت‌ها و مشاوره تخصصی', path: '/tickets' }, + { icon: Building2, label: 'پرتال B2B و همکاران', path: '/b2b' }, + { icon: PhoneCall, label: 'اطلاعات تماس و پیام‌ها', path: '/contact' }, + ], + }, + { + id: 'content', + title: 'محتوا و دانشنامه', + icon: Layers, + items: [ + { icon: Image, label: 'بنرها و اسلایدرها', path: '/banners' }, + { icon: FileText, label: 'وبلاگ و مقالات', path: '/blogs' }, + { icon: FlaskConical, label: 'دانشنامه ترکیبات', path: '/ingredients' }, + { icon: BookOpen, label: 'دانشنامه عمومی و علمی', path: '/wiki' }, + { icon: MessageSquareQuote, label: 'نظرات و گواهی متخصصان', path: '/testimonials' }, + { icon: Video, label: 'ویدئوها و آموزش‌ها', path: '/videos' }, + { icon: Image, label: 'کتابخانه رسانه', path: '/media' }, + ], + }, + { + id: 'settings', + title: 'سیستم و تنظیمات', + icon: Settings, + items: [ + { icon: Settings, label: 'تنظیمات اصلی فروشگاه', path: '/settings' }, + { icon: Languages, label: 'متون رابط کاربری', path: '/ui-texts' }, + { icon: MessageSquare, label: 'درگاه پیامک (MeliPayamak)', path: '/settings/sms' }, + { icon: ShieldCheck, label: 'گواهی SSL و امنیت', path: '/settings/ssl' }, + { icon: Globe, label: 'تنظیمات سئو (SEO)', path: '/settings/seo' }, + { icon: Sliders, label: 'تنظیمات سیستمی پیشرفته', path: '/settings/system' }, + ], + }, + ]; + + // Auto expand active group + const [expandedGroups, setExpandedGroups] = useState>(() => { + const initial: Record = {}; + menuGroups.forEach((g) => { + const hasActive = g.items.some( + (item) => + location.pathname === item.path || + (item.path !== '/' && location.pathname.startsWith(item.path)) + ); + initial[g.id] = hasActive || g.id === 'main' || g.id === 'orders'; + }); + return initial; + }); + + const toggleGroup = (groupId: string) => { + setExpandedGroups((prev) => ({ + ...prev, + [groupId]: !prev[groupId], + })); + }; + return ( <> {/* Mobile Backdrop */} @@ -97,43 +189,100 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) { /> )} -