Add FA/EN locale, home activity charts, and theme-aware polish.

Ship shared LocaleProvider, business/customer i18n, branding defaultLocale, curated home tiles with dual 30-day charts, and chart/page aura tokens; refresh PROJECT_CONTEXT.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-01 09:35:27 +03:30
co-authored by Cursor
parent f5b2193ba1
commit 66004a0fba
112 changed files with 4338 additions and 1061 deletions
+147 -122
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
import {
Home,
@@ -15,7 +15,10 @@ import {
Building2,
} 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,
@@ -26,111 +29,29 @@ import meshkeeLogo from '../assets/meshkee-logo.png'
import styles from './Sidebar.module.css'
interface NavChild {
label: string
labelKey: BusinessMessageKey
to: string
}
interface NavGroup {
type: 'group'
id: string
icon: LucideIcon
label: string
labelKey: BusinessMessageKey
basePath: string
children: NavChild[]
}
interface NavLinkItem {
type: 'link'
id: string
icon: LucideIcon
label: string
labelKey: BusinessMessageKey
to: string
}
type NavItem = NavLinkItem | NavGroup
const navItems: NavItem[] = [
{ type: 'link', icon: Home, label: 'Home', to: '/' },
{ type: 'link', icon: Building2, label: 'Business Profile', to: '/business-profile' },
{
type: 'group',
icon: ShoppingBag,
label: 'Products',
basePath: '/products',
children: [
{ label: 'Overview', to: '/products' },
{ label: 'My Products', to: '/products/list' },
{ label: 'Add New Product', to: '/products/new' },
{ label: 'Categories', to: '/products/categories' },
{ label: 'Brands', to: '/products/brands' },
{ label: 'Settings', to: '/products/settings' },
],
},
{
type: 'group',
icon: Store,
label: 'Store',
basePath: '/store',
children: [
{ label: 'Overview', to: '/store' },
{ label: 'My Store Items', to: '/store/items' },
{ label: 'My Orders', to: '/store/orders' },
{ label: 'Shipping Fees', to: '/store/shipping' },
{ label: 'Shopping Cards', to: '/store/cards' },
{ label: 'Settings', to: '/store/settings' },
],
},
{ type: 'link', icon: Users, label: 'Customers', to: '/customers' },
{ type: 'link', icon: Settings, label: 'Settings', to: '/settings' },
{
type: 'group',
icon: FileText,
label: 'Blog',
basePath: '/blog',
children: [
{ label: 'Overview', to: '/blog' },
{ label: 'My Blogs', to: '/blog/list' },
{ label: 'Add New Blog', to: '/blog/new' },
{ label: 'Categories', to: '/blog/categories' },
{ label: 'Settings', to: '/blog/settings' },
],
},
{
type: 'group',
icon: Briefcase,
label: 'Portfolios',
basePath: '/portfolios',
children: [
{ label: 'Overview', to: '/portfolios' },
{ label: 'My Portfolios', to: '/portfolios/list' },
{ label: 'Add New Portfolio', to: '/portfolios/new' },
{ label: 'Categories', to: '/portfolios/categories' },
{ label: 'Settings', to: '/portfolios/settings' },
],
},
{
type: 'group',
icon: Globe,
label: 'Website',
basePath: '/website',
children: [
{ label: 'Overview', to: '/website' },
{ label: 'Sliders', to: '/website/sliders' },
{ label: 'Special Categories', to: '/website/special-categories' },
{ label: 'Special Brands', to: '/website/special-brands' },
{ label: 'Special Items', to: '/website/special-items' },
{ label: 'Contact Us Form', to: '/website/contact' },
{ label: 'Subscriptions', to: '/website/subscriptions' },
{ label: 'FAQ', to: '/website/faq' },
{ label: 'Badges', to: '/website/badges' },
{ label: 'E-Payment', to: '/website/e-payment' },
],
},
]
const footerItems = [
{ icon: HelpCircle, label: 'Help Center' },
{ icon: LogOut, label: 'Logout' },
]
function isGroupActive(basePath: string, pathname: string) {
return pathname === basePath || pathname.startsWith(`${basePath}/`)
}
@@ -139,12 +60,115 @@ export function Sidebar() {
const { pathname } = useLocation()
const navigate = useNavigate()
const { user, logout } = useAuth()
const { locale } = useLocale()
const t = useT()
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
const [brandLogoUrl, setBrandLogoUrl] = useState<string | null>(null)
const [brandName, setBrandName] = useState('')
const [nameEn, setNameEn] = useState('')
const [nameFa, setNameFa] = useState('')
const businessDomain = getActiveBusinessDomain()
const fallbackBusinessName = user?.businesses[0]?.name ?? 'Business'
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[]>(
() => [
{ 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',
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',
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: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
{
type: 'group',
id: 'blog',
icon: FileText,
labelKey: 'nav.blog',
basePath: '/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',
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' },
],
},
],
[],
)
useEffect(() => {
const controller = new AbortController()
@@ -153,11 +177,13 @@ export function Sidebar() {
try {
const data = await getBusinessProfile(controller.signal)
setBrandLogoUrl(data.profile.logoUrl)
setBrandName(data.profile.nameEn.trim() || data.profile.nameFa.trim() || fallbackBusinessName)
setNameEn(data.profile.nameEn.trim())
setNameFa(data.profile.nameFa.trim())
} catch (err) {
if (isAbortError(err)) return
setBrandLogoUrl(null)
setBrandName(fallbackBusinessName)
setNameEn('')
setNameFa('')
}
}
@@ -172,18 +198,18 @@ export function Sidebar() {
controller.abort()
window.removeEventListener(BUSINESS_PROFILE_UPDATED_EVENT, handleProfileUpdated)
}
}, [fallbackBusinessName])
}, [])
useEffect(() => {
navItems.forEach((item) => {
if (item.type === 'group' && isGroupActive(item.basePath, pathname)) {
setOpenGroups((prev) => ({ ...prev, [item.label]: true }))
setOpenGroups((prev) => ({ ...prev, [item.id]: true }))
}
})
}, [pathname])
}, [pathname, navItems])
function toggleGroup(label: string) {
setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] }))
function toggleGroup(id: string) {
setOpenGroups((prev) => ({ ...prev, [id]: !prev[id] }))
}
return (
@@ -191,12 +217,12 @@ export function Sidebar() {
<div className={styles.brand}>
<img
src={brandLogoUrl ?? meshkeeLogo}
alt={brandName || 'Business logo'}
alt={brandName || t('app.storeFallback')}
className={`${styles.brandLogo} ${brandLogoUrl ? styles.brandLogoUploaded : ''}`}
/>
<div className={styles.brandText}>
<span className={styles.brandDomain}>{businessDomain}</span>
<span className={styles.brandName}>{brandName || fallbackBusinessName}</span>
<span className={styles.brandDomain}>{businessDomain}</span>
</div>
</div>
@@ -205,7 +231,7 @@ export function Sidebar() {
if (item.type === 'link') {
return (
<NavLink
key={item.label}
key={item.id}
to={item.to}
end={item.to === '/'}
className={({ isActive }) =>
@@ -213,24 +239,24 @@ export function Sidebar() {
}
>
<item.icon size={20} />
<span>{item.label}</span>
<span>{t(item.labelKey)}</span>
</NavLink>
)
}
const isOpen = openGroups[item.label] ?? false
const isOpen = openGroups[item.id] ?? false
const groupActive = isGroupActive(item.basePath, pathname)
return (
<div key={item.label} className={styles.navGroup}>
<div key={item.id} className={styles.navGroup}>
<button
type="button"
className={`${styles.navItem} ${styles.navGroupBtn} ${groupActive ? styles.active : ''}`}
onClick={() => toggleGroup(item.label)}
onClick={() => toggleGroup(item.id)}
aria-expanded={isOpen}
>
<item.icon size={20} />
<span className={styles.navGroupLabel}>{item.label}</span>
<span className={styles.navGroupLabel}>{t(item.labelKey)}</span>
<ChevronDown
size={16}
className={`${styles.chevron} ${isOpen ? styles.chevronOpen : ''}`}
@@ -248,7 +274,7 @@ export function Sidebar() {
`${styles.subNavItem} ${isActive ? styles.subNavActive : ''}`
}
>
{child.label}
{t(child.labelKey)}
</NavLink>
))}
</div>
@@ -259,22 +285,21 @@ export function Sidebar() {
</nav>
<div className={styles.footer}>
{footerItems.map(({ icon: Icon, label }) => (
<button
key={label}
type="button"
className={styles.navItem}
onClick={() => {
if (label === 'Logout') {
logout()
navigate('/login')
}
}}
>
<Icon size={20} />
<span>{label}</span>
</button>
))}
<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>
)