diff --git a/apps/business/src/App.tsx b/apps/business/src/App.tsx index 74c72f2..11d6ef3 100644 --- a/apps/business/src/App.tsx +++ b/apps/business/src/App.tsx @@ -68,6 +68,7 @@ import { WebsiteFaqPage } from './pages/WebsiteFaqPage' import { WebsiteBadgesPage } from './pages/WebsiteBadgesPage' import { SettingsPage } from './pages/SettingsPage' import { WebsiteStaticImagesPage } from './pages/WebsiteStaticImagesPage' +import { WebsiteSettingsPage } from './pages/WebsiteSettingsPage' function App() { return ( @@ -152,6 +153,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/apps/business/src/components/Sidebar.tsx b/apps/business/src/components/Sidebar.tsx index 28fb750..4a6bf14 100644 --- a/apps/business/src/components/Sidebar.tsx +++ b/apps/business/src/components/Sidebar.tsx @@ -214,6 +214,7 @@ export function Sidebar({ open = false }: SidebarProps) { { labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' }, { labelKey: 'nav.website.faq', to: '/website/faq' }, { labelKey: 'nav.website.badges', to: '/website/badges' }, + { labelKey: 'nav.website.settings', to: '/website/settings' }, ], }, { type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' }, diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 871ddc6..1d43143 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -78,6 +78,7 @@ const en = { 'nav.website.subscriptions': 'Subscriptions', 'nav.website.faq': 'FAQ', 'nav.website.badges': 'Badges', + 'nav.website.settings': 'Settings', 'nav.website.ePayment': 'E-Payment', 'nav.help': 'Help Center', 'nav.logout': 'Logout', @@ -1326,7 +1327,7 @@ const en = { 'website.card.specialBrands.desc': 'Showcase partner or featured brands on your website homepage.', 'website.card.specialBrands.link': 'Manage brands', 'website.card.specialItems.desc': - 'Curate featured store items into categories for your website carousels.', + 'Curate featured products into categories for your website carousels.', 'website.card.specialItems.link': 'Manage special items', 'website.card.contact.desc': 'View submissions from your website contact form.', 'website.card.contact.link': 'View submissions', @@ -1337,6 +1338,8 @@ const en = { 'website.card.faq.link': 'Manage FAQ', 'website.card.badges.desc': 'Show trust badges, certifications, and highlights on your website.', 'website.card.badges.link': 'Manage badges', + 'website.card.settings.desc': 'Choose how special products are sourced for the website.', + 'website.card.settings.link': 'Manage settings', 'settings.subtitle': 'Configure online payment gateways and checkout payment options.', 'website.scrollLeft': 'Scroll left', @@ -1529,7 +1532,7 @@ const en = { 'website.specialBrands.pickErrorLoad': 'Unable to load brands.', 'website.specialItems.subtitle': - 'Curate featured store items into categories for your website carousels.', + 'Curate featured products into categories for your website carousels.', 'website.specialItems.loading': 'Loading special categories...', 'website.specialItems.empty': 'You have not added any special categories yet.', 'website.specialItems.errorLoad': 'Unable to load special categories.', @@ -1563,6 +1566,19 @@ const en = { 'website.specialItems.pickEmpty': 'All store items are already in this category.', 'website.specialItems.pickSearch': 'Type 3+ characters to search store items', 'website.specialItems.pickErrorLoad': 'Unable to load store items.', + + 'website.settings.title': 'Website settings', + 'website.settings.subtitle': 'Configure how website content is sourced and displayed.', + 'website.settings.section': 'Special products', + 'website.settings.loading': 'Loading settings...', + 'website.settings.errorLoad': 'Unable to load settings.', + 'website.settings.errorSave': 'Unable to save settings.', + 'website.settings.saved': 'Settings saved.', + 'website.settings.specialSource': 'Read special products from', + 'website.settings.specialSourceDesc': + 'Choose whether featured products on the website come from catalog products or store items. Defaults to store items when the store module is enabled.', + 'website.settings.source.product': 'Product', + 'website.settings.source.storeItem': 'Store item', 'website.pick.addOneItem': 'Add 1 item', 'website.pick.addItems': 'Add {count} items', @@ -2072,11 +2088,12 @@ const fa: Record = { 'nav.website.sliders': 'اسلایدرها', 'nav.website.specialCategories': 'دسته‌های ویژه', 'nav.website.specialBrands': 'برندهای ویژه', - 'nav.website.specialItems': 'محصولات ویژه فروشگاه', + 'nav.website.specialItems': 'محصولات ویژه', 'nav.website.contact': 'فرم تماس با ما', 'nav.website.subscriptions': 'عضویت‌ها', 'nav.website.faq': 'سوالات متداول', 'nav.website.badges': 'نشان‌ها', + 'nav.website.settings': 'تنظیمات', 'nav.website.ePayment': 'پرداخت الکترونیک', 'nav.help': 'مرکز راهنما', 'nav.logout': 'خروج', @@ -3325,7 +3342,7 @@ const fa: Record = { 'website.card.specialBrands.desc': 'برندهای شریک یا ویژه را در صفحه اصلی وب‌سایت نمایش دهید.', 'website.card.specialBrands.link': 'مدیریت برندها', 'website.card.specialItems.desc': - 'محصولات ویژه فروشگاه را در دسته‌ها برای کاروسل‌های وب‌سایت گردآوری کنید.', + 'محصولات ویژه را در دسته‌ها برای کاروسل‌های وب‌سایت گردآوری کنید.', 'website.card.specialItems.link': 'مدیریت محصولات ویژه', 'website.card.contact.desc': 'ارسال‌های فرم تماس وب‌سایت را مشاهده کنید.', 'website.card.contact.link': 'مشاهده ارسال‌ها', @@ -3335,6 +3352,8 @@ const fa: Record = { 'website.card.faq.link': 'مدیریت سوالات متداول', 'website.card.badges.desc': 'نشان‌های اعتماد، گواهی‌ها و برجسته‌ها را در وب‌سایت نمایش دهید.', 'website.card.badges.link': 'مدیریت نشان‌ها', + 'website.card.settings.desc': 'منبع خواندن محصولات ویژه وب‌سایت را انتخاب کنید.', + 'website.card.settings.link': 'مدیریت تنظیمات', 'settings.subtitle': 'درگاه‌های پرداخت آنلاین و گزینه‌های تسویه حساب را پیکربندی کنید.', 'website.scrollLeft': 'اسکرول به چپ', @@ -3526,7 +3545,7 @@ const fa: Record = { 'website.specialBrands.pickErrorLoad': 'بارگذاری برندها ممکن نشد.', 'website.specialItems.subtitle': - 'محصولات ویژه فروشگاه را برای کاروسل‌های وب‌سایت در دسته‌ها گردآوری کنید.', + 'محصولات ویژه را برای کاروسل‌های وب‌سایت در دسته‌ها گردآوری کنید.', 'website.specialItems.loading': 'در حال بارگذاری دسته‌های ویژه...', 'website.specialItems.empty': 'هنوز دسته ویژه‌ای ثبت نکرده‌اید.', 'website.specialItems.errorLoad': 'بارگذاری دسته‌های ویژه ممکن نشد.', @@ -3559,6 +3578,19 @@ const fa: Record = { 'website.specialItems.pickEmpty': 'همه محصولات فروشگاه قبلاً در این دسته هستند.', 'website.specialItems.pickSearch': 'حداقل ۳ حرف برای جستجوی محصولات فروشگاه تایپ کنید', 'website.specialItems.pickErrorLoad': 'بارگذاری محصولات فروشگاه ممکن نشد.', + + 'website.settings.title': 'تنظیمات وب‌سایت', + 'website.settings.subtitle': 'نحوه منبع‌گیری و نمایش محتوای وب‌سایت را پیکربندی کنید.', + 'website.settings.section': 'محصولات ویژه', + 'website.settings.loading': 'در حال بارگذاری تنظیمات...', + 'website.settings.errorLoad': 'بارگذاری تنظیمات ممکن نشد.', + 'website.settings.errorSave': 'ذخیره تنظیمات ممکن نشد.', + 'website.settings.saved': 'تنظیمات ذخیره شد.', + 'website.settings.specialSource': 'خواندن محصولات ویژه از', + 'website.settings.specialSourceDesc': + 'انتخاب کنید محصولات ویژه وب‌سایت از کاتالوگ محصول خوانده شوند یا از محصولات فروشگاه. اگر ماژول فروشگاه فعال باشد، پیش‌فرض محصول فروشگاه است.', + 'website.settings.source.product': 'محصول', + 'website.settings.source.storeItem': 'محصول فروشگاه', 'website.pick.addOneItem': 'افزودن ۱ قلم', 'website.pick.addItems': 'افزودن {count} قلم', @@ -3688,7 +3720,7 @@ const fa: Record = { 'title.sliders': 'اسلایدرها', 'title.specialCategories': 'دسته‌های ویژه', 'title.specialBrands': 'برندهای ویژه', - 'title.specialItems': 'محصولات ویژه فروشگاه', + 'title.specialItems': 'محصولات ویژه', 'title.contactForm': 'فرم تماس با ما', 'title.subscriptions': 'عضویت‌ها', 'title.faq': 'سوالات متداول', @@ -4166,6 +4198,7 @@ export function getBusinessRouteTitleRules(locale: DashboardLocale): RouteTitleR { match: '/website/subscriptions', labels: [t('title.website'), t('title.subscriptions')] }, { match: '/website/faq', labels: [t('title.website'), t('title.faq')] }, { match: '/website/badges', labels: [t('title.website'), t('title.badges')] }, + { match: '/website/settings', labels: [t('title.website'), t('title.settings')] }, { match: '/website', labels: [t('title.website')] }, { match: '/settings', labels: [t('title.settings')] }, { match: '/', labels: [t('title.home')] }, diff --git a/apps/business/src/pages/WebsitePage.tsx b/apps/business/src/pages/WebsitePage.tsx index 6e418d0..f916af1 100644 --- a/apps/business/src/pages/WebsitePage.tsx +++ b/apps/business/src/pages/WebsitePage.tsx @@ -5,6 +5,7 @@ import { Award, Image, Images, + Settings, Sparkles, } from 'lucide-react' import { SectionCard } from '../components/SectionCard' @@ -63,6 +64,13 @@ const websiteSections: { linkKey: 'website.card.badges.link', href: '/website/badges', }, + { + icon: Settings, + titleKey: 'nav.website.settings', + descKey: 'website.card.settings.desc', + linkKey: 'website.card.settings.link', + href: '/website/settings', + }, ] export function WebsitePage() { diff --git a/apps/business/src/pages/WebsiteSettingsPage.module.css b/apps/business/src/pages/WebsiteSettingsPage.module.css new file mode 100644 index 0000000..9434082 --- /dev/null +++ b/apps/business/src/pages/WebsiteSettingsPage.module.css @@ -0,0 +1,52 @@ +.sourceRow { + align-items: flex-start; +} + +.radioGroup { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 200px; + flex-shrink: 0; +} + +.radioOption { + display: flex; + align-items: center; + gap: 8px; + min-height: var(--field-height); + padding: var(--field-padding-y) var(--field-padding-x); + font-size: var(--field-font-size); + line-height: 1.4; + color: var(--text-primary); + background-color: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(148, 163, 184, 0.35); + border-radius: var(--radius-sm); + cursor: pointer; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.radioOption:has(input:checked) { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12); +} + +.radioOption:has(input:disabled) { + opacity: 0.6; + cursor: not-allowed; +} + +.radioOption input { + width: 16px; + height: 16px; + margin: 0; + accent-color: var(--primary); + flex-shrink: 0; +} + +@media (max-width: 768px) { + .radioGroup { + width: 100%; + min-width: 0; + } +} diff --git a/apps/business/src/pages/WebsiteSettingsPage.tsx b/apps/business/src/pages/WebsiteSettingsPage.tsx new file mode 100644 index 0000000..65a4ec2 --- /dev/null +++ b/apps/business/src/pages/WebsiteSettingsPage.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react' +import { Breadcrumbs } from '../components/Breadcrumbs' +import { PageTitle } from '../components/PageTitle' +import { useToast } from '../context/ToastContext' +import { useT } from '../i18n/useT' +import { ApiError } from '../lib/api' +import { + getSettings, + updateWebsiteSettings, + type SpecialProductsSource, +} from '../services/settingsService' +import { hasBusinessModule, type BusinessModuleId } from '../utils/businessModules' +import pageStyles from '../components/PageContent.module.css' +import sharedStyles from './ProductSettingsPage.module.css' +import styles from './WebsiteSettingsPage.module.css' + +const SOURCE_OPTIONS: SpecialProductsSource[] = ['product', 'store_item'] + +function defaultSpecialProductsSource( + enabledModules: string[] | undefined, +): SpecialProductsSource { + return hasBusinessModule( + enabledModules as BusinessModuleId[] | undefined, + 'store', + ) + ? 'store_item' + : 'product' +} + +export function WebsiteSettingsPage() { + const t = useT() + const { showToast } = useToast() + const [source, setSource] = useState('product') + const [isLoading, setIsLoading] = useState(true) + const [isSaving, setIsSaving] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + const controller = new AbortController() + void loadSettings(controller.signal) + return () => controller.abort() + }, []) + + async function loadSettings(signal?: AbortSignal) { + setIsLoading(true) + setError('') + + try { + const data = await getSettings(signal) + setSource( + data.settings.website?.specialProductsSource ?? + defaultSpecialProductsSource(data.settings.modules?.enabled), + ) + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') return + if (err instanceof ApiError) { + setError(err.message) + } else { + setError(t('website.settings.errorLoad')) + } + } finally { + setIsLoading(false) + } + } + + async function handleSourceChange(next: SpecialProductsSource) { + if (next === source || isSaving) return + + const previous = source + setSource(next) + setIsSaving(true) + setError('') + + try { + const data = await updateWebsiteSettings({ specialProductsSource: next }) + setSource( + data.settings.website?.specialProductsSource ?? next, + ) + showToast(t('website.settings.saved'), 'success') + } catch (err) { + setSource(previous) + if (err instanceof ApiError) { + setError(err.message) + } else { + setError(t('website.settings.errorSave')) + } + } finally { + setIsSaving(false) + } + } + + return ( +
+ + +
+
+ {t('website.settings.title')} +

{t('website.settings.subtitle')}

+
+
+ + {error && ( +
+ {error} +
+ )} + +
+

{t('website.settings.section')}

+ + {isLoading ? ( +

{t('website.settings.loading')}

+ ) : ( +
+
+
+ + {t('website.settings.specialSource')} + +

+ {t('website.settings.specialSourceDesc')} +

+
+
+ {SOURCE_OPTIONS.map((option) => ( + + ))} +
+
+
+ )} +
+
+ ) +} diff --git a/apps/business/src/services/settingsService.ts b/apps/business/src/services/settingsService.ts index 5e494e1..a198b4d 100644 --- a/apps/business/src/services/settingsService.ts +++ b/apps/business/src/services/settingsService.ts @@ -90,11 +90,18 @@ export interface ModulesSettings { enabled: string[] } +export type SpecialProductsSource = 'product' | 'store_item' + +export interface WebsiteSettings { + specialProductsSource: SpecialProductsSource +} + export interface BusinessSettings { branding: BrandingSettings dashboard: DashboardSettings store: StoreSettings modules?: ModulesSettings + website?: WebsiteSettings } export interface SettingsResponse { @@ -158,3 +165,11 @@ export async function updateStoreSettings(store: Partial) { body: { store }, }) } + +export async function updateWebsiteSettings(website: Partial) { + return apiRequest(settingsPath(), { + method: 'PATCH', + auth: true, + body: { website }, + }) +} diff --git a/apps/business/src/services/tenantService.ts b/apps/business/src/services/tenantService.ts index a641bdc..2ad143b 100644 --- a/apps/business/src/services/tenantService.ts +++ b/apps/business/src/services/tenantService.ts @@ -13,6 +13,7 @@ export interface ResolvedTenant { defaultLocale?: DashboardLocale enabledModules?: BusinessModuleId[] homeCharts?: [HomeChartId, HomeChartId] + specialProductsSource?: 'product' | 'store_item' logoUrl?: string | null faviconUrl?: string | null } diff --git a/apps/super-admin/src/services/businessSettingsService.ts b/apps/super-admin/src/services/businessSettingsService.ts index ed58511..417b7dc 100644 --- a/apps/super-admin/src/services/businessSettingsService.ts +++ b/apps/super-admin/src/services/businessSettingsService.ts @@ -28,6 +28,9 @@ export interface BusinessSettings { orderProcessSteps: Array<{ id: string; label: string; color: string }> } modules: ModulesSettings + website?: { + specialProductsSource: 'product' | 'store_item' + } } export interface BusinessSettingsResponse { diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 8abc56c..64690cf 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -3,7 +3,7 @@ > **For AI agents:** Read this file at the start of a new chat before making changes. > Update this document when a major feature is completed or architecture changes. -Last updated: August 20, 2026 +Last updated: August 21, 2026 --- @@ -216,6 +216,7 @@ Add to `/etc/hosts` (one line per tenant): | `/website` | Website hub | Placeholder | | `/website/static-images` | Named website image slots, grouped by page | Yes | | `/website/special-items` | Special item carousels | Yes | +| `/website/settings` | Website settings (special products source: product vs store item) | Yes | | `/website/contact` | Contact us submissions list | Yes | | `/website/subscriptions` | Subscriptions | Placeholder | | `/website/faq` | FAQ | Placeholder | @@ -272,11 +273,12 @@ Products accept optional `brandId` on create/update. - `POST /tenants/:host/contact-submissions` — public website contact form ### Business settings -- `GET/PATCH /businesses/:businessId/settings` — `branding`, `dashboard`, `store`, `modules` +- `GET/PATCH /businesses/:businessId/settings` — `branding`, `dashboard`, `store`, `modules`, `website` +- `website.specialProductsSource`: `product` \| `store_item`. Unset → `store_item` if the store module is enabled, otherwise `product`. - `modules.enabled`: optional modules — business: `products`, `store`, `portfolio`, `blog`, `warehouse`, `videos`, `finance`; customer: `customer_products`. Missing key → all business modules enabled (legacy); customer modules stay opt-in. Super-admin only for modules PATCH. - `modules.charts`: two home chart slots (`none`, `orders_30d`, `customers_joined_1y`, `blog_views_30d`, `products_added_1y`); `none` hides that slot; defaults orders + customers. - Always-on (not in modules list): business — customers, website, profile, settings, home; customer — home, profile, addresses, orders, favorites -- Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts` +- Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts` + `specialProductsSource` ### Portfolios - `GET/POST/PATCH/DELETE /businesses/:businessId/portfolios`