@nextsparkjs/theme-productivity 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/README.md +76 -0
- package/about.md +123 -0
- package/components/CardDetailModal.tsx +318 -0
- package/components/KanbanBoard.tsx +612 -0
- package/components/KanbanCard.tsx +218 -0
- package/components/KanbanColumn.tsx +264 -0
- package/components/SortableList.tsx +46 -0
- package/components/index.ts +4 -0
- package/config/app.config.ts +172 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +357 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +256 -0
- package/config/flows.config.ts +484 -0
- package/config/permissions.config.ts +167 -0
- package/config/theme.config.ts +106 -0
- package/entities/boards/boards.config.ts +61 -0
- package/entities/boards/boards.fields.ts +154 -0
- package/entities/boards/boards.service.ts +256 -0
- package/entities/boards/boards.types.ts +57 -0
- package/entities/boards/messages/en.json +80 -0
- package/entities/boards/messages/es.json +80 -0
- package/entities/boards/migrations/001_boards_table.sql +83 -0
- package/entities/cards/cards.config.ts +61 -0
- package/entities/cards/cards.fields.ts +242 -0
- package/entities/cards/cards.service.ts +336 -0
- package/entities/cards/cards.types.ts +79 -0
- package/entities/cards/messages/en.json +114 -0
- package/entities/cards/messages/es.json +114 -0
- package/entities/cards/migrations/020_cards_table.sql +92 -0
- package/entities/lists/lists.config.ts +61 -0
- package/entities/lists/lists.fields.ts +105 -0
- package/entities/lists/lists.service.ts +252 -0
- package/entities/lists/lists.types.ts +55 -0
- package/entities/lists/messages/en.json +60 -0
- package/entities/lists/messages/es.json +60 -0
- package/entities/lists/migrations/010_lists_table.sql +79 -0
- package/lib/selectors.ts +206 -0
- package/messages/en.json +79 -0
- package/messages/es.json +79 -0
- package/migrations/999_theme_sample_data.sql +922 -0
- package/migrations/999a_initial_sample_data.sql +377 -0
- package/migrations/999b_abundant_sample_data.sql +346 -0
- package/package.json +17 -0
- package/permissions-matrix.md +122 -0
- package/styles/components.css +460 -0
- package/styles/globals.css +560 -0
- package/templates/dashboard/(main)/boards/[id]/[cardId]/page.tsx +238 -0
- package/templates/dashboard/(main)/boards/[id]/edit/page.tsx +390 -0
- package/templates/dashboard/(main)/boards/[id]/page.tsx +236 -0
- package/templates/dashboard/(main)/boards/create/page.tsx +236 -0
- package/templates/dashboard/(main)/boards/page.tsx +335 -0
- package/templates/dashboard/(main)/layout.tsx +32 -0
- package/templates/dashboard/(main)/page.tsx +592 -0
- package/templates/shared/ProductivityMobileNav.tsx +410 -0
- package/templates/shared/ProductivitySidebar.tsx +538 -0
- package/templates/shared/ProductivityTopBar.tsx +317 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Productivity Sidebar - Board-Focused Design
|
|
3
|
+
* Clean, always-visible sidebar optimized for board navigation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import Link from 'next/link'
|
|
9
|
+
import Image from 'next/image'
|
|
10
|
+
import { usePathname, useParams, useRouter } from 'next/navigation'
|
|
11
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
12
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
13
|
+
import { useAuth } from '@nextsparkjs/core/hooks/useAuth'
|
|
14
|
+
import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
|
|
15
|
+
import { useTeamsConfig } from '@nextsparkjs/core/hooks/useTeamsConfig'
|
|
16
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from '@nextsparkjs/core/components/ui/dropdown-menu'
|
|
24
|
+
import {
|
|
25
|
+
Dialog,
|
|
26
|
+
DialogContent,
|
|
27
|
+
DialogHeader,
|
|
28
|
+
DialogTitle,
|
|
29
|
+
DialogTrigger,
|
|
30
|
+
} from '@nextsparkjs/core/components/ui/dialog'
|
|
31
|
+
import { Input } from '@nextsparkjs/core/components/ui/input'
|
|
32
|
+
import { Label } from '@nextsparkjs/core/components/ui/label'
|
|
33
|
+
import {
|
|
34
|
+
Kanban,
|
|
35
|
+
Plus,
|
|
36
|
+
ChevronDown,
|
|
37
|
+
LogOut,
|
|
38
|
+
Settings,
|
|
39
|
+
Users,
|
|
40
|
+
Check,
|
|
41
|
+
Loader2,
|
|
42
|
+
LayoutGrid,
|
|
43
|
+
Star,
|
|
44
|
+
MoreHorizontal,
|
|
45
|
+
Pencil,
|
|
46
|
+
Trash2,
|
|
47
|
+
Archive
|
|
48
|
+
} from 'lucide-react'
|
|
49
|
+
|
|
50
|
+
interface Board {
|
|
51
|
+
id: string
|
|
52
|
+
name: string
|
|
53
|
+
color: string
|
|
54
|
+
description?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Team {
|
|
58
|
+
id: string
|
|
59
|
+
name: string
|
|
60
|
+
type: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Board colors with CSS variables
|
|
64
|
+
const BOARD_COLORS = [
|
|
65
|
+
{ id: 'blue', class: 'bg-[var(--board-blue)]', label: 'Blue' },
|
|
66
|
+
{ id: 'green', class: 'bg-[var(--board-green)]', label: 'Green' },
|
|
67
|
+
{ id: 'purple', class: 'bg-[var(--board-purple)]', label: 'Purple' },
|
|
68
|
+
{ id: 'orange', class: 'bg-[var(--board-orange)]', label: 'Orange' },
|
|
69
|
+
{ id: 'red', class: 'bg-[var(--board-red)]', label: 'Red' },
|
|
70
|
+
{ id: 'pink', class: 'bg-[var(--board-pink)]', label: 'Pink' },
|
|
71
|
+
{ id: 'gray', class: 'bg-[var(--board-gray)]', label: 'Gray' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
const getColorClass = (color: string) => {
|
|
75
|
+
return BOARD_COLORS.find(c => c.id === color)?.class || BOARD_COLORS[0].class
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Quick Create Board Dialog
|
|
79
|
+
function CreateBoardDialog({
|
|
80
|
+
open,
|
|
81
|
+
onOpenChange,
|
|
82
|
+
onCreated
|
|
83
|
+
}: {
|
|
84
|
+
open: boolean
|
|
85
|
+
onOpenChange: (open: boolean) => void
|
|
86
|
+
onCreated: () => void
|
|
87
|
+
}) {
|
|
88
|
+
const { currentTeam } = useTeamContext()
|
|
89
|
+
const router = useRouter()
|
|
90
|
+
const [name, setName] = useState('')
|
|
91
|
+
const [color, setColor] = useState('blue')
|
|
92
|
+
const [isCreating, setIsCreating] = useState(false)
|
|
93
|
+
|
|
94
|
+
const handleCreate = async () => {
|
|
95
|
+
if (!name.trim() || !currentTeam?.id) return
|
|
96
|
+
|
|
97
|
+
setIsCreating(true)
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch('/api/v1/boards', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
'x-team-id': currentTeam.id,
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify({ name: name.trim(), color }),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (response.ok) {
|
|
109
|
+
const data = await response.json()
|
|
110
|
+
setName('')
|
|
111
|
+
setColor('blue')
|
|
112
|
+
onOpenChange(false)
|
|
113
|
+
onCreated()
|
|
114
|
+
router.push(`/dashboard/boards/${data.data.id}`)
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Failed to create board:', error)
|
|
118
|
+
} finally {
|
|
119
|
+
setIsCreating(false)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
125
|
+
<DialogContent className="sm:max-w-md">
|
|
126
|
+
<DialogHeader>
|
|
127
|
+
<DialogTitle>Create new board</DialogTitle>
|
|
128
|
+
</DialogHeader>
|
|
129
|
+
<div className="space-y-4 pt-4">
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<Label htmlFor="board-name">Board name</Label>
|
|
132
|
+
<Input
|
|
133
|
+
id="board-name"
|
|
134
|
+
placeholder="My awesome board"
|
|
135
|
+
value={name}
|
|
136
|
+
onChange={(e) => setName(e.target.value)}
|
|
137
|
+
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
|
138
|
+
autoFocus
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
<Label>Color</Label>
|
|
143
|
+
<div className="flex gap-2">
|
|
144
|
+
{BOARD_COLORS.map((c) => (
|
|
145
|
+
<button
|
|
146
|
+
key={c.id}
|
|
147
|
+
onClick={() => setColor(c.id)}
|
|
148
|
+
className={cn(
|
|
149
|
+
'w-8 h-8 rounded-lg transition-all',
|
|
150
|
+
c.class,
|
|
151
|
+
color === c.id
|
|
152
|
+
? 'ring-2 ring-offset-2 ring-primary scale-110'
|
|
153
|
+
: 'hover:scale-105'
|
|
154
|
+
)}
|
|
155
|
+
title={c.label}
|
|
156
|
+
/>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
161
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
162
|
+
Cancel
|
|
163
|
+
</Button>
|
|
164
|
+
<Button
|
|
165
|
+
onClick={handleCreate}
|
|
166
|
+
disabled={!name.trim() || isCreating}
|
|
167
|
+
>
|
|
168
|
+
{isCreating ? (
|
|
169
|
+
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
|
170
|
+
) : (
|
|
171
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
172
|
+
)}
|
|
173
|
+
Create
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</DialogContent>
|
|
178
|
+
</Dialog>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Board Item with context menu
|
|
183
|
+
function BoardItem({
|
|
184
|
+
board,
|
|
185
|
+
isActive,
|
|
186
|
+
onRefresh
|
|
187
|
+
}: {
|
|
188
|
+
board: Board
|
|
189
|
+
isActive: boolean
|
|
190
|
+
onRefresh: () => void
|
|
191
|
+
}) {
|
|
192
|
+
const { currentTeam } = useTeamContext()
|
|
193
|
+
const colorClass = getColorClass(board.color)
|
|
194
|
+
|
|
195
|
+
const handleDelete = async () => {
|
|
196
|
+
if (!confirm('Delete this board? This cannot be undone.')) return
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await fetch(`/api/v1/boards/${board.id}`, {
|
|
200
|
+
method: 'DELETE',
|
|
201
|
+
headers: { 'x-team-id': currentTeam?.id || '' },
|
|
202
|
+
})
|
|
203
|
+
onRefresh()
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('Failed to delete board:', error)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="group relative">
|
|
211
|
+
<Link
|
|
212
|
+
href={`/dashboard/boards/${board.id}`}
|
|
213
|
+
className={cn(
|
|
214
|
+
'flex items-center gap-3 px-3 py-2 rounded-lg transition-all text-sm',
|
|
215
|
+
isActive
|
|
216
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
217
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
<div className={cn('w-3 h-3 rounded-sm shrink-0', colorClass)} />
|
|
221
|
+
<span className="truncate flex-1">{board.name}</span>
|
|
222
|
+
</Link>
|
|
223
|
+
|
|
224
|
+
{/* Context menu on hover */}
|
|
225
|
+
<DropdownMenu>
|
|
226
|
+
<DropdownMenuTrigger asChild>
|
|
227
|
+
<button
|
|
228
|
+
className={cn(
|
|
229
|
+
'absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity',
|
|
230
|
+
'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
<MoreHorizontal className="w-4 h-4" />
|
|
234
|
+
</button>
|
|
235
|
+
</DropdownMenuTrigger>
|
|
236
|
+
<DropdownMenuContent align="end" className="w-40">
|
|
237
|
+
<DropdownMenuItem asChild>
|
|
238
|
+
<Link href={`/dashboard/boards/${board.id}/edit`}>
|
|
239
|
+
<Pencil className="w-4 h-4 mr-2" />
|
|
240
|
+
Edit
|
|
241
|
+
</Link>
|
|
242
|
+
</DropdownMenuItem>
|
|
243
|
+
<DropdownMenuSeparator />
|
|
244
|
+
<DropdownMenuItem
|
|
245
|
+
className="text-destructive focus:text-destructive"
|
|
246
|
+
onClick={handleDelete}
|
|
247
|
+
>
|
|
248
|
+
<Trash2 className="w-4 h-4 mr-2" />
|
|
249
|
+
Delete
|
|
250
|
+
</DropdownMenuItem>
|
|
251
|
+
</DropdownMenuContent>
|
|
252
|
+
</DropdownMenu>
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function ProductivitySidebar() {
|
|
258
|
+
const pathname = usePathname()
|
|
259
|
+
const params = useParams()
|
|
260
|
+
const router = useRouter()
|
|
261
|
+
const { user, signOut } = useAuth()
|
|
262
|
+
const { currentTeam, userTeams, switchTeam } = useTeamContext()
|
|
263
|
+
const { canSwitch } = useTeamsConfig()
|
|
264
|
+
|
|
265
|
+
const [boards, setBoards] = useState<Board[]>([])
|
|
266
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
267
|
+
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
|
268
|
+
|
|
269
|
+
const currentBoardId = params?.id as string
|
|
270
|
+
|
|
271
|
+
// Get current user's role in the active team
|
|
272
|
+
const currentMembership = userTeams.find(m => m.team.id === currentTeam?.id)
|
|
273
|
+
const userRole = currentMembership?.role || 'member'
|
|
274
|
+
|
|
275
|
+
// Only owners and admins can create boards
|
|
276
|
+
const canCreateBoard = userRole === 'owner' || userRole === 'admin'
|
|
277
|
+
|
|
278
|
+
// Fetch boards
|
|
279
|
+
const fetchBoards = useCallback(async () => {
|
|
280
|
+
if (!currentTeam?.id) {
|
|
281
|
+
setIsLoading(false)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
setIsLoading(true)
|
|
286
|
+
const response = await fetch('/api/v1/boards?limit=100', {
|
|
287
|
+
headers: {
|
|
288
|
+
'Content-Type': 'application/json',
|
|
289
|
+
'x-team-id': currentTeam.id
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
if (response.ok) {
|
|
293
|
+
const data = await response.json()
|
|
294
|
+
setBoards(data.data || [])
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('Failed to fetch boards:', error)
|
|
298
|
+
} finally {
|
|
299
|
+
setIsLoading(false)
|
|
300
|
+
}
|
|
301
|
+
}, [currentTeam?.id])
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
fetchBoards()
|
|
305
|
+
}, [fetchBoards])
|
|
306
|
+
|
|
307
|
+
const getUserInitials = useCallback(() => {
|
|
308
|
+
if (!user) return 'U'
|
|
309
|
+
if (user.firstName && user.lastName) {
|
|
310
|
+
return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
|
|
311
|
+
}
|
|
312
|
+
return user.email?.slice(0, 2).toUpperCase() || 'U'
|
|
313
|
+
}, [user])
|
|
314
|
+
|
|
315
|
+
const getTeamInitials = (name: string) => {
|
|
316
|
+
return name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const handleTeamSwitch = async (teamId: string) => {
|
|
320
|
+
try {
|
|
321
|
+
await switchTeam(teamId)
|
|
322
|
+
router.push('/dashboard/boards')
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('Failed to switch team:', error)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<>
|
|
330
|
+
<aside className="hidden lg:flex flex-col fixed left-0 top-0 h-screen w-64 bg-sidebar border-r border-sidebar-border z-50">
|
|
331
|
+
{/* Team Header - Switcher only visible when canSwitch is true */}
|
|
332
|
+
<div className="p-3 border-b border-sidebar-border">
|
|
333
|
+
{canSwitch ? (
|
|
334
|
+
// Full team switcher dropdown (multi-tenant/hybrid modes)
|
|
335
|
+
<DropdownMenu>
|
|
336
|
+
<DropdownMenuTrigger asChild>
|
|
337
|
+
<button className="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-sidebar-accent transition-colors text-left">
|
|
338
|
+
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center text-sm font-bold text-primary-foreground shrink-0">
|
|
339
|
+
{currentTeam ? getTeamInitials(currentTeam.name) : 'T'}
|
|
340
|
+
</div>
|
|
341
|
+
<div className="flex-1 min-w-0">
|
|
342
|
+
<p className="font-semibold text-sm text-sidebar-foreground truncate">
|
|
343
|
+
{currentTeam?.name || 'Select Team'}
|
|
344
|
+
</p>
|
|
345
|
+
<p className="text-xs text-muted-foreground">
|
|
346
|
+
{boards.length} board{boards.length !== 1 ? 's' : ''}
|
|
347
|
+
</p>
|
|
348
|
+
</div>
|
|
349
|
+
<ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
|
|
350
|
+
</button>
|
|
351
|
+
</DropdownMenuTrigger>
|
|
352
|
+
<DropdownMenuContent align="start" className="w-56">
|
|
353
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
354
|
+
Switch Team
|
|
355
|
+
</div>
|
|
356
|
+
{userTeams.map((membership) => (
|
|
357
|
+
<DropdownMenuItem
|
|
358
|
+
key={membership.team.id}
|
|
359
|
+
onClick={() => handleTeamSwitch(membership.team.id)}
|
|
360
|
+
className="flex items-center gap-2"
|
|
361
|
+
>
|
|
362
|
+
<div className="w-6 h-6 rounded bg-muted flex items-center justify-center text-[10px] font-bold">
|
|
363
|
+
{getTeamInitials(membership.team.name)}
|
|
364
|
+
</div>
|
|
365
|
+
<span className="flex-1 truncate">{membership.team.name}</span>
|
|
366
|
+
{currentTeam?.id === membership.team.id && (
|
|
367
|
+
<Check className="w-4 h-4 text-primary" />
|
|
368
|
+
)}
|
|
369
|
+
</DropdownMenuItem>
|
|
370
|
+
))}
|
|
371
|
+
<DropdownMenuSeparator />
|
|
372
|
+
<DropdownMenuItem asChild>
|
|
373
|
+
<Link href="/dashboard/settings/teams" className="flex items-center gap-2">
|
|
374
|
+
<Users className="w-4 h-4" />
|
|
375
|
+
Manage Teams
|
|
376
|
+
</Link>
|
|
377
|
+
</DropdownMenuItem>
|
|
378
|
+
</DropdownMenuContent>
|
|
379
|
+
</DropdownMenu>
|
|
380
|
+
) : (
|
|
381
|
+
// Static team display (single-user/collaborative/single-tenant modes)
|
|
382
|
+
<div className="flex items-center gap-3 p-2">
|
|
383
|
+
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center text-sm font-bold text-primary-foreground shrink-0">
|
|
384
|
+
{currentTeam ? getTeamInitials(currentTeam.name) : 'T'}
|
|
385
|
+
</div>
|
|
386
|
+
<div className="flex-1 min-w-0">
|
|
387
|
+
<p className="font-semibold text-sm text-sidebar-foreground truncate">
|
|
388
|
+
{currentTeam?.name || 'Workspace'}
|
|
389
|
+
</p>
|
|
390
|
+
<p className="text-xs text-muted-foreground">
|
|
391
|
+
{boards.length} board{boards.length !== 1 ? 's' : ''}
|
|
392
|
+
</p>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
{/* Boards Section */}
|
|
399
|
+
<div className="flex-1 overflow-y-auto">
|
|
400
|
+
{/* Section Header */}
|
|
401
|
+
<div className="flex items-center justify-between px-4 py-3">
|
|
402
|
+
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
403
|
+
Boards
|
|
404
|
+
</span>
|
|
405
|
+
{canCreateBoard && (
|
|
406
|
+
<button
|
|
407
|
+
onClick={() => setCreateDialogOpen(true)}
|
|
408
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
409
|
+
title="Create board"
|
|
410
|
+
>
|
|
411
|
+
<Plus className="w-4 h-4" />
|
|
412
|
+
</button>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{/* Boards List */}
|
|
417
|
+
<nav className="px-2 pb-4">
|
|
418
|
+
{isLoading ? (
|
|
419
|
+
<div className="flex items-center justify-center py-8">
|
|
420
|
+
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
|
421
|
+
</div>
|
|
422
|
+
) : boards.length === 0 ? (
|
|
423
|
+
<div className="text-center py-8 px-4">
|
|
424
|
+
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center mx-auto mb-3">
|
|
425
|
+
<Kanban className="w-6 h-6 text-muted-foreground" />
|
|
426
|
+
</div>
|
|
427
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
428
|
+
{canCreateBoard ? 'No boards yet' : 'No boards available'}
|
|
429
|
+
</p>
|
|
430
|
+
{canCreateBoard && (
|
|
431
|
+
<Button
|
|
432
|
+
size="sm"
|
|
433
|
+
onClick={() => setCreateDialogOpen(true)}
|
|
434
|
+
className="gap-1"
|
|
435
|
+
>
|
|
436
|
+
<Plus className="w-4 h-4" />
|
|
437
|
+
Create Board
|
|
438
|
+
</Button>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
) : (
|
|
442
|
+
<div className="space-y-0.5">
|
|
443
|
+
{boards.map((board) => (
|
|
444
|
+
<BoardItem
|
|
445
|
+
key={board.id}
|
|
446
|
+
board={board}
|
|
447
|
+
isActive={currentBoardId === board.id}
|
|
448
|
+
onRefresh={fetchBoards}
|
|
449
|
+
/>
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</nav>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* Bottom Section */}
|
|
457
|
+
<div className="border-t border-sidebar-border p-3 space-y-1">
|
|
458
|
+
{/* All Boards Link */}
|
|
459
|
+
<Link
|
|
460
|
+
href="/dashboard/boards"
|
|
461
|
+
className={cn(
|
|
462
|
+
'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm',
|
|
463
|
+
pathname === '/dashboard/boards'
|
|
464
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
|
|
465
|
+
: 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'
|
|
466
|
+
)}
|
|
467
|
+
>
|
|
468
|
+
<LayoutGrid className="w-4 h-4" />
|
|
469
|
+
All Boards
|
|
470
|
+
</Link>
|
|
471
|
+
|
|
472
|
+
{/* Settings Link */}
|
|
473
|
+
<Link
|
|
474
|
+
href="/dashboard/settings"
|
|
475
|
+
className={cn(
|
|
476
|
+
'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm',
|
|
477
|
+
pathname.startsWith('/dashboard/settings')
|
|
478
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
|
|
479
|
+
: 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'
|
|
480
|
+
)}
|
|
481
|
+
>
|
|
482
|
+
<Settings className="w-4 h-4" />
|
|
483
|
+
Settings
|
|
484
|
+
</Link>
|
|
485
|
+
|
|
486
|
+
{/* User */}
|
|
487
|
+
<DropdownMenu>
|
|
488
|
+
<DropdownMenuTrigger asChild>
|
|
489
|
+
<button className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-sidebar-accent/50 transition-colors text-left">
|
|
490
|
+
{user?.image ? (
|
|
491
|
+
<Image
|
|
492
|
+
src={user.image}
|
|
493
|
+
alt=""
|
|
494
|
+
width={28}
|
|
495
|
+
height={28}
|
|
496
|
+
className="w-7 h-7 rounded-full object-cover"
|
|
497
|
+
/>
|
|
498
|
+
) : (
|
|
499
|
+
<div className="w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center text-xs font-medium text-primary">
|
|
500
|
+
{getUserInitials()}
|
|
501
|
+
</div>
|
|
502
|
+
)}
|
|
503
|
+
<span className="flex-1 text-sm text-sidebar-foreground truncate">
|
|
504
|
+
{user?.firstName || user?.email?.split('@')[0] || 'User'}
|
|
505
|
+
</span>
|
|
506
|
+
</button>
|
|
507
|
+
</DropdownMenuTrigger>
|
|
508
|
+
<DropdownMenuContent align="start" className="w-48">
|
|
509
|
+
<DropdownMenuItem asChild>
|
|
510
|
+
<Link href="/dashboard/settings/profile">
|
|
511
|
+
<Settings className="w-4 h-4 mr-2" />
|
|
512
|
+
Profile Settings
|
|
513
|
+
</Link>
|
|
514
|
+
</DropdownMenuItem>
|
|
515
|
+
<DropdownMenuSeparator />
|
|
516
|
+
<DropdownMenuItem
|
|
517
|
+
onClick={() => signOut()}
|
|
518
|
+
className="text-destructive focus:text-destructive"
|
|
519
|
+
>
|
|
520
|
+
<LogOut className="w-4 h-4 mr-2" />
|
|
521
|
+
Sign Out
|
|
522
|
+
</DropdownMenuItem>
|
|
523
|
+
</DropdownMenuContent>
|
|
524
|
+
</DropdownMenu>
|
|
525
|
+
</div>
|
|
526
|
+
</aside>
|
|
527
|
+
|
|
528
|
+
{/* Create Board Dialog */}
|
|
529
|
+
<CreateBoardDialog
|
|
530
|
+
open={createDialogOpen}
|
|
531
|
+
onOpenChange={setCreateDialogOpen}
|
|
532
|
+
onCreated={fetchBoards}
|
|
533
|
+
/>
|
|
534
|
+
</>
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export default ProductivitySidebar
|