diff --git a/backend/prisma/seed-products.ts b/backend/prisma/seed-products.ts index c03d11f..7606070 100644 --- a/backend/prisma/seed-products.ts +++ b/backend/prisma/seed-products.ts @@ -50,16 +50,24 @@ function getProductImageUrl(baseSlug: string): string { 'https://www.canina.de/media/01/be/93/710003_Flexan_Canina-Pharma_1280x1280.png', 'canina-herz-vital': 'https://www.canina.de/media/b9/8b/4c/112036_Herz_Vital_Canina-Pharma_1280x1280.png', + 'canina-immun-booster': + 'https://www.canina.de/media/8c/fb/8e/311000_Canino_Immun_Booster_Paste_Abwehrkraefte_1280x1280.png', 'canina-immun-booster-paste': 'https://www.canina.de/media/8c/fb/8e/311000_Canino_Immun_Booster_Paste_Abwehrkraefte_1280x1280.png', 'canina-katzenmilch': 'https://www.canina.de/media/6b/48/81/731000_Canino_Lachsoel_Lachs_Oel_1280x1280.png', + 'canina-lachsol': + 'https://www.canina.de/media/6b/48/81/731000_Canino_Lachsoel_Lachs_Oel_1280x1280.png', 'canina-lachs-l': 'https://www.canina.de/media/6b/48/81/731000_Canino_Lachsoel_Lachs_Oel_1280x1280.png', 'canina-lachs-ol': 'https://www.canina.de/media/6b/48/81/731000_Canino_Lachsoel_Lachs_Oel_1280x1280.png', + 'canina-marine-olmischung-premium': + 'https://www.canina.de/media/ad/28/71/153008_Marine_Oelmischung_Premium_Canina-Pharma_1280x1280.png', 'canina-marine-lmischung-premium': 'https://www.canina.de/media/ad/28/71/153008_Marine_Oelmischung_Premium_Canina-Pharma_1280x1280.png', + 'canina-moortranke': + 'https://www.canina.de/media/9d/b2/87/112708_Moortraenke_Canina-Pharma_1280x1280.png', 'canina-moortrnke': 'https://www.canina.de/media/9d/b2/87/112708_Moortraenke_Canina-Pharma_1280x1280.png', 'canina-petvital-arthro-tabletten': @@ -80,10 +88,20 @@ function getProductImageUrl(baseSlug: string): string { 'https://www.canina.de/media/14/d0/0d/792016_791514_Rinderblut_Pulver_Canina-Pharma_1280x1280.png', 'canina-rinderfett-pulver': 'https://www.canina.de/media/9a/31/59/131235_Rinderfett_Pulver_Canina-Pharma_1280x1280.png', + 'canina-schwarz-kummel-samen': + 'https://www.canina.de/media/a9/c8/aa/131105_Schwarzkuemmelsamen_Canina-Pharma_1280x1280.png', 'canina-schwarz-kmmel-samen': 'https://www.canina.de/media/a9/c8/aa/131105_Schwarzkuemmelsamen_Canina-Pharma_1280x1280.png', + 'canina-seelgen-tabletten': + 'https://www.canina.de/media/e4/c4/b2/130504_130511_130412_Seealgen_Bio-Seealgenmehl_Canina-Pharma_1280x1280.png', + 'canina-bio-seelgenmehl': + 'https://www.canina.de/media/e4/c4/b2/130504_130511_130412_Seealgen_Bio-Seealgenmehl_Canina-Pharma_1280x1280.png', + 'canina-seealgen-tabletten': + 'https://www.canina.de/media/e4/c4/b2/130504_130511_130412_Seealgen_Bio-Seealgenmehl_Canina-Pharma_1280x1280.png', 'canina-seealgen-bio-seealgenmehl': 'https://www.canina.de/media/e4/c4/b2/130504_130511_130412_Seealgen_Bio-Seealgenmehl_Canina-Pharma_1280x1280.png', + 'canina-taurin-fur-katzen': + 'https://www.canina.de/media/c6/aa/62/229505_Taurin_fuer_Katzen_Canina-Pharma_1280x1280.png', 'canina-taurin-fr-katzen': 'https://www.canina.de/media/c6/aa/62/229505_Taurin_fuer_Katzen_Canina-Pharma_1280x1280.png', 'canina-velox-gelenkenergie': @@ -101,7 +119,10 @@ function getProductImageUrl(baseSlug: string): string { 'canina-novagard-green-pfotenpflege': 'https://www.canina.de/media/6b/48/81/731000_Canino_Lachsoel_Lachs_Oel_1280x1280.png', }; - return images[baseSlug] || `/products/${baseSlug}.png`; + return ( + images[baseSlug] || + 'https://www.canina.de/media/83/86/e1/123000_123005_Canhydrox_GAG_Canina-Pharma_1280x1280.png' + ); } async function findOrCreateCategory( diff --git a/backend/prisma/seed-ui-texts.ts b/backend/prisma/seed-ui-texts.ts index 3fce864..3efcc4f 100644 --- a/backend/prisma/seed-ui-texts.ts +++ b/backend/prisma/seed-ui-texts.ts @@ -3,6 +3,13 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const UI_TEXTS: Record = { + // === Catalog & Maintenance Mode === + "maintenance_mode": "false", + "catalog_mode": "false", + "catalog_hide_prices": "false", + "catalog_disable_cart": "false", + "catalog_disable_checkout": "false", + // === Header / Navigation === "shipping_notice": "ارسال رایگان برای سفارش‌های بالای ۱۵۰ هزار تومان", "brand_name_fa": "ایران", @@ -131,10 +138,11 @@ async function main() { const entries = Object.entries(UI_TEXTS); for (const [key, value] of entries) { - const existing = await prisma.uiText.findUnique({ where: { key } }); - if (!existing) { - await prisma.uiText.create({ data: { key, value } }); - } + await prisma.uiText.upsert({ + where: { key }, + update: { value }, + create: { key, value }, + }); } console.log(`Seeded ${entries.length} UI texts.`); diff --git a/backend/src/settings/settings.controller.ts b/backend/src/settings/settings.controller.ts index 96f5128..2324b30 100644 --- a/backend/src/settings/settings.controller.ts +++ b/backend/src/settings/settings.controller.ts @@ -39,7 +39,8 @@ export class SettingsController { @Roles('Admin') @ApiBearerAuth() @Patch('ui-texts/:key') - @ApiOperation({ summary: 'ویرایش متن یک کلید در رابط کاربری' }) + @Put('ui-texts/:key') + @ApiOperation({ summary: 'ویرایش یا ثبت متن یک کلید در رابط کاربری' }) updateUiText(@Param('key') key: string, @Body('value') value: string) { return this.settingsService.updateUiText(key, value); } diff --git a/frontend/admin-panel/src/pages/UITexts.tsx b/frontend/admin-panel/src/pages/UITexts.tsx index 7b85efa..2864ad8 100644 --- a/frontend/admin-panel/src/pages/UITexts.tsx +++ b/frontend/admin-panel/src/pages/UITexts.tsx @@ -99,7 +99,19 @@ export default function UITexts() { useEffect(() => { let isSubscribed = true; api.get('/settings/ui-texts').then(res => { - if (isSubscribed) setTexts(res.data || {}); + if (isSubscribed) { + const textsObj: Record = {}; + if (Array.isArray(res.data)) { + res.data.forEach((item: { key?: string; value?: string }) => { + if (item && item.key) { + textsObj[item.key] = typeof item.value === 'string' ? item.value : (item.value ? JSON.stringify(item.value) : ''); + } + }); + } else if (typeof res.data === 'object' && res.data !== null) { + Object.assign(textsObj, res.data); + } + setTexts(textsObj); + } }).catch(e => { console.error('Failed to load UI texts:', e); }).finally(() => { diff --git a/frontend/application/components/Hero.tsx b/frontend/application/components/Hero.tsx index 908d1c7..d504e6a 100644 --- a/frontend/application/components/Hero.tsx +++ b/frontend/application/components/Hero.tsx @@ -38,11 +38,21 @@ function StatCounter({ target }: { target: number }) { import { useRouter } from 'next/navigation'; import { Banner } from "../lib/types"; -export default function Hero({ banners = [] }: { banners?: Banner[] }) { +export default function Hero({ banners = [], onShopNavigate }: { banners?: Banner[]; onShopNavigate?: () => void }) { const router = useRouter(); - const { getText, isInitialized } = useSettingsStore(); - const title = (isInitialized ? getText('hero_title', '') : '') || (banners.length > 0 && banners[0].title ? banners[0].title : "تخصص آلمانی در خدمت\nسلامت پت‌های خانگی"); - const subtitle = (isInitialized ? getText('hero_desc', '') : '') || (banners.length > 0 && banners[0].subtitle ? banners[0].subtitle : 'از سال ۱۹۸۴، شرکت Canina pharma GmbH در برگیش گلادباخ آلمان، با بهره‌گیری از مواد اولیه طبیعی و فرآیندهای پیشرفته، استاندارد طلایی مکمل‌های دامپزشکی را تعریف می‌کند. کانینا نماینده رسمی این برند در ایران است.'); + const getText = useSettingsStore((state) => state?.getText || ((_k: string, fb: string) => fb)); + const isInitialized = useSettingsStore((state) => state?.isInitialized ?? false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const defaultTitle = banners.length > 0 && banners[0].title ? banners[0].title : "تخصص آلمانی در خدمت\nسلامت پت‌های خانگی"; + const defaultSubtitle = banners.length > 0 && banners[0].subtitle ? banners[0].subtitle : 'از سال ۱۹۸۴، شرکت Canina pharma GmbH در برگیش گلادباخ آلمان، با بهره‌گیری از مواد اولیه طبیعی و فرآیندهای پیشرفته، استاندارد طلایی مکمل‌های دامپزشکی را تعریف می‌کند. کانینا نماینده رسمی این برند در ایران است.'; + + const title = (mounted && isInitialized ? getText('hero_title', '') : '') || defaultTitle; + const subtitle = (mounted && isInitialized ? getText('hero_desc', '') : '') || defaultSubtitle; const titleParts = title.split('\n'); return ( @@ -64,7 +74,7 @@ export default function Hero({ banners = [] }: { banners?: Banner[] }) {
- {isInitialized ? getText('hero_badge', "از ۱۹۸۴ در خدمت سلامت حیوانات") : "از ۱۹۸۴ در خدمت سلامت حیوانات"} + {mounted && isInitialized ? getText('hero_badge', "از ۱۹۸۴ در خدمت سلامت حیوانات") : "از ۱۹۸۴ در خدمت سلامت حیوانات"}
@@ -90,14 +100,20 @@ export default function Hero({ banners = [] }: { banners?: Banner[] }) { }} className="bg-canina-blue text-white px-10 py-5 rounded-full font-bold text-lg hover:bg-canina-blue/90 hover:shadow-2xl focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all flex items-center gap-2 group font-vazir min-h-[58px] shadow-lg shadow-canina-blue/20 cursor-pointer" > - {isInitialized ? getText('hero_btn_advisor', "دستیار سلامت پت") : "دستیار سلامت پت"} + {mounted && isInitialized ? getText('hero_btn_advisor', "دستیار سلامت پت") : "دستیار سلامت پت"} @@ -113,15 +129,19 @@ export default function Hero({ banners = [] }: { banners?: Banner[] }) { >
0 && banners[0].imageUrl ? banners[0].imageUrl : "/assets/images/hero-section-image.png")} + src={(mounted && isInitialized ? getText('hero_image_url', '') : '') || (banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "/assets/images/hero-section-image.png")} alt={banners.length > 0 ? banners[0].title : "Canina Pharma Germany"} className="w-full h-full object-cover" loading="eager" referrerPolicy="no-referrer" />
-
{getText('hero_image_badge', "سرآمد علمی در پزشکی پت‌ها")}
-
{getText('hero_image_title', "مکمل‌های تایید شده دامپزشکی")}
+
+ {mounted && isInitialized ? getText('hero_image_badge', "سرآمد علمی در پزشکی پت‌ها") : "سرآمد علمی در پزشکی پت‌ها"} +
+
+ {mounted && isInitialized ? getText('hero_image_title', "مکمل‌های تایید شده دامپزشکی با گواهی IFS") : "مکمل‌های تایید شده دامپزشکی با گواهی IFS"} +
{/* Quality Badge inside the image container to prevent text collision */} @@ -132,7 +152,9 @@ export default function Hero({ banners = [] }: { banners?: Banner[] }) { >
DE - {getText('hero_quality_standard', "استاندارد کیفی آلمان")} + + {mounted && isInitialized ? getText('hero_quality_standard', "استاندارد کیفی آلمان (IFS & HACCP)") : "استاندارد کیفی آلمان (IFS & HACCP)"} +
@@ -161,7 +183,9 @@ export default function Hero({ banners = [] }: { banners?: Banner[] }) {
۱۹۸۴
-
{getText('hero_stat_founded', "سال تاسیس برند")}
+
+ {mounted && isInitialized ? getText('hero_stat_founded', "سال تاسیس برند") : "سال تاسیس برند"} +
@@ -174,7 +198,9 @@ export default function Hero({ banners = [] }: { banners?: Banner[] }) {
-
{getText('hero_stat_agencies', "نمایندگی فعال")}
+
+ {mounted && isInitialized ? getText('hero_stat_agencies', "نمایندگی فعال") : "نمایندگی فعال"} +
@@ -185,7 +211,9 @@ export default function Hero({ banners = [] }: { banners?: Banner[] }) {
۱۰۰٪
-
{getText('hero_stat_german_formula', "فرمول آلمانی")}
+
+ {mounted && isInitialized ? getText('hero_stat_german_formula', "فرمول آلمانی") : "فرمول آلمانی"} +
diff --git a/frontend/application/components/__tests__/Hero.test.tsx b/frontend/application/components/__tests__/Hero.test.tsx index 09658dd..18d1163 100644 --- a/frontend/application/components/__tests__/Hero.test.tsx +++ b/frontend/application/components/__tests__/Hero.test.tsx @@ -14,25 +14,25 @@ describe('Hero', () => { vi.clearAllMocks(); }); - it('renders title and description from settingsStore', () => { + it('renders title and description from settingsStore', async () => { const mockGetText = vi.fn().mockImplementation((key, fallback) => { if (key === 'hero_title') return 'Test Title Line 1\nTest Title Line 2'; if (key === 'hero_desc') return 'Test Description Text'; return fallback; }); - vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string }) => unknown) => selector({ getText: mockGetText }) as ReturnType); + vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string; isInitialized: boolean }) => unknown) => selector({ getText: mockGetText, isInitialized: true }) as ReturnType); render(); - expect(screen.getByText('Test Title Line 1')).toBeInTheDocument(); - expect(screen.getByText('Test Title Line 2')).toBeInTheDocument(); - expect(screen.getByText('Test Description Text')).toBeInTheDocument(); + expect(await screen.findByText('Test Title Line 1')).toBeInTheDocument(); + expect(await screen.findByText('Test Title Line 2')).toBeInTheDocument(); + expect(await screen.findByText('Test Description Text')).toBeInTheDocument(); }); it('calls onShopNavigate when "مشاهده محصولات" button is clicked', () => { const mockGetText = vi.fn().mockImplementation((_key, fallback) => fallback); - vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string }) => unknown) => selector({ getText: mockGetText }) as ReturnType); + vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string; isInitialized: boolean }) => unknown) => selector({ getText: mockGetText, isInitialized: true }) as ReturnType); const handleShopNavigate = vi.fn(); render(); diff --git a/frontend/application/lib/services/productService.ts b/frontend/application/lib/services/productService.ts index 7fae3ba..4ff2ccb 100644 --- a/frontend/application/lib/services/productService.ts +++ b/frontend/application/lib/services/productService.ts @@ -121,9 +121,11 @@ export class ProductService { optimisticTemplate: data.optimisticTemplate, feedingAdvice: data.dosageLogic || data.feedingAdvice || '', slug: data.slug, - image: (data.imageUrl && data.imageUrl.startsWith('http')) - ? data.imageUrl - : (local?.image || (data.imageUrl ? `/api${data.imageUrl}` : data.image || '')), + image: (data.imageUrl && (data.imageUrl.startsWith('http://') || data.imageUrl.startsWith('https://'))) + ? data.imageUrl + : (data.imageUrl && data.imageUrl.startsWith('/uploads/') + ? data.imageUrl + : (local?.image || (data.imageUrl && !data.imageUrl.startsWith('/products/') ? data.imageUrl : '') || '/assets/images/hero-section-image.png')), main_ingredients: data.ingredientList?.map(i => i.ingredient) || data.ingredients?.split(/[،,-]/).map(s => s.trim()).filter(Boolean) || [], symptoms: data.symptoms?.map(s => typeof s === 'string' ? s : s.symptom || '').filter(Boolean) || [],