mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Ship my-products / customer-products UI with gallery uploads, status controls, module gating, and related shared UI polish. Co-authored-by: Cursor <cursoragent@cursor.com>
347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
|
|
import {
|
|
Home,
|
|
ShoppingBag,
|
|
Store,
|
|
Users,
|
|
Settings,
|
|
FileText,
|
|
Briefcase,
|
|
Globe,
|
|
HelpCircle,
|
|
LogOut,
|
|
ChevronDown,
|
|
Building2,
|
|
Pencil,
|
|
Package,
|
|
} from 'lucide-react'
|
|
import type { LucideIcon } from 'lucide-react'
|
|
import { useLocale } from '@meshkee/dashboard-ui'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import { useT } from '../i18n/useT'
|
|
import type { BusinessMessageKey } from '../i18n/messages'
|
|
import {
|
|
BUSINESS_PROFILE_UPDATED_EVENT,
|
|
getActiveBusinessDomain,
|
|
} from '../lib/businessContext'
|
|
import { isAbortError } from '../lib/api'
|
|
import { getBusinessProfile } from '../services/businessProfileService'
|
|
import { useTenantBranding } from '../context/TenantBrandingContext'
|
|
import {
|
|
hasBusinessModule,
|
|
type BusinessModuleId,
|
|
} from '../utils/businessModules'
|
|
import { Tooltip } from './Tooltip'
|
|
import styles from './Sidebar.module.css'
|
|
|
|
interface NavChild {
|
|
labelKey: BusinessMessageKey
|
|
to: string
|
|
}
|
|
|
|
interface NavGroup {
|
|
type: 'group'
|
|
id: string
|
|
icon: LucideIcon
|
|
labelKey: BusinessMessageKey
|
|
basePath: string
|
|
children: NavChild[]
|
|
moduleId?: BusinessModuleId
|
|
}
|
|
|
|
interface NavLinkItem {
|
|
type: 'link'
|
|
id: string
|
|
icon: LucideIcon
|
|
labelKey: BusinessMessageKey
|
|
to: string
|
|
moduleId?: BusinessModuleId
|
|
}
|
|
|
|
type NavItem = NavLinkItem | NavGroup
|
|
|
|
function isGroupActive(basePath: string, pathname: string) {
|
|
return pathname === basePath || pathname.startsWith(`${basePath}/`)
|
|
}
|
|
|
|
export function Sidebar() {
|
|
const { pathname } = useLocation()
|
|
const navigate = useNavigate()
|
|
const { user, logout } = useAuth()
|
|
const { locale } = useLocale()
|
|
const { enabledModules } = useTenantBranding()
|
|
const t = useT()
|
|
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
|
|
const [brandLogoUrl, setBrandLogoUrl] = useState<string | null>(null)
|
|
const [nameEn, setNameEn] = useState('')
|
|
const [nameFa, setNameFa] = useState('')
|
|
|
|
const businessDomain = getActiveBusinessDomain()
|
|
const fallbackBusinessName = user?.businesses[0]?.name ?? t('app.storeFallback')
|
|
|
|
const brandName = useMemo(() => {
|
|
if (locale === 'fa') {
|
|
return nameFa.trim() || nameEn.trim() || fallbackBusinessName
|
|
}
|
|
return nameEn.trim() || nameFa.trim() || fallbackBusinessName
|
|
}, [locale, nameEn, nameFa, fallbackBusinessName])
|
|
|
|
const navItems = useMemo<NavItem[]>(() => {
|
|
const items: NavItem[] = [
|
|
{ type: 'link', id: 'home', icon: Home, labelKey: 'nav.home', to: '/' },
|
|
{
|
|
type: 'link',
|
|
id: 'business-profile',
|
|
icon: Building2,
|
|
labelKey: 'nav.businessProfile',
|
|
to: '/business-profile',
|
|
},
|
|
{
|
|
type: 'group',
|
|
id: 'products',
|
|
icon: ShoppingBag,
|
|
labelKey: 'nav.products',
|
|
basePath: '/products',
|
|
moduleId: 'products',
|
|
children: [
|
|
{ labelKey: 'nav.products.overview', to: '/products' },
|
|
{ labelKey: 'nav.products.list', to: '/products/list' },
|
|
{ labelKey: 'nav.products.new', to: '/products/new' },
|
|
{ labelKey: 'nav.products.categories', to: '/products/categories' },
|
|
{ labelKey: 'nav.products.brands', to: '/products/brands' },
|
|
{ labelKey: 'nav.products.settings', to: '/products/settings' },
|
|
],
|
|
},
|
|
{
|
|
type: 'group',
|
|
id: 'store',
|
|
icon: Store,
|
|
labelKey: 'nav.store',
|
|
basePath: '/store',
|
|
moduleId: 'store',
|
|
children: [
|
|
{ labelKey: 'nav.store.overview', to: '/store' },
|
|
{ labelKey: 'nav.store.items', to: '/store/items' },
|
|
{ labelKey: 'nav.store.orders', to: '/store/orders' },
|
|
{ labelKey: 'nav.store.shipping', to: '/store/shipping' },
|
|
{ labelKey: 'nav.store.cards', to: '/store/cards' },
|
|
{ labelKey: 'nav.store.settings', to: '/store/settings' },
|
|
],
|
|
},
|
|
{ type: 'link', id: 'customers', icon: Users, labelKey: 'nav.customers', to: '/customers' },
|
|
{
|
|
type: 'link',
|
|
id: 'customer-products',
|
|
icon: Package,
|
|
labelKey: 'nav.customerProducts',
|
|
to: '/customer-products',
|
|
moduleId: 'customer_products',
|
|
},
|
|
{
|
|
type: 'group',
|
|
id: 'blog',
|
|
icon: FileText,
|
|
labelKey: 'nav.blog',
|
|
basePath: '/blog',
|
|
moduleId: 'blog',
|
|
children: [
|
|
{ labelKey: 'nav.blog.overview', to: '/blog' },
|
|
{ labelKey: 'nav.blog.list', to: '/blog/list' },
|
|
{ labelKey: 'nav.blog.new', to: '/blog/new' },
|
|
{ labelKey: 'nav.blog.categories', to: '/blog/categories' },
|
|
{ labelKey: 'nav.blog.settings', to: '/blog/settings' },
|
|
],
|
|
},
|
|
{
|
|
type: 'group',
|
|
id: 'portfolios',
|
|
icon: Briefcase,
|
|
labelKey: 'nav.portfolios',
|
|
basePath: '/portfolios',
|
|
moduleId: 'portfolio',
|
|
children: [
|
|
{ labelKey: 'nav.portfolios.overview', to: '/portfolios' },
|
|
{ labelKey: 'nav.portfolios.list', to: '/portfolios/list' },
|
|
{ labelKey: 'nav.portfolios.new', to: '/portfolios/new' },
|
|
{ labelKey: 'nav.portfolios.categories', to: '/portfolios/categories' },
|
|
{ labelKey: 'nav.portfolios.settings', to: '/portfolios/settings' },
|
|
],
|
|
},
|
|
{
|
|
type: 'group',
|
|
id: 'website',
|
|
icon: Globe,
|
|
labelKey: 'nav.website',
|
|
basePath: '/website',
|
|
children: [
|
|
{ labelKey: 'nav.website.overview', to: '/website' },
|
|
{ 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' },
|
|
{ labelKey: 'nav.website.faq', to: '/website/faq' },
|
|
{ labelKey: 'nav.website.badges', to: '/website/badges' },
|
|
{ labelKey: 'nav.website.ePayment', to: '/website/e-payment' },
|
|
],
|
|
},
|
|
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
|
|
]
|
|
|
|
return items.filter(
|
|
(item) => !item.moduleId || hasBusinessModule(enabledModules, item.moduleId),
|
|
)
|
|
}, [enabledModules])
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController()
|
|
|
|
async function loadBranding() {
|
|
try {
|
|
const data = await getBusinessProfile(controller.signal)
|
|
setBrandLogoUrl(data.profile.logoUrl)
|
|
setNameEn(data.profile.nameEn.trim())
|
|
setNameFa(data.profile.nameFa.trim())
|
|
} catch (err) {
|
|
if (isAbortError(err)) return
|
|
setBrandLogoUrl(null)
|
|
setNameEn('')
|
|
setNameFa('')
|
|
}
|
|
}
|
|
|
|
void loadBranding()
|
|
|
|
function handleProfileUpdated() {
|
|
void loadBranding()
|
|
}
|
|
|
|
window.addEventListener(BUSINESS_PROFILE_UPDATED_EVENT, handleProfileUpdated)
|
|
return () => {
|
|
controller.abort()
|
|
window.removeEventListener(BUSINESS_PROFILE_UPDATED_EVENT, handleProfileUpdated)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const activeGroup = navItems.find(
|
|
(item) => item.type === 'group' && isGroupActive(item.basePath, pathname),
|
|
)
|
|
if (!activeGroup || activeGroup.type !== 'group') return
|
|
setOpenGroups({ [activeGroup.id]: true })
|
|
}, [pathname, navItems])
|
|
|
|
function toggleGroup(id: string) {
|
|
setOpenGroups((prev) => {
|
|
const willOpen = !prev[id]
|
|
return willOpen ? { [id]: true } : {}
|
|
})
|
|
}
|
|
|
|
return (
|
|
<aside className={styles.sidebar}>
|
|
<div className={styles.brand}>
|
|
{brandLogoUrl ? (
|
|
<img
|
|
src={brandLogoUrl}
|
|
alt={brandName || t('app.storeFallback')}
|
|
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>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className={styles.nav}>
|
|
{navItems.map((item) => {
|
|
if (item.type === 'link') {
|
|
return (
|
|
<NavLink
|
|
key={item.id}
|
|
to={item.to}
|
|
end={item.to === '/'}
|
|
className={({ isActive }) =>
|
|
`${styles.navItem} ${isActive ? styles.active : ''}`
|
|
}
|
|
>
|
|
<item.icon size={20} />
|
|
<span>{t(item.labelKey)}</span>
|
|
</NavLink>
|
|
)
|
|
}
|
|
|
|
const isOpen = openGroups[item.id] ?? false
|
|
const groupActive = isGroupActive(item.basePath, pathname)
|
|
|
|
return (
|
|
<div key={item.id} className={styles.navGroup}>
|
|
<button
|
|
type="button"
|
|
className={`${styles.navItem} ${styles.navGroupBtn} ${groupActive ? styles.active : ''}`}
|
|
onClick={() => toggleGroup(item.id)}
|
|
aria-expanded={isOpen}
|
|
>
|
|
<item.icon size={20} />
|
|
<span className={styles.navGroupLabel}>{t(item.labelKey)}</span>
|
|
<ChevronDown
|
|
size={16}
|
|
className={`${styles.chevron} ${isOpen ? styles.chevronOpen : ''}`}
|
|
/>
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className={styles.subNav}>
|
|
{item.children.map((child) => (
|
|
<NavLink
|
|
key={child.to}
|
|
to={child.to}
|
|
end={child.to === item.basePath}
|
|
className={({ isActive }) =>
|
|
`${styles.subNavItem} ${isActive ? styles.subNavActive : ''}`
|
|
}
|
|
>
|
|
{t(child.labelKey)}
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
<div className={styles.footer}>
|
|
<button type="button" className={styles.navItem}>
|
|
<HelpCircle size={20} />
|
|
<span>{t('nav.help')}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={styles.navItem}
|
|
onClick={() => {
|
|
logout()
|
|
navigate('/login')
|
|
}}
|
|
>
|
|
<LogOut size={20} />
|
|
<span>{t('nav.logout')}</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|