@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,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
|
+
}
|