From b861ab3dc1c8d03e2c9eb3d618c92b76d192aa4a Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Mon, 17 Aug 2026 09:55:03 +0330 Subject: [PATCH] Refresh static image slots from the website catalog. The dashboard can import and drop slots from the live site instead of creating them by hand. Co-authored-by: Cursor --- .../src/components/Sidebar.module.css | 22 +++--- apps/business/src/i18n/messages.ts | 14 +++- .../pages/WebsiteStaticImagesPage.module.css | 17 +++++ .../src/pages/WebsiteStaticImagesPage.tsx | 76 ++++++++++++++----- .../src/services/websiteStaticImageService.ts | 13 ++++ .../src/components/Sidebar.module.css | 22 +++--- docs/PROJECT_CONTEXT.md | 1 + 7 files changed, 128 insertions(+), 37 deletions(-) diff --git a/apps/business/src/components/Sidebar.module.css b/apps/business/src/components/Sidebar.module.css index 9ded9b7..5c37f94 100644 --- a/apps/business/src/components/Sidebar.module.css +++ b/apps/business/src/components/Sidebar.module.css @@ -104,6 +104,7 @@ gap: 12px; padding: 10px 14px; border-radius: var(--radius-sm); + border: 1px solid transparent; font-size: 14px; font-weight: 500; color: var(--text-secondary); @@ -118,17 +119,18 @@ } .navItem.active { - background: var(--primary); - color: #fff; + background: rgba(var(--primary-rgb) / 0.1); + color: var(--primary); + border-color: var(--primary); } .navItem.active:hover { - background: var(--primary-dark); - color: #fff; + background: rgba(var(--primary-rgb) / 0.16); + color: var(--primary); } .navItem.active .chevron { - color: #fff; + color: var(--primary); } .navGroupBtn { @@ -163,6 +165,7 @@ display: block; padding: 8px 12px; border-radius: 8px; + border: 1px solid transparent; font-size: 13px; font-weight: 500; color: var(--text-secondary); @@ -175,13 +178,14 @@ } .subNavActive { - background: var(--primary); - color: #fff; + background: rgba(var(--primary-rgb) / 0.1); + color: var(--primary); + border-color: var(--primary); } .subNavActive:hover { - background: var(--primary-dark); - color: #fff; + background: rgba(var(--primary-rgb) / 0.16); + color: var(--primary); } .footer { diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 6f525d2..7950c1c 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -1351,7 +1351,7 @@ const en = { 'website.staticImages.subtitle': 'Websites load these by key from GET /tenants/{domain}/website/static-images', 'website.staticImages.loading': 'Loading static images...', - 'website.staticImages.empty': 'No image slots yet for this website.', + 'website.staticImages.empty': 'No image slots yet. Refresh to read them from the website.', 'website.staticImages.emptyPage': 'No static images on this page yet.', 'website.staticImages.errorLoad': 'Unable to load static images.', 'website.staticImages.errorCreate': 'Unable to create image slot.', @@ -1360,11 +1360,16 @@ const en = { 'website.staticImages.errorUpload': 'Unable to upload image.', 'website.staticImages.errorAddImage': 'Unable to save image.', 'website.staticImages.errorRemoveImage': 'Unable to remove image.', + 'website.staticImages.errorRefresh': 'Unable to read image slots from the website.', 'website.staticImages.toastCreated': 'Image slot created.', 'website.staticImages.toastUpdated': 'Image slot updated.', 'website.staticImages.toastDeleted': 'Image slot deleted.', 'website.staticImages.toastImageSaved': 'Image saved.', 'website.staticImages.toastImageRemoved': 'Image removed.', + 'website.staticImages.toastRefreshed': + '{created} slots added, {deleted} removed.', + 'website.staticImages.toastRefreshedNone': 'No new slots. The list is up to date.', + 'website.staticImages.refresh': 'Refresh slots from website', 'website.staticImages.addSlot': 'Add image slot', 'website.staticImages.createTitle': 'Add image slot', 'website.staticImages.editTitle': 'Edit image slot', @@ -3286,7 +3291,7 @@ const fa: Record = { 'website.staticImages.subtitle': 'وب‌سایت‌ها این تصاویر را با کلید از GET /tenants/{domain}/website/static-images می‌خوانند', 'website.staticImages.loading': 'در حال بارگذاری تصاویر ثابت...', - 'website.staticImages.empty': 'هنوز اسلاتی برای این وب‌سایت نیست.', + 'website.staticImages.empty': 'هنوز اسلاتی نیست. تازه‌سازی را بزنید تا از وب‌سایت خوانده شود.', 'website.staticImages.emptyPage': 'در این صفحه هنوز تصویر ثابتی نیست.', 'website.staticImages.errorLoad': 'بارگذاری تصاویر ثابت ممکن نشد.', 'website.staticImages.errorCreate': 'ساخت اسلات تصویر ممکن نشد.', @@ -3295,11 +3300,16 @@ const fa: Record = { 'website.staticImages.errorUpload': 'آپلود تصویر ممکن نشد.', 'website.staticImages.errorAddImage': 'ذخیره تصویر ممکن نشد.', 'website.staticImages.errorRemoveImage': 'حذف تصویر ممکن نشد.', + 'website.staticImages.errorRefresh': 'خواندن اسلات‌ها از وب‌سایت ممکن نشد.', 'website.staticImages.toastCreated': 'اسلات تصویر ساخته شد.', 'website.staticImages.toastUpdated': 'اسلات تصویر به‌روز شد.', 'website.staticImages.toastDeleted': 'اسلات تصویر حذف شد.', 'website.staticImages.toastImageSaved': 'تصویر ذخیره شد.', 'website.staticImages.toastImageRemoved': 'تصویر حذف شد.', + 'website.staticImages.toastRefreshed': + '{created} اسلات اضافه و {deleted} اسلات حذف شد.', + 'website.staticImages.toastRefreshedNone': 'اسلات جدیدی نبود. فهرست به‌روز است.', + 'website.staticImages.refresh': 'تازه‌سازی اسلات‌ها از وب‌سایت', 'website.staticImages.addSlot': 'افزودن اسلات تصویر', 'website.staticImages.createTitle': 'افزودن اسلات تصویر', 'website.staticImages.editTitle': 'ویرایش اسلات تصویر', diff --git a/apps/business/src/pages/WebsiteStaticImagesPage.module.css b/apps/business/src/pages/WebsiteStaticImagesPage.module.css index fbe5fe0..0529c92 100644 --- a/apps/business/src/pages/WebsiteStaticImagesPage.module.css +++ b/apps/business/src/pages/WebsiteStaticImagesPage.module.css @@ -78,3 +78,20 @@ background-size: var(--select-arrow-size); cursor: pointer; } + +.headerTools { + display: flex; + align-items: flex-end; + gap: 12px; + flex-shrink: 0; +} + +.spin { + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/apps/business/src/pages/WebsiteStaticImagesPage.tsx b/apps/business/src/pages/WebsiteStaticImagesPage.tsx index 624152a..f9e0348 100644 --- a/apps/business/src/pages/WebsiteStaticImagesPage.tsx +++ b/apps/business/src/pages/WebsiteStaticImagesPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react' -import { Pencil, Trash2 } from 'lucide-react' +import { Pencil, RefreshCw, Trash2 } from 'lucide-react' import { AddWebsiteSliderSlideModal, type WebsiteSliderSlideFormData, @@ -23,6 +23,7 @@ import { deleteWebsiteStaticImageSlot, listWebsiteStaticImageSlots, staticImageItemsToPayload, + syncWebsiteStaticImageSlots, updateWebsiteStaticImageSlot, } from '../services/websiteStaticImageService' import type { @@ -79,6 +80,7 @@ export function WebsiteStaticImagesPage() { const [slots, setSlots] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) + const [isSyncing, setIsSyncing] = useState(false) const [error, setError] = useState('') const [pageKey, setPageKey] = useState('home') const [editSlot, setEditSlot] = useState(null) @@ -117,6 +119,33 @@ export function WebsiteStaticImagesPage() { } } + async function handleRefreshSlots() { + setIsSyncing(true) + setError('') + + try { + const result = await syncWebsiteStaticImageSlots() + setSlots(result.items) + showToast( + result.created > 0 || result.deleted > 0 + ? t('website.staticImages.toastRefreshed', { + created: result.created, + deleted: result.deleted, + }) + : t('website.staticImages.toastRefreshedNone'), + 'success', + ) + } catch (err) { + if (err instanceof ApiError) { + setError(err.message) + } else { + setError(t('website.staticImages.errorRefresh')) + } + } finally { + setIsSyncing(false) + } + } + function replaceSlot(updated: WebsiteStaticImageSlot) { setSlots((prev) => prev.map((entry) => (entry.id === updated.id ? updated : entry))) } @@ -285,23 +314,36 @@ export function WebsiteStaticImagesPage() { {t('website.staticImages.subtitle', { domain })}

- {pageOptions.length > 1 ? ( -
- - -
- ) : null} + + + + {pageOptions.length > 1 ? ( +
+ + +
+ ) : null} + {error &&

{error}

} diff --git a/apps/business/src/services/websiteStaticImageService.ts b/apps/business/src/services/websiteStaticImageService.ts index 30d10cc..702e51f 100644 --- a/apps/business/src/services/websiteStaticImageService.ts +++ b/apps/business/src/services/websiteStaticImageService.ts @@ -37,6 +37,19 @@ export async function createWebsiteStaticImageSlot( }) } +export async function syncWebsiteStaticImageSlots() { + return apiRequest<{ + message: string + created: number + updated: number + deleted: number + items: WebsiteStaticImageSlot[] + }>(businessPath('/sync'), { + method: 'POST', + auth: true, + }) +} + export async function updateWebsiteStaticImageSlot( slotId: string, payload: UpdateWebsiteStaticImageSlotPayload, diff --git a/apps/customer/src/components/Sidebar.module.css b/apps/customer/src/components/Sidebar.module.css index 14d3195..e4dd746 100644 --- a/apps/customer/src/components/Sidebar.module.css +++ b/apps/customer/src/components/Sidebar.module.css @@ -101,6 +101,7 @@ gap: 12px; padding: 10px 14px; border-radius: var(--radius-sm); + border: 1px solid transparent; font-size: 14px; font-weight: 500; color: var(--text-secondary); @@ -115,17 +116,18 @@ } .navItem.active { - background: var(--primary); - color: #fff; + background: rgba(var(--primary-rgb) / 0.1); + color: var(--primary); + border-color: var(--primary); } .navItem.active:hover { - background: var(--primary-dark); - color: #fff; + background: rgba(var(--primary-rgb) / 0.16); + color: var(--primary); } .navItem.active .chevron { - color: #fff; + color: var(--primary); } .navGroupBtn { @@ -160,6 +162,7 @@ display: block; padding: 8px 12px; border-radius: 8px; + border: 1px solid transparent; font-size: 13px; font-weight: 500; color: var(--text-secondary); @@ -172,13 +175,14 @@ } .subNavActive { - background: var(--primary); - color: #fff; + background: rgba(var(--primary-rgb) / 0.1); + color: var(--primary); + border-color: var(--primary); } .subNavActive:hover { - background: var(--primary-dark); - color: #fff; + background: rgba(var(--primary-rgb) / 0.16); + color: var(--primary); } .footer { diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 14d3910..edfbb9f 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -298,6 +298,7 @@ Products accept optional `brandId` on create/update. - `GET/POST/PATCH/DELETE /businesses/:businessId/website/brand-groups` — groups with `brandIds[]` - `GET /tenants/:host/website/category-groups` — public category groups - `GET/POST/PATCH/DELETE /businesses/:businessId/website/static-images` — named image slots (`single` or `list`) with `pageKey` + per-image `titleFa` / `titleEn` / `subtext` / `linkUrl` +- `POST /businesses/:businessId/website/static-images/sync` — read slot keys from the live website (`GET https://{domain}/meshkee/static-image-slots`) and upsert them - `GET /tenants/:host/website/static-images` — public slots (`?pageKey=home` to filter by page) - `GET /tenants/:host/website/static-images/:key` — one public slot - `GET/POST/PATCH/DELETE /businesses/:businessId/website/sliders` — sliders with `slides[]` (imageMediaId, title, linkUrl)