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:
co-authored by
Cursor
parent
a7950f1f13
commit
193c63bfd2
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user