Add a Torob toggle in store settings and show parent category on product chips.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
fde6a34e44
commit
8c187b18be
@@ -93,7 +93,8 @@
|
||||
.panelInFlow {
|
||||
position: static;
|
||||
margin-top: 6px;
|
||||
max-height: min(42vh, 320px);
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ interface CategoryTreeSelectProps {
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
/** Overlay floats over content; inFlow expands the parent (use inside modals). */
|
||||
dropdownVariant?: 'overlay' | 'inFlow'
|
||||
}
|
||||
|
||||
function categoryLabel(category: Category, isFa: boolean) {
|
||||
@@ -163,6 +165,7 @@ export function CategoryTreeSelect({
|
||||
placeholder,
|
||||
disabled = false,
|
||||
id,
|
||||
dropdownVariant = 'overlay',
|
||||
}: CategoryTreeSelectProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
@@ -269,7 +272,14 @@ export function CategoryTreeSelect({
|
||||
</div>
|
||||
|
||||
{open && !disabled ? (
|
||||
<div className={styles.panel}>
|
||||
<div
|
||||
className={[
|
||||
styles.panel,
|
||||
dropdownVariant === 'inFlow' ? styles.panelInFlow : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{!hasVisible ? (
|
||||
<p className={styles.noResults}>{t('categories.select.empty')}</p>
|
||||
) : (
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
.form {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.treeField {
|
||||
|
||||
@@ -274,6 +274,7 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
|
||||
value={categoryId}
|
||||
onChange={setCategoryId}
|
||||
placeholder={t('website.linkGenerator.pickCategory')}
|
||||
dropdownVariant="inFlow"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -144,10 +144,12 @@
|
||||
|
||||
.categoryChip {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
padding: 3px 10px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border-radius: 50px;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import type { Product } from '../types/product'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import { formatProductCategoryChip } from '../services/productService'
|
||||
import styles from './ProductCard.module.css'
|
||||
|
||||
interface ProductCardProps {
|
||||
@@ -47,7 +48,7 @@ export function ProductCard({
|
||||
? product.nameEn
|
||||
: ''
|
||||
: product.nameFa
|
||||
const categoryLabel = isFa ? product.categoryFa || product.category : product.category
|
||||
const categoryLabel = formatProductCategoryChip(product, isFa)
|
||||
const imageSrc = product.image?.trim() || ''
|
||||
const [imageFailed, setImageFailed] = useState(false)
|
||||
const showImage = Boolean(imageSrc) && !imageFailed
|
||||
|
||||
@@ -1017,6 +1017,13 @@ const en = {
|
||||
'storeSettings.onlineSell': 'Online sell',
|
||||
'storeSettings.onlineSellDesc':
|
||||
'When disabled, all sales on your website are turned off. Customers will not be able to place new orders online.',
|
||||
'storeSettings.torob': 'Torob',
|
||||
'storeSettings.torobDesc':
|
||||
'Turn on to share your catalog with Torob. Give them this product API URL.',
|
||||
'storeSettings.torobLinkAria': 'Torob product API URL',
|
||||
'storeSettings.torobCopy': 'Copy link',
|
||||
'storeSettings.torobCopied': 'Torob link copied.',
|
||||
'storeSettings.torobCopyError': 'Unable to copy the Torob link.',
|
||||
'storeSettings.orderProcess': 'Order process',
|
||||
'storeSettings.orderProcessDesc':
|
||||
'Define the steps an order can move through — for example: under processing, ready for shipping, shipped, delivered.',
|
||||
@@ -3347,6 +3354,13 @@ const fa: Record<MessageKey, string> = {
|
||||
'storeSettings.onlineSell': 'فروش آنلاین',
|
||||
'storeSettings.onlineSellDesc':
|
||||
'در صورت غیرفعال بودن، همه فروشهای وبسایت خاموش میشوند و مشتریان نمیتوانند سفارش آنلاین ثبت کنند.',
|
||||
'storeSettings.torob': 'ترب',
|
||||
'storeSettings.torobDesc':
|
||||
'برای همگامسازی کاتالوگ با ترب این گزینه را روشن کنید و این آدرس API را به ترب بدهید.',
|
||||
'storeSettings.torobLinkAria': 'آدرس API محصولات ترب',
|
||||
'storeSettings.torobCopy': 'کپی لینک',
|
||||
'storeSettings.torobCopied': 'لینک ترب کپی شد.',
|
||||
'storeSettings.torobCopyError': 'کپی لینک ترب ممکن نشد.',
|
||||
'storeSettings.orderProcess': 'فرآیند سفارش',
|
||||
'storeSettings.orderProcessDesc':
|
||||
'مراحل طیشده توسط سفارش را تعریف کنید — مثلاً: در حال پردازش، آماده ارسال، ارسالشده، تحویلشده.',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ProductDetailsTabs } from '../components/ProductDetailsTabs'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError } from '../lib/api'
|
||||
import {
|
||||
formatProductCategoryChip,
|
||||
getProduct,
|
||||
mapProductApiToUi,
|
||||
} from '../services/productService'
|
||||
@@ -101,7 +102,8 @@ export function ProductDetailsPage() {
|
||||
const descriptionLocale = textLocaleAttrs(
|
||||
product.description?.replace(/<[^>]+>/g, ' ') ?? '',
|
||||
)
|
||||
const categoryLocale = textLocaleAttrs(product.category)
|
||||
const categoryLabel = formatProductCategoryChip(product, isFa)
|
||||
const categoryLocale = textLocaleAttrs(categoryLabel)
|
||||
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
@@ -154,15 +156,15 @@ export function ProductDetailsPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
{product.category && (
|
||||
{categoryLabel ? (
|
||||
<span
|
||||
className={[styles.categoryChip, categoryLocale.className].filter(Boolean).join(' ')}
|
||||
lang={categoryLocale.lang}
|
||||
dir={categoryLocale.dir}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
>
|
||||
{product.category}
|
||||
{categoryLabel}
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<h1
|
||||
className={[styles.nameEn, primaryLocale.className].filter(Boolean).join(' ')}
|
||||
|
||||
@@ -119,6 +119,37 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.torobLinkRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.torobUrl {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
font-family: var(--font-en);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.torobUrlBlurred {
|
||||
filter: blur(6px);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stepRow {
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react'
|
||||
import { ChevronDown, ChevronUp, Copy, Plus, Trash2 } from 'lucide-react'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { PageTitle } from '../components/PageTitle'
|
||||
import { StepColorPicker } from '../components/StepColorPicker'
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type StoreSettings,
|
||||
} from '../services/settingsService'
|
||||
import { createId } from '../utils/id'
|
||||
import { getWebsiteOrigin } from '../utils/websiteLinks'
|
||||
import {
|
||||
defaultStepColorForId,
|
||||
normalizeStepColor,
|
||||
@@ -29,6 +30,7 @@ import styles from './StoreSettingsPage.module.css'
|
||||
|
||||
const DEFAULT_STORE_SETTINGS: StoreSettings = {
|
||||
onlineSellEnabled: true,
|
||||
torobEnabled: false,
|
||||
orderProcessSteps: DEFAULT_ORDER_PROCESS_STEPS,
|
||||
ePayment: {
|
||||
enabled: false,
|
||||
@@ -74,6 +76,14 @@ function stepsAreEqual(a: OrderProcessStep[], b: OrderProcessStep[]) {
|
||||
})
|
||||
}
|
||||
|
||||
function applyStore(store: StoreSettings): StoreSettings {
|
||||
return {
|
||||
...DEFAULT_STORE_SETTINGS,
|
||||
...store,
|
||||
torobEnabled: store.torobEnabled ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
export function StoreSettingsPage() {
|
||||
const t = useT()
|
||||
const { showToast } = useToast()
|
||||
@@ -97,7 +107,7 @@ export function StoreSettingsPage() {
|
||||
|
||||
try {
|
||||
const data = await getSettings(signal)
|
||||
setSettings(data.settings.store)
|
||||
setSettings(applyStore(data.settings.store))
|
||||
setDraftSteps(data.settings.store.orderProcessSteps)
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
@@ -117,7 +127,7 @@ export function StoreSettingsPage() {
|
||||
|
||||
try {
|
||||
const data = await updateStoreSettings({ onlineSellEnabled: checked })
|
||||
setSettings(data.settings.store)
|
||||
setSettings(applyStore(data.settings.store))
|
||||
showToast(t('productSettings.saved'), 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
@@ -130,6 +140,35 @@ export function StoreSettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTorobChange(checked: boolean) {
|
||||
setSavingKey('torob')
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const data = await updateStoreSettings({ torobEnabled: checked })
|
||||
setSettings(applyStore(data.settings.store))
|
||||
showToast(t('productSettings.saved'), 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError(t('productSettings.errorSave'))
|
||||
}
|
||||
} finally {
|
||||
setSavingKey(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyTorobUrl() {
|
||||
if (!settings.torobEnabled) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(torobUrl)
|
||||
showToast(t('storeSettings.torobCopied'), 'success')
|
||||
} catch {
|
||||
setError(t('storeSettings.torobCopyError'))
|
||||
}
|
||||
}
|
||||
|
||||
function updateStepLabel(id: string, label: string) {
|
||||
setDraftSteps((current) =>
|
||||
current.map((step) => (step.id === id ? { ...step, label } : step)),
|
||||
@@ -199,7 +238,7 @@ export function StoreSettingsPage() {
|
||||
|
||||
try {
|
||||
const data = await updateStoreSettings({ orderProcessSteps: normalized })
|
||||
setSettings(data.settings.store)
|
||||
setSettings(applyStore(data.settings.store))
|
||||
setDraftSteps(data.settings.store.orderProcessSteps)
|
||||
showToast(t('storeSettings.stepsSaved'), 'success')
|
||||
} catch (err) {
|
||||
@@ -223,6 +262,7 @@ export function StoreSettingsPage() {
|
||||
draftSteps.every(
|
||||
(step) => step.label.trim().length > 0 && step.labelFa.trim().length > 0,
|
||||
)
|
||||
const torobUrl = `${getWebsiteOrigin()}/torob_api/v3/products`
|
||||
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
@@ -269,6 +309,45 @@ export function StoreSettingsPage() {
|
||||
onChange={(checked) => void handleOnlineSellChange(checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className={sharedStyles.row}>
|
||||
<div className={sharedStyles.rowText}>
|
||||
<label htmlFor="torob-sync" className={sharedStyles.rowLabel}>
|
||||
{t('storeSettings.torob')}
|
||||
</label>
|
||||
<p className={sharedStyles.rowDescription}>
|
||||
{t('storeSettings.torobDesc')}
|
||||
</p>
|
||||
<div className={styles.torobLinkRow}>
|
||||
<p
|
||||
className={`${styles.torobUrl} ${settings.torobEnabled ? '' : styles.torobUrlBlurred}`}
|
||||
dir="ltr"
|
||||
lang="en"
|
||||
aria-hidden={!settings.torobEnabled}
|
||||
aria-label={t('storeSettings.torobLinkAria')}
|
||||
>
|
||||
{torobUrl}
|
||||
</p>
|
||||
<Tooltip label={t('storeSettings.torobCopy')}>
|
||||
<button
|
||||
type="button"
|
||||
className={controlStyles.controlBtn}
|
||||
onClick={() => void handleCopyTorobUrl()}
|
||||
disabled={!settings.torobEnabled || savingKey === 'torob'}
|
||||
aria-label={t('storeSettings.torobCopy')}
|
||||
>
|
||||
<Copy size={15} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="torob-sync"
|
||||
checked={settings.torobEnabled}
|
||||
disabled={savingKey === 'torob'}
|
||||
aria-label={t('storeSettings.torob')}
|
||||
onChange={(checked) => void handleTorobChange(checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface ProductApi {
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
categoryNameFa?: string
|
||||
categoryParentName?: string
|
||||
categoryParentNameFa?: string
|
||||
brandId: string | null
|
||||
brand: {
|
||||
id: string
|
||||
@@ -86,6 +88,8 @@ export function mapProductApiToUi(product: ProductApi): Product {
|
||||
categoryId: product.categoryId ?? '',
|
||||
category: product.categoryName,
|
||||
categoryFa: product.categoryNameFa || '',
|
||||
categoryParent: product.categoryParentName || '',
|
||||
categoryParentFa: product.categoryParentNameFa || '',
|
||||
summary: product.summary,
|
||||
description: product.descriptionHtml,
|
||||
image: product.image || product.thumbnail || '',
|
||||
@@ -98,6 +102,19 @@ export function mapProductApiToUi(product: ProductApi): Product {
|
||||
}
|
||||
}
|
||||
|
||||
export function formatProductCategoryChip(
|
||||
product: Pick<Product, 'category' | 'categoryFa' | 'categoryParent' | 'categoryParentFa'>,
|
||||
isFa: boolean,
|
||||
): string {
|
||||
const leaf = isFa ? product.categoryFa || product.category : product.category
|
||||
if (!leaf) return ''
|
||||
const parent = isFa
|
||||
? product.categoryParentFa || product.categoryParent
|
||||
: product.categoryParent
|
||||
if (!parent) return leaf
|
||||
return `${parent} >> ${leaf}`
|
||||
}
|
||||
|
||||
export function getProductGalleryUrls(product: ProductApi): string[] {
|
||||
const urls: string[] = []
|
||||
const featured = product.thumbnailUrl || product.thumbnail || product.image
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface EPaymentSettings {
|
||||
|
||||
export interface StoreSettings {
|
||||
onlineSellEnabled: boolean
|
||||
torobEnabled: boolean
|
||||
orderProcessSteps: OrderProcessStep[]
|
||||
ePayment: EPaymentSettings
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface Product {
|
||||
categoryId: string
|
||||
category: string
|
||||
categoryFa?: string
|
||||
categoryParent?: string
|
||||
categoryParentFa?: string
|
||||
summary: string
|
||||
description: string
|
||||
image: string
|
||||
|
||||
Reference in New Issue
Block a user