Add Website settings for special-products source.

Businesses can choose product vs store item sourcing, with store as the default when the store module is enabled, and rename the FA special-products section.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-21 11:57:49 +03:30
co-authored by Cursor
parent a7950f1f13
commit 193c63bfd2
10 changed files with 286 additions and 9 deletions
+2
View File
@@ -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() {
<Route path="website/subscriptions" element={<WebsiteSubscriptionsPage />} />
<Route path="website/faq" element={<WebsiteFaqPage />} />
<Route path="website/badges" element={<WebsiteBadgesPage />} />
<Route path="website/settings" element={<WebsiteSettingsPage />} />
<Route path="website/e-payment" element={<Navigate to="/settings" replace />} />
</Route>
</Route>
+1
View File
@@ -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' },
+39 -6
View File
@@ -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<MessageKey, string> = {
'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<MessageKey, string> = {
'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<MessageKey, string> = {
'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<MessageKey, string> = {
'website.specialBrands.pickErrorLoad': 'بارگذاری برندها ممکن نشد.',
'website.specialItems.subtitle':
'محصولات ویژه فروشگاه را برای کاروسل‌های وب‌سایت در دسته‌ها گردآوری کنید.',
'محصولات ویژه را برای کاروسل‌های وب‌سایت در دسته‌ها گردآوری کنید.',
'website.specialItems.loading': 'در حال بارگذاری دسته‌های ویژه...',
'website.specialItems.empty': 'هنوز دسته ویژه‌ای ثبت نکرده‌اید.',
'website.specialItems.errorLoad': 'بارگذاری دسته‌های ویژه ممکن نشد.',
@@ -3559,6 +3578,19 @@ const fa: Record<MessageKey, string> = {
'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<MessageKey, string> = {
'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')] },
+8
View File
@@ -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() {
@@ -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;
}
}
@@ -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<SpecialProductsSource>('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 (
<main className={pageStyles.content}>
<Breadcrumbs
items={[
{ label: 'Dashboard', href: '/' },
{ label: 'Website', href: '/website' },
{ label: 'Settings' },
]}
/>
<div className={pageStyles.pageHeader}>
<div>
<PageTitle en="WEBSITE SETTINGS">{t('website.settings.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('website.settings.subtitle')}</p>
</div>
</div>
{error && (
<div className={sharedStyles.alertError} role="alert">
{error}
</div>
)}
<section className={sharedStyles.panel}>
<h3 className={sharedStyles.sectionTitle}>{t('website.settings.section')}</h3>
{isLoading ? (
<p className={sharedStyles.status}>{t('website.settings.loading')}</p>
) : (
<div className={sharedStyles.list}>
<div className={`${sharedStyles.row} ${styles.sourceRow}`}>
<div className={sharedStyles.rowText}>
<span className={sharedStyles.rowLabel} id="special-products-source-label">
{t('website.settings.specialSource')}
</span>
<p className={sharedStyles.rowDescription}>
{t('website.settings.specialSourceDesc')}
</p>
</div>
<div
className={styles.radioGroup}
role="radiogroup"
aria-labelledby="special-products-source-label"
>
{SOURCE_OPTIONS.map((option) => (
<label key={option} className={styles.radioOption}>
<input
type="radio"
name="special-products-source"
value={option}
checked={source === option}
disabled={isSaving}
onChange={() => void handleSourceChange(option)}
/>
<span>
{option === 'product'
? t('website.settings.source.product')
: t('website.settings.source.storeItem')}
</span>
</label>
))}
</div>
</div>
</div>
)}
</section>
</main>
)
}
@@ -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<StoreSettings>) {
body: { store },
})
}
export async function updateWebsiteSettings(website: Partial<WebsiteSettings>) {
return apiRequest<SettingsResponse>(settingsPath(), {
method: 'PATCH',
auth: true,
body: { website },
})
}
@@ -13,6 +13,7 @@ export interface ResolvedTenant {
defaultLocale?: DashboardLocale
enabledModules?: BusinessModuleId[]
homeCharts?: [HomeChartId, HomeChartId]
specialProductsSource?: 'product' | 'store_item'
logoUrl?: string | null
faviconUrl?: string | null
}
@@ -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 {
+5 -3
View File
@@ -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`