mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Home,
|
||||
ShoppingBag,
|
||||
Store,
|
||||
Users,
|
||||
Settings,
|
||||
FileText,
|
||||
Briefcase,
|
||||
Globe,
|
||||
HelpCircle,
|
||||
LogOut,
|
||||
ChevronDown,
|
||||
Building2,
|
||||
} from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import {
|
||||
BUSINESS_PROFILE_UPDATED_EVENT,
|
||||
getActiveBusinessDomain,
|
||||
} from '../lib/businessContext'
|
||||
import { isAbortError } from '../lib/api'
|
||||
import { getBusinessProfile } from '../services/businessProfileService'
|
||||
import meshkeeLogo from '../assets/meshkee-logo.png'
|
||||
import styles from './Sidebar.module.css'
|
||||
|
||||
interface NavChild {
|
||||
label: string
|
||||
to: string
|
||||
}
|
||||
|
||||
interface NavGroup {
|
||||
type: 'group'
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
basePath: string
|
||||
children: NavChild[]
|
||||
}
|
||||
|
||||
interface NavLinkItem {
|
||||
type: 'link'
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
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}/`)
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const { pathname } = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuth()
|
||||
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
|
||||
const [brandLogoUrl, setBrandLogoUrl] = useState<string | null>(null)
|
||||
const [brandName, setBrandName] = useState('')
|
||||
|
||||
const businessDomain = getActiveBusinessDomain()
|
||||
const fallbackBusinessName = user?.businesses[0]?.name ?? 'Business'
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
async function loadBranding() {
|
||||
try {
|
||||
const data = await getBusinessProfile(controller.signal)
|
||||
setBrandLogoUrl(data.profile.logoUrl)
|
||||
setBrandName(data.profile.nameEn.trim() || data.profile.nameFa.trim() || fallbackBusinessName)
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) return
|
||||
setBrandLogoUrl(null)
|
||||
setBrandName(fallbackBusinessName)
|
||||
}
|
||||
}
|
||||
|
||||
void loadBranding()
|
||||
|
||||
function handleProfileUpdated() {
|
||||
void loadBranding()
|
||||
}
|
||||
|
||||
window.addEventListener(BUSINESS_PROFILE_UPDATED_EVENT, handleProfileUpdated)
|
||||
return () => {
|
||||
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 }))
|
||||
}
|
||||
})
|
||||
}, [pathname])
|
||||
|
||||
function toggleGroup(label: string) {
|
||||
setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] }))
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<div className={styles.brand}>
|
||||
<img
|
||||
src={brandLogoUrl ?? meshkeeLogo}
|
||||
alt={brandName || 'Business logo'}
|
||||
className={`${styles.brandLogo} ${brandLogoUrl ? styles.brandLogoUploaded : ''}`}
|
||||
/>
|
||||
<div className={styles.brandText}>
|
||||
<span className={styles.brandDomain}>{businessDomain}</span>
|
||||
<span className={styles.brandName}>{brandName || fallbackBusinessName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className={styles.nav}>
|
||||
{navItems.map((item) => {
|
||||
if (item.type === 'link') {
|
||||
return (
|
||||
<NavLink
|
||||
key={item.label}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
className={({ isActive }) =>
|
||||
`${styles.navItem} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
const isOpen = openGroups[item.label] ?? false
|
||||
const groupActive = isGroupActive(item.basePath, pathname)
|
||||
|
||||
return (
|
||||
<div key={item.label} className={styles.navGroup}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.navItem} ${styles.navGroupBtn} ${groupActive ? styles.active : ''}`}
|
||||
onClick={() => toggleGroup(item.label)}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span className={styles.navGroupLabel}>{item.label}</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 : ''}`
|
||||
}
|
||||
>
|
||||
{child.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user