@nextsparkjs/theme-crm 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CRM_PLAN.md +343 -0
- package/about.md +122 -0
- package/config/app.config.ts +185 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +372 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +336 -0
- package/config/flows.config.ts +511 -0
- package/config/permissions.config.ts +297 -0
- package/config/theme.config.ts +111 -0
- package/entities/activities/activities.config.ts +61 -0
- package/entities/activities/activities.fields.ts +362 -0
- package/entities/activities/activities.service.ts +503 -0
- package/entities/activities/activities.types.ts +117 -0
- package/entities/activities/messages/en.json +123 -0
- package/entities/activities/messages/es.json +123 -0
- package/entities/activities/migrations/020_activities_table.sql +123 -0
- package/entities/activities/migrations/021_activities_metas.sql +114 -0
- package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
- package/entities/campaigns/campaigns.config.ts +61 -0
- package/entities/campaigns/campaigns.fields.ts +413 -0
- package/entities/campaigns/campaigns.service.ts +426 -0
- package/entities/campaigns/campaigns.types.ts +124 -0
- package/entities/campaigns/messages/en.json +145 -0
- package/entities/campaigns/messages/es.json +145 -0
- package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
- package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
- package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
- package/entities/companies/companies.config.ts +61 -0
- package/entities/companies/companies.fields.ts +429 -0
- package/entities/companies/companies.service.ts +566 -0
- package/entities/companies/companies.types.ts +125 -0
- package/entities/companies/messages/en.json +146 -0
- package/entities/companies/messages/es.json +146 -0
- package/entities/companies/migrations/001_companies_table.sql +150 -0
- package/entities/companies/migrations/002_companies_metas.sql +114 -0
- package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
- package/entities/contacts/contacts.config.ts +61 -0
- package/entities/contacts/contacts.fields.ts +359 -0
- package/entities/contacts/contacts.service.ts +509 -0
- package/entities/contacts/contacts.types.ts +108 -0
- package/entities/contacts/messages/en.json +117 -0
- package/entities/contacts/messages/es.json +117 -0
- package/entities/contacts/migrations/001_contacts_table.sql +134 -0
- package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
- package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
- package/entities/leads/leads.config.ts +61 -0
- package/entities/leads/leads.fields.ts +336 -0
- package/entities/leads/leads.service.ts +496 -0
- package/entities/leads/leads.types.ts +114 -0
- package/entities/leads/messages/en.json +132 -0
- package/entities/leads/messages/es.json +132 -0
- package/entities/leads/migrations/001_leads_table.sql +150 -0
- package/entities/leads/migrations/002_leads_metas.sql +120 -0
- package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
- package/entities/notes/messages/en.json +114 -0
- package/entities/notes/messages/es.json +114 -0
- package/entities/notes/migrations/020_notes_table.sql +118 -0
- package/entities/notes/migrations/021_notes_metas.sql +114 -0
- package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
- package/entities/notes/notes.config.ts +61 -0
- package/entities/notes/notes.fields.ts +283 -0
- package/entities/notes/notes.service.ts +320 -0
- package/entities/notes/notes.types.ts +102 -0
- package/entities/opportunities/messages/en.json +107 -0
- package/entities/opportunities/messages/es.json +107 -0
- package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
- package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
- package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
- package/entities/opportunities/opportunities.config.ts +61 -0
- package/entities/opportunities/opportunities.fields.ts +416 -0
- package/entities/opportunities/opportunities.service.ts +525 -0
- package/entities/opportunities/opportunities.types.ts +135 -0
- package/entities/pipelines/messages/en.json +115 -0
- package/entities/pipelines/messages/es.json +115 -0
- package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
- package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
- package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
- package/entities/pipelines/pipelines.config.ts +62 -0
- package/entities/pipelines/pipelines.fields.ts +193 -0
- package/entities/pipelines/pipelines.service.ts +383 -0
- package/entities/pipelines/pipelines.types.ts +78 -0
- package/entities/products/messages/en.json +135 -0
- package/entities/products/messages/es.json +135 -0
- package/entities/products/migrations/001_products_table.sql +117 -0
- package/entities/products/migrations/002_products_metas.sql +114 -0
- package/entities/products/migrations/003_products_sample_data.sql +247 -0
- package/entities/products/products.config.ts +62 -0
- package/entities/products/products.fields.ts +361 -0
- package/entities/products/products.service.ts +437 -0
- package/entities/products/products.types.ts +125 -0
- package/lib/crm-constants.ts +77 -0
- package/lib/crm-utils.ts +185 -0
- package/lib/selectors.ts +333 -0
- package/messages/en.json +131 -0
- package/messages/es.json +131 -0
- package/migrations/999_theme_sample_data.sql +473 -0
- package/package.json +18 -0
- package/pendings.md +205 -0
- package/permissions-matrix.md +216 -0
- package/styles/components.css +414 -0
- package/styles/crm-theme.css +358 -0
- package/styles/globals.css +576 -0
- package/styles/variables.css +111 -0
- package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
- package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
- package/templates/dashboard/(main)/activities/page.tsx +297 -0
- package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
- package/templates/dashboard/(main)/companies/page.tsx +296 -0
- package/templates/dashboard/(main)/contacts/page.tsx +347 -0
- package/templates/dashboard/(main)/layout.tsx +98 -0
- package/templates/dashboard/(main)/leads/page.tsx +335 -0
- package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
- package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
- package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
- package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
- package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
- package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
- package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
- package/templates/dashboard/(main)/products/create/page.tsx +96 -0
- package/templates/dashboard/(main)/products/page.tsx +308 -0
- package/templates/shared/ActionButtons.tsx +41 -0
- package/templates/shared/CRMDashboard.tsx +519 -0
- package/templates/shared/CRMDataTable.tsx +441 -0
- package/templates/shared/CRMMetricCard.tsx +76 -0
- package/templates/shared/CRMMobileNav.tsx +172 -0
- package/templates/shared/CRMSidebar.tsx +346 -0
- package/templates/shared/CRMTopBar.tsx +265 -0
- package/templates/shared/DealCard.tsx +123 -0
- package/templates/shared/EntityCard.tsx +58 -0
- package/templates/shared/OpportunityForm.tsx +649 -0
- package/templates/shared/PipelineForm.tsx +367 -0
- package/templates/shared/PipelineKanban.tsx +194 -0
- package/templates/shared/QuickFilters.tsx +47 -0
- package/templates/shared/StageColumn.tsx +175 -0
- package/templates/shared/StageSelect.tsx +177 -0
- package/templates/shared/StagesRepeater.tsx +317 -0
- package/templates/shared/index.ts +9 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRM Mobile Navigation Component
|
|
3
|
+
* Bottom navigation for mobile devices
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import Link from 'next/link'
|
|
9
|
+
import { usePathname } from 'next/navigation'
|
|
10
|
+
import { useState } from 'react'
|
|
11
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
12
|
+
import { TeamSwitcherCompact } from '@nextsparkjs/core/components/teams/TeamSwitcherCompact'
|
|
13
|
+
import { useTranslations } from 'next-intl'
|
|
14
|
+
import {
|
|
15
|
+
LayoutDashboard,
|
|
16
|
+
Users,
|
|
17
|
+
UserPlus,
|
|
18
|
+
Building2,
|
|
19
|
+
Layers,
|
|
20
|
+
MoreHorizontal,
|
|
21
|
+
X,
|
|
22
|
+
Package,
|
|
23
|
+
Megaphone,
|
|
24
|
+
Calendar,
|
|
25
|
+
Settings
|
|
26
|
+
} from 'lucide-react'
|
|
27
|
+
import { Sheet, SheetContent, SheetTrigger } from '@nextsparkjs/core/components/ui/sheet'
|
|
28
|
+
|
|
29
|
+
interface NavItem {
|
|
30
|
+
href: string
|
|
31
|
+
icon: React.ElementType
|
|
32
|
+
labelKey: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mainNavItems: NavItem[] = [
|
|
36
|
+
{ href: '/dashboard', icon: LayoutDashboard, labelKey: 'common.mobileNav.home' },
|
|
37
|
+
{ href: '/dashboard/leads', icon: UserPlus, labelKey: 'crm.navigation.leads' },
|
|
38
|
+
{ href: '/dashboard/contacts', icon: Users, labelKey: 'crm.navigation.contacts' },
|
|
39
|
+
{ href: '/dashboard/pipelines', icon: Layers, labelKey: 'pipelines.entity.plural' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const moreNavItems: NavItem[] = [
|
|
43
|
+
{ href: '/dashboard/companies', icon: Building2, labelKey: 'crm.navigation.companies' },
|
|
44
|
+
{ href: '/dashboard/products', icon: Package, labelKey: 'products.entity.plural' },
|
|
45
|
+
{ href: '/dashboard/campaigns', icon: Megaphone, labelKey: 'crm.navigation.campaigns' },
|
|
46
|
+
{ href: '/dashboard/activities', icon: Calendar, labelKey: 'crm.navigation.activities' },
|
|
47
|
+
{ href: '/dashboard/settings', icon: Settings, labelKey: 'crm.navigation.settings' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
export function CRMMobileNav() {
|
|
51
|
+
const pathname = usePathname()
|
|
52
|
+
const t = useTranslations()
|
|
53
|
+
const [moreOpen, setMoreOpen] = useState(false)
|
|
54
|
+
|
|
55
|
+
const isActive = (href: string) => {
|
|
56
|
+
if (href === '/dashboard') {
|
|
57
|
+
return pathname === '/dashboard'
|
|
58
|
+
}
|
|
59
|
+
return pathname.startsWith(href)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isMoreActive = moreNavItems.some(item => isActive(item.href))
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
{/* Mobile Top Bar */}
|
|
67
|
+
<header className="lg:hidden fixed top-0 left-0 right-0 z-50 h-14 bg-background/95 backdrop-blur-md border-b border-border" data-cy="crm-mobile-topbar">
|
|
68
|
+
<div className="flex items-center justify-between h-full px-4">
|
|
69
|
+
<Link href="/dashboard" className="flex items-center gap-2">
|
|
70
|
+
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
|
71
|
+
<Layers className="w-4 h-4 text-primary-foreground" />
|
|
72
|
+
</div>
|
|
73
|
+
<span className="font-bold text-lg">CRM</span>
|
|
74
|
+
</Link>
|
|
75
|
+
|
|
76
|
+
<TeamSwitcherCompact />
|
|
77
|
+
</div>
|
|
78
|
+
</header>
|
|
79
|
+
|
|
80
|
+
{/* Mobile Bottom Navigation */}
|
|
81
|
+
<nav className="lg:hidden fixed bottom-0 left-0 right-0 z-50 h-20 bg-background/95 backdrop-blur-md border-t border-border safe-area-inset-bottom" data-cy="crm-mobile-nav">
|
|
82
|
+
<div className="flex items-center justify-around h-full px-2">
|
|
83
|
+
{mainNavItems.map((item) => {
|
|
84
|
+
const Icon = item.icon
|
|
85
|
+
const active = isActive(item.href)
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Link
|
|
89
|
+
key={item.href}
|
|
90
|
+
href={item.href}
|
|
91
|
+
className={cn(
|
|
92
|
+
'flex flex-col items-center justify-center gap-1 min-w-[4rem] py-2 rounded-xl transition-colors',
|
|
93
|
+
active
|
|
94
|
+
? 'text-primary'
|
|
95
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
96
|
+
)}
|
|
97
|
+
data-cy={`crm-mobile-nav-${item.href.split('/').pop() || 'home'}`}
|
|
98
|
+
>
|
|
99
|
+
<div className={cn(
|
|
100
|
+
'flex items-center justify-center w-10 h-7 rounded-lg transition-colors',
|
|
101
|
+
active && 'bg-primary/10'
|
|
102
|
+
)}>
|
|
103
|
+
<Icon className="w-5 h-5" />
|
|
104
|
+
</div>
|
|
105
|
+
<span className="text-[11px] font-medium">{t(item.labelKey)}</span>
|
|
106
|
+
</Link>
|
|
107
|
+
)
|
|
108
|
+
})}
|
|
109
|
+
|
|
110
|
+
{/* More Button */}
|
|
111
|
+
<Sheet open={moreOpen} onOpenChange={setMoreOpen}>
|
|
112
|
+
<SheetTrigger asChild>
|
|
113
|
+
<button
|
|
114
|
+
className={cn(
|
|
115
|
+
'flex flex-col items-center justify-center gap-1 min-w-[4rem] py-2 rounded-xl transition-colors',
|
|
116
|
+
isMoreActive
|
|
117
|
+
? 'text-primary'
|
|
118
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
119
|
+
)}
|
|
120
|
+
data-cy="crm-mobile-nav-more"
|
|
121
|
+
>
|
|
122
|
+
<div className={cn(
|
|
123
|
+
'flex items-center justify-center w-10 h-7 rounded-lg transition-colors',
|
|
124
|
+
isMoreActive && 'bg-primary/10'
|
|
125
|
+
)}>
|
|
126
|
+
<MoreHorizontal className="w-5 h-5" />
|
|
127
|
+
</div>
|
|
128
|
+
<span className="text-[11px] font-medium">{t('common.mobileNav.more')}</span>
|
|
129
|
+
</button>
|
|
130
|
+
</SheetTrigger>
|
|
131
|
+
<SheetContent side="bottom" className="h-auto rounded-t-3xl pb-8" data-cy="crm-mobile-more-sheet">
|
|
132
|
+
<div className="flex justify-center mb-4">
|
|
133
|
+
<div className="w-10 h-1 rounded-full bg-muted" />
|
|
134
|
+
</div>
|
|
135
|
+
<div className="grid grid-cols-4 gap-4">
|
|
136
|
+
{moreNavItems.map((item) => {
|
|
137
|
+
const Icon = item.icon
|
|
138
|
+
const active = isActive(item.href)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Link
|
|
142
|
+
key={item.href}
|
|
143
|
+
href={item.href}
|
|
144
|
+
onClick={() => setMoreOpen(false)}
|
|
145
|
+
className={cn(
|
|
146
|
+
'flex flex-col items-center justify-center gap-2 p-3 rounded-xl transition-colors',
|
|
147
|
+
active
|
|
148
|
+
? 'bg-primary/10 text-primary'
|
|
149
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
150
|
+
)}
|
|
151
|
+
data-cy={`crm-mobile-more-${item.href.split('/').pop()}`}
|
|
152
|
+
>
|
|
153
|
+
<div className={cn(
|
|
154
|
+
'w-12 h-12 rounded-xl flex items-center justify-center',
|
|
155
|
+
active ? 'bg-primary text-primary-foreground' : 'bg-muted'
|
|
156
|
+
)}>
|
|
157
|
+
<Icon className="w-6 h-6" />
|
|
158
|
+
</div>
|
|
159
|
+
<span className="text-xs font-medium">{t(item.labelKey)}</span>
|
|
160
|
+
</Link>
|
|
161
|
+
)
|
|
162
|
+
})}
|
|
163
|
+
</div>
|
|
164
|
+
</SheetContent>
|
|
165
|
+
</Sheet>
|
|
166
|
+
</div>
|
|
167
|
+
</nav>
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default CRMMobileNav
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRM Sidebar Component
|
|
3
|
+
* Professional sidebar with smooth expand-on-hover animation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import Link from 'next/link'
|
|
9
|
+
import { usePathname } from 'next/navigation'
|
|
10
|
+
import { useState, useCallback } from 'react'
|
|
11
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
12
|
+
import { TeamSwitcherCompact } from '@nextsparkjs/core/components/teams/TeamSwitcherCompact'
|
|
13
|
+
import { useAuth } from '@nextsparkjs/core/hooks/useAuth'
|
|
14
|
+
import Image from 'next/image'
|
|
15
|
+
import {
|
|
16
|
+
LayoutDashboard,
|
|
17
|
+
Users,
|
|
18
|
+
UserPlus,
|
|
19
|
+
Building2,
|
|
20
|
+
Target,
|
|
21
|
+
Layers,
|
|
22
|
+
Package,
|
|
23
|
+
Megaphone,
|
|
24
|
+
Calendar,
|
|
25
|
+
Settings,
|
|
26
|
+
ChevronLeft,
|
|
27
|
+
ChevronRight,
|
|
28
|
+
LogOut,
|
|
29
|
+
Sparkles
|
|
30
|
+
} from 'lucide-react'
|
|
31
|
+
|
|
32
|
+
interface NavItem {
|
|
33
|
+
href: string
|
|
34
|
+
icon: React.ElementType
|
|
35
|
+
label: string
|
|
36
|
+
badge?: number
|
|
37
|
+
color?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mainNavItems: NavItem[] = [
|
|
41
|
+
{ href: '/dashboard', icon: LayoutDashboard, label: 'Dashboard', color: 'text-primary' },
|
|
42
|
+
{ href: '/dashboard/leads', icon: UserPlus, label: 'Leads', color: 'text-amber-500' },
|
|
43
|
+
{ href: '/dashboard/contacts', icon: Users, label: 'Contacts', color: 'text-emerald-500' },
|
|
44
|
+
{ href: '/dashboard/companies', icon: Building2, label: 'Companies', color: 'text-blue-500' },
|
|
45
|
+
{ href: '/dashboard/pipelines', icon: Layers, label: 'Pipelines', color: 'text-violet-500' },
|
|
46
|
+
{ href: '/dashboard/products', icon: Package, label: 'Products', color: 'text-rose-500' },
|
|
47
|
+
{ href: '/dashboard/campaigns', icon: Megaphone, label: 'Campaigns', color: 'text-orange-500' },
|
|
48
|
+
{ href: '/dashboard/activities', icon: Calendar, label: 'Activities', color: 'text-cyan-500' },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const bottomNavItems: NavItem[] = [
|
|
52
|
+
{ href: '/dashboard/settings', icon: Settings, label: 'Settings' },
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
// Tooltip component
|
|
56
|
+
function Tooltip({ children, content, side = 'right', show = true }: {
|
|
57
|
+
children: React.ReactNode
|
|
58
|
+
content: string
|
|
59
|
+
side?: 'right' | 'bottom'
|
|
60
|
+
show?: boolean
|
|
61
|
+
}) {
|
|
62
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
className="relative"
|
|
67
|
+
onMouseEnter={() => setIsVisible(true)}
|
|
68
|
+
onMouseLeave={() => setIsVisible(false)}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
{show && isVisible && (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
'absolute z-[60] px-3 py-1.5 text-sm font-medium text-white bg-foreground rounded-lg whitespace-nowrap shadow-lg',
|
|
75
|
+
'animate-in fade-in-0 zoom-in-95 duration-100',
|
|
76
|
+
side === 'right' && 'left-full ml-3 top-1/2 -translate-y-1/2',
|
|
77
|
+
side === 'bottom' && 'top-full mt-2 left-1/2 -translate-x-1/2'
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{content}
|
|
81
|
+
{/* Arrow */}
|
|
82
|
+
<div
|
|
83
|
+
className={cn(
|
|
84
|
+
'absolute w-2 h-2 bg-foreground rotate-45',
|
|
85
|
+
side === 'right' && 'left-0 top-1/2 -translate-y-1/2 -translate-x-1',
|
|
86
|
+
side === 'bottom' && 'top-0 left-1/2 -translate-x-1/2 -translate-y-1'
|
|
87
|
+
)}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Nav item component
|
|
96
|
+
function NavItemButton({ item, isActive, expanded }: {
|
|
97
|
+
item: NavItem
|
|
98
|
+
isActive: boolean
|
|
99
|
+
expanded: boolean
|
|
100
|
+
}) {
|
|
101
|
+
const Icon = item.icon
|
|
102
|
+
const slug = item.href.split('/').pop() || 'dashboard'
|
|
103
|
+
|
|
104
|
+
const button = (
|
|
105
|
+
<Link
|
|
106
|
+
href={item.href}
|
|
107
|
+
className={cn(
|
|
108
|
+
'group/item flex items-center gap-3 rounded-xl transition-all duration-200 relative overflow-hidden',
|
|
109
|
+
expanded ? 'px-3 py-2.5' : 'p-2.5 justify-center',
|
|
110
|
+
isActive
|
|
111
|
+
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/25'
|
|
112
|
+
: 'text-muted-foreground hover:bg-muted/80 hover:text-foreground'
|
|
113
|
+
)}
|
|
114
|
+
data-cy={`crm-sidebar-nav-${slug}`}
|
|
115
|
+
>
|
|
116
|
+
{/* Gradient overlay for active state */}
|
|
117
|
+
{isActive && (
|
|
118
|
+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
<Icon className={cn(
|
|
122
|
+
'shrink-0 w-5 h-5 relative z-10 transition-transform duration-200',
|
|
123
|
+
!isActive && 'group-hover/item:scale-110',
|
|
124
|
+
!isActive && item.color
|
|
125
|
+
)} />
|
|
126
|
+
|
|
127
|
+
{expanded && (
|
|
128
|
+
<span className="text-sm font-medium truncate relative z-10">{item.label}</span>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{expanded && item.badge && item.badge > 0 && (
|
|
132
|
+
<span className="ml-auto px-2 py-0.5 text-xs font-semibold bg-white/20 text-white rounded-full relative z-10">
|
|
133
|
+
{item.badge}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</Link>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if (expanded) {
|
|
140
|
+
return button
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Tooltip content={item.label} show={!expanded}>
|
|
145
|
+
{button}
|
|
146
|
+
</Tooltip>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function CRMSidebar() {
|
|
151
|
+
const pathname = usePathname()
|
|
152
|
+
const { user, signOut } = useAuth()
|
|
153
|
+
const [expanded, setExpanded] = useState(false)
|
|
154
|
+
|
|
155
|
+
const isActive = (href: string) => {
|
|
156
|
+
if (href === '/dashboard') {
|
|
157
|
+
return pathname === '/dashboard'
|
|
158
|
+
}
|
|
159
|
+
return pathname.startsWith(href)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const getUserInitials = useCallback(() => {
|
|
163
|
+
if (!user) return 'U'
|
|
164
|
+
if (user.firstName && user.lastName) {
|
|
165
|
+
return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
|
|
166
|
+
}
|
|
167
|
+
if (user.firstName) {
|
|
168
|
+
return user.firstName.slice(0, 2).toUpperCase()
|
|
169
|
+
}
|
|
170
|
+
return user.email?.slice(0, 2).toUpperCase() || 'U'
|
|
171
|
+
}, [user])
|
|
172
|
+
|
|
173
|
+
const handleSignOut = useCallback(async () => {
|
|
174
|
+
try {
|
|
175
|
+
await signOut()
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error('Sign out failed:', error)
|
|
178
|
+
}
|
|
179
|
+
}, [signOut])
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<aside
|
|
183
|
+
data-cy="crm-sidebar"
|
|
184
|
+
className={cn(
|
|
185
|
+
'hidden lg:flex flex-col fixed left-0 top-0 h-screen z-50 transition-all duration-300 ease-out',
|
|
186
|
+
'bg-gradient-to-b from-card via-card to-card/95 border-r border-border/50',
|
|
187
|
+
'shadow-[2px_0_20px_-5px_rgba(0,0,0,0.1)]',
|
|
188
|
+
expanded ? 'w-64' : 'w-16'
|
|
189
|
+
)}
|
|
190
|
+
onMouseEnter={() => setExpanded(true)}
|
|
191
|
+
onMouseLeave={() => setExpanded(false)}
|
|
192
|
+
style={{
|
|
193
|
+
// CSS variable for layout
|
|
194
|
+
['--crm-sidebar-width' as any]: expanded ? '16rem' : '4rem'
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{/* Header with Logo */}
|
|
198
|
+
<div className={cn(
|
|
199
|
+
'flex items-center h-16 border-b border-border/50',
|
|
200
|
+
expanded ? 'px-4 justify-between' : 'justify-center'
|
|
201
|
+
)}>
|
|
202
|
+
<Link href="/dashboard" className="flex items-center gap-3 group" data-cy="crm-sidebar-logo">
|
|
203
|
+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/80 flex items-center justify-center shrink-0 shadow-lg shadow-primary/20 transition-transform group-hover:scale-105">
|
|
204
|
+
<Sparkles className="w-5 h-5 text-primary-foreground" />
|
|
205
|
+
</div>
|
|
206
|
+
{expanded && (
|
|
207
|
+
<div className="animate-in fade-in slide-in-from-left-2 duration-200">
|
|
208
|
+
<span className="font-bold text-lg text-foreground tracking-tight">SalesHub</span>
|
|
209
|
+
<span className="text-[10px] text-muted-foreground block -mt-0.5 uppercase tracking-widest">CRM Pro</span>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</Link>
|
|
213
|
+
{expanded && (
|
|
214
|
+
<button
|
|
215
|
+
onClick={(e) => {
|
|
216
|
+
e.stopPropagation()
|
|
217
|
+
setExpanded(false)
|
|
218
|
+
}}
|
|
219
|
+
className="p-1.5 rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-all hover:scale-105"
|
|
220
|
+
data-cy="crm-sidebar-collapse-btn"
|
|
221
|
+
>
|
|
222
|
+
<ChevronLeft className="w-4 h-4" />
|
|
223
|
+
</button>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Main Navigation */}
|
|
228
|
+
<nav className={cn('flex-1 overflow-y-auto py-4 scrollbar-thin', expanded ? 'px-3' : 'px-2')}>
|
|
229
|
+
{expanded && (
|
|
230
|
+
<p className="px-3 text-[11px] font-semibold text-muted-foreground uppercase tracking-widest mb-3 animate-in fade-in duration-200">
|
|
231
|
+
Navigation
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
<div className="space-y-1">
|
|
235
|
+
{mainNavItems.map((item, index) => (
|
|
236
|
+
<div
|
|
237
|
+
key={item.href}
|
|
238
|
+
style={{ animationDelay: `${index * 30}ms` }}
|
|
239
|
+
className={expanded ? 'animate-in fade-in slide-in-from-left-2' : ''}
|
|
240
|
+
>
|
|
241
|
+
<NavItemButton
|
|
242
|
+
item={item}
|
|
243
|
+
isActive={isActive(item.href)}
|
|
244
|
+
expanded={expanded}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
</nav>
|
|
250
|
+
|
|
251
|
+
{/* Bottom Section */}
|
|
252
|
+
<div className={cn('border-t border-border/50 py-3', expanded ? 'px-3' : 'px-2')}>
|
|
253
|
+
{/* Settings */}
|
|
254
|
+
<div className="space-y-1 mb-3">
|
|
255
|
+
{bottomNavItems.map((item) => (
|
|
256
|
+
<NavItemButton
|
|
257
|
+
key={item.href}
|
|
258
|
+
item={item}
|
|
259
|
+
isActive={isActive(item.href)}
|
|
260
|
+
expanded={expanded}
|
|
261
|
+
/>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Team Switcher */}
|
|
266
|
+
{expanded && (
|
|
267
|
+
<div className="mb-3 animate-in fade-in slide-in-from-left-2 duration-200">
|
|
268
|
+
<TeamSwitcherCompact />
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* User Profile Section */}
|
|
273
|
+
<div className={cn(
|
|
274
|
+
'rounded-xl transition-all duration-200',
|
|
275
|
+
expanded ? 'bg-muted/50 p-3' : 'flex justify-center'
|
|
276
|
+
)}>
|
|
277
|
+
{expanded ? (
|
|
278
|
+
<div className="flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200">
|
|
279
|
+
{user?.image ? (
|
|
280
|
+
<Image
|
|
281
|
+
src={user.image}
|
|
282
|
+
alt=""
|
|
283
|
+
width={36}
|
|
284
|
+
height={36}
|
|
285
|
+
className="w-9 h-9 rounded-full object-cover ring-2 ring-background"
|
|
286
|
+
data-cy="crm-sidebar-user-avatar"
|
|
287
|
+
/>
|
|
288
|
+
) : (
|
|
289
|
+
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-primary to-primary/80 flex items-center justify-center text-xs font-bold text-primary-foreground ring-2 ring-background" data-cy="crm-sidebar-user-avatar">
|
|
290
|
+
{getUserInitials()}
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
<div className="flex-1 min-w-0">
|
|
294
|
+
<p className="text-sm font-medium text-foreground truncate" data-cy="crm-sidebar-user-name">
|
|
295
|
+
{user?.firstName || 'User'}
|
|
296
|
+
</p>
|
|
297
|
+
<p className="text-xs text-muted-foreground truncate" data-cy="crm-sidebar-user-email">
|
|
298
|
+
{user?.email}
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
<Tooltip content="Sign out" side="bottom">
|
|
302
|
+
<button
|
|
303
|
+
onClick={handleSignOut}
|
|
304
|
+
className="p-1.5 rounded-lg text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
|
|
305
|
+
data-cy="crm-sidebar-signout-btn"
|
|
306
|
+
>
|
|
307
|
+
<LogOut className="w-4 h-4" />
|
|
308
|
+
</button>
|
|
309
|
+
</Tooltip>
|
|
310
|
+
</div>
|
|
311
|
+
) : (
|
|
312
|
+
<Tooltip content={user?.firstName || 'Profile'}>
|
|
313
|
+
<Link
|
|
314
|
+
href="/dashboard/settings/profile"
|
|
315
|
+
className="block"
|
|
316
|
+
>
|
|
317
|
+
{user?.image ? (
|
|
318
|
+
<Image
|
|
319
|
+
src={user.image}
|
|
320
|
+
alt=""
|
|
321
|
+
width={36}
|
|
322
|
+
height={36}
|
|
323
|
+
className="w-9 h-9 rounded-full object-cover ring-2 ring-border hover:ring-primary transition-all"
|
|
324
|
+
data-cy="crm-sidebar-user-avatar"
|
|
325
|
+
/>
|
|
326
|
+
) : (
|
|
327
|
+
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-primary to-primary/80 flex items-center justify-center text-xs font-bold text-primary-foreground ring-2 ring-border hover:ring-primary transition-all" data-cy="crm-sidebar-user-avatar">
|
|
328
|
+
{getUserInitials()}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</Link>
|
|
332
|
+
</Tooltip>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* Hover indicator edge */}
|
|
338
|
+
<div className={cn(
|
|
339
|
+
'absolute right-0 top-0 bottom-0 w-1 bg-gradient-to-b from-primary/0 via-primary/50 to-primary/0 opacity-0 transition-opacity duration-300',
|
|
340
|
+
expanded && 'opacity-100'
|
|
341
|
+
)} />
|
|
342
|
+
</aside>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export default CRMSidebar
|