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