|
@@ -74,7 +79,13 @@ export function OrderRow({ order, processSteps, onViewItems }: OrderRowProps) {
stepColor(processSteps, processStepId, order.processStepColor),
)}
>
- {stepLabel(processSteps, processStepId, order.processStepLabel)}
+ {stepLabel(
+ processSteps,
+ processStepId,
+ order.processStepLabel,
+ order.processStepLabelFa,
+ locale,
+ )}
|
@@ -88,8 +99,8 @@ export function OrderRow({ order, processSteps, onViewItems }: OrderRowProps) {
type="button"
className={styles.actionBtn}
onClick={() => onViewItems(order)}
- aria-label="View items"
- title="View items"
+ aria-label={t('orders.viewItems')}
+ title={t('orders.viewItems')}
>
diff --git a/apps/customer/src/components/PageLayout.module.css b/apps/customer/src/components/PageLayout.module.css
index 99e700e..a9cc624 100644
--- a/apps/customer/src/components/PageLayout.module.css
+++ b/apps/customer/src/components/PageLayout.module.css
@@ -4,7 +4,7 @@
}
.main {
- margin-left: var(--sidebar-width);
+ margin-inline-start: var(--sidebar-width);
min-height: 100vh;
position: relative;
z-index: 1;
@@ -12,6 +12,6 @@
@media (max-width: 768px) {
.main {
- margin-left: 0;
+ margin-inline-start: 0;
}
}
diff --git a/apps/customer/src/components/Sidebar.module.css b/apps/customer/src/components/Sidebar.module.css
index b2b1157..9f55dbb 100644
--- a/apps/customer/src/components/Sidebar.module.css
+++ b/apps/customer/src/components/Sidebar.module.css
@@ -1,7 +1,7 @@
.sidebar {
position: fixed;
top: 0;
- left: 0;
+ inset-inline-start: 0;
width: var(--sidebar-width);
height: 100vh;
display: flex;
@@ -10,7 +10,7 @@
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border-right: 1px solid var(--glass-border);
+ border-inline-end: 1px solid var(--glass-border);
z-index: 100;
}
@@ -61,17 +61,18 @@
}
.brandDomain {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-primary);
- line-height: 1.2;
-}
-
-.brandName {
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
margin-top: 2px;
+ line-height: 1.2;
+}
+
+.brandName {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text-primary);
+ line-height: 1.2;
}
.nav {
@@ -80,7 +81,7 @@
flex-direction: column;
gap: 2px;
overflow-y: auto;
- padding-right: 2px;
+ padding-inline-end: 2px;
}
.navGroup {
@@ -99,7 +100,7 @@
color: var(--text-secondary);
transition: all 0.2s ease;
width: 100%;
- text-align: left;
+ text-align: start;
}
.navItem:hover {
@@ -134,9 +135,10 @@
display: flex;
flex-direction: column;
gap: 2px;
- margin: 2px 0 4px 12px;
- padding-left: 12px;
- border-left: 2px solid rgba(148, 163, 184, 0.2);
+ margin-block: 2px 4px;
+ margin-inline: 12px 0;
+ padding-inline-start: 12px;
+ border-inline-start: 2px solid rgba(148, 163, 184, 0.2);
}
.subNavItem {
diff --git a/apps/customer/src/components/Sidebar.tsx b/apps/customer/src/components/Sidebar.tsx
index 400f6aa..ddbc2af 100644
--- a/apps/customer/src/components/Sidebar.tsx
+++ b/apps/customer/src/components/Sidebar.tsx
@@ -1,32 +1,35 @@
import { useEffect, useState } from 'react'
import { NavLink, useNavigate } from 'react-router-dom'
-import { Home, User, MapPin, ShoppingBag, ShoppingCart, Heart, HelpCircle, LogOut } from 'lucide-react'
+import { Home, User, MapPin, ShoppingBag, Heart, HelpCircle, LogOut } from 'lucide-react'
+import { useLocale } from '@meshkee/dashboard-ui'
import { useAuth } from '../context/AuthContext'
+import { useT } from '../i18n/useT'
import { getActiveBusinessDomain } from '../lib/businessContext'
import { isAbortError } from '../lib/api'
import { getWebsiteBusinessInfo } from '../services/websiteService'
import meshkeeLogo from '../assets/meshkee-logo.png'
import styles from './Sidebar.module.css'
-const navItems = [
- { icon: Home, label: 'Home', to: '/' },
- { icon: ShoppingCart, label: 'Shopping Cart', to: '/checkout' },
- { icon: User, label: 'My Profile', to: '/profile' },
- { icon: MapPin, label: 'My Addresses', to: '/addresses' },
- { icon: ShoppingBag, label: 'My Orders', to: '/orders' },
- { icon: Heart, label: 'My Favorites', to: '/favorites' },
-]
-
export function Sidebar() {
const navigate = useNavigate()
const { user, logout } = useAuth()
+ const { locale } = useLocale()
+ const t = useT()
const [brandName, setBrandName] = useState('')
const [logoUrl, setLogoUrl] = useState(null)
const businessDomain = getActiveBusinessDomain()
- const fallbackBusinessName = user?.customerBusinesses[0]?.name ?? 'Store'
+ const fallbackBusinessName = user?.customerBusinesses[0]?.name ?? t('app.storeFallback')
const displayName = brandName || fallbackBusinessName
+ const navItems = [
+ { icon: Home, label: t('nav.home'), to: '/' },
+ { icon: User, label: t('nav.profile'), to: '/profile' },
+ { icon: MapPin, label: t('nav.addresses'), to: '/addresses' },
+ { icon: ShoppingBag, label: t('nav.orders'), to: '/orders' },
+ { icon: Heart, label: t('nav.favorites'), to: '/favorites' },
+ ]
+
useEffect(() => {
const controller = new AbortController()
@@ -34,7 +37,11 @@ export function Sidebar() {
try {
const info = await getWebsiteBusinessInfo(businessDomain, controller.signal)
if (controller.signal.aborted) return
- setBrandName(info.nameFa?.trim() || info.name.trim() || fallbackBusinessName)
+ const localized =
+ locale === 'fa'
+ ? info.nameFa?.trim() || info.name.trim()
+ : info.name.trim() || info.nameFa?.trim()
+ setBrandName(localized || fallbackBusinessName)
setLogoUrl(info.logoUrl?.trim() || null)
} catch (err) {
if (isAbortError(err)) return
@@ -48,7 +55,7 @@ export function Sidebar() {
return () => {
controller.abort()
}
- }, [businessDomain, fallbackBusinessName])
+ }, [businessDomain, fallbackBusinessName, locale])
return (
diff --git a/apps/customer/src/components/checkout/CheckoutBackButton.module.css b/apps/customer/src/components/checkout/CheckoutBackButton.module.css
index b78af3c..95973b6 100644
--- a/apps/customer/src/components/checkout/CheckoutBackButton.module.css
+++ b/apps/customer/src/components/checkout/CheckoutBackButton.module.css
@@ -17,3 +17,7 @@
.backBtn span {
line-height: 1.4;
}
+
+:global([dir='rtl']) .backBtn svg {
+ transform: scaleX(-1);
+}
diff --git a/apps/customer/src/context/TenantBrandingContext.tsx b/apps/customer/src/context/TenantBrandingContext.tsx
index 1a03a4f..9a90078 100644
--- a/apps/customer/src/context/TenantBrandingContext.tsx
+++ b/apps/customer/src/context/TenantBrandingContext.tsx
@@ -3,10 +3,12 @@ import {
useContext,
useEffect,
useMemo,
+ useRef,
useState,
type ReactNode,
} from 'react'
import { applyDocumentFavicon } from '@meshkee/dashboard-core'
+import { useLocale } from '@meshkee/dashboard-ui'
import { isAbortError } from '../lib/api'
import { getTenantDomain } from '../lib/config'
import { getWebsiteBusinessInfo } from '../services/websiteService'
@@ -31,9 +33,11 @@ function pickBusinessName(
}
export function TenantBrandingProvider({ children }: { children: ReactNode }) {
+ const { setLocale } = useLocale()
const [businessName, setBusinessName] = useState('')
const [logoUrl, setLogoUrl] = useState(null)
const [faviconUrl, setFaviconUrl] = useState(null)
+ const defaultLocaleAppliedRef = useRef(false)
const domain = getTenantDomain()
useEffect(() => {
@@ -68,6 +72,15 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
setLogoUrl(nextLogo)
setFaviconUrl(nextFavicon)
applyDocumentFavicon(nextFavicon)
+
+ if (!defaultLocaleAppliedRef.current) {
+ defaultLocaleAppliedRef.current = true
+ if (tenant.defaultLocale === 'en' || tenant.defaultLocale === 'fa') {
+ setLocale(tenant.defaultLocale)
+ } else {
+ setLocale('fa')
+ }
+ }
} catch (err) {
if (isAbortError(err) || controller.signal.aborted) return
setBusinessName(domain)
@@ -82,7 +95,7 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
return () => {
controller.abort()
}
- }, [domain])
+ }, [domain, setLocale])
const value = useMemo(
() => ({ businessName, logoUrl, faviconUrl }),
diff --git a/apps/customer/src/i18n/messages.ts b/apps/customer/src/i18n/messages.ts
new file mode 100644
index 0000000..5a1bf4a
--- /dev/null
+++ b/apps/customer/src/i18n/messages.ts
@@ -0,0 +1,506 @@
+import type { DashboardLocale } from '@meshkee/dashboard-core'
+
+const en = {
+ 'app.dashboardName': 'Customer Dashboard',
+ 'app.role.customer': 'Customer',
+ 'app.storeFallback': 'Store',
+ 'app.poweredBy': 'powered by Meshkee.app',
+
+ 'nav.home': 'Home',
+ 'nav.cart': 'Shopping Cart',
+ 'nav.profile': 'My Profile',
+ 'nav.addresses': 'My Addresses',
+ 'nav.orders': 'My Orders',
+ 'nav.favorites': 'My Favorites',
+ 'nav.help': 'Help Center',
+ 'nav.logout': 'Logout',
+
+ 'header.toggleMenu': 'Toggle menu',
+ 'header.messages': 'Messages',
+ 'header.notifications': 'Notifications',
+ 'header.changePassword': 'Change password',
+ 'header.myProfile': 'My Profile',
+
+ 'home.welcome': 'Welcome, dear {name}.',
+ 'home.welcomeFallback': 'there',
+ 'home.subtitle': 'Manage your profile, addresses, orders, and favorites in one place.',
+ 'home.card.profile.title': 'My Profile',
+ 'home.card.profile.desc': 'View and update your personal information and contact details.',
+ 'home.card.profile.link': 'View profile',
+ 'home.card.addresses.title': 'My Addresses',
+ 'home.card.addresses.desc': 'Manage your shipping addresses for checkout and deliveries.',
+ 'home.card.addresses.link': 'View addresses',
+ 'home.card.orders.title': 'My Orders',
+ 'home.card.orders.desc': 'Track your orders, view order history and order details.',
+ 'home.card.orders.link': 'View orders',
+ 'home.card.favorites.title': 'My Favorites',
+ 'home.card.favorites.desc': 'Browse and manage your saved favorite products.',
+ 'home.card.favorites.link': 'View favorites',
+
+ 'profile.title': 'My Profile',
+ 'profile.subtitle': 'Update your personal information and contact details.',
+ 'profile.section.account': 'Account',
+ 'profile.section.about': 'About',
+ 'profile.section.social': 'Social',
+ 'profile.mobile': 'Mobile number',
+ 'profile.email': 'Email',
+ 'profile.firstName': 'First name (FA)',
+ 'profile.lastName': 'Last name (FA)',
+ 'profile.firstNameEn': 'First name (EN)',
+ 'profile.lastNameEn': 'Last name (EN)',
+ 'profile.landline': 'Landline',
+ 'profile.backupPhone': 'Backup phone number',
+ 'profile.about': 'About',
+ 'profile.instagram': 'Instagram',
+ 'profile.telegram': 'Telegram',
+ 'profile.linkedin': 'LinkedIn',
+ 'profile.emailPlaceholder': 'you@example.com',
+ 'profile.save': 'Save changes',
+ 'profile.saving': 'Saving...',
+ 'profile.toast.success': 'Profile updated successfully.',
+ 'profile.error.update': 'Unable to update profile. Please try again.',
+
+ 'addresses.title': 'My Addresses',
+ 'addresses.subtitle': 'Manage your shipping addresses for orders at this store.',
+ 'addresses.save': 'Save addresses',
+ 'addresses.saveOne': 'Save address',
+ 'addresses.saving': 'Saving...',
+ 'addresses.cancel': 'Cancel',
+ 'addresses.editorTitle': 'Saved addresses',
+ 'addresses.add': 'Add address',
+ 'addresses.edit': 'Edit address',
+ 'addresses.empty': 'No addresses yet.',
+ 'addresses.loading': 'Loading addresses...',
+ 'addresses.label': 'Address name',
+ 'addresses.labelPlaceholder': 'e.g. Home, Office',
+ 'addresses.optional': '(optional)',
+ 'addresses.province': 'Province',
+ 'addresses.city': 'City',
+ 'addresses.address': 'Address',
+ 'addresses.postalCode': 'Postal code',
+ 'addresses.landline': 'Landline',
+ 'addresses.selectProvince': 'Select province',
+ 'addresses.selectCity': 'Select city',
+ 'addresses.streetPlaceholder': 'Street, plaque, unit',
+ 'addresses.postalPlaceholder': 'Postal code',
+ 'addresses.landlinePlaceholder': 'Landline',
+ 'addresses.remove': 'Remove address',
+ 'addresses.modal.addTitle': 'Add address',
+ 'addresses.modal.editTitle': 'Edit address',
+ 'addresses.modal.subtitle': 'Enter the shipping address details.',
+ 'addresses.toast.removed': 'Address removed.',
+ 'addresses.toast.saved': 'Addresses saved.',
+ 'addresses.toast.created': 'Address saved.',
+ 'addresses.toast.updated': 'Address updated.',
+ 'addresses.error.load': 'Unable to load addresses.',
+ 'addresses.error.remove': 'Unable to remove address.',
+ 'addresses.error.incomplete': 'Add at least one complete address.',
+ 'addresses.error.incompleteForm': 'Please fill in all required fields.',
+ 'addresses.error.save': 'Unable to save address. Please try again.',
+ 'addresses.error.loadLocations': 'Unable to load provinces.',
+ 'addresses.error.loadCities': 'Unable to load cities.',
+
+ 'orders.title': 'My Orders',
+ 'orders.subtitle': 'View your order history and details.',
+ 'orders.listTitle': 'Order list',
+ 'orders.showing': 'Showing {from} - {to} of {total}',
+ 'orders.none': 'No orders',
+ 'orders.empty': 'You have no orders yet.',
+ 'orders.loading': 'Loading orders...',
+ 'orders.error.load': 'Unable to load orders.',
+ 'orders.col.orderId': 'Order ID',
+ 'orders.col.items': 'Items',
+ 'orders.col.total': 'Total cost',
+ 'orders.col.date': 'Date & time',
+ 'orders.col.step': 'Step',
+ 'orders.col.source': 'Registered by',
+ 'orders.col.actions': 'Actions',
+ 'orders.pageMeta': 'Page {page} / {totalPages} · {pageSize} per page · {total} total',
+ 'orders.viewItems': 'View items',
+ 'orders.source.operator': 'Operator',
+ 'orders.source.app': 'Application',
+ 'orders.source.website': 'Website',
+ 'orders.step.processing': 'Under processing',
+ 'orders.step.ready': 'Ready for shipping',
+ 'orders.step.shipped': 'Shipped',
+ 'orders.step.delivered': 'Delivered',
+
+ 'orderItems.title': 'Order items',
+ 'orderItems.customer': 'Customer',
+ 'orderItems.phone': 'Phone',
+ 'orderItems.items': 'Items',
+ 'orderItems.empty': 'No items in this order.',
+ 'orderItems.product': 'Product',
+ 'orderItems.qty': 'Qty',
+ 'orderItems.unitPrice': 'Unit price',
+ 'orderItems.lineTotal': 'Line total',
+ 'orderItems.sku': 'SKU: {sku}',
+ 'orderItems.summary': 'Order total · {count} {itemsLabel}',
+ 'orderItems.item': 'item',
+ 'orderItems.itemsPlural': 'items',
+ 'orderItems.close': 'Close',
+
+ 'favorites.title': 'My Favorites',
+ 'favorites.subtitle': 'Products you have saved for later.',
+ 'favorites.loading': 'Loading favorites...',
+ 'favorites.empty': 'No favorites yet.',
+ 'favorites.error.load': 'Unable to load favorites.',
+ 'favorites.toast.removed': 'Removed from favorites.',
+ 'favorites.error.remove': 'Unable to remove favorite.',
+ 'favorites.cartSoon': 'Shopping cart is coming soon.',
+ 'favorites.remove': 'Remove from favorites',
+ 'favorites.addToCart': 'Add to shopping cart',
+ 'favorites.festival': 'Festival',
+ 'favorites.inStock': '{count} in stock',
+ 'favorites.variantOne': '1 variant',
+ 'favorites.variantMany': '{count} variants',
+
+ 'title.signIn': 'Sign in',
+ 'title.checkout': 'Checkout',
+ 'title.cart': 'Shopping Cart',
+ 'title.delivery': 'Delivery',
+ 'title.payment': 'Payment',
+ 'title.success': 'Success',
+ 'title.failed': 'Failed',
+
+ 'login.welcome': 'Welcome back',
+ 'login.subtitle': 'Sign in with your mobile number',
+ 'login.mobile': 'Mobile number',
+ 'login.password': 'Password',
+ 'login.passwordPlaceholder': 'Enter your password',
+ 'login.hidePassword': 'Hide password',
+ 'login.showPassword': 'Show password',
+ 'login.forgot': 'Forgot password?',
+ 'login.signIn': 'Sign in',
+ 'login.signingIn': 'Signing in...',
+ 'login.or': 'or',
+ 'login.otp': 'One-time login with SMS',
+ 'login.noAccount': "Don't have an account?",
+ 'login.signUp': 'Sign up',
+ 'login.error.signIn': 'Unable to sign in. Check your connection and try again.',
+ 'login.error.sendCode': 'Unable to send verification code.',
+
+ 'signup.title': 'Create account',
+ 'signup.subtitle': 'Register as a customer of {domain}',
+ 'signup.firstName': 'First name',
+ 'signup.lastName': 'Last name',
+ 'signup.passwordPlaceholder': 'Choose a password',
+ 'signup.confirm': 'Confirm password',
+ 'signup.confirmPlaceholder': 'Repeat your password',
+ 'signup.create': 'Create account',
+ 'signup.creating': 'Creating account...',
+ 'signup.hasAccount': 'Already have an account?',
+ 'signup.signIn': 'Sign in',
+ 'signup.error.match': 'Passwords do not match.',
+ 'signup.error.length': 'Password must be at least 8 characters.',
+ 'signup.error.create': 'Unable to create account.',
+
+ 'forgot.back': 'Back to sign in',
+ 'forgot.title': 'Forgot password',
+ 'forgot.subtitlePhone': 'We will send a verification code via SMS',
+ 'forgot.subtitleCode': 'Enter the code and your new password',
+ 'forgot.sendCode': 'Send SMS code',
+ 'forgot.sending': 'Sending...',
+ 'forgot.code': 'SMS verification code',
+ 'forgot.newPassword': 'New password',
+ 'forgot.newPasswordPlaceholder': 'Enter new password',
+ 'forgot.reset': 'Reset password',
+ 'forgot.verifying': 'Verifying...',
+ 'forgot.error.length': 'Password must be at least 8 characters.',
+ 'forgot.error.verify': 'Unable to verify code.',
+ 'forgot.info.partial':
+ 'Phone number verified. Full password reset via SMS is not available yet — please contact support or sign in if you remember your password.',
+
+ 'otp.back': 'Back to sign in',
+ 'otp.title': 'One-time login',
+ 'otp.subtitlePhone': 'Verify your mobile number with a one-time SMS code',
+ 'otp.subtitleCode': 'Enter the SMS code and your password',
+ 'otp.sendCode': 'Send SMS code',
+ 'otp.sending': 'Sending...',
+ 'otp.code': 'SMS verification code',
+ 'otp.password': 'Password',
+ 'otp.passwordPlaceholder': 'Your account password',
+ 'otp.signIn': 'Sign in',
+ 'otp.signingIn': 'Signing in...',
+ 'otp.error.password': 'Enter your account password to complete sign-in after SMS verification.',
+ 'otp.error.signIn': 'Unable to sign in with SMS verification.',
+
+ 'common.close': 'Close',
+ 'common.resendIn': 'Resend code in {seconds}s',
+ 'common.resend': 'Resend SMS code',
+ 'common.codeSent': 'Verification code sent to {phone}',
+} as const
+
+type MessageKey = keyof typeof en
+
+const fa: Record = {
+ 'app.dashboardName': 'پنل مشتری',
+ 'app.role.customer': 'مشتری',
+ 'app.storeFallback': 'فروشگاه',
+ 'app.poweredBy': 'قدرتگرفته از Meshkee.app',
+
+ 'nav.home': 'خانه',
+ 'nav.cart': 'سبد خرید',
+ 'nav.profile': 'پروفایل من',
+ 'nav.addresses': 'آدرسهای من',
+ 'nav.orders': 'سفارشهای من',
+ 'nav.favorites': 'علاقهمندیها',
+ 'nav.help': 'مرکز راهنما',
+ 'nav.logout': 'خروج',
+
+ 'header.toggleMenu': 'باز و بسته کردن منو',
+ 'header.messages': 'پیامها',
+ 'header.notifications': 'اعلانها',
+ 'header.changePassword': 'تغییر رمز عبور',
+ 'header.myProfile': 'پروفایل من',
+
+ 'home.welcome': '{name} عزیز، خوش آمدی.',
+ 'home.welcomeFallback': 'کاربر',
+ 'home.subtitle': 'پروفایل، آدرسها، سفارشها و علاقهمندیها را از یکجا مدیریت کنید.',
+ 'home.card.profile.title': 'پروفایل من',
+ 'home.card.profile.desc': 'اطلاعات شخصی و راههای ارتباطی خود را مشاهده و بهروزرسانی کنید.',
+ 'home.card.profile.link': 'مشاهده پروفایل',
+ 'home.card.addresses.title': 'آدرسهای من',
+ 'home.card.addresses.desc': 'آدرسهای ارسال برای تسویهحساب و تحویل را مدیریت کنید.',
+ 'home.card.addresses.link': 'مشاهده آدرسها',
+ 'home.card.orders.title': 'سفارشهای من',
+ 'home.card.orders.desc': 'سفارشها را پیگیری کنید و تاریخچه و جزئیات را ببینید.',
+ 'home.card.orders.link': 'مشاهده سفارشها',
+ 'home.card.favorites.title': 'علاقهمندیها',
+ 'home.card.favorites.desc': 'محصولات ذخیرهشده مورد علاقهتان را ببینید و مدیریت کنید.',
+ 'home.card.favorites.link': 'مشاهده علاقهمندیها',
+
+ 'profile.title': 'پروفایل من',
+ 'profile.subtitle': 'اطلاعات شخصی و راههای ارتباطی خود را بهروزرسانی کنید.',
+ 'profile.section.account': 'حساب کاربری',
+ 'profile.section.about': 'درباره من',
+ 'profile.section.social': 'شبکههای اجتماعی',
+ 'profile.mobile': 'شماره موبایل',
+ 'profile.email': 'ایمیل',
+ 'profile.firstName': 'نام (فارسی)',
+ 'profile.lastName': 'نام خانوادگی (فارسی)',
+ 'profile.firstNameEn': 'نام (انگلیسی)',
+ 'profile.lastNameEn': 'نام خانوادگی (انگلیسی)',
+ 'profile.landline': 'تلفن ثابت',
+ 'profile.backupPhone': 'شماره تماس پشتیبان',
+ 'profile.about': 'درباره من',
+ 'profile.instagram': 'اینستاگرام',
+ 'profile.telegram': 'تلگرام',
+ 'profile.linkedin': 'لینکدین',
+ 'profile.emailPlaceholder': 'you@example.com',
+ 'profile.save': 'ذخیره تغییرات',
+ 'profile.saving': 'در حال ذخیره...',
+ 'profile.toast.success': 'پروفایل با موفقیت بهروزرسانی شد.',
+ 'profile.error.update': 'بهروزرسانی پروفایل ممکن نشد. دوباره تلاش کنید.',
+
+ 'addresses.title': 'آدرسهای من',
+ 'addresses.subtitle': 'آدرسهای ارسال سفارش در این فروشگاه را مدیریت کنید.',
+ 'addresses.save': 'ذخیره آدرسها',
+ 'addresses.saveOne': 'ذخیره آدرس',
+ 'addresses.saving': 'در حال ذخیره...',
+ 'addresses.cancel': 'انصراف',
+ 'addresses.editorTitle': 'آدرسهای ذخیرهشده',
+ 'addresses.add': 'افزودن آدرس',
+ 'addresses.edit': 'ویرایش آدرس',
+ 'addresses.empty': 'هنوز آدرسی ثبت نشده است.',
+ 'addresses.loading': 'در حال بارگذاری آدرسها...',
+ 'addresses.label': 'عنوان آدرس',
+ 'addresses.labelPlaceholder': 'مثلاً خانه، محل کار',
+ 'addresses.optional': '(اختیاری)',
+ 'addresses.province': 'استان',
+ 'addresses.city': 'شهر',
+ 'addresses.address': 'آدرس',
+ 'addresses.postalCode': 'کد پستی',
+ 'addresses.landline': 'تلفن ثابت',
+ 'addresses.selectProvince': 'انتخاب استان',
+ 'addresses.selectCity': 'انتخاب شهر',
+ 'addresses.streetPlaceholder': 'خیابان، پلاک، واحد',
+ 'addresses.postalPlaceholder': 'کد پستی',
+ 'addresses.landlinePlaceholder': 'تلفن ثابت',
+ 'addresses.remove': 'حذف آدرس',
+ 'addresses.modal.addTitle': 'افزودن آدرس',
+ 'addresses.modal.editTitle': 'ویرایش آدرس',
+ 'addresses.modal.subtitle': 'جزئیات آدرس ارسال را وارد کنید.',
+ 'addresses.toast.removed': 'آدرس حذف شد.',
+ 'addresses.toast.saved': 'آدرسها ذخیره شدند.',
+ 'addresses.toast.created': 'آدرس ذخیره شد.',
+ 'addresses.toast.updated': 'آدرس بهروزرسانی شد.',
+ 'addresses.error.load': 'بارگذاری آدرسها ممکن نشد.',
+ 'addresses.error.remove': 'حذف آدرس ممکن نشد.',
+ 'addresses.error.incomplete': 'حداقل یک آدرس کامل اضافه کنید.',
+ 'addresses.error.incompleteForm': 'لطفاً همه فیلدهای الزامی را تکمیل کنید.',
+ 'addresses.error.save': 'ذخیره آدرس ممکن نشد. دوباره تلاش کنید.',
+ 'addresses.error.loadLocations': 'بارگذاری استانها ممکن نشد.',
+ 'addresses.error.loadCities': 'بارگذاری شهرها ممکن نشد.',
+
+ 'orders.title': 'سفارشهای من',
+ 'orders.subtitle': 'تاریخچه و جزئیات سفارشهای خود را ببینید.',
+ 'orders.listTitle': 'فهرست سفارشها',
+ 'orders.showing': 'نمایش {from} تا {to} از {total}',
+ 'orders.none': 'بدون سفارش',
+ 'orders.empty': 'هنوز سفارشی ندارید.',
+ 'orders.loading': 'در حال بارگذاری سفارشها...',
+ 'orders.error.load': 'بارگذاری سفارشها ممکن نشد.',
+ 'orders.col.orderId': 'شماره سفارش',
+ 'orders.col.items': 'اقلام',
+ 'orders.col.total': 'مبلغ کل',
+ 'orders.col.date': 'تاریخ و ساعت',
+ 'orders.col.step': 'وضعیت',
+ 'orders.col.source': 'ثبتشده توسط',
+ 'orders.col.actions': 'عملیات',
+ 'orders.pageMeta': 'صفحه {page} / {totalPages} · {pageSize} در هر صفحه · {total} کل',
+ 'orders.viewItems': 'مشاهده اقلام',
+ 'orders.source.operator': 'اپراتور',
+ 'orders.source.app': 'اپلیکیشن',
+ 'orders.source.website': 'وبسایت',
+ 'orders.step.processing': 'در حال پردازش',
+ 'orders.step.ready': 'آماده ارسال',
+ 'orders.step.shipped': 'ارسالشده',
+ 'orders.step.delivered': 'تحویلشده',
+
+ 'orderItems.title': 'اقلام سفارش',
+ 'orderItems.customer': 'مشتری',
+ 'orderItems.phone': 'تلفن',
+ 'orderItems.items': 'اقلام',
+ 'orderItems.empty': 'اقلامی در این سفارش نیست.',
+ 'orderItems.product': 'محصول',
+ 'orderItems.qty': 'تعداد',
+ 'orderItems.unitPrice': 'قیمت واحد',
+ 'orderItems.lineTotal': 'جمع ردیف',
+ 'orderItems.sku': 'کد کالا: {sku}',
+ 'orderItems.summary': 'جمع سفارش · {count} {itemsLabel}',
+ 'orderItems.item': 'قلم',
+ 'orderItems.itemsPlural': 'قلم',
+ 'orderItems.close': 'بستن',
+
+ 'favorites.title': 'علاقهمندیها',
+ 'favorites.subtitle': 'محصولاتی که برای بعد ذخیره کردهاید.',
+ 'favorites.loading': 'در حال بارگذاری علاقهمندیها...',
+ 'favorites.empty': 'هنوز علاقهمندی ندارید.',
+ 'favorites.error.load': 'بارگذاری علاقهمندیها ممکن نشد.',
+ 'favorites.toast.removed': 'از علاقهمندیها حذف شد.',
+ 'favorites.error.remove': 'حذف از علاقهمندیها ممکن نشد.',
+ 'favorites.cartSoon': 'سبد خرید بهزودی فعال میشود.',
+ 'favorites.remove': 'حذف از علاقهمندیها',
+ 'favorites.addToCart': 'افزودن به سبد خرید',
+ 'favorites.festival': 'جشنواره',
+ 'favorites.inStock': '{count} موجود',
+ 'favorites.variantOne': '۱ تنوع',
+ 'favorites.variantMany': '{count} تنوع',
+
+ 'title.signIn': 'ورود',
+ 'title.checkout': 'تسویهحساب',
+ 'title.cart': 'سبد خرید',
+ 'title.delivery': 'ارسال',
+ 'title.payment': 'پرداخت',
+ 'title.success': 'موفق',
+ 'title.failed': 'ناموفق',
+
+ 'login.welcome': 'خوش آمدید',
+ 'login.subtitle': 'با شماره موبایل وارد شوید',
+ 'login.mobile': 'شماره موبایل',
+ 'login.password': 'رمز عبور',
+ 'login.passwordPlaceholder': 'رمز عبور خود را وارد کنید',
+ 'login.hidePassword': 'مخفی کردن رمز',
+ 'login.showPassword': 'نمایش رمز',
+ 'login.forgot': 'رمز عبور را فراموش کردهاید؟',
+ 'login.signIn': 'ورود',
+ 'login.signingIn': 'در حال ورود...',
+ 'login.or': 'یا',
+ 'login.otp': 'ورود یکبارمصرف با پیامک',
+ 'login.noAccount': 'حساب کاربری ندارید؟',
+ 'login.signUp': 'ثبتنام',
+ 'login.error.signIn': 'ورود ممکن نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید.',
+ 'login.error.sendCode': 'ارسال کد تأیید ممکن نشد.',
+
+ 'signup.title': 'ایجاد حساب',
+ 'signup.subtitle': 'بهعنوان مشتری {domain} ثبتنام کنید',
+ 'signup.firstName': 'نام',
+ 'signup.lastName': 'نام خانوادگی',
+ 'signup.passwordPlaceholder': 'یک رمز عبور انتخاب کنید',
+ 'signup.confirm': 'تأیید رمز عبور',
+ 'signup.confirmPlaceholder': 'رمز عبور را تکرار کنید',
+ 'signup.create': 'ایجاد حساب',
+ 'signup.creating': 'در حال ایجاد حساب...',
+ 'signup.hasAccount': 'قبلاً حساب دارید؟',
+ 'signup.signIn': 'ورود',
+ 'signup.error.match': 'رمزهای عبور یکسان نیستند.',
+ 'signup.error.length': 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
+ 'signup.error.create': 'ایجاد حساب ممکن نشد.',
+
+ 'forgot.back': 'بازگشت به ورود',
+ 'forgot.title': 'فراموشی رمز عبور',
+ 'forgot.subtitlePhone': 'کد تأیید را از طریق پیامک ارسال میکنیم',
+ 'forgot.subtitleCode': 'کد و رمز عبور جدید را وارد کنید',
+ 'forgot.sendCode': 'ارسال کد پیامکی',
+ 'forgot.sending': 'در حال ارسال...',
+ 'forgot.code': 'کد تأیید پیامکی',
+ 'forgot.newPassword': 'رمز عبور جدید',
+ 'forgot.newPasswordPlaceholder': 'رمز عبور جدید را وارد کنید',
+ 'forgot.reset': 'بازیابی رمز عبور',
+ 'forgot.verifying': 'در حال تأیید...',
+ 'forgot.error.length': 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
+ 'forgot.error.verify': 'تأیید کد ممکن نشد.',
+ 'forgot.info.partial':
+ 'شماره موبایل تأیید شد. بازیابی کامل رمز با پیامک هنوز در دسترس نیست — با پشتیبانی تماس بگیرید یا اگر رمز را بهخاطر دارید وارد شوید.',
+
+ 'otp.back': 'بازگشت به ورود',
+ 'otp.title': 'ورود یکبارمصرف',
+ 'otp.subtitlePhone': 'شماره موبایل خود را با کد یکبارمصرف پیامکی تأیید کنید',
+ 'otp.subtitleCode': 'کد پیامکی و رمز عبور خود را وارد کنید',
+ 'otp.sendCode': 'ارسال کد پیامکی',
+ 'otp.sending': 'در حال ارسال...',
+ 'otp.code': 'کد تأیید پیامکی',
+ 'otp.password': 'رمز عبور',
+ 'otp.passwordPlaceholder': 'رمز عبور حساب شما',
+ 'otp.signIn': 'ورود',
+ 'otp.signingIn': 'در حال ورود...',
+ 'otp.error.password': 'برای تکمیل ورود پس از تأیید پیامکی، رمز حساب خود را وارد کنید.',
+ 'otp.error.signIn': 'ورود با تأیید پیامکی ممکن نشد.',
+
+ 'common.close': 'بستن',
+ 'common.resendIn': 'ارسال مجدد کد تا {seconds} ثانیه',
+ 'common.resend': 'ارسال مجدد کد پیامکی',
+ 'common.codeSent': 'کد تأیید به {phone} ارسال شد',
+}
+
+const dictionaries: Record> = {
+ en: en as Record,
+ fa,
+}
+
+export type CustomerMessageKey = MessageKey
+
+export function translate(
+ locale: DashboardLocale,
+ key: MessageKey,
+ vars?: Record,
+): string {
+ const dict = dictionaries[locale] ?? dictionaries.en
+ let text = dict[key] ?? dictionaries.en[key] ?? key
+ if (vars) {
+ for (const [name, value] of Object.entries(vars)) {
+ text = text.replaceAll(`{${name}}`, String(value))
+ }
+ }
+ return text
+}
+
+export function getCustomerRouteTitleRules(locale: DashboardLocale) {
+ const t = (key: MessageKey) => translate(locale, key)
+ return [
+ { match: '/login', labels: [t('title.signIn')] },
+ { match: '/checkout/login', labels: [t('title.checkout'), t('title.signIn')] },
+ { match: '/checkout/cart', labels: [t('title.checkout'), t('title.cart')] },
+ { match: '/checkout/delivery', labels: [t('title.checkout'), t('title.delivery')] },
+ { match: '/checkout/payment', labels: [t('title.checkout'), t('title.payment')] },
+ { match: '/checkout/success', labels: [t('title.checkout'), t('title.success')] },
+ { match: '/checkout/failed', labels: [t('title.checkout'), t('title.failed')] },
+ { match: '/checkout', labels: [t('title.checkout')] },
+ { match: '/profile', labels: [t('nav.profile')] },
+ { match: '/addresses', labels: [t('nav.addresses')] },
+ { match: '/orders', labels: [t('nav.orders')] },
+ { match: '/favorites', labels: [t('nav.favorites')] },
+ { match: '/', labels: [t('nav.home')] },
+ ]
+}
diff --git a/apps/customer/src/i18n/useT.ts b/apps/customer/src/i18n/useT.ts
new file mode 100644
index 0000000..2caeca0
--- /dev/null
+++ b/apps/customer/src/i18n/useT.ts
@@ -0,0 +1,13 @@
+import { useCallback } from 'react'
+import { useLocale } from '@meshkee/dashboard-ui'
+import { translate, type CustomerMessageKey } from './messages'
+
+export function useT() {
+ const { locale } = useLocale()
+
+ return useCallback(
+ (key: CustomerMessageKey, vars?: Record) =>
+ translate(locale, key, vars),
+ [locale],
+ )
+}
diff --git a/apps/customer/src/index.css b/apps/customer/src/index.css
index 65fd3f6..651317c 100644
--- a/apps/customer/src/index.css
+++ b/apps/customer/src/index.css
@@ -1,6 +1,8 @@
@import '@meshkee/dashboard-core/styles/tokens.css';
-/* Customer app Farsi typography — Yekan Bakh for body, inputs, and placeholders */
+/* Customer: Montserrat (EN) + Yekan Bakh (FA) via --font-ui stack */
:root {
+ --font-en: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-fa: 'YekanBakh', Tahoma, sans-serif;
+ --font-ui: var(--font-en), var(--font-fa);
}
diff --git a/apps/customer/src/pages/AddressesPage.module.css b/apps/customer/src/pages/AddressesPage.module.css
index 51f253d..a427e82 100644
--- a/apps/customer/src/pages/AddressesPage.module.css
+++ b/apps/customer/src/pages/AddressesPage.module.css
@@ -1,9 +1,3 @@
-.form {
- display: flex;
- flex-direction: column;
- gap: 24px;
-}
-
.section {
width: 100%;
background: var(--glass-bg);
@@ -15,26 +9,127 @@
padding: 24px;
}
-.actions {
- display: flex;
- justify-content: flex-end;
+.sectionTitle {
+ margin: 0 0 16px;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
}
-.saveBtn {
- padding: 10px 20px;
+.status {
+ margin: 0;
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+.empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ padding: 40px 16px;
+ color: var(--text-muted);
+ text-align: center;
+}
+
+.empty p {
+ margin: 0;
+ font-size: 14px;
+}
+
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-height: 48px;
+ padding: 10px 12px;
+ border: 1px solid rgba(148, 163, 184, 0.22);
+ border-radius: var(--radius-sm);
+ background: rgba(255, 255, 255, 0.55);
+}
+
+.rowLine {
+ min-width: 0;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.rowLabel {
+ flex-shrink: 0;
font-size: 14px;
font-weight: 600;
- color: #fff;
- background: var(--primary);
- border-radius: var(--radius-sm);
+ color: var(--text-primary);
}
-.saveBtn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
+.rowText {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.rowActions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.editBtn {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ border-radius: 8px;
+ color: var(--text-muted);
+ transition: background 0.2s, color 0.2s;
+}
+
+.editBtn:hover {
+ background: rgba(var(--primary-rgb) / 0.1);
+ color: var(--primary);
+}
+
+.addFab {
+ position: fixed;
+ inset-inline-end: 32px;
+ bottom: 32px;
+ z-index: 50;
+ width: 56px;
+ height: 56px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ color: white;
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
+ box-shadow: 0 8px 24px rgba(var(--primary-rgb) / 0.4);
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.addFab:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 28px rgba(var(--primary-rgb) / 0.45);
}
.error {
+ margin-bottom: 16px;
padding: 12px 16px;
border-radius: var(--radius-sm);
font-size: 13px;
@@ -42,3 +137,10 @@
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
}
+
+@media (max-width: 768px) {
+ .addFab {
+ inset-inline-end: 20px;
+ bottom: 20px;
+ }
+}
diff --git a/apps/customer/src/pages/AddressesPage.tsx b/apps/customer/src/pages/AddressesPage.tsx
index 184ed95..3611795 100644
--- a/apps/customer/src/pages/AddressesPage.tsx
+++ b/apps/customer/src/pages/AddressesPage.tsx
@@ -1,259 +1,201 @@
-import { useEffect, useMemo, useState } from 'react'
-import {
- AddressListEditor,
- Breadcrumbs,
- createEmptyAddressItem,
- matchCityByName,
- matchProvinceByName,
- useToast,
- type AddressListItem,
- type CityOption,
-} from '@meshkee/dashboard-ui'
+import { useCallback, useEffect, useState } from 'react'
+import { MapPin, Pencil, Plus, Trash2 } from 'lucide-react'
+import { Breadcrumbs, useToast } from '@meshkee/dashboard-ui'
+import { AddressFormModal } from '../components/AddressFormModal'
+import { Tooltip } from '../components/Tooltip'
+import { useT } from '../i18n/useT'
import { ApiError, isAbortError } from '../lib/api'
import {
- createAddress,
listAddresses,
removeAddress,
- updateAddress,
type UserAddress,
- type UserAddressInput,
} from '../services/addressService'
-import {
- listCitiesByProvinceSlug,
- listIranProvinces,
-} from '../services/citiesService'
import pageStyles from '../components/PageContent.module.css'
+import rowBtnStyles from '../components/VariationsModal.module.css'
import styles from './AddressesPage.module.css'
-type AddressDraft = AddressListItem
-
-function toDraft(
- item: UserAddress,
- provinces: CityOption[],
- citiesByProvince: Record,
-): AddressDraft {
- const province = matchProvinceByName(item.province, provinces)
- const cities = province ? (citiesByProvince[province.slug] ?? []) : []
- const city = matchCityByName(item.city, cities)
- return {
- id: item.id,
- provinceSlug: province?.slug ?? '',
- province: province?.nameEn ?? item.province,
- city: city?.nameEn ?? item.city,
- address: item.address,
- postalCode: item.postalCode ?? '',
- landline: item.landline ?? '',
- }
-}
-
-function isCompleteAddress(item: AddressDraft) {
- return item.province.trim() && item.city.trim() && item.address.trim()
-}
-
export function AddressesPage() {
const { showToast } = useToast()
- const [provinces, setProvinces] = useState([])
- const [citiesByProvince, setCitiesByProvince] = useState>({})
- const [addresses, setAddresses] = useState([createEmptyAddressItem()])
+ const t = useT()
+ const [addresses, setAddresses] = useState([])
const [loading, setLoading] = useState(true)
- const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
+ const [removingId, setRemovingId] = useState(null)
+ const [modalOpen, setModalOpen] = useState(false)
+ const [editingAddress, setEditingAddress] = useState(null)
+
+ const loadAddresses = useCallback(
+ async (signal?: AbortSignal) => {
+ setLoading(true)
+ setError('')
+ try {
+ const data = await listAddresses(signal)
+ if (signal?.aborted) return
+ setAddresses(data.items)
+ } catch (err) {
+ if (isAbortError(err) || signal?.aborted) return
+ setError(err instanceof ApiError ? err.message : t('addresses.error.load'))
+ } finally {
+ if (!signal?.aborted) setLoading(false)
+ }
+ },
+ [t],
+ )
useEffect(() => {
const controller = new AbortController()
-
- async function load() {
- setLoading(true)
- setError('')
-
- try {
- const [provinceItems, data] = await Promise.all([
- listIranProvinces(controller.signal),
- listAddresses(controller.signal),
- ])
- if (controller.signal.aborted) return
-
- setProvinces(provinceItems)
-
- const draftItems =
- data.items.length > 0 ? data.items : []
-
- const slugs = [
- ...new Set(
- draftItems
- .map((item) => matchProvinceByName(item.province, provinceItems)?.slug)
- .filter(Boolean) as string[],
- ),
- ]
- const cityGroups = await Promise.all(
- slugs.map(async (slug) => ({
- slug,
- cities: await listCitiesByProvinceSlug(slug, controller.signal),
- })),
- )
- if (controller.signal.aborted) return
-
- const citiesMap = Object.fromEntries(cityGroups.map((group) => [group.slug, group.cities]))
- setCitiesByProvince(citiesMap)
- setAddresses(
- draftItems.length > 0
- ? draftItems.map((item) => toDraft(item, provinceItems, citiesMap))
- : [createEmptyAddressItem()],
- )
- } catch (err) {
- if (isAbortError(err) || controller.signal.aborted) return
- setError(err instanceof ApiError ? err.message : 'Unable to load addresses.')
- } finally {
- if (!controller.signal.aborted) setLoading(false)
- }
- }
-
- void load()
+ void loadAddresses(controller.signal)
return () => controller.abort()
- }, [])
+ }, [loadAddresses])
- function updateAddressDraft(index: number, patch: Partial) {
- setAddresses((prev) =>
- prev.map((item, i) => (i === index ? { ...item, ...patch } : item)),
- )
+ function openCreateModal() {
+ setEditingAddress(null)
+ setModalOpen(true)
}
- async function handleProvinceChange(index: number, provinceSlug: string) {
- const province = provinces.find((item) => item.slug === provinceSlug)
- updateAddressDraft(index, {
- provinceSlug,
- province: province?.nameEn ?? '',
- city: '',
- })
-
- if (provinceSlug && !citiesByProvince[provinceSlug]) {
- const cities = await listCitiesByProvinceSlug(provinceSlug)
- setCitiesByProvince((prev) => ({ ...prev, [provinceSlug]: cities }))
- }
+ function openEditModal(item: UserAddress) {
+ setEditingAddress(item)
+ setModalOpen(true)
}
- function addAddressRow() {
- setAddresses((prev) => [...prev, createEmptyAddressItem()])
- }
-
- async function handleRemove(index: number) {
- const target = addresses[index]
- if (!target) return
-
- if (!target.id) {
- setAddresses((prev) =>
- prev.length === 1 ? [createEmptyAddressItem()] : prev.filter((_, i) => i !== index),
- )
- return
- }
-
- setSaving(true)
- setError('')
-
- try {
- await removeAddress(target.id)
- setAddresses((prev) => {
- const next = prev.filter((_, i) => i !== index)
- return next.length > 0 ? next : [createEmptyAddressItem()]
- })
- showToast('Address removed.', 'success')
- } catch (err) {
- setError(err instanceof ApiError ? err.message : 'Unable to remove address.')
- } finally {
- setSaving(false)
- }
- }
-
- async function handleSubmit(e: React.FormEvent) {
- e.preventDefault()
- setError('')
-
- const payload = addresses.filter(isCompleteAddress)
- if (payload.length === 0) {
- setError('Add at least one complete address.')
- return
- }
-
- setSaving(true)
-
- try {
- const saved: AddressDraft[] = []
-
- for (const item of payload) {
- const input: UserAddressInput = {
- 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) {
- const result = await updateAddress(item.id, input)
- saved.push(toDraft(result.address, provinces, citiesByProvince))
- } else {
- const result = await createAddress(input)
- saved.push(toDraft(result.address, provinces, citiesByProvince))
- }
+ function handleSaved(saved: UserAddress) {
+ setAddresses((prev) => {
+ const index = prev.findIndex((item) => item.id === saved.id)
+ if (index >= 0) {
+ const next = [...prev]
+ next[index] = saved
+ return next
}
+ return [saved, ...prev]
+ })
+ }
- setAddresses(saved.length > 0 ? saved : [createEmptyAddressItem()])
- showToast('Addresses saved.', 'success')
+ async function handleRemove(item: UserAddress) {
+ setRemovingId(item.id)
+ setError('')
+ try {
+ await removeAddress(item.id)
+ setAddresses((prev) => prev.filter((row) => row.id !== item.id))
+ showToast(t('addresses.toast.removed'), 'success')
} catch (err) {
- const message =
- err instanceof ApiError ? err.message : 'Unable to save addresses. Please try again.'
+ const message = err instanceof ApiError ? err.message : t('addresses.error.remove')
setError(message)
showToast(message, 'error')
} finally {
- setSaving(false)
+ setRemovingId(null)
}
}
- const hasAddresses = useMemo(
- () => addresses.some((item) => isCompleteAddress(item)),
- [addresses],
- )
+ function formatRowLine(item: UserAddress) {
+ return [
+ item.label || null,
+ item.province,
+ item.city,
+ item.address,
+ item.postalCode,
+ item.landline,
+ ]
+ .filter(Boolean)
+ .join(' · ')
+ }
return (
-
+
- My Addresses
-
- Manage your shipping addresses for orders at this store.
-
+ {t('addresses.title')}
+ {t('addresses.subtitle')}
- |