@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,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Productivity TopBar Component
|
|
3
|
+
* Professional top navigation bar for Productivity dashboard
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import Link from 'next/link'
|
|
9
|
+
import Image from 'next/image'
|
|
10
|
+
import { usePathname, useParams } from 'next/navigation'
|
|
11
|
+
import { useAuth } from '@nextsparkjs/core/hooks/useAuth'
|
|
12
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuLabel,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
DropdownMenuTrigger,
|
|
20
|
+
} from '@nextsparkjs/core/components/ui/dropdown-menu'
|
|
21
|
+
import {
|
|
22
|
+
Bell,
|
|
23
|
+
Search,
|
|
24
|
+
Plus,
|
|
25
|
+
User,
|
|
26
|
+
Settings,
|
|
27
|
+
CreditCard,
|
|
28
|
+
LogOut,
|
|
29
|
+
Sun,
|
|
30
|
+
Moon,
|
|
31
|
+
HelpCircle,
|
|
32
|
+
ChevronDown,
|
|
33
|
+
Home,
|
|
34
|
+
ChevronRight,
|
|
35
|
+
Kanban
|
|
36
|
+
} from 'lucide-react'
|
|
37
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
38
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
39
|
+
import { useTheme } from 'next-themes'
|
|
40
|
+
|
|
41
|
+
interface Board {
|
|
42
|
+
id: string
|
|
43
|
+
name: string
|
|
44
|
+
color: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ProductivityTopBar() {
|
|
48
|
+
const { user, signOut, isLoading } = useAuth()
|
|
49
|
+
const pathname = usePathname()
|
|
50
|
+
const params = useParams()
|
|
51
|
+
const { theme, setTheme } = useTheme()
|
|
52
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
53
|
+
const [showSearch, setShowSearch] = useState(false)
|
|
54
|
+
const [currentBoard, setCurrentBoard] = useState<Board | null>(null)
|
|
55
|
+
|
|
56
|
+
const boardId = params?.id as string
|
|
57
|
+
|
|
58
|
+
// Fetch current board name if on a board page
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const fetchBoard = async () => {
|
|
61
|
+
if (boardId) {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(`/api/v1/boards/${boardId}`)
|
|
64
|
+
if (response.ok) {
|
|
65
|
+
const data = await response.json()
|
|
66
|
+
setCurrentBoard(data.data)
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Failed to fetch board:', error)
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
setCurrentBoard(null)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fetchBoard()
|
|
77
|
+
}, [boardId])
|
|
78
|
+
|
|
79
|
+
// Generate user initials
|
|
80
|
+
const getUserInitials = (user: { firstName?: string; lastName?: string; email: string }) => {
|
|
81
|
+
if (user.firstName && user.lastName) {
|
|
82
|
+
return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
|
|
83
|
+
}
|
|
84
|
+
if (user.firstName) {
|
|
85
|
+
return user.firstName.slice(0, 2).toUpperCase()
|
|
86
|
+
}
|
|
87
|
+
return user.email.slice(0, 2).toUpperCase()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleSignOut = useCallback(async () => {
|
|
91
|
+
try {
|
|
92
|
+
await signOut()
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Sign out failed:', error)
|
|
95
|
+
}
|
|
96
|
+
}, [signOut])
|
|
97
|
+
|
|
98
|
+
// Get breadcrumb items
|
|
99
|
+
const getBreadcrumb = () => {
|
|
100
|
+
const items: { label: string; href?: string; icon?: React.ElementType }[] = [
|
|
101
|
+
{ label: 'Home', href: '/dashboard', icon: Home }
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
if (pathname.includes('/boards')) {
|
|
105
|
+
items.push({ label: 'Boards', href: '/dashboard/boards' })
|
|
106
|
+
|
|
107
|
+
if (currentBoard) {
|
|
108
|
+
items.push({ label: currentBoard.name })
|
|
109
|
+
} else if (pathname.includes('/create')) {
|
|
110
|
+
items.push({ label: 'New Board' })
|
|
111
|
+
}
|
|
112
|
+
} else if (pathname.includes('/settings')) {
|
|
113
|
+
items.push({ label: 'Settings', href: '/dashboard/settings' })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return items
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const breadcrumb = getBreadcrumb()
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<header
|
|
123
|
+
className={cn(
|
|
124
|
+
'hidden lg:block bg-background/80 backdrop-blur-md border-b border-border/50 fixed top-0 right-0 z-40 transition-all duration-300 ease-out'
|
|
125
|
+
)}
|
|
126
|
+
style={{ left: 'var(--productivity-sidebar-width, 4rem)' }}
|
|
127
|
+
>
|
|
128
|
+
<div className="h-16 px-6 flex items-center justify-between gap-4">
|
|
129
|
+
{/* Left side: Breadcrumb */}
|
|
130
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
131
|
+
{breadcrumb.map((item, index) => (
|
|
132
|
+
<div key={index} className="flex items-center gap-2">
|
|
133
|
+
{index > 0 && (
|
|
134
|
+
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
|
135
|
+
)}
|
|
136
|
+
{item.href ? (
|
|
137
|
+
<Link
|
|
138
|
+
href={item.href}
|
|
139
|
+
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
140
|
+
>
|
|
141
|
+
{item.icon && <item.icon className="w-4 h-4" />}
|
|
142
|
+
{item.label}
|
|
143
|
+
</Link>
|
|
144
|
+
) : (
|
|
145
|
+
<span className="flex items-center gap-1.5 text-sm font-medium text-foreground truncate">
|
|
146
|
+
{currentBoard && (
|
|
147
|
+
<div
|
|
148
|
+
className={cn(
|
|
149
|
+
'w-3 h-3 rounded-sm',
|
|
150
|
+
`bg-[var(--board-${currentBoard.color})]`
|
|
151
|
+
)}
|
|
152
|
+
style={{
|
|
153
|
+
backgroundColor: `var(--board-${currentBoard.color}, var(--board-blue))`
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
{item.label}
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Center: Search */}
|
|
165
|
+
<div className="hidden md:flex flex-1 max-w-md mx-4">
|
|
166
|
+
<div className="relative w-full">
|
|
167
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
168
|
+
<input
|
|
169
|
+
type="text"
|
|
170
|
+
placeholder="Search cards..."
|
|
171
|
+
value={searchQuery}
|
|
172
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
173
|
+
className="w-full pl-10 pr-4 py-2 bg-muted/50 border border-border rounded-lg text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
|
174
|
+
/>
|
|
175
|
+
{searchQuery && (
|
|
176
|
+
<kbd className="absolute right-3 top-1/2 -translate-y-1/2 hidden sm:inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
|
177
|
+
ESC
|
|
178
|
+
</kbd>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Right side: Actions */}
|
|
184
|
+
<div className="flex items-center gap-2">
|
|
185
|
+
{/* Mobile search toggle */}
|
|
186
|
+
<Button
|
|
187
|
+
variant="ghost"
|
|
188
|
+
size="icon"
|
|
189
|
+
className="md:hidden h-9 w-9"
|
|
190
|
+
onClick={() => setShowSearch(!showSearch)}
|
|
191
|
+
>
|
|
192
|
+
<Search className="h-4 w-4" />
|
|
193
|
+
</Button>
|
|
194
|
+
|
|
195
|
+
{/* Quick Create Card */}
|
|
196
|
+
{currentBoard && (
|
|
197
|
+
<Button size="sm" className="gap-2 h-9">
|
|
198
|
+
<Plus className="h-4 w-4" />
|
|
199
|
+
<span className="hidden sm:inline">Add Card</span>
|
|
200
|
+
</Button>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Notifications */}
|
|
204
|
+
<Button variant="ghost" size="icon" className="h-9 w-9 relative">
|
|
205
|
+
<Bell className="h-4 w-4" />
|
|
206
|
+
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-destructive rounded-full" />
|
|
207
|
+
</Button>
|
|
208
|
+
|
|
209
|
+
{/* Help */}
|
|
210
|
+
<Button variant="ghost" size="icon" className="h-9 w-9 hidden sm:flex">
|
|
211
|
+
<HelpCircle className="h-4 w-4" />
|
|
212
|
+
</Button>
|
|
213
|
+
|
|
214
|
+
{/* Theme Toggle */}
|
|
215
|
+
<Button
|
|
216
|
+
variant="ghost"
|
|
217
|
+
size="icon"
|
|
218
|
+
className="h-9 w-9"
|
|
219
|
+
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
220
|
+
>
|
|
221
|
+
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
222
|
+
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
223
|
+
</Button>
|
|
224
|
+
|
|
225
|
+
{/* User Menu */}
|
|
226
|
+
{isLoading ? (
|
|
227
|
+
<div className="h-9 w-9 bg-muted animate-pulse rounded-full" />
|
|
228
|
+
) : user ? (
|
|
229
|
+
<DropdownMenu>
|
|
230
|
+
<DropdownMenuTrigger asChild>
|
|
231
|
+
<Button variant="ghost" className="h-9 gap-2 pl-2 pr-3">
|
|
232
|
+
{user.image ? (
|
|
233
|
+
<Image
|
|
234
|
+
src={user.image}
|
|
235
|
+
alt=""
|
|
236
|
+
width={28}
|
|
237
|
+
height={28}
|
|
238
|
+
className="h-7 w-7 rounded-full object-cover"
|
|
239
|
+
/>
|
|
240
|
+
) : (
|
|
241
|
+
<div className="h-7 w-7 rounded-full bg-primary flex items-center justify-center text-xs font-medium text-primary-foreground">
|
|
242
|
+
{getUserInitials(user)}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
<ChevronDown className="h-3 w-3 opacity-50 hidden sm:block" />
|
|
246
|
+
</Button>
|
|
247
|
+
</DropdownMenuTrigger>
|
|
248
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
249
|
+
<DropdownMenuLabel>
|
|
250
|
+
<div className="flex flex-col space-y-1">
|
|
251
|
+
<p className="text-sm font-medium">{user.firstName || 'User'}</p>
|
|
252
|
+
<p className="text-xs text-muted-foreground">{user.email}</p>
|
|
253
|
+
</div>
|
|
254
|
+
</DropdownMenuLabel>
|
|
255
|
+
<DropdownMenuSeparator />
|
|
256
|
+
<DropdownMenuItem asChild>
|
|
257
|
+
<Link href="/dashboard/settings/profile" className="flex items-center">
|
|
258
|
+
<User className="mr-2 h-4 w-4" />
|
|
259
|
+
Profile
|
|
260
|
+
</Link>
|
|
261
|
+
</DropdownMenuItem>
|
|
262
|
+
<DropdownMenuItem asChild>
|
|
263
|
+
<Link href="/dashboard/settings" className="flex items-center">
|
|
264
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
265
|
+
Settings
|
|
266
|
+
</Link>
|
|
267
|
+
</DropdownMenuItem>
|
|
268
|
+
<DropdownMenuItem asChild>
|
|
269
|
+
<Link href="/dashboard/settings/billing" className="flex items-center">
|
|
270
|
+
<CreditCard className="mr-2 h-4 w-4" />
|
|
271
|
+
Billing
|
|
272
|
+
</Link>
|
|
273
|
+
</DropdownMenuItem>
|
|
274
|
+
<DropdownMenuSeparator />
|
|
275
|
+
<DropdownMenuItem
|
|
276
|
+
onClick={handleSignOut}
|
|
277
|
+
className="text-destructive focus:text-destructive"
|
|
278
|
+
>
|
|
279
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
280
|
+
Sign out
|
|
281
|
+
</DropdownMenuItem>
|
|
282
|
+
</DropdownMenuContent>
|
|
283
|
+
</DropdownMenu>
|
|
284
|
+
) : (
|
|
285
|
+
<div className="flex items-center gap-2">
|
|
286
|
+
<Button variant="ghost" size="sm" asChild>
|
|
287
|
+
<Link href="/login">Sign in</Link>
|
|
288
|
+
</Button>
|
|
289
|
+
<Button size="sm" asChild>
|
|
290
|
+
<Link href="/signup">Sign up</Link>
|
|
291
|
+
</Button>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/* Mobile search bar */}
|
|
298
|
+
{showSearch && (
|
|
299
|
+
<div className="md:hidden px-4 pb-4 animate-in slide-in-from-top-2">
|
|
300
|
+
<div className="relative">
|
|
301
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
302
|
+
<input
|
|
303
|
+
type="text"
|
|
304
|
+
placeholder="Search cards..."
|
|
305
|
+
value={searchQuery}
|
|
306
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
307
|
+
className="w-full pl-10 pr-4 py-2 bg-muted/50 border border-border rounded-lg text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
|
308
|
+
autoFocus
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</header>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export default ProductivityTopBar
|