Add store-product Excel bulk edit and polish the business dashboard.
Export/import price and stock from Store Products, keep card order after saves, localize variations, and drop unused website nav items. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
7b51dda82a
commit
832ac32a09
@@ -116,6 +116,8 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
|
||||
cellNumber: created.cellNumber,
|
||||
firstName: created.firstName,
|
||||
lastName: created.lastName,
|
||||
firstNameEn: created.firstNameEn,
|
||||
lastNameEn: created.lastNameEn,
|
||||
email: created.email,
|
||||
label: created.label,
|
||||
createdAt: created.createdAt,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { BusinessMessageKey } from '../i18n/messages'
|
||||
import type { Variation, VariationType } from '../types/variation'
|
||||
import { COLOR_OPTIONS, SIZE_OPTIONS } from '../types/variation'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import styles from './VariationsModal.module.css'
|
||||
|
||||
interface AddVariationModalProps {
|
||||
@@ -14,6 +18,9 @@ interface AddVariationModalProps {
|
||||
const ANIMATION_MS = 220
|
||||
|
||||
export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModalProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const isFa = locale === 'fa'
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
const [type, setType] = useState<VariationType>('color')
|
||||
@@ -98,11 +105,11 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
e.preventDefault()
|
||||
|
||||
if (type === 'color') {
|
||||
onSubmit({ name: 'Color', type: 'color', values: selectedColors })
|
||||
onSubmit({ name: t('categories.variations.type.color'), type: 'color', values: selectedColors })
|
||||
} else if (type === 'size') {
|
||||
onSubmit({ name: 'Size', type: 'size', values: selectedSizes })
|
||||
onSubmit({ name: t('categories.variations.type.size'), type: 'size', values: selectedSizes })
|
||||
} else {
|
||||
const name = customName.trim() || 'Custom'
|
||||
const name = customName.trim() || t('categories.variations.type.custom')
|
||||
const values = customValues.map((v) => v.trim()).filter(Boolean)
|
||||
onSubmit({ name, type: 'custom', values })
|
||||
}
|
||||
@@ -126,33 +133,35 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="add-variation-title"
|
||||
lang={isFa ? 'fa' : 'en'}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h3 id="add-variation-title" className={styles.title}>
|
||||
Add Variation
|
||||
{t('categories.variations.addTitle')}
|
||||
</h3>
|
||||
<button className={styles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<button className={styles.closeBtn} onClick={onClose} aria-label={t('common.close')}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form className={styles.body} onSubmit={handleSubmit}>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="variationType">Type</label>
|
||||
<label htmlFor="variationType">{t('categories.variations.type')}</label>
|
||||
<select
|
||||
id="variationType"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as VariationType)}
|
||||
>
|
||||
<option value="color">Color</option>
|
||||
<option value="size">Size</option>
|
||||
<option value="custom">Your own</option>
|
||||
<option value="color">{t('categories.variations.type.color')}</option>
|
||||
<option value="size">{t('categories.variations.type.size')}</option>
|
||||
<option value="custom">{t('categories.variations.type.custom')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{type === 'color' && (
|
||||
<div className={styles.field}>
|
||||
<label>Select colors</label>
|
||||
<label>{t('categories.variations.selectColors')}</label>
|
||||
<div className={styles.chipGrid}>
|
||||
{COLOR_OPTIONS.map((color) => (
|
||||
<button
|
||||
@@ -161,7 +170,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
className={`${styles.chip} ${selectedColors.includes(color) ? styles.chipSelected : ''}`}
|
||||
onClick={() => toggleOption(color, selectedColors, setSelectedColors)}
|
||||
>
|
||||
{color}
|
||||
{t(`categories.variations.color.${color}` as BusinessMessageKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -170,7 +179,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
|
||||
{type === 'size' && (
|
||||
<div className={styles.field}>
|
||||
<label>Select sizes</label>
|
||||
<label>{t('categories.variations.selectSizes')}</label>
|
||||
<div className={styles.chipGrid}>
|
||||
{SIZE_OPTIONS.map((size) => (
|
||||
<button
|
||||
@@ -189,17 +198,17 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
{type === 'custom' && (
|
||||
<>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="customName">Variation name</label>
|
||||
<label htmlFor="customName">{t('categories.variations.name')}</label>
|
||||
<input
|
||||
id="customName"
|
||||
type="text"
|
||||
placeholder="e.g. Guarantee, Material..."
|
||||
placeholder={t('categories.variations.namePlaceholder')}
|
||||
value={customName}
|
||||
onChange={(e) => setCustomName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label>Values</label>
|
||||
<label>{t('categories.variations.values')}</label>
|
||||
<div className={styles.customValues}>
|
||||
{customValues.map((value, index) => (
|
||||
<div key={index} className={styles.customRow}>
|
||||
@@ -208,26 +217,28 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
valueInputRefs.current[index] = el
|
||||
}}
|
||||
type="text"
|
||||
placeholder={`Value ${index + 1}`}
|
||||
placeholder={t('categories.variations.valueN', { n: index + 1 })}
|
||||
value={value}
|
||||
onChange={(e) => updateCustomValue(index, e.target.value)}
|
||||
onKeyDown={(e) => handleValueKeyDown(index, e)}
|
||||
/>
|
||||
{customValues.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.removeValueBtn}
|
||||
onClick={() => removeCustomRow(index)}
|
||||
aria-label="Remove value"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
<Tooltip label={t('categories.variations.removeValue')}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.removeValueBtn}
|
||||
onClick={() => removeCustomRow(index)}
|
||||
aria-label={t('categories.variations.removeValue')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className={styles.addValueBtn} onClick={addCustomRow}>
|
||||
<Plus size={14} />
|
||||
Add value
|
||||
{t('categories.variations.addValue')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,10 +247,10 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button type="button" className={styles.cancelBtn} onClick={onClose}>
|
||||
Cancel
|
||||
{t('categories.modal.cancel')}
|
||||
</button>
|
||||
<button type="submit" className={styles.submitBtn} disabled={!canSubmit}>
|
||||
Add
|
||||
{t('categories.variations.submit')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { formatCellForDisplay, toE164CellNumber } from '../lib/cellNumber'
|
||||
import { updateCustomer, type BusinessCustomerListItem } from '../services/customerService'
|
||||
import formStyles from './AddCustomerModal.module.css'
|
||||
import modalStyles from './VariationsModal.module.css'
|
||||
|
||||
interface EditCustomerModalProps {
|
||||
@@ -21,10 +24,15 @@ export function EditCustomerModal({
|
||||
onClose,
|
||||
onSaved,
|
||||
}: EditCustomerModalProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const isFa = locale === 'fa'
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
const [firstName, setFirstName] = useState('')
|
||||
const [lastName, setLastName] = useState('')
|
||||
const [firstNameEn, setFirstNameEn] = useState('')
|
||||
const [lastNameEn, setLastNameEn] = useState('')
|
||||
const [cellNumber, setCellNumber] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@@ -36,6 +44,8 @@ export function EditCustomerModal({
|
||||
setClosing(false)
|
||||
setFirstName(customer.firstName ?? '')
|
||||
setLastName(customer.lastName ?? '')
|
||||
setFirstNameEn(customer.firstNameEn ?? '')
|
||||
setLastNameEn(customer.lastNameEn ?? '')
|
||||
setCellNumber(formatCellForDisplay(customer.cellNumber))
|
||||
setEmail(customer.email ?? '')
|
||||
setError('')
|
||||
@@ -71,12 +81,24 @@ export function EditCustomerModal({
|
||||
const normalizedCell = toE164CellNumber(cellNumber.trim())
|
||||
if (!normalizedCell) return
|
||||
|
||||
const trimmedFirstEn = firstNameEn.trim()
|
||||
const trimmedLastEn = lastNameEn.trim()
|
||||
if (
|
||||
(trimmedFirstEn && trimmedFirstEn.length < 2) ||
|
||||
(trimmedLastEn && trimmedLastEn.length < 2)
|
||||
) {
|
||||
setError(t('customers.addModal.errorNameEnLength'))
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
setError('')
|
||||
try {
|
||||
const updated = await updateCustomer(customer.id, {
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
firstNameEn: trimmedFirstEn,
|
||||
lastNameEn: trimmedLastEn,
|
||||
cellNumber: normalizedCell,
|
||||
email: email.trim(),
|
||||
})
|
||||
@@ -84,6 +106,8 @@ export function EditCustomerModal({
|
||||
...customer,
|
||||
firstName: updated.firstName,
|
||||
lastName: updated.lastName,
|
||||
firstNameEn: updated.firstNameEn,
|
||||
lastNameEn: updated.lastNameEn,
|
||||
cellNumber: updated.cellNumber,
|
||||
email: updated.email,
|
||||
label: updated.label,
|
||||
@@ -91,7 +115,7 @@ export function EditCustomerModal({
|
||||
})
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to update customer.')
|
||||
setError(err instanceof ApiError ? err.message : t('customers.editModal.error'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -103,20 +127,22 @@ export function EditCustomerModal({
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={`${modalStyles.modal} ${closing ? modalStyles.modalOut : modalStyles.modalIn}`}
|
||||
className={`${modalStyles.modal} ${formStyles.wideModal} ${closing ? modalStyles.modalOut : modalStyles.modalIn}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="edit-customer-title"
|
||||
lang={isFa ? 'fa' : 'en'}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<div className={modalStyles.header}>
|
||||
<div>
|
||||
<h3 id="edit-customer-title" className={modalStyles.title}>
|
||||
Edit customer
|
||||
{t('customers.edit')}
|
||||
</h3>
|
||||
<p className={modalStyles.subtitle}>Update customer contact details.</p>
|
||||
<p className={modalStyles.subtitle}>{t('customers.editModal.subtitle')}</p>
|
||||
</div>
|
||||
<button type="button" className={modalStyles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<button type="button" className={modalStyles.closeBtn} onClick={onClose} aria-label={t('common.close')}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -124,46 +150,76 @@ export function EditCustomerModal({
|
||||
<div className={modalStyles.body}>
|
||||
{error && <p className={modalStyles.errorText}>{error}</p>}
|
||||
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-first-name">First name</label>
|
||||
<input
|
||||
id="edit-customer-first-name"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-last-name">Last name</label>
|
||||
<input
|
||||
id="edit-customer-last-name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-cell">Cell number</label>
|
||||
<input
|
||||
id="edit-customer-cell"
|
||||
value={cellNumber}
|
||||
onChange={(e) => setCellNumber(e.target.value)}
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-email">Email (optional)</label>
|
||||
<input
|
||||
id="edit-customer-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<div className={formStyles.formGrid}>
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-first-name">{t('customers.addModal.firstNameFa')}</label>
|
||||
<input
|
||||
id="edit-customer-first-name"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
dir="rtl"
|
||||
lang="fa"
|
||||
className="faText"
|
||||
/>
|
||||
</div>
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-last-name">{t('customers.addModal.lastNameFa')}</label>
|
||||
<input
|
||||
id="edit-customer-last-name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
dir="rtl"
|
||||
lang="fa"
|
||||
className="faText"
|
||||
/>
|
||||
</div>
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-first-name-en">{t('customers.addModal.firstNameEn')}</label>
|
||||
<input
|
||||
id="edit-customer-first-name-en"
|
||||
value={firstNameEn}
|
||||
onChange={(e) => setFirstNameEn(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
dir="ltr"
|
||||
lang="en"
|
||||
/>
|
||||
</div>
|
||||
<div className={modalStyles.field}>
|
||||
<label htmlFor="edit-customer-last-name-en">{t('customers.addModal.lastNameEn')}</label>
|
||||
<input
|
||||
id="edit-customer-last-name-en"
|
||||
value={lastNameEn}
|
||||
onChange={(e) => setLastNameEn(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
dir="ltr"
|
||||
lang="en"
|
||||
/>
|
||||
</div>
|
||||
<div className={`${modalStyles.field} ${formStyles.formFull}`}>
|
||||
<label htmlFor="edit-customer-cell">{t('customers.addModal.cell')}</label>
|
||||
<input
|
||||
id="edit-customer-cell"
|
||||
value={cellNumber}
|
||||
onChange={(e) => setCellNumber(e.target.value)}
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
inputMode="tel"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
<div className={`${modalStyles.field} ${formStyles.formFull}`}>
|
||||
<label htmlFor="edit-customer-email">{t('customers.editModal.email')}</label>
|
||||
<input
|
||||
id="edit-customer-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.actions}>
|
||||
@@ -173,7 +229,7 @@ export function EditCustomerModal({
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t('customers.addModal.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -181,13 +237,12 @@ export function EditCustomerModal({
|
||||
onClick={() => void handleSubmit()}
|
||||
disabled={isSubmitting || !canSave}
|
||||
>
|
||||
Save
|
||||
{t('customers.editModal.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
,
|
||||
document.body,
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
grid-column: span 2;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -121,7 +121,6 @@
|
||||
.navItem.active {
|
||||
background: rgba(var(--primary-rgb) / 0.1);
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.navItem.active:hover {
|
||||
@@ -180,7 +179,6 @@
|
||||
.subNavActive {
|
||||
background: rgba(var(--primary-rgb) / 0.1);
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.subNavActive:hover {
|
||||
|
||||
@@ -209,9 +209,6 @@ export function Sidebar({ open = false }: SidebarProps) {
|
||||
children: [
|
||||
{ labelKey: 'nav.website.overview', to: '/website' },
|
||||
{ labelKey: 'nav.website.staticImages', to: '/website/static-images' },
|
||||
{ labelKey: 'nav.website.sliders', to: '/website/sliders' },
|
||||
{ labelKey: 'nav.website.specialCategories', to: '/website/special-categories' },
|
||||
{ labelKey: 'nav.website.specialBrands', to: '/website/special-brands' },
|
||||
{ labelKey: 'nav.website.specialItems', to: '/website/special-items' },
|
||||
{ labelKey: 'nav.website.contact', to: '/website/contact' },
|
||||
{ labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' },
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { BusinessMessageKey } from '../i18n/messages'
|
||||
import type { Variation, VariationType } from '../types/variation'
|
||||
import { COLOR_OPTIONS } from '../types/variation'
|
||||
import { createId } from '../utils/id'
|
||||
import { textLocaleAttrs } from '../utils/textLocale'
|
||||
import { AddVariationModal } from './AddVariationModal'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import styles from './VariationsModal.module.css'
|
||||
|
||||
interface VariationsModalProps {
|
||||
@@ -28,6 +33,9 @@ export function VariationsModal({
|
||||
onChange,
|
||||
onSave,
|
||||
}: VariationsModalProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const isFa = locale === 'fa'
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
@@ -74,9 +82,23 @@ export function VariationsModal({
|
||||
}
|
||||
|
||||
function typeLabel(type: VariationType) {
|
||||
if (type === 'color') return 'Color'
|
||||
if (type === 'size') return 'Size'
|
||||
return 'Custom'
|
||||
if (type === 'color') return t('categories.variations.type.color')
|
||||
if (type === 'size') return t('categories.variations.type.size')
|
||||
return t('categories.variations.type.custom')
|
||||
}
|
||||
|
||||
function variationTitle(variation: Variation) {
|
||||
if (variation.type === 'color' || variation.type === 'size') {
|
||||
return typeLabel(variation.type)
|
||||
}
|
||||
return variation.name
|
||||
}
|
||||
|
||||
function localizeValue(value: string) {
|
||||
if ((COLOR_OPTIONS as string[]).includes(value)) {
|
||||
return t(`categories.variations.color.${value}` as BusinessMessageKey)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
@@ -91,11 +113,13 @@ export function VariationsModal({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="variations-modal-title"
|
||||
lang={isFa ? 'fa' : 'en'}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<h3 id="variations-modal-title" className={styles.title}>
|
||||
Variations
|
||||
{t('categories.variations.title')}
|
||||
</h3>
|
||||
{categoryName && (
|
||||
<p
|
||||
@@ -107,7 +131,7 @@ export function VariationsModal({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button className={styles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<button className={styles.closeBtn} onClick={onClose} aria-label={t('common.close')}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -115,8 +139,10 @@ export function VariationsModal({
|
||||
<div className={styles.body}>
|
||||
<div className={styles.list}>
|
||||
{list.map((variation) => {
|
||||
const nameLocale = textLocaleAttrs(variation.name)
|
||||
const valuesText = variation.values.join(' · ') || 'No values'
|
||||
const title = variationTitle(variation)
|
||||
const nameLocale = textLocaleAttrs(title)
|
||||
const valuesText =
|
||||
variation.values.map(localizeValue).join(' · ') || t('categories.variations.noValues')
|
||||
const valuesLocale = textLocaleAttrs(valuesText)
|
||||
return (
|
||||
<div key={variation.id} className={styles.listItem}>
|
||||
@@ -126,7 +152,7 @@ export function VariationsModal({
|
||||
lang={nameLocale.lang}
|
||||
dir={nameLocale.dir}
|
||||
>
|
||||
{variation.name}
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
className={[styles.listItemValues, valuesLocale.className].filter(Boolean).join(' ')}
|
||||
@@ -138,34 +164,36 @@ export function VariationsModal({
|
||||
</div>
|
||||
<div className={styles.listItemEnd}>
|
||||
<span className={styles.listItemType}>{typeLabel(variation.type)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.removeRowBtn}
|
||||
onClick={() => handleRemove(variation.id)}
|
||||
aria-label={`Remove ${variation.name}`}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
<Tooltip label={t('categories.variations.remove')}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.removeRowBtn}
|
||||
onClick={() => handleRemove(variation.id)}
|
||||
aria-label={t('categories.variations.remove')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{!list.length && (
|
||||
<p className={styles.emptyText}>No variations yet. Add color, size, or custom options.</p>
|
||||
<p className={styles.emptyText}>{t('categories.variations.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="button" className={styles.addBtn} onClick={() => setAddOpen(true)}>
|
||||
<Plus size={18} />
|
||||
Add Variation
|
||||
{t('categories.variations.add')}
|
||||
</button>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button type="button" className={styles.cancelBtn} onClick={onClose} disabled={isSaving}>
|
||||
Cancel
|
||||
{t('categories.modal.cancel')}
|
||||
</button>
|
||||
<button type="button" className={styles.submitBtn} onClick={onSave} disabled={isSaving}>
|
||||
{isSaving ? 'Saving…' : 'Save variations'}
|
||||
{isSaving ? t('categories.variations.saving') : t('categories.variations.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user