@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.
Files changed (57) hide show
  1. package/README.md +76 -0
  2. package/about.md +123 -0
  3. package/components/CardDetailModal.tsx +318 -0
  4. package/components/KanbanBoard.tsx +612 -0
  5. package/components/KanbanCard.tsx +218 -0
  6. package/components/KanbanColumn.tsx +264 -0
  7. package/components/SortableList.tsx +46 -0
  8. package/components/index.ts +4 -0
  9. package/config/app.config.ts +172 -0
  10. package/config/billing.config.ts +187 -0
  11. package/config/dashboard.config.ts +357 -0
  12. package/config/dev.config.ts +55 -0
  13. package/config/features.config.ts +256 -0
  14. package/config/flows.config.ts +484 -0
  15. package/config/permissions.config.ts +167 -0
  16. package/config/theme.config.ts +106 -0
  17. package/entities/boards/boards.config.ts +61 -0
  18. package/entities/boards/boards.fields.ts +154 -0
  19. package/entities/boards/boards.service.ts +256 -0
  20. package/entities/boards/boards.types.ts +57 -0
  21. package/entities/boards/messages/en.json +80 -0
  22. package/entities/boards/messages/es.json +80 -0
  23. package/entities/boards/migrations/001_boards_table.sql +83 -0
  24. package/entities/cards/cards.config.ts +61 -0
  25. package/entities/cards/cards.fields.ts +242 -0
  26. package/entities/cards/cards.service.ts +336 -0
  27. package/entities/cards/cards.types.ts +79 -0
  28. package/entities/cards/messages/en.json +114 -0
  29. package/entities/cards/messages/es.json +114 -0
  30. package/entities/cards/migrations/020_cards_table.sql +92 -0
  31. package/entities/lists/lists.config.ts +61 -0
  32. package/entities/lists/lists.fields.ts +105 -0
  33. package/entities/lists/lists.service.ts +252 -0
  34. package/entities/lists/lists.types.ts +55 -0
  35. package/entities/lists/messages/en.json +60 -0
  36. package/entities/lists/messages/es.json +60 -0
  37. package/entities/lists/migrations/010_lists_table.sql +79 -0
  38. package/lib/selectors.ts +206 -0
  39. package/messages/en.json +79 -0
  40. package/messages/es.json +79 -0
  41. package/migrations/999_theme_sample_data.sql +922 -0
  42. package/migrations/999a_initial_sample_data.sql +377 -0
  43. package/migrations/999b_abundant_sample_data.sql +346 -0
  44. package/package.json +17 -0
  45. package/permissions-matrix.md +122 -0
  46. package/styles/components.css +460 -0
  47. package/styles/globals.css +560 -0
  48. package/templates/dashboard/(main)/boards/[id]/[cardId]/page.tsx +238 -0
  49. package/templates/dashboard/(main)/boards/[id]/edit/page.tsx +390 -0
  50. package/templates/dashboard/(main)/boards/[id]/page.tsx +236 -0
  51. package/templates/dashboard/(main)/boards/create/page.tsx +236 -0
  52. package/templates/dashboard/(main)/boards/page.tsx +335 -0
  53. package/templates/dashboard/(main)/layout.tsx +32 -0
  54. package/templates/dashboard/(main)/page.tsx +592 -0
  55. package/templates/shared/ProductivityMobileNav.tsx +410 -0
  56. package/templates/shared/ProductivitySidebar.tsx +538 -0
  57. package/templates/shared/ProductivityTopBar.tsx +317 -0
@@ -0,0 +1,592 @@
1
+ /**
2
+ * Productivity Dashboard Page
3
+ * Shows board metrics and statistics
4
+ */
5
+
6
+ 'use client'
7
+
8
+ import { useCallback, useEffect, useState } from 'react'
9
+ import Link from 'next/link'
10
+ import { useRouter } from 'next/navigation'
11
+ import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
12
+ import { useUserProfile } from '@nextsparkjs/core/hooks/useUserProfile'
13
+ import { cn } from '@nextsparkjs/core/lib/utils'
14
+ import { Button } from '@nextsparkjs/core/components/ui/button'
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from '@nextsparkjs/core/components/ui/dialog'
21
+ import { Input } from '@nextsparkjs/core/components/ui/input'
22
+ import { Label } from '@nextsparkjs/core/components/ui/label'
23
+ import {
24
+ LayoutGrid,
25
+ CheckCircle2,
26
+ Clock,
27
+ AlertCircle,
28
+ Plus,
29
+ Loader2,
30
+ Kanban,
31
+ ListTodo,
32
+ TrendingUp,
33
+ Users,
34
+ ChevronRight,
35
+ Sparkles
36
+ } from 'lucide-react'
37
+ import { PermissionGate } from '@nextsparkjs/core/components/permissions/PermissionGate'
38
+
39
+ interface Board {
40
+ id: string
41
+ name: string
42
+ color: string
43
+ createdAt: string
44
+ }
45
+
46
+ interface List {
47
+ id: string
48
+ name: string
49
+ boardId: string
50
+ }
51
+
52
+ interface Card {
53
+ id: string
54
+ title: string
55
+ listId: string
56
+ boardId: string
57
+ createdAt: string
58
+ }
59
+
60
+ interface DashboardStats {
61
+ totalBoards: number
62
+ totalCards: number
63
+ totalLists: number
64
+ todoCards: number
65
+ inProgressCards: number
66
+ doneCards: number
67
+ recentBoards: Board[]
68
+ }
69
+
70
+ // Board colors
71
+ const BOARD_COLORS = [
72
+ { id: 'blue', class: 'bg-[var(--board-blue)]' },
73
+ { id: 'green', class: 'bg-[var(--board-green)]' },
74
+ { id: 'purple', class: 'bg-[var(--board-purple)]' },
75
+ { id: 'orange', class: 'bg-[var(--board-orange)]' },
76
+ { id: 'red', class: 'bg-[var(--board-red)]' },
77
+ { id: 'pink', class: 'bg-[var(--board-pink)]' },
78
+ { id: 'gray', class: 'bg-[var(--board-gray)]' },
79
+ ]
80
+
81
+ const getColorClass = (color: string) => {
82
+ return BOARD_COLORS.find(c => c.id === color)?.class || BOARD_COLORS[0].class
83
+ }
84
+
85
+ export default function ProductivityDashboard() {
86
+ const { user, isLoading: userLoading } = useUserProfile()
87
+ const { currentTeam } = useTeamContext()
88
+ const router = useRouter()
89
+
90
+ const [stats, setStats] = useState<DashboardStats>({
91
+ totalBoards: 0,
92
+ totalCards: 0,
93
+ totalLists: 0,
94
+ todoCards: 0,
95
+ inProgressCards: 0,
96
+ doneCards: 0,
97
+ recentBoards: []
98
+ })
99
+ const [isLoading, setIsLoading] = useState(true)
100
+ const [createDialogOpen, setCreateDialogOpen] = useState(false)
101
+ const [newBoardName, setNewBoardName] = useState('')
102
+ const [newBoardColor, setNewBoardColor] = useState('blue')
103
+ const [isCreating, setIsCreating] = useState(false)
104
+
105
+ // Helper function to classify list by name into status categories
106
+ const classifyListStatus = (listName: string): 'todo' | 'inProgress' | 'done' | 'other' => {
107
+ const name = listName.toLowerCase()
108
+
109
+ // Done/Completed indicators
110
+ if (name.includes('done') || name.includes('complete') || name.includes('finished') ||
111
+ name.includes('closed') || name.includes('resolved') || name.includes('shipped')) {
112
+ return 'done'
113
+ }
114
+
115
+ // In Progress indicators
116
+ if (name.includes('progress') || name.includes('doing') || name.includes('working') ||
117
+ name.includes('active') || name.includes('in review') || name.includes('testing') ||
118
+ name.includes('development') || name.includes('building')) {
119
+ return 'inProgress'
120
+ }
121
+
122
+ // To Do indicators
123
+ if (name.includes('todo') || name.includes('to do') || name.includes('to-do') ||
124
+ name.includes('backlog') || name.includes('planned') || name.includes('pending') ||
125
+ name.includes('new') || name.includes('open') || name.includes('inbox')) {
126
+ return 'todo'
127
+ }
128
+
129
+ // Default to todo for unclassified
130
+ return 'todo'
131
+ }
132
+
133
+ // Fetch dashboard statistics
134
+ const fetchStats = useCallback(async () => {
135
+ if (!currentTeam?.id) {
136
+ setIsLoading(false)
137
+ return
138
+ }
139
+
140
+ try {
141
+ setIsLoading(true)
142
+
143
+ // Fetch boards
144
+ const boardsResponse = await fetch('/api/v1/boards?limit=100', {
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'x-team-id': currentTeam.id
148
+ }
149
+ })
150
+
151
+ let boards: Board[] = []
152
+ if (boardsResponse.ok) {
153
+ const boardsData = await boardsResponse.json()
154
+ boards = boardsData.data || []
155
+ }
156
+
157
+ // Fetch all lists
158
+ const listsResponse = await fetch('/api/v1/lists?limit=500', {
159
+ headers: {
160
+ 'Content-Type': 'application/json',
161
+ 'x-team-id': currentTeam.id
162
+ }
163
+ })
164
+
165
+ let allLists: List[] = []
166
+ if (listsResponse.ok) {
167
+ const listsData = await listsResponse.json()
168
+ allLists = listsData.data || []
169
+ }
170
+
171
+ // Create a map of listId -> status
172
+ const listStatusMap = new Map<string, 'todo' | 'inProgress' | 'done' | 'other'>()
173
+ allLists.forEach(list => {
174
+ listStatusMap.set(list.id, classifyListStatus(list.name))
175
+ })
176
+
177
+ // Fetch all cards
178
+ const cardsResponse = await fetch('/api/v1/cards?limit=1000', {
179
+ headers: {
180
+ 'Content-Type': 'application/json',
181
+ 'x-team-id': currentTeam.id
182
+ }
183
+ })
184
+
185
+ let allCards: Card[] = []
186
+ if (cardsResponse.ok) {
187
+ const cardsData = await cardsResponse.json()
188
+ allCards = cardsData.data || []
189
+ }
190
+
191
+ // Calculate stats based on list status
192
+ let todoCards = 0
193
+ let inProgressCards = 0
194
+ let doneCards = 0
195
+
196
+ allCards.forEach(card => {
197
+ const status = listStatusMap.get(card.listId) || 'todo'
198
+ if (status === 'todo' || status === 'other') {
199
+ todoCards++
200
+ } else if (status === 'inProgress') {
201
+ inProgressCards++
202
+ } else if (status === 'done') {
203
+ doneCards++
204
+ }
205
+ })
206
+
207
+ setStats({
208
+ totalBoards: boards.length,
209
+ totalCards: allCards.length,
210
+ totalLists: allLists.length,
211
+ todoCards,
212
+ inProgressCards,
213
+ doneCards,
214
+ recentBoards: boards.slice(0, 5)
215
+ })
216
+ } catch (error) {
217
+ console.error('Failed to fetch dashboard stats:', error)
218
+ } finally {
219
+ setIsLoading(false)
220
+ }
221
+ }, [currentTeam?.id])
222
+
223
+ useEffect(() => {
224
+ fetchStats()
225
+ }, [fetchStats])
226
+
227
+ const handleCreateBoard = async () => {
228
+ if (!newBoardName.trim() || !currentTeam?.id) return
229
+
230
+ setIsCreating(true)
231
+ try {
232
+ const response = await fetch('/api/v1/boards', {
233
+ method: 'POST',
234
+ headers: {
235
+ 'Content-Type': 'application/json',
236
+ 'x-team-id': currentTeam.id,
237
+ },
238
+ body: JSON.stringify({ name: newBoardName.trim(), color: newBoardColor }),
239
+ })
240
+
241
+ if (response.ok) {
242
+ const data = await response.json()
243
+ setNewBoardName('')
244
+ setNewBoardColor('blue')
245
+ setCreateDialogOpen(false)
246
+ router.push(`/dashboard/boards/${data.data.id}`)
247
+ }
248
+ } catch (error) {
249
+ console.error('Failed to create board:', error)
250
+ } finally {
251
+ setIsCreating(false)
252
+ }
253
+ }
254
+
255
+ const getGreeting = () => {
256
+ const hour = new Date().getHours()
257
+ if (hour < 12) return 'Good morning'
258
+ if (hour < 18) return 'Good afternoon'
259
+ return 'Good evening'
260
+ }
261
+
262
+ if (userLoading || isLoading) {
263
+ return (
264
+ <div className="min-h-screen flex items-center justify-center">
265
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
266
+ </div>
267
+ )
268
+ }
269
+
270
+ const completionRate = stats.totalCards > 0
271
+ ? Math.round((stats.doneCards / stats.totalCards) * 100)
272
+ : 0
273
+
274
+ return (
275
+ <div className="min-h-screen bg-background">
276
+ {/* Header */}
277
+ <div className="border-b border-border/50 bg-background/95 backdrop-blur-sm sticky top-0 z-10">
278
+ <div className="px-6 py-6">
279
+ <div className="flex items-center justify-between">
280
+ <div>
281
+ <h1 className="text-2xl font-bold text-foreground">
282
+ {getGreeting()}, {user?.firstName || 'there'}!
283
+ </h1>
284
+ <p className="text-muted-foreground mt-1">
285
+ Here's what's happening with your boards
286
+ </p>
287
+ </div>
288
+ <PermissionGate permission="boards.create">
289
+ <Button onClick={() => setCreateDialogOpen(true)} className="gap-2">
290
+ <Plus className="w-4 h-4" />
291
+ New Board
292
+ </Button>
293
+ </PermissionGate>
294
+ </div>
295
+ </div>
296
+ </div>
297
+
298
+ <div className="p-6 space-y-8">
299
+ {/* Stats Grid */}
300
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
301
+ {/* Total Boards */}
302
+ <div className="bg-card rounded-2xl p-5 border border-border/50 shadow-sm">
303
+ <div className="flex items-center gap-3">
304
+ <div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
305
+ <LayoutGrid className="w-6 h-6 text-primary" />
306
+ </div>
307
+ <div>
308
+ <p className="text-3xl font-bold text-foreground">{stats.totalBoards}</p>
309
+ <p className="text-sm text-muted-foreground">Total Boards</p>
310
+ </div>
311
+ </div>
312
+ </div>
313
+
314
+ {/* Total Cards */}
315
+ <div className="bg-card rounded-2xl p-5 border border-border/50 shadow-sm">
316
+ <div className="flex items-center gap-3">
317
+ <div className="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center">
318
+ <ListTodo className="w-6 h-6 text-blue-500" />
319
+ </div>
320
+ <div>
321
+ <p className="text-3xl font-bold text-foreground">{stats.totalCards}</p>
322
+ <p className="text-sm text-muted-foreground">Total Cards</p>
323
+ </div>
324
+ </div>
325
+ </div>
326
+
327
+ {/* In Progress */}
328
+ <div className="bg-card rounded-2xl p-5 border border-border/50 shadow-sm">
329
+ <div className="flex items-center gap-3">
330
+ <div className="w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center">
331
+ <Clock className="w-6 h-6 text-amber-500" />
332
+ </div>
333
+ <div>
334
+ <p className="text-3xl font-bold text-foreground">{stats.inProgressCards}</p>
335
+ <p className="text-sm text-muted-foreground">In Progress</p>
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ {/* Completed */}
341
+ <div className="bg-card rounded-2xl p-5 border border-border/50 shadow-sm">
342
+ <div className="flex items-center gap-3">
343
+ <div className="w-12 h-12 rounded-xl bg-green-500/10 flex items-center justify-center">
344
+ <CheckCircle2 className="w-6 h-6 text-green-500" />
345
+ </div>
346
+ <div>
347
+ <p className="text-3xl font-bold text-foreground">{stats.doneCards}</p>
348
+ <p className="text-sm text-muted-foreground">Completed</p>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ {/* Progress Overview */}
355
+ <div className="bg-card rounded-2xl p-6 border border-border/50 shadow-sm">
356
+ <div className="flex items-center justify-between mb-4">
357
+ <div>
358
+ <h2 className="text-lg font-semibold text-foreground">Progress Overview</h2>
359
+ <p className="text-sm text-muted-foreground">Card completion across all boards</p>
360
+ </div>
361
+ <div className="flex items-center gap-2">
362
+ <TrendingUp className="w-5 h-5 text-green-500" />
363
+ <span className="text-2xl font-bold text-foreground">{completionRate}%</span>
364
+ </div>
365
+ </div>
366
+
367
+ {/* Progress Bar */}
368
+ <div className="h-4 bg-muted rounded-full overflow-hidden flex">
369
+ {stats.totalCards > 0 ? (
370
+ <>
371
+ <div
372
+ className="h-full bg-green-500 transition-all duration-500"
373
+ style={{ width: `${(stats.doneCards / stats.totalCards) * 100}%` }}
374
+ />
375
+ <div
376
+ className="h-full bg-amber-500 transition-all duration-500"
377
+ style={{ width: `${(stats.inProgressCards / stats.totalCards) * 100}%` }}
378
+ />
379
+ <div
380
+ className="h-full bg-slate-300 transition-all duration-500"
381
+ style={{ width: `${(stats.todoCards / stats.totalCards) * 100}%` }}
382
+ />
383
+ </>
384
+ ) : (
385
+ <div className="h-full bg-muted w-full" />
386
+ )}
387
+ </div>
388
+
389
+ {/* Legend */}
390
+ <div className="flex items-center gap-6 mt-4">
391
+ <div className="flex items-center gap-2">
392
+ <div className="w-3 h-3 rounded-full bg-green-500" />
393
+ <span className="text-sm text-muted-foreground">Done ({stats.doneCards})</span>
394
+ </div>
395
+ <div className="flex items-center gap-2">
396
+ <div className="w-3 h-3 rounded-full bg-amber-500" />
397
+ <span className="text-sm text-muted-foreground">In Progress ({stats.inProgressCards})</span>
398
+ </div>
399
+ <div className="flex items-center gap-2">
400
+ <div className="w-3 h-3 rounded-full bg-slate-300" />
401
+ <span className="text-sm text-muted-foreground">To Do ({stats.todoCards})</span>
402
+ </div>
403
+ </div>
404
+ </div>
405
+
406
+ {/* Two Column Layout */}
407
+ <div className="grid lg:grid-cols-2 gap-6">
408
+ {/* Recent Boards */}
409
+ <div className="bg-card rounded-2xl p-6 border border-border/50 shadow-sm">
410
+ <div className="flex items-center justify-between mb-4">
411
+ <h2 className="text-lg font-semibold text-foreground">Recent Boards</h2>
412
+ <Link
413
+ href="/dashboard/boards"
414
+ className="text-sm text-primary hover:underline flex items-center gap-1"
415
+ >
416
+ View all
417
+ <ChevronRight className="w-4 h-4" />
418
+ </Link>
419
+ </div>
420
+
421
+ {stats.recentBoards.length === 0 ? (
422
+ <div className="text-center py-8">
423
+ <div className="w-16 h-16 rounded-2xl bg-muted flex items-center justify-center mx-auto mb-4">
424
+ <Kanban className="w-8 h-8 text-muted-foreground" />
425
+ </div>
426
+ <p className="text-muted-foreground mb-4">No boards yet</p>
427
+ <PermissionGate permission="boards.create">
428
+ <Button
429
+ variant="outline"
430
+ onClick={() => setCreateDialogOpen(true)}
431
+ className="gap-2"
432
+ >
433
+ <Plus className="w-4 h-4" />
434
+ Create your first board
435
+ </Button>
436
+ </PermissionGate>
437
+ </div>
438
+ ) : (
439
+ <div className="space-y-2">
440
+ {stats.recentBoards.map((board) => (
441
+ <Link
442
+ key={board.id}
443
+ href={`/dashboard/boards/${board.id}`}
444
+ className="flex items-center gap-3 p-3 rounded-xl hover:bg-muted/50 transition-colors group"
445
+ >
446
+ <div className={cn('w-4 h-4 rounded', getColorClass(board.color))} />
447
+ <span className="flex-1 font-medium text-foreground group-hover:text-primary transition-colors">
448
+ {board.name}
449
+ </span>
450
+ <ChevronRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
451
+ </Link>
452
+ ))}
453
+ </div>
454
+ )}
455
+ </div>
456
+
457
+ {/* Quick Actions */}
458
+ <div className="bg-card rounded-2xl p-6 border border-border/50 shadow-sm">
459
+ <h2 className="text-lg font-semibold text-foreground mb-4">Quick Actions</h2>
460
+
461
+ <div className="space-y-3">
462
+ <PermissionGate permission="boards.create">
463
+ <button
464
+ onClick={() => setCreateDialogOpen(true)}
465
+ className="w-full flex items-center gap-4 p-4 rounded-xl bg-primary/5 hover:bg-primary/10 transition-colors text-left group"
466
+ >
467
+ <div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
468
+ <Plus className="w-6 h-6 text-primary" />
469
+ </div>
470
+ <div>
471
+ <p className="font-medium text-foreground">Create New Board</p>
472
+ <p className="text-sm text-muted-foreground">Start organizing your tasks</p>
473
+ </div>
474
+ </button>
475
+ </PermissionGate>
476
+
477
+ <Link
478
+ href="/dashboard/boards"
479
+ className="w-full flex items-center gap-4 p-4 rounded-xl bg-blue-500/5 hover:bg-blue-500/10 transition-colors text-left group"
480
+ >
481
+ <div className="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center group-hover:bg-blue-500/20 transition-colors">
482
+ <LayoutGrid className="w-6 h-6 text-blue-500" />
483
+ </div>
484
+ <div>
485
+ <p className="font-medium text-foreground">View All Boards</p>
486
+ <p className="text-sm text-muted-foreground">Browse your {stats.totalBoards} boards</p>
487
+ </div>
488
+ </Link>
489
+
490
+ <Link
491
+ href="/dashboard/settings/teams"
492
+ className="w-full flex items-center gap-4 p-4 rounded-xl bg-purple-500/5 hover:bg-purple-500/10 transition-colors text-left group"
493
+ >
494
+ <div className="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center group-hover:bg-purple-500/20 transition-colors">
495
+ <Users className="w-6 h-6 text-purple-500" />
496
+ </div>
497
+ <div>
498
+ <p className="font-medium text-foreground">Manage Team</p>
499
+ <p className="text-sm text-muted-foreground">Invite members and set permissions</p>
500
+ </div>
501
+ </Link>
502
+ </div>
503
+ </div>
504
+ </div>
505
+
506
+ {/* Tips Card */}
507
+ {stats.totalBoards === 0 && (
508
+ <PermissionGate permission="boards.create">
509
+ <div className="bg-gradient-to-r from-primary/10 via-primary/5 to-transparent rounded-2xl p-6 border border-primary/20">
510
+ <div className="flex items-start gap-4">
511
+ <div className="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center flex-shrink-0">
512
+ <Sparkles className="w-6 h-6 text-primary" />
513
+ </div>
514
+ <div>
515
+ <h3 className="font-semibold text-foreground mb-1">Getting Started</h3>
516
+ <p className="text-muted-foreground text-sm mb-4">
517
+ Create your first board to start organizing your tasks. Boards help you
518
+ visualize your workflow with columns like "To Do", "In Progress", and "Done".
519
+ </p>
520
+ <Button onClick={() => setCreateDialogOpen(true)} className="gap-2">
521
+ <Plus className="w-4 h-4" />
522
+ Create Your First Board
523
+ </Button>
524
+ </div>
525
+ </div>
526
+ </div>
527
+ </PermissionGate>
528
+ )}
529
+ </div>
530
+
531
+ {/* Create Board Dialog */}
532
+ <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
533
+ <DialogContent className="sm:max-w-md">
534
+ <DialogHeader>
535
+ <DialogTitle>Create new board</DialogTitle>
536
+ </DialogHeader>
537
+ <div className="space-y-4 pt-4">
538
+ <div className="space-y-2">
539
+ <Label htmlFor="board-name">Board name</Label>
540
+ <Input
541
+ id="board-name"
542
+ placeholder="My awesome board"
543
+ value={newBoardName}
544
+ onChange={(e) => setNewBoardName(e.target.value)}
545
+ onKeyDown={(e) => e.key === 'Enter' && handleCreateBoard()}
546
+ autoFocus
547
+ />
548
+ </div>
549
+ <div className="space-y-2">
550
+ <Label>Color</Label>
551
+ <div className="flex gap-2">
552
+ {BOARD_COLORS.map((c) => (
553
+ <button
554
+ key={c.id}
555
+ onClick={() => setNewBoardColor(c.id)}
556
+ className={cn(
557
+ 'w-9 h-9 rounded-lg transition-all',
558
+ c.class,
559
+ newBoardColor === c.id
560
+ ? 'ring-2 ring-offset-2 ring-primary scale-110'
561
+ : 'hover:scale-105'
562
+ )}
563
+ />
564
+ ))}
565
+ </div>
566
+ </div>
567
+ <div className="flex gap-2 pt-2">
568
+ <Button
569
+ variant="outline"
570
+ className="flex-1"
571
+ onClick={() => setCreateDialogOpen(false)}
572
+ >
573
+ Cancel
574
+ </Button>
575
+ <Button
576
+ className="flex-1"
577
+ onClick={handleCreateBoard}
578
+ disabled={!newBoardName.trim() || isCreating}
579
+ >
580
+ {isCreating ? (
581
+ <Loader2 className="w-4 h-4 animate-spin" />
582
+ ) : (
583
+ 'Create'
584
+ )}
585
+ </Button>
586
+ </div>
587
+ </div>
588
+ </DialogContent>
589
+ </Dialog>
590
+ </div>
591
+ )
592
+ }