Polish business dashboard profile, FA layout, and pagination.

Add a real user profile page with multi-address CRUD, localize business profile, fix RTL category tree and login/sidebar logo fallbacks, and keep pagination controls centered with aligned page meta.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-03 10:40:56 +03:30
co-authored by Cursor
parent 26b6b0a1b6
commit d8c9c0b017
20 changed files with 1031 additions and 78 deletions
+2
View File
@@ -18,6 +18,7 @@ import { MyProductsPage } from './pages/MyProductsPage'
import { ProductDetailsPage } from './pages/ProductDetailsPage'
import { ProductSettingsPage } from './pages/ProductSettingsPage'
import { BusinessProfilePage } from './pages/BusinessProfilePage'
import { ProfilePage } from './pages/ProfilePage'
import { StorePage } from './pages/StorePage'
import { StoreItemsPage } from './pages/StoreItemsPage'
import { StoreSpecialsPage } from './pages/StoreSpecialsPage'
@@ -66,6 +67,7 @@ function App() {
<Route element={<ProtectedRoute />}>
<Route element={<PageLayout />}>
<Route index element={<HomePage />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="business-profile" element={<BusinessProfilePage />} />
<Route path="products" element={<ProductsPage />} />
<Route path="products/categories" element={<CategoriesPage />} />
@@ -37,7 +37,7 @@ export function BlogCategoryRow({
: category.nameFa
return (
<div className={styles.row} style={{ marginLeft: depth * 8 }}>
<div className={styles.row} style={{ marginInlineStart: depth * 8 }}>
<div
className={`${styles.info} ${hasChildren ? styles.clickable : ''}`}
onClick={hasChildren ? onToggle : undefined}
+1 -1
View File
@@ -47,7 +47,7 @@ export function CategoryRow({
return (
<div
className={styles.row}
style={{ marginLeft: depth * 8 }}
style={{ marginInlineStart: depth * 8 }}
>
<div
className={`${styles.info} ${hasChildren ? styles.clickable : ''}`}
@@ -10,7 +10,7 @@
.childrenInner {
overflow: hidden;
margin-left: 20px;
padding-left: 20px;
border-left: 2px solid rgba(148, 163, 184, 0.3);
margin-inline-start: 20px;
padding-inline-start: 20px;
border-inline-start: 2px solid rgba(148, 163, 184, 0.3);
}
@@ -38,6 +38,26 @@
background: rgba(255, 255, 255, 0.75);
}
.brandEditBtn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 8px;
color: var(--text-secondary);
background: rgba(var(--primary-rgb) / 0.08);
border: 1px dashed rgba(var(--primary-rgb) / 0.35);
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.brandEditBtn:hover {
background: rgba(var(--primary-rgb) / 0.14);
color: var(--primary);
border-color: rgba(var(--primary-rgb) / 0.5);
}
.brandText {
display: flex;
flex-direction: column;
+17 -4
View File
@@ -13,6 +13,7 @@ import {
LogOut,
ChevronDown,
Building2,
Pencil,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
@@ -25,7 +26,7 @@ import {
} from '../lib/businessContext'
import { isAbortError } from '../lib/api'
import { getBusinessProfile } from '../services/businessProfileService'
import meshkeeLogo from '../assets/meshkee-logo.png'
import { Tooltip } from './Tooltip'
import styles from './Sidebar.module.css'
interface NavChild {
@@ -118,7 +119,6 @@ export function Sidebar() {
],
},
{ type: 'link', id: 'customers', icon: Users, labelKey: 'nav.customers', to: '/customers' },
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
{
type: 'group',
id: 'blog',
@@ -166,6 +166,7 @@ export function Sidebar() {
{ labelKey: 'nav.website.ePayment', to: '/website/e-payment' },
],
},
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
],
[],
)
@@ -218,11 +219,23 @@ export function Sidebar() {
return (
<aside className={styles.sidebar}>
<div className={styles.brand}>
{brandLogoUrl ? (
<img
src={brandLogoUrl ?? meshkeeLogo}
src={brandLogoUrl}
alt={brandName || t('app.storeFallback')}
className={`${styles.brandLogo} ${brandLogoUrl ? styles.brandLogoUploaded : ''}`}
className={`${styles.brandLogo} ${styles.brandLogoUploaded}`}
/>
) : (
<Tooltip label={t('sidebar.addLogo')}>
<NavLink
to="/business-profile"
className={styles.brandEditBtn}
aria-label={t('sidebar.addLogo')}
>
<Pencil size={16} />
</NavLink>
</Tooltip>
)}
<div className={styles.brandText}>
<span className={styles.brandName}>{brandName || fallbackBusinessName}</span>
<span className={styles.brandDomain}>{businessDomain}</span>
+157 -2
View File
@@ -12,6 +12,7 @@ const en = {
'nav.home': 'Home',
'nav.businessProfile': 'Business Profile',
'sidebar.addLogo': 'Add logo',
'nav.products': 'Products',
'nav.products.overview': 'Overview',
'nav.products.list': 'My Products',
@@ -62,6 +63,81 @@ const en = {
'header.setting': 'Setting',
'header.changePassword': 'Change password',
'profile.title': 'My Profile',
'profile.subtitle': 'Manage your personal information and contact details.',
'profile.section.general': 'General',
'profile.section.social': 'Social media',
'profile.firstName': 'First name (FA)',
'profile.lastName': 'Last name (FA)',
'profile.firstNameEn': 'First name (EN)',
'profile.lastNameEn': 'Last name (EN)',
'profile.mobile': 'Mobile number',
'profile.email': 'Email',
'profile.emailPlaceholder': 'you@example.com',
'profile.about': 'About yourself',
'profile.addresses': 'Addresses',
'profile.addresses.add': 'Add address',
'profile.addresses.loading': 'Loading addresses...',
'profile.addresses.remove': 'Remove address',
'profile.addresses.error.load': 'Unable to load addresses.',
'profile.addresses.error.incomplete': 'Complete province, city, and address for each row you start.',
'profile.province': 'Province',
'profile.city': 'City',
'profile.postalCode': 'Postal code',
'profile.address': 'Address',
'profile.landline': 'Landline',
'profile.selectProvince': 'Select province',
'profile.selectCity': 'Select city',
'profile.streetPlaceholder': 'Street address',
'profile.instagram': 'Instagram',
'profile.telegram': 'Telegram ID',
'profile.linkedin': 'LinkedIn',
'profile.save': 'Save changes',
'profile.saving': 'Saving...',
'profile.toast.success': 'Profile saved successfully.',
'profile.error.update': 'Unable to save profile.',
'bizProfile.subtitle': 'Manage your storefront identity, contact details, and social links.',
'bizProfile.loading': 'Loading business profile...',
'bizProfile.loadError': 'Unable to load business profile.',
'bizProfile.saveError': 'Unable to save business profile.',
'bizProfile.toast.saved': 'Business profile saved.',
'bizProfile.section.basic': 'Basic info',
'bizProfile.section.contact': 'Contact',
'bizProfile.section.social': 'Social media',
'bizProfile.logo': 'Logo',
'bizProfile.logo.upload': 'Upload logo',
'bizProfile.logo.hint': 'Transparent PNG recommended',
'bizProfile.logo.change': 'Change logo',
'bizProfile.categories': 'Activity categories',
'bizProfile.categories.placeholder': 'Select activity categories',
'bizProfile.nameFa': 'Name (FA)',
'bizProfile.nameEn': 'Name (EN)',
'bizProfile.email': 'Email address',
'bizProfile.about': 'About us',
'bizProfile.vision': 'Our vision',
'bizProfile.addresses': 'Addresses',
'bizProfile.addresses.add': 'Add address',
'bizProfile.province': 'Province',
'bizProfile.city': 'City',
'bizProfile.address': 'Address',
'bizProfile.postalCode': 'Postal code',
'bizProfile.landline': 'Landline',
'bizProfile.selectProvince': 'Select province',
'bizProfile.selectCity': 'Select city',
'bizProfile.streetPlaceholder': 'Street address',
'bizProfile.removeAddress': 'Remove address',
'bizProfile.phones': 'Phone numbers',
'bizProfile.phones.add': 'Add number',
'bizProfile.phones.type': 'Type',
'bizProfile.phones.number': 'Phone number',
'bizProfile.phones.cell': 'Cell number',
'bizProfile.phones.landline': 'Landline',
'bizProfile.phones.remove': 'Remove phone number',
'bizProfile.social.placeholder': '{name} link or ID',
'bizProfile.save': 'Save profile',
'bizProfile.saving': 'Saving...',
'bc.dashboard': 'Dashboard',
'bc.editProduct': 'Edit Product',
'bc.productDetails': 'Product Details',
@@ -193,7 +269,7 @@ const en = {
'products.form.save': 'Save Product',
'products.form.update': 'Update Product',
'categories.page.subtitle': 'Manage your digital product categories and subcategories.',
'categories.page.subtitle': 'Manage your product categories and subcategories.',
'categories.loading': 'Loading categories...',
'categories.empty': 'No categories yet. Use + or AI to add categories.',
'categories.add': 'Add Category',
@@ -873,6 +949,7 @@ const en = {
'title.signIn': 'Sign in',
'title.home': 'Home',
'title.profile': 'Profile',
'title.businessProfile': 'Business Profile',
'title.products': 'Products',
'title.myProducts': 'My Products',
@@ -993,6 +1070,7 @@ const fa: Record<MessageKey, string> = {
'nav.home': 'خانه',
'nav.businessProfile': 'پروفایل کسب‌وکار',
'sidebar.addLogo': 'افزودن لوگو',
'nav.products': 'محصولات',
'nav.products.overview': 'نمای کلی',
'nav.products.list': 'محصولات من',
@@ -1043,6 +1121,81 @@ const fa: Record<MessageKey, string> = {
'header.setting': 'تنظیمات',
'header.changePassword': 'تغییر رمز عبور',
'profile.title': 'پروفایل من',
'profile.subtitle': 'اطلاعات شخصی و راه‌های ارتباطی خود را مدیریت کنید.',
'profile.section.general': 'عمومی',
'profile.section.social': 'شبکه‌های اجتماعی',
'profile.firstName': 'نام (فارسی)',
'profile.lastName': 'نام خانوادگی (فارسی)',
'profile.firstNameEn': 'نام (انگلیسی)',
'profile.lastNameEn': 'نام خانوادگی (انگلیسی)',
'profile.mobile': 'شماره موبایل',
'profile.email': 'ایمیل',
'profile.emailPlaceholder': 'you@example.com',
'profile.about': 'درباره من',
'profile.addresses': 'آدرس‌ها',
'profile.addresses.add': 'افزودن آدرس',
'profile.addresses.loading': 'در حال بارگذاری آدرس‌ها...',
'profile.addresses.remove': 'حذف آدرس',
'profile.addresses.error.load': 'بارگذاری آدرس‌ها ممکن نشد.',
'profile.addresses.error.incomplete': 'برای هر ردیفی که شروع کرده‌اید، استان، شهر و آدرس را کامل کنید.',
'profile.province': 'استان',
'profile.city': 'شهر',
'profile.postalCode': 'کد پستی',
'profile.address': 'آدرس',
'profile.landline': 'تلفن ثابت',
'profile.selectProvince': 'انتخاب استان',
'profile.selectCity': 'انتخاب شهر',
'profile.streetPlaceholder': 'آدرس خیابان',
'profile.instagram': 'اینستاگرام',
'profile.telegram': 'آیدی تلگرام',
'profile.linkedin': 'لینکدین',
'profile.save': 'ذخیره تغییرات',
'profile.saving': 'در حال ذخیره...',
'profile.toast.success': 'پروفایل با موفقیت ذخیره شد.',
'profile.error.update': 'ذخیره پروفایل ممکن نشد.',
'bizProfile.subtitle': 'هویت فروشگاه، اطلاعات تماس و لینک‌های شبکه‌های اجتماعی را مدیریت کنید.',
'bizProfile.loading': 'در حال بارگذاری پروفایل کسب‌وکار...',
'bizProfile.loadError': 'بارگذاری پروفایل کسب‌وکار ممکن نشد.',
'bizProfile.saveError': 'ذخیره پروفایل کسب‌وکار ممکن نشد.',
'bizProfile.toast.saved': 'پروفایل کسب‌وکار ذخیره شد.',
'bizProfile.section.basic': 'اطلاعات پایه',
'bizProfile.section.contact': 'تماس',
'bizProfile.section.social': 'شبکه‌های اجتماعی',
'bizProfile.logo': 'لوگو',
'bizProfile.logo.upload': 'آپلود لوگو',
'bizProfile.logo.hint': 'PNG شفاف توصیه می‌شود',
'bizProfile.logo.change': 'تغییر لوگو',
'bizProfile.categories': 'دسته‌بندی‌های فعالیت',
'bizProfile.categories.placeholder': 'انتخاب دسته‌بندی‌های فعالیت',
'bizProfile.nameFa': 'نام (فارسی)',
'bizProfile.nameEn': 'نام (انگلیسی)',
'bizProfile.email': 'آدرس ایمیل',
'bizProfile.about': 'درباره ما',
'bizProfile.vision': 'چشم‌انداز ما',
'bizProfile.addresses': 'آدرس‌ها',
'bizProfile.addresses.add': 'افزودن آدرس',
'bizProfile.province': 'استان',
'bizProfile.city': 'شهر',
'bizProfile.address': 'آدرس',
'bizProfile.postalCode': 'کد پستی',
'bizProfile.landline': 'تلفن ثابت',
'bizProfile.selectProvince': 'انتخاب استان',
'bizProfile.selectCity': 'انتخاب شهر',
'bizProfile.streetPlaceholder': 'آدرس خیابان',
'bizProfile.removeAddress': 'حذف آدرس',
'bizProfile.phones': 'شماره‌های تماس',
'bizProfile.phones.add': 'افزودن شماره',
'bizProfile.phones.type': 'نوع',
'bizProfile.phones.number': 'شماره تلفن',
'bizProfile.phones.cell': 'شماره موبایل',
'bizProfile.phones.landline': 'تلفن ثابت',
'bizProfile.phones.remove': 'حذف شماره تماس',
'bizProfile.social.placeholder': 'لینک یا آیدی {name}',
'bizProfile.save': 'ذخیره پروفایل',
'bizProfile.saving': 'در حال ذخیره...',
'bc.dashboard': 'داشبورد',
'bc.editProduct': 'ویرایش محصول',
'bc.productDetails': 'جزئیات محصول',
@@ -1174,7 +1327,7 @@ const fa: Record<MessageKey, string> = {
'products.form.save': 'ذخیره محصول',
'products.form.update': 'به‌روزرسانی محصول',
'categories.page.subtitle': 'دسته‌بندی‌ها و زیردسته‌های محصولات دیجیتال خود را مدیریت کنید.',
'categories.page.subtitle': 'دسته‌بندی‌ها و زیردسته‌های محصولات خود را مدیریت کنید.',
'categories.loading': 'در حال بارگذاری دسته‌بندی‌ها...',
'categories.empty': 'هنوز دسته‌بندی‌ای نیست. با + یا هوش مصنوعی اضافه کنید.',
'categories.add': 'افزودن دسته‌بندی',
@@ -1853,6 +2006,7 @@ const fa: Record<MessageKey, string> = {
'title.signIn': 'ورود',
'title.home': 'خانه',
'title.profile': 'پروفایل',
'title.businessProfile': 'پروفایل کسب‌وکار',
'title.products': 'محصولات',
'title.myProducts': 'محصولات من',
@@ -2036,6 +2190,7 @@ export function getBusinessRouteTitleRules(locale: DashboardLocale): RouteTitleR
const t = (key: MessageKey) => translate(locale, key)
return [
{ match: '/login', labels: [t('title.signIn')] },
{ match: '/profile', labels: [t('title.profile')] },
{ match: '/business-profile', labels: [t('title.businessProfile')] },
{ match: '/products/categories', labels: [t('title.products'), t('title.categories')] },
{ match: '/products/brands', labels: [t('title.products'), t('title.brands')] },
+91 -46
View File
@@ -3,8 +3,10 @@ import { Plus, Trash2 } from 'lucide-react'
import {
AddressListEditor,
createEmptyAddressItem,
getLocationOptionLabel,
matchCityByName,
matchProvinceByName,
useLocale,
type AddressListItem,
} from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
@@ -12,6 +14,7 @@ import { ImageCropper } from '../components/ImageCropper'
import { MultiSelectDropdown } from '../components/MultiSelectDropdown'
import { RichTextEditor } from '../components/RichTextEditor'
import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
import { ApiError } from '../lib/api'
import { dispatchBusinessProfileUpdated } from '../lib/businessContext'
import { listBusinessActivityCategories } from '../services/businessActivityCategoryService'
@@ -44,12 +47,23 @@ const EMPTY_SOCIAL: BusinessSocialMedia = {
aparat: '',
}
const SOCIAL_FIELDS = [
['whatsapp', 'WhatsApp'],
['telegram', 'Telegram'],
['instagram', 'Instagram'],
['linkedin', 'LinkedIn'],
['youtube', 'YouTube'],
['aparat', 'Aparat'],
] as const
function createEmptyPhone(): BusinessPhoneNumber {
return { type: 'cell', number: '' }
}
export function BusinessProfilePage() {
const { showToast } = useToast()
const { locale } = useLocale()
const t = useT()
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState('')
@@ -76,12 +90,47 @@ export function BusinessProfilePage() {
[activityCategories],
)
const addressLabels = useMemo(
() => ({
title: t('bizProfile.addresses'),
addLabel: t('bizProfile.addresses.add'),
province: t('bizProfile.province'),
city: t('bizProfile.city'),
address: t('bizProfile.address'),
postalCode: t('bizProfile.postalCode'),
landline: t('bizProfile.landline'),
selectProvince: t('bizProfile.selectProvince'),
selectCity: t('bizProfile.selectCity'),
streetPlaceholder: t('bizProfile.streetPlaceholder'),
postalPlaceholder: t('bizProfile.postalCode'),
landlinePlaceholder: t('bizProfile.landline'),
removeAriaLabel: t('bizProfile.removeAddress'),
}),
[t],
)
useEffect(() => {
const controller = new AbortController()
void loadData(controller.signal)
return () => controller.abort()
}, [])
useEffect(() => {
setAddresses((prev) =>
prev.map((item) => {
if (!item.provinceSlug) return item
const province = provinces.find((p) => p.slug === item.provinceSlug)
const cities = citiesByProvince[item.provinceSlug] ?? []
const city = matchCityByName(item.city, cities)
return {
...item,
province: province ? getLocationOptionLabel(province, locale) : item.province,
city: city ? getLocationOptionLabel(city, locale) : item.city,
}
}),
)
}, [locale, provinces, citiesByProvince])
async function loadData(signal?: AbortSignal) {
setIsLoading(true)
setError('')
@@ -111,7 +160,9 @@ export function BusinessProfilePage() {
return {
id: item.id,
provinceSlug: province?.slug ?? '',
province: province?.nameEn ?? item.province,
province: province
? getLocationOptionLabel(province, locale)
: item.province,
city: item.city,
address: item.address,
postalCode: item.postalCode,
@@ -136,7 +187,7 @@ export function BusinessProfilePage() {
const city = matchCityByName(item.city, cities)
return {
...item,
city: city?.nameEn ?? item.city,
city: city ? getLocationOptionLabel(city, locale) : item.city,
}
}),
)
@@ -153,7 +204,7 @@ export function BusinessProfilePage() {
if (err instanceof ApiError) {
setError(err.message)
} else {
setError('Unable to load business profile.')
setError(t('bizProfile.loadError'))
}
} finally {
setIsLoading(false)
@@ -170,7 +221,7 @@ export function BusinessProfilePage() {
const province = provinces.find((item) => item.slug === provinceSlug)
updateAddress(index, {
provinceSlug,
province: province?.nameEn ?? '',
province: province ? getLocationOptionLabel(province, locale) : '',
city: '',
})
@@ -261,14 +312,14 @@ export function BusinessProfilePage() {
addresses: payloadAddresses,
})
showToast('Business profile saved.', 'success')
showToast(t('bizProfile.toast.saved'), 'success')
dispatchBusinessProfileUpdated()
await loadData()
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
} else {
setError('Unable to save business profile.')
setError(t('bizProfile.saveError'))
}
} finally {
setIsSaving(false)
@@ -278,7 +329,7 @@ export function BusinessProfilePage() {
if (isLoading) {
return (
<main className={pageStyles.content}>
<p className={styles.status}>Loading business profile...</p>
<p className={styles.status}>{t('bizProfile.loading')}</p>
</main>
)
}
@@ -289,10 +340,8 @@ export function BusinessProfilePage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>Business profile</h2>
<p className={pageStyles.pageSubtitle}>
Manage your storefront identity, contact details, and social links.
</p>
<h2 className={pageStyles.pageTitle}>{t('title.businessProfile')}</h2>
<p className={pageStyles.pageSubtitle}>{t('bizProfile.subtitle')}</p>
</div>
</div>
@@ -304,56 +353,60 @@ export function BusinessProfilePage() {
<form className={styles.form} onSubmit={(e) => void handleSubmit(e)}>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Basic info</h3>
<h3 className={styles.sectionTitle}>{t('bizProfile.section.basic')}</h3>
<div className={formStyles.formGrid}>
<div className={`${formStyles.field} ${styles.logoCol}`}>
<label>Logo</label>
<label>{t('bizProfile.logo')}</label>
<ImageCropper
value={logo}
onChange={setLogo}
outputFormat="png"
accept="image/png"
uploadLabel="Upload logo"
hint="Transparent PNG recommended"
changeLabel="Change logo"
uploadLabel={t('bizProfile.logo.upload')}
hint={t('bizProfile.logo.hint')}
changeLabel={t('bizProfile.logo.change')}
/>
</div>
<div className={styles.basicAside}>
<div className={`${formStyles.field} ${styles.fullRow}`}>
<label htmlFor="activity-categories">Activity categories</label>
<label htmlFor="activity-categories">{t('bizProfile.categories')}</label>
<MultiSelectDropdown
id="activity-categories"
options={categoryOptions}
value={categoryIds}
onChange={setCategoryIds}
placeholder="Select activity categories"
placeholder={t('bizProfile.categories.placeholder')}
searchable
disabled={categoryOptions.length === 0}
/>
</div>
<div className={`${formStyles.field} ${styles.halfRow}`}>
<label htmlFor="name-fa">Name (FA)</label>
<label htmlFor="name-fa">{t('bizProfile.nameFa')}</label>
<input
id="name-fa"
value={nameFa}
onChange={(e) => setNameFa(e.target.value)}
dir="rtl"
lang="fa"
className="faText"
/>
</div>
<div className={`${formStyles.field} ${styles.halfRow}`}>
<label htmlFor="name-en">Name (EN)</label>
<label htmlFor="name-en">{t('bizProfile.nameEn')}</label>
<input
id="name-en"
value={nameEn}
onChange={(e) => setNameEn(e.target.value)}
dir="ltr"
lang="en"
/>
</div>
<div className={`${formStyles.field} ${styles.halfRow}`}>
<label htmlFor="email">Email address</label>
<label htmlFor="email">{t('bizProfile.email')}</label>
<input
id="email"
type="email"
@@ -365,19 +418,19 @@ export function BusinessProfilePage() {
</div>
<div className={`${formStyles.field} ${formStyles.col12}`}>
<label>About us</label>
<label>{t('bizProfile.about')}</label>
<RichTextEditor value={about} onChange={setAbout} />
</div>
<div className={`${formStyles.field} ${formStyles.col12}`}>
<label>Our vision</label>
<label>{t('bizProfile.vision')}</label>
<RichTextEditor value={vision} onChange={setVision} />
</div>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Contact</h3>
<h3 className={styles.sectionTitle}>{t('bizProfile.section.contact')}</h3>
<div className={styles.duplicatorBlock}>
<AddressListEditor
@@ -388,23 +441,24 @@ export function BusinessProfilePage() {
onProvinceChange={handleProvinceChange}
onAdd={addAddress}
onRemove={removeAddress}
title="Addresses"
locale={locale}
labels={addressLabels}
/>
</div>
<div className={styles.duplicatorBlock}>
<div className={styles.sectionHeader}>
<h4 className={styles.subTitle}>Phone numbers</h4>
<h4 className={styles.subTitle}>{t('bizProfile.phones')}</h4>
<button type="button" className={styles.addBtn} onClick={addPhone}>
<Plus size={16} />
Add number
{t('bizProfile.phones.add')}
</button>
</div>
<div className={styles.phoneGrid}>
<div className={styles.gridHeader}>
<span>Type</span>
<span>Phone number</span>
<span>{t('bizProfile.phones.type')}</span>
<span>{t('bizProfile.phones.number')}</span>
<span />
</div>
@@ -419,22 +473,22 @@ export function BusinessProfilePage() {
})
}
>
<option value="cell">Cell number</option>
<option value="landline">Landline</option>
<option value="cell">{t('bizProfile.phones.cell')}</option>
<option value="landline">{t('bizProfile.phones.landline')}</option>
</select>
<input
className={styles.textField}
value={item.number}
onChange={(e) => updatePhone(index, { number: e.target.value })}
placeholder="Phone number"
placeholder={t('bizProfile.phones.number')}
/>
<button
type="button"
className={styles.removeBtn}
onClick={() => removePhone(index)}
aria-label="Remove phone number"
aria-label={t('bizProfile.phones.remove')}
>
<Trash2 size={15} />
</button>
@@ -445,25 +499,16 @@ export function BusinessProfilePage() {
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Social media</h3>
<h3 className={styles.sectionTitle}>{t('bizProfile.section.social')}</h3>
<div className={formStyles.formGrid}>
{(
[
['whatsapp', 'WhatsApp'],
['telegram', 'Telegram'],
['instagram', 'Instagram'],
['linkedin', 'LinkedIn'],
['youtube', 'YouTube'],
['aparat', 'Aparat'],
] as const
).map(([key, label]) => (
{SOCIAL_FIELDS.map(([key, label]) => (
<div key={key} className={`${formStyles.field} ${formStyles.col4}`}>
<label htmlFor={`social-${key}`}>{label}</label>
<input
id={`social-${key}`}
value={socialMedia[key]}
onChange={(e) => updateSocial(key, e.target.value)}
placeholder={`${label} link or ID`}
placeholder={t('bizProfile.social.placeholder', { name: label })}
/>
</div>
))}
@@ -472,7 +517,7 @@ export function BusinessProfilePage() {
<div className={styles.actions}>
<button type="submit" className={styles.saveBtn} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save profile'}
{isSaving ? t('bizProfile.saving') : t('bizProfile.save')}
</button>
</div>
</form>
@@ -197,17 +197,32 @@
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
min-height: 38px;
color: var(--text-secondary);
font-weight: 600;
font-weight: 500;
font-size: 12px;
box-sizing: border-box;
}
.pagination > div:first-child {
grid-column: 1;
justify-self: start;
display: inline-flex;
align-items: center;
height: 38px;
margin: 0;
padding: 0;
line-height: 38px;
font-family: var(--font-ui);
transform: translateY(2px);
}
.pagination > nav {
grid-column: 2;
justify-self: center;
height: 38px;
margin: 0;
padding: 0;
}
.pagerBtns {
@@ -181,6 +181,7 @@
font-size: 15px;
font-weight: 600;
color: white;
text-align: center;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius: var(--radius-sm);
box-shadow: 0 4px 14px rgba(var(--primary-rgb) / 0.35);
+1 -2
View File
@@ -15,7 +15,6 @@ import {
verifyOtp,
} from '../services/authService'
import { useT } from '../i18n/useT'
import meshkeeLogo from '../assets/meshkee-logo.png'
import styles from './LoginPage.module.css'
type AuthView = 'login' | 'signup' | 'forgot' | 'otp'
@@ -222,7 +221,7 @@ export function LoginPage() {
<div className={styles.page}>
<div className={styles.card}>
<div className={styles.brand}>
<img src={logoUrl || meshkeeLogo} alt="" className={styles.logo} />
{logoUrl ? <img src={logoUrl} alt="" className={styles.logo} /> : null}
<div className={styles.brandText}>
<span className={styles.businessName}>{businessName || businessDomain}</span>
<span className={styles.appName}>{t('app.poweredBy')}</span>
@@ -26,30 +26,35 @@
.pagination {
margin-top: 20px;
padding: 12px 4px;
padding: 0 4px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
min-height: 38px;
color: var(--text-secondary);
font-weight: 600;
font-weight: 500;
font-size: 12px;
direction: ltr;
}
.paginationMeta {
grid-column: 3;
justify-self: end;
display: flex;
grid-column: 1;
justify-self: start;
display: inline-flex;
align-items: center;
height: 38px;
line-height: 1;
margin: 0;
padding: 0;
line-height: 38px;
font-family: var(--font-ui);
transform: translateY(2px);
}
.pagination > nav {
grid-column: 2;
justify-self: center;
align-self: center;
height: 38px;
margin: 0;
padding: 0;
}
@media (min-width: 640px) {
+16 -1
View File
@@ -270,17 +270,32 @@
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
min-height: 38px;
color: var(--text-secondary);
font-weight: 600;
font-weight: 500;
font-size: 12px;
box-sizing: border-box;
}
.pagination > nav {
grid-column: 2;
justify-self: center;
height: 38px;
margin: 0;
padding: 0;
}
.pagination > div:first-child {
grid-column: 1;
justify-self: start;
display: inline-flex;
align-items: center;
height: 38px;
margin: 0;
padding: 0;
line-height: 38px;
font-family: var(--font-ui);
transform: translateY(2px);
}
.pagerBtns {
@@ -0,0 +1,150 @@
.form {
display: flex;
flex-direction: column;
gap: 24px;
}
.alertError {
padding: 12px 14px;
border-radius: var(--radius-sm);
font-size: 13px;
line-height: 1.5;
background: rgba(239, 68, 68, 0.08);
border: 1px solid rgba(239, 68, 68, 0.25);
color: #b91c1c;
}
.section {
padding: 28px;
background: var(--glass-bg);
backdrop-filter: blur(var(--blur-glass));
-webkit-backdrop-filter: blur(var(--blur-glass));
border: 1px solid var(--glass-border);
border-radius: var(--radius);
box-shadow: var(--glass-shadow);
}
.sectionTitle {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 14px;
}
.col3 {
grid-column: span 6;
}
.col4 {
grid-column: span 6;
}
.fullWidth {
grid-column: span 12;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.field label {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
}
.field input,
.field textarea {
min-height: var(--field-height);
padding: var(--field-padding-y) var(--field-padding-x);
font-size: var(--field-font-size);
font-family: var(--font-ui);
line-height: 1.4;
color: var(--text-primary);
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: var(--radius-sm);
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.field textarea {
min-height: 96px;
resize: vertical;
}
.field input:focus,
.field textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12);
}
.field input:disabled,
.field input.readOnly {
opacity: 0.7;
cursor: not-allowed;
background: rgba(148, 163, 184, 0.12);
}
.actions {
display: flex;
justify-content: flex-end;
}
.saveBtn {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: var(--field-height);
padding: var(--field-padding-y) 18px;
font-size: var(--field-font-size);
font-weight: 600;
color: white;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius: var(--radius-sm);
box-shadow: 0 4px 14px rgba(var(--primary-rgb) / 0.35);
transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
}
.saveBtn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(var(--primary-rgb) / 0.4);
}
.saveBtn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
@media (min-width: 1280px) {
.col3 {
grid-column: span 3;
}
.col4 {
grid-column: span 4;
}
}
@media (max-width: 768px) {
.section {
padding: 20px;
}
.col3,
.col4,
.fullWidth {
grid-column: span 12;
}
}
+445
View File
@@ -0,0 +1,445 @@
import { useEffect, useMemo, useState } from 'react'
import { Save } from 'lucide-react'
import {
AddressListEditor,
createEmptyAddressItem,
getLocationOptionLabel,
matchCityByName,
matchProvinceByName,
useLocale,
type AddressListItem,
} from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import pageStyles from '../components/PageContent.module.css'
import { useAuth } from '../context/AuthContext'
import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
import { ApiError } from '../lib/api'
import { formatCellForDisplay } from '../lib/cellNumber'
import {
createAddress,
listAddresses,
removeAddress,
updateAddress,
} from '../services/addressService'
import { updateProfile } from '../services/authService'
import {
listCitiesByProvinceSlug,
listIranProvinces,
type CityOption,
} from '../services/citiesService'
import styles from './ProfilePage.module.css'
type AddressDraft = AddressListItem
function isAddressFilled(item: AddressDraft) {
return Boolean(
item.province.trim() && item.city.trim() && item.address.trim(),
)
}
export function ProfilePage() {
const { user, setUser } = useAuth()
const { showToast } = useToast()
const { locale } = useLocale()
const t = useT()
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [firstNameEn, setFirstNameEn] = useState('')
const [lastNameEn, setLastNameEn] = useState('')
const [email, setEmail] = useState('')
const [about, setAbout] = useState('')
const [instagram, setInstagram] = useState('')
const [telegramId, setTelegramId] = useState('')
const [linkedin, setLinkedin] = useState('')
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState('')
const [provinces, setProvinces] = useState<CityOption[]>([])
const [citiesByProvince, setCitiesByProvince] = useState<Record<string, CityOption[]>>({})
const [addresses, setAddresses] = useState<AddressDraft[]>([createEmptyAddressItem()])
const [initialAddressIds, setInitialAddressIds] = useState<string[]>([])
const [addressesLoading, setAddressesLoading] = useState(true)
const addressLabels = useMemo(
() => ({
title: t('profile.addresses'),
addLabel: t('profile.addresses.add'),
loading: t('profile.addresses.loading'),
province: t('profile.province'),
city: t('profile.city'),
address: t('profile.address'),
postalCode: t('profile.postalCode'),
landline: t('profile.landline'),
selectProvince: t('profile.selectProvince'),
selectCity: t('profile.selectCity'),
streetPlaceholder: t('profile.streetPlaceholder'),
postalPlaceholder: t('profile.postalCode'),
landlinePlaceholder: t('profile.landline'),
removeAriaLabel: t('profile.addresses.remove'),
}),
[t],
)
useEffect(() => {
if (!user) return
setFirstName(user.firstName ?? '')
setLastName(user.lastName ?? '')
setFirstNameEn(user.firstNameEn ?? '')
setLastNameEn(user.lastNameEn ?? '')
setEmail(user.email ?? '')
setAbout(user.profile.about ?? '')
setInstagram(user.profile.instagram ?? '')
setTelegramId(user.profile.telegramId ?? '')
setLinkedin(user.profile.linkedin ?? '')
}, [user])
useEffect(() => {
const controller = new AbortController()
void loadAddresses(controller.signal)
return () => controller.abort()
}, [])
useEffect(() => {
setAddresses((prev) =>
prev.map((item) => {
if (!item.provinceSlug) return item
const province = provinces.find((p) => p.slug === item.provinceSlug)
const cities = citiesByProvince[item.provinceSlug] ?? []
const city = matchCityByName(item.city, cities)
return {
...item,
province: province ? getLocationOptionLabel(province, locale) : item.province,
city: city ? getLocationOptionLabel(city, locale) : item.city,
}
}),
)
}, [locale, provinces, citiesByProvince])
async function loadAddresses(signal?: AbortSignal) {
setAddressesLoading(true)
setError('')
try {
const [provinceItems, addressData] = await Promise.all([
listIranProvinces(signal),
listAddresses(signal),
])
setProvinces(provinceItems)
const nextAddresses =
addressData.items.length > 0
? addressData.items.map((item) => {
const province = matchProvinceByName(item.province, provinceItems)
return {
id: item.id,
provinceSlug: province?.slug ?? '',
province: province
? getLocationOptionLabel(province, locale)
: item.province,
city: item.city,
address: item.address,
postalCode: item.postalCode ?? '',
landline: item.landline ?? '',
}
})
: [createEmptyAddressItem()]
const slugs = [...new Set(nextAddresses.map((item) => item.provinceSlug).filter(Boolean))]
const cityGroups = await Promise.all(
slugs.map(async (slug) => ({
slug,
cities: await listCitiesByProvinceSlug(slug, signal),
})),
)
const citiesMap = Object.fromEntries(cityGroups.map((group) => [group.slug, group.cities]))
const localized = nextAddresses.map((item) => {
if (!item.provinceSlug) return item
const cities = citiesMap[item.provinceSlug] ?? []
const city = matchCityByName(item.city, cities)
return {
...item,
city: city ? getLocationOptionLabel(city, locale) : item.city,
}
})
setCitiesByProvince(citiesMap)
setAddresses(localized)
setInitialAddressIds(
addressData.items.map((item) => item.id).filter(Boolean),
)
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
setError(err instanceof ApiError ? err.message : t('profile.addresses.error.load'))
} finally {
setAddressesLoading(false)
}
}
function updateAddressDraft(index: number, patch: Partial<AddressDraft>) {
setAddresses((prev) =>
prev.map((item, i) => (i === index ? { ...item, ...patch } : item)),
)
}
async function handleProvinceChange(index: number, provinceSlug: string) {
const province = provinces.find((item) => item.slug === provinceSlug)
updateAddressDraft(index, {
provinceSlug,
province: province ? getLocationOptionLabel(province, locale) : '',
city: '',
})
if (provinceSlug && !citiesByProvince[provinceSlug]) {
const cities = await listCitiesByProvinceSlug(provinceSlug)
setCitiesByProvince((prev) => ({ ...prev, [provinceSlug]: cities }))
}
}
function addAddressRow() {
setAddresses((prev) => [...prev, createEmptyAddressItem()])
}
function removeAddressRow(index: number) {
setAddresses((prev) =>
prev.length === 1 ? [createEmptyAddressItem()] : prev.filter((_, i) => i !== index),
)
}
async function syncAddresses() {
const filled = addresses.filter(isAddressFilled)
const keptIds = new Set(filled.map((item) => item.id).filter(Boolean) as string[])
for (const id of initialAddressIds) {
if (!keptIds.has(id)) {
await removeAddress(id)
}
}
for (const item of filled) {
const payload = {
province: item.province.trim(),
city: item.city.trim(),
address: item.address.trim(),
postalCode: item.postalCode.trim() || undefined,
landline: item.landline.trim() || undefined,
}
if (item.id) {
await updateAddress(item.id, payload)
} else {
await createAddress(payload)
}
}
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
setIsSaving(true)
const incomplete = addresses.some(
(item) =>
(item.province.trim() ||
item.city.trim() ||
item.address.trim() ||
item.postalCode.trim() ||
item.landline.trim()) &&
!isAddressFilled(item),
)
if (incomplete) {
setError(t('profile.addresses.error.incomplete'))
setIsSaving(false)
return
}
try {
const result = await updateProfile({
firstName: firstName.trim(),
lastName: lastName.trim(),
firstNameEn: firstNameEn.trim(),
lastNameEn: lastNameEn.trim(),
email: email.trim() || undefined,
about: about.trim(),
instagram: instagram.trim(),
telegramId: telegramId.trim(),
linkedin: linkedin.trim(),
})
await syncAddresses()
await loadAddresses()
setUser(result.user)
showToast(t('profile.toast.success'), 'success')
} catch (err) {
setError(err instanceof ApiError ? err.message : t('profile.error.update'))
} finally {
setIsSaving(false)
}
}
return (
<main className={pageStyles.content}>
<Breadcrumbs items={[{ label: 'Dashboard', href: '/' }, { label: 'Profile' }]} />
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('profile.title')}</h2>
<p className={pageStyles.pageSubtitle}>{t('profile.subtitle')}</p>
</div>
</div>
<form className={styles.form} onSubmit={(e) => void handleSubmit(e)}>
{error && (
<div className={styles.alertError} role="alert">
{error}
</div>
)}
<section className={styles.section}>
<h3 className={styles.sectionTitle}>{t('profile.section.general')}</h3>
<div className={styles.grid}>
<div className={`${styles.field} ${styles.col3}`}>
<label htmlFor="profile-first">{t('profile.firstName')}</label>
<input
id="profile-first"
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
dir="rtl"
lang="fa"
className="faText"
/>
</div>
<div className={`${styles.field} ${styles.col3}`}>
<label htmlFor="profile-last">{t('profile.lastName')}</label>
<input
id="profile-last"
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
dir="rtl"
lang="fa"
className="faText"
/>
</div>
<div className={`${styles.field} ${styles.col3}`}>
<label htmlFor="profile-first-en">{t('profile.firstNameEn')}</label>
<input
id="profile-first-en"
type="text"
value={firstNameEn}
onChange={(e) => setFirstNameEn(e.target.value)}
dir="ltr"
lang="en"
/>
</div>
<div className={`${styles.field} ${styles.col3}`}>
<label htmlFor="profile-last-en">{t('profile.lastNameEn')}</label>
<input
id="profile-last-en"
type="text"
value={lastNameEn}
onChange={(e) => setLastNameEn(e.target.value)}
dir="ltr"
lang="en"
/>
</div>
<div className={`${styles.field} ${styles.col3}`}>
<label htmlFor="profile-cell">{t('profile.mobile')}</label>
<input
id="profile-cell"
type="tel"
value={user ? formatCellForDisplay(user.cellNumber) : ''}
readOnly
className={styles.readOnly}
/>
</div>
<div className={`${styles.field} ${styles.col3}`}>
<label htmlFor="profile-email">{t('profile.email')}</label>
<input
id="profile-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('profile.emailPlaceholder')}
/>
</div>
<div className={`${styles.field} ${styles.fullWidth}`}>
<label htmlFor="profile-about">{t('profile.about')}</label>
<textarea
id="profile-about"
value={about}
onChange={(e) => setAbout(e.target.value)}
rows={4}
/>
</div>
</div>
</section>
<section className={styles.section}>
<AddressListEditor
addresses={addresses}
provinces={provinces}
citiesByProvince={citiesByProvince}
onAddressChange={updateAddressDraft}
onProvinceChange={handleProvinceChange}
onAdd={addAddressRow}
onRemove={removeAddressRow}
loading={addressesLoading}
locale={locale}
labels={addressLabels}
/>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>{t('profile.section.social')}</h3>
<div className={styles.grid}>
<div className={`${styles.field} ${styles.col4}`}>
<label htmlFor="profile-instagram">{t('profile.instagram')}</label>
<input
id="profile-instagram"
type="text"
value={instagram}
onChange={(e) => setInstagram(e.target.value)}
placeholder="@username"
/>
</div>
<div className={`${styles.field} ${styles.col4}`}>
<label htmlFor="profile-telegram">{t('profile.telegram')}</label>
<input
id="profile-telegram"
type="text"
value={telegramId}
onChange={(e) => setTelegramId(e.target.value)}
placeholder="@username"
/>
</div>
<div className={`${styles.field} ${styles.col4}`}>
<label htmlFor="profile-linkedin">{t('profile.linkedin')}</label>
<input
id="profile-linkedin"
type="url"
value={linkedin}
onChange={(e) => setLinkedin(e.target.value)}
placeholder="https://linkedin.com/in/username"
/>
</div>
</div>
</section>
<div className={styles.actions}>
<button type="submit" className={styles.saveBtn} disabled={isSaving || addressesLoading}>
<Save size={18} />
{isSaving ? t('profile.saving') : t('profile.save')}
</button>
</div>
</form>
</main>
)
}
@@ -227,8 +227,6 @@ export function StoreSettingsPage() {
)}
<section className={sharedStyles.panel}>
<h3 className={sharedStyles.sectionTitle}>{t('storeSettings.sales')}</h3>
{isLoading ? (
<p className={sharedStyles.status}>{t('productSettings.loading')}</p>
) : (
@@ -91,13 +91,32 @@
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
min-height: 38px;
color: var(--text-secondary);
font-weight: 600;
font-weight: 500;
font-size: 12px;
box-sizing: border-box;
}
.pagination > nav {
grid-column: 2;
justify-self: center;
height: 38px;
margin: 0;
padding: 0;
}
.pagination > div:first-child {
grid-column: 1;
justify-self: start;
display: inline-flex;
align-items: center;
height: 38px;
margin: 0;
padding: 0;
line-height: 38px;
font-family: var(--font-ui);
transform: translateY(2px);
}
.pagerBtns {
@@ -0,0 +1,49 @@
import { apiRequest } from '../lib/api'
export interface UserAddress {
id: string
label: string | null
province: string
city: string
address: string
postalCode: string | null
landline: string | null
createdAt: string
updatedAt: string
}
export interface UserAddressInput {
label?: string
province: string
city: string
address: string
postalCode?: string
landline?: string
}
export async function listAddresses(signal?: AbortSignal) {
return apiRequest<{ items: UserAddress[] }>('/auth/addresses', { auth: true, signal })
}
export async function createAddress(input: UserAddressInput) {
return apiRequest<{ address: UserAddress }>('/auth/addresses', {
method: 'POST',
auth: true,
body: input,
})
}
export async function updateAddress(addressId: string, input: UserAddressInput) {
return apiRequest<{ address: UserAddress }>(`/auth/addresses/${addressId}`, {
method: 'PATCH',
auth: true,
body: input,
})
}
export async function removeAddress(addressId: string) {
return apiRequest<{ message: string }>(`/auth/addresses/${addressId}`, {
method: 'DELETE',
auth: true,
})
}
+19
View File
@@ -1,6 +1,7 @@
import { apiRequest, setTokens, clearTokens } from '../lib/api'
import { clearActiveBusiness } from '../lib/businessContext'
import type {
AuthUser,
LoginResponse,
MeResponse,
OtpSendResponse,
@@ -61,6 +62,24 @@ export async function changePassword(currentPassword: string, newPassword: strin
})
}
export async function updateProfile(payload: {
firstName?: string
lastName?: string
firstNameEn?: string
lastNameEn?: string
email?: string | null
about?: string
instagram?: string
telegramId?: string
linkedin?: string
}) {
return apiRequest<{ message: string; user: AuthUser }>('/auth/profile', {
method: 'PATCH',
auth: true,
body: payload,
})
}
export function logout() {
clearTokens()
clearActiveBusiness()
@@ -8,9 +8,12 @@
}
.paginationInline {
margin-top: 0;
padding-top: 0;
justify-content: flex-end;
margin: 0;
padding: 0;
min-height: 38px;
height: 38px;
justify-content: center;
align-items: center;
}
.navBtn {