@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,335 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Productivity Theme - Boards List Page Override
|
|
5
|
+
*
|
|
6
|
+
* Shows boards as visual cards with color preview and stats.
|
|
7
|
+
* Modern Trello-like grid design.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
11
|
+
import Link from 'next/link'
|
|
12
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
13
|
+
import { Skeleton } from '@nextsparkjs/core/components/ui/skeleton'
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
DropdownMenuTrigger,
|
|
20
|
+
} from '@nextsparkjs/core/components/ui/dropdown-menu'
|
|
21
|
+
import { Plus, MoreHorizontal, Kanban, Archive, Trash2, Edit, List, CreditCard, Clock } from 'lucide-react'
|
|
22
|
+
import { PermissionGate } from '@nextsparkjs/core/components/permissions/PermissionGate'
|
|
23
|
+
import { useToast } from '@nextsparkjs/core/hooks/useToast'
|
|
24
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
25
|
+
import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
|
|
26
|
+
|
|
27
|
+
interface Board {
|
|
28
|
+
id: string
|
|
29
|
+
name: string
|
|
30
|
+
description?: string | null
|
|
31
|
+
color?: string | null
|
|
32
|
+
status?: string | null
|
|
33
|
+
createdAt: string
|
|
34
|
+
updatedAt?: string
|
|
35
|
+
listsCount?: number
|
|
36
|
+
cardsCount?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Board color classes using CSS variables
|
|
40
|
+
const boardColorClasses: Record<string, string> = {
|
|
41
|
+
blue: 'bg-[var(--board-blue)]',
|
|
42
|
+
green: 'bg-[var(--board-green)]',
|
|
43
|
+
purple: 'bg-[var(--board-purple)]',
|
|
44
|
+
orange: 'bg-[var(--board-orange)]',
|
|
45
|
+
red: 'bg-[var(--board-red)]',
|
|
46
|
+
pink: 'bg-[var(--board-pink)]',
|
|
47
|
+
gray: 'bg-[var(--board-gray)]',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatRelativeTime(dateString: string): string {
|
|
51
|
+
const date = new Date(dateString)
|
|
52
|
+
const now = new Date()
|
|
53
|
+
const diffMs = now.getTime() - date.getTime()
|
|
54
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
55
|
+
|
|
56
|
+
if (diffDays === 0) return 'Today'
|
|
57
|
+
if (diffDays === 1) return 'Yesterday'
|
|
58
|
+
if (diffDays < 7) return `${diffDays} days ago`
|
|
59
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
|
|
60
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default function BoardsPage() {
|
|
64
|
+
const [boards, setBoards] = useState<Board[]>([])
|
|
65
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
66
|
+
const { toast } = useToast()
|
|
67
|
+
const { currentTeam } = useTeamContext()
|
|
68
|
+
const hasFetchedRef = useRef(false)
|
|
69
|
+
const toastRef = useRef(toast)
|
|
70
|
+
|
|
71
|
+
// Keep toast ref updated
|
|
72
|
+
toastRef.current = toast
|
|
73
|
+
|
|
74
|
+
const fetchBoards = useCallback(async (teamId: string) => {
|
|
75
|
+
try {
|
|
76
|
+
setIsLoading(true)
|
|
77
|
+
const response = await fetch('/api/v1/boards?limit=50', {
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
'x-team-id': teamId,
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`HTTP ${response.status}`)
|
|
85
|
+
}
|
|
86
|
+
const data = await response.json()
|
|
87
|
+
setBoards(data.data || [])
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Error fetching boards:', error)
|
|
90
|
+
// Only show toast once
|
|
91
|
+
if (!hasFetchedRef.current) {
|
|
92
|
+
toastRef.current({
|
|
93
|
+
title: 'Error',
|
|
94
|
+
description: 'Could not load boards.',
|
|
95
|
+
variant: 'destructive',
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
setIsLoading(false)
|
|
100
|
+
hasFetchedRef.current = true
|
|
101
|
+
}
|
|
102
|
+
}, [])
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!currentTeam?.id) {
|
|
106
|
+
setIsLoading(false)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
hasFetchedRef.current = false
|
|
110
|
+
fetchBoards(currentTeam.id)
|
|
111
|
+
}, [currentTeam?.id, fetchBoards])
|
|
112
|
+
|
|
113
|
+
const handleArchive = async (boardId: string) => {
|
|
114
|
+
if (!currentTeam?.id) return
|
|
115
|
+
try {
|
|
116
|
+
await fetch(`/api/v1/boards/${boardId}`, {
|
|
117
|
+
method: 'PATCH',
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
'x-team-id': currentTeam.id,
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({ status: 'archived' }),
|
|
123
|
+
})
|
|
124
|
+
toast({ title: 'Board archived' })
|
|
125
|
+
fetchBoards(currentTeam.id)
|
|
126
|
+
} catch {
|
|
127
|
+
toast({
|
|
128
|
+
title: 'Error',
|
|
129
|
+
description: 'Could not archive board.',
|
|
130
|
+
variant: 'destructive',
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handleDelete = async (boardId: string) => {
|
|
136
|
+
if (!currentTeam?.id) return
|
|
137
|
+
if (!confirm('Are you sure you want to delete this board? This action cannot be undone.')) return
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await fetch(`/api/v1/boards/${boardId}`, {
|
|
141
|
+
method: 'DELETE',
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
'x-team-id': currentTeam.id,
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
toast({ title: 'Board deleted' })
|
|
148
|
+
fetchBoards(currentTeam.id)
|
|
149
|
+
} catch {
|
|
150
|
+
toast({
|
|
151
|
+
title: 'Error',
|
|
152
|
+
description: 'Could not delete board.',
|
|
153
|
+
variant: 'destructive',
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isLoading) {
|
|
159
|
+
return (
|
|
160
|
+
<div className="p-6">
|
|
161
|
+
<div className="flex items-center justify-between mb-8">
|
|
162
|
+
<div>
|
|
163
|
+
<Skeleton className="h-8 w-32 mb-2" />
|
|
164
|
+
<Skeleton className="h-4 w-48" />
|
|
165
|
+
</div>
|
|
166
|
+
<Skeleton className="h-10 w-32" />
|
|
167
|
+
</div>
|
|
168
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
169
|
+
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
170
|
+
<Skeleton key={i} className="h-44 rounded-xl" />
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="p-6" data-cy="boards-page">
|
|
179
|
+
{/* Header */}
|
|
180
|
+
<div className="flex items-center justify-between mb-8">
|
|
181
|
+
<div>
|
|
182
|
+
<h1 className="text-2xl font-bold text-foreground">Your Boards</h1>
|
|
183
|
+
<p className="text-muted-foreground text-sm mt-1">
|
|
184
|
+
{boards.length === 0
|
|
185
|
+
? 'Create your first board to get started'
|
|
186
|
+
: `${boards.length} board${boards.length !== 1 ? 's' : ''} total`}
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
<PermissionGate permission="boards.create">
|
|
190
|
+
<Button asChild className="gap-2" data-cy="boards-create-btn">
|
|
191
|
+
<Link href="/dashboard/boards/create">
|
|
192
|
+
<Plus className="h-4 w-4" />
|
|
193
|
+
New Board
|
|
194
|
+
</Link>
|
|
195
|
+
</Button>
|
|
196
|
+
</PermissionGate>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Boards Grid */}
|
|
200
|
+
{boards.length === 0 ? (
|
|
201
|
+
<div className="flex flex-col items-center justify-center py-16 px-4">
|
|
202
|
+
<div className="w-20 h-20 rounded-2xl bg-primary/10 flex items-center justify-center mb-6">
|
|
203
|
+
<Kanban className="h-10 w-10 text-primary" />
|
|
204
|
+
</div>
|
|
205
|
+
<h2 className="text-xl font-semibold text-foreground mb-2">No boards yet</h2>
|
|
206
|
+
<p className="text-muted-foreground text-center max-w-md mb-6">
|
|
207
|
+
Boards help you organize your work into manageable projects.
|
|
208
|
+
Create your first board to start tracking tasks.
|
|
209
|
+
</p>
|
|
210
|
+
<PermissionGate permission="boards.create">
|
|
211
|
+
<Button asChild size="lg" className="gap-2">
|
|
212
|
+
<Link href="/dashboard/boards/create">
|
|
213
|
+
<Plus className="h-5 w-5" />
|
|
214
|
+
Create Your First Board
|
|
215
|
+
</Link>
|
|
216
|
+
</Button>
|
|
217
|
+
</PermissionGate>
|
|
218
|
+
</div>
|
|
219
|
+
) : (
|
|
220
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
221
|
+
{/* New Board Card */}
|
|
222
|
+
<PermissionGate permission="boards.create">
|
|
223
|
+
<Link
|
|
224
|
+
href="/dashboard/boards/create"
|
|
225
|
+
className="group flex flex-col items-center justify-center h-44 rounded-xl border-2 border-dashed border-border hover:border-primary/50 bg-muted/20 hover:bg-muted/40 transition-all"
|
|
226
|
+
data-cy="boards-create-card"
|
|
227
|
+
>
|
|
228
|
+
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center mb-3 group-hover:bg-primary/10 transition-colors">
|
|
229
|
+
<Plus className="h-6 w-6 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
230
|
+
</div>
|
|
231
|
+
<span className="text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors">
|
|
232
|
+
Create new board
|
|
233
|
+
</span>
|
|
234
|
+
</Link>
|
|
235
|
+
</PermissionGate>
|
|
236
|
+
|
|
237
|
+
{/* Board Cards */}
|
|
238
|
+
{boards.map((board) => {
|
|
239
|
+
const colorClass = boardColorClasses[board.color || 'blue'] || boardColorClasses.blue
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
key={board.id}
|
|
244
|
+
className="group relative rounded-xl overflow-hidden border border-border/50 bg-card hover:shadow-lg hover:border-border transition-all duration-200"
|
|
245
|
+
data-cy={`boards-card-${board.id}`}
|
|
246
|
+
>
|
|
247
|
+
{/* Color header */}
|
|
248
|
+
<div className={cn('h-24 relative', colorClass)}>
|
|
249
|
+
{/* Gradient overlay */}
|
|
250
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent" />
|
|
251
|
+
|
|
252
|
+
{/* Menu button */}
|
|
253
|
+
<div className="absolute top-2 right-2">
|
|
254
|
+
<DropdownMenu>
|
|
255
|
+
<DropdownMenuTrigger asChild>
|
|
256
|
+
<Button
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="icon"
|
|
259
|
+
className="h-8 w-8 bg-black/20 hover:bg-black/40 text-white opacity-0 group-hover:opacity-100 transition-opacity"
|
|
260
|
+
data-cy={`boards-card-menu-${board.id}`}
|
|
261
|
+
>
|
|
262
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
263
|
+
</Button>
|
|
264
|
+
</DropdownMenuTrigger>
|
|
265
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
266
|
+
<DropdownMenuItem asChild data-cy={`boards-card-edit-${board.id}`}>
|
|
267
|
+
<Link href={`/dashboard/boards/${board.id}/edit`} className="flex items-center">
|
|
268
|
+
<Edit className="h-4 w-4 mr-2" />
|
|
269
|
+
Edit board
|
|
270
|
+
</Link>
|
|
271
|
+
</DropdownMenuItem>
|
|
272
|
+
<DropdownMenuSeparator />
|
|
273
|
+
<PermissionGate permission="boards.archive">
|
|
274
|
+
<DropdownMenuItem onClick={() => handleArchive(board.id)} data-cy={`boards-card-archive-${board.id}`}>
|
|
275
|
+
<Archive className="h-4 w-4 mr-2" />
|
|
276
|
+
Archive
|
|
277
|
+
</DropdownMenuItem>
|
|
278
|
+
</PermissionGate>
|
|
279
|
+
<PermissionGate permission="boards.delete">
|
|
280
|
+
<DropdownMenuItem
|
|
281
|
+
className="text-destructive focus:text-destructive"
|
|
282
|
+
onClick={() => handleDelete(board.id)}
|
|
283
|
+
data-cy={`boards-card-delete-${board.id}`}
|
|
284
|
+
>
|
|
285
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
286
|
+
Delete
|
|
287
|
+
</DropdownMenuItem>
|
|
288
|
+
</PermissionGate>
|
|
289
|
+
</DropdownMenuContent>
|
|
290
|
+
</DropdownMenu>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{/* Board info */}
|
|
295
|
+
<Link href={`/dashboard/boards/${board.id}`} className="block p-4">
|
|
296
|
+
<h3 className="font-semibold text-foreground truncate mb-1 group-hover:text-primary transition-colors">
|
|
297
|
+
{board.name}
|
|
298
|
+
</h3>
|
|
299
|
+
{board.description && (
|
|
300
|
+
<p className="text-xs text-muted-foreground line-clamp-1 mb-3">
|
|
301
|
+
{board.description}
|
|
302
|
+
</p>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
{/* Stats row */}
|
|
306
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
307
|
+
{board.listsCount !== undefined && (
|
|
308
|
+
<div className="flex items-center gap-1">
|
|
309
|
+
<List className="h-3 w-3" />
|
|
310
|
+
<span>{board.listsCount} lists</span>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
{board.cardsCount !== undefined && (
|
|
314
|
+
<div className="flex items-center gap-1">
|
|
315
|
+
<CreditCard className="h-3 w-3" />
|
|
316
|
+
<span>{board.cardsCount} cards</span>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
{board.updatedAt && (
|
|
320
|
+
<div className="flex items-center gap-1 ml-auto">
|
|
321
|
+
<Clock className="h-3 w-3" />
|
|
322
|
+
<span>{formatRelativeTime(board.updatedAt)}</span>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
</Link>
|
|
327
|
+
</div>
|
|
328
|
+
)
|
|
329
|
+
})}
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Productivity Dashboard Layout
|
|
3
|
+
* Clean layout with fixed sidebar for boards navigation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import { ReactNode } from 'react'
|
|
9
|
+
import { ProductivitySidebar } from '@/themes/productivity/templates/shared/ProductivitySidebar'
|
|
10
|
+
import { ProductivityMobileNav } from '@/themes/productivity/templates/shared/ProductivityMobileNav'
|
|
11
|
+
|
|
12
|
+
interface ProductivityDashboardLayoutProps {
|
|
13
|
+
children: ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function ProductivityDashboardLayout({ children }: ProductivityDashboardLayoutProps) {
|
|
17
|
+
console.log('🎨 [ProductivityDashboardLayout] RENDERING - ProductivitySidebar should be visible')
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen bg-background">
|
|
20
|
+
{/* Desktop: Fixed Sidebar */}
|
|
21
|
+
<ProductivitySidebar />
|
|
22
|
+
|
|
23
|
+
{/* Mobile: Bottom Navigation */}
|
|
24
|
+
<ProductivityMobileNav />
|
|
25
|
+
|
|
26
|
+
{/* Main Content Area */}
|
|
27
|
+
<main className="min-h-screen pb-20 lg:pb-0 lg:ml-64">
|
|
28
|
+
{children}
|
|
29
|
+
</main>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|