@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,236 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Productivity Theme - Board Detail Page (Kanban View)
|
|
5
|
+
*
|
|
6
|
+
* Shows a board with its lists and cards in a Kanban layout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect, use } from 'react'
|
|
10
|
+
import Link from 'next/link'
|
|
11
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
12
|
+
import { Badge } from '@nextsparkjs/core/components/ui/badge'
|
|
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 { ArrowLeft, MoreVertical, Settings, Archive, Trash2 } from 'lucide-react'
|
|
22
|
+
import { KanbanBoard } from '@/themes/productivity/components/KanbanBoard'
|
|
23
|
+
import { PermissionGate } from '@nextsparkjs/core/components/permissions/PermissionGate'
|
|
24
|
+
import { useToast } from '@nextsparkjs/core/hooks/useToast'
|
|
25
|
+
import { useRouter } from 'next/navigation'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get headers with x-team-id for API calls
|
|
29
|
+
*/
|
|
30
|
+
function getTeamHeaders(): HeadersInit {
|
|
31
|
+
const headers: HeadersInit = {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
}
|
|
34
|
+
if (typeof window !== 'undefined') {
|
|
35
|
+
const teamId = localStorage.getItem('activeTeamId')
|
|
36
|
+
console.log('[BoardPage] activeTeamId from localStorage:', teamId)
|
|
37
|
+
if (teamId) {
|
|
38
|
+
headers['x-team-id'] = teamId
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return headers
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Board {
|
|
45
|
+
id: string
|
|
46
|
+
name: string
|
|
47
|
+
description?: string | null
|
|
48
|
+
color?: string | null
|
|
49
|
+
status?: string | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface PageProps {
|
|
53
|
+
params: Promise<{ id: string }>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function BoardDetailPage({ params }: PageProps) {
|
|
57
|
+
const resolvedParams = use(params)
|
|
58
|
+
const boardId = resolvedParams.id
|
|
59
|
+
|
|
60
|
+
const [board, setBoard] = useState<Board | null>(null)
|
|
61
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
62
|
+
const { toast } = useToast()
|
|
63
|
+
const router = useRouter()
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const fetchBoard = async () => {
|
|
67
|
+
try {
|
|
68
|
+
setIsLoading(true)
|
|
69
|
+
const response = await fetch(`/api/v1/boards/${boardId}`, {
|
|
70
|
+
headers: getTeamHeaders(),
|
|
71
|
+
})
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error('Board not found')
|
|
74
|
+
}
|
|
75
|
+
const data = await response.json()
|
|
76
|
+
setBoard(data.data || data)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error fetching board:', error)
|
|
79
|
+
toast({
|
|
80
|
+
title: 'Error',
|
|
81
|
+
description: 'Could not load board.',
|
|
82
|
+
variant: 'destructive',
|
|
83
|
+
})
|
|
84
|
+
} finally {
|
|
85
|
+
setIsLoading(false)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fetchBoard()
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
+
}, [boardId])
|
|
92
|
+
|
|
93
|
+
const handleArchive = async () => {
|
|
94
|
+
try {
|
|
95
|
+
await fetch(`/api/v1/boards/${boardId}`, {
|
|
96
|
+
method: 'PATCH',
|
|
97
|
+
headers: getTeamHeaders(),
|
|
98
|
+
body: JSON.stringify({ status: 'archived' }),
|
|
99
|
+
})
|
|
100
|
+
toast({ title: 'Board archived' })
|
|
101
|
+
router.push('/dashboard/boards')
|
|
102
|
+
} catch (error) {
|
|
103
|
+
toast({
|
|
104
|
+
title: 'Error',
|
|
105
|
+
description: 'Could not archive board.',
|
|
106
|
+
variant: 'destructive',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const handleDelete = async () => {
|
|
112
|
+
if (!confirm('Are you sure you want to delete this board? All lists and cards will be deleted.')) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await fetch(`/api/v1/boards/${boardId}`, {
|
|
118
|
+
method: 'DELETE',
|
|
119
|
+
headers: getTeamHeaders(),
|
|
120
|
+
})
|
|
121
|
+
toast({ title: 'Board deleted' })
|
|
122
|
+
router.push('/dashboard/boards')
|
|
123
|
+
} catch (error) {
|
|
124
|
+
toast({
|
|
125
|
+
title: 'Error',
|
|
126
|
+
description: 'Could not delete board.',
|
|
127
|
+
variant: 'destructive',
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isLoading) {
|
|
133
|
+
return (
|
|
134
|
+
<div className="h-full flex flex-col">
|
|
135
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
136
|
+
<div className="flex items-center gap-4">
|
|
137
|
+
<Skeleton className="h-8 w-8" />
|
|
138
|
+
<Skeleton className="h-6 w-48" />
|
|
139
|
+
</div>
|
|
140
|
+
<Skeleton className="h-8 w-8" />
|
|
141
|
+
</div>
|
|
142
|
+
<div className="flex-1 p-4">
|
|
143
|
+
<div className="flex gap-4">
|
|
144
|
+
{[1, 2, 3].map((i) => (
|
|
145
|
+
<Skeleton key={i} className="w-72 h-96" />
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!board) {
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex flex-col items-center justify-center h-full">
|
|
156
|
+
<h2 className="text-xl font-semibold mb-2">Board not found</h2>
|
|
157
|
+
<p className="text-muted-foreground mb-4">
|
|
158
|
+
This board may have been deleted or you don't have access.
|
|
159
|
+
</p>
|
|
160
|
+
<Link href="/dashboard/boards">
|
|
161
|
+
<Button variant="outline">
|
|
162
|
+
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
163
|
+
Back to Boards
|
|
164
|
+
</Button>
|
|
165
|
+
</Link>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className="h-full flex flex-col">
|
|
172
|
+
{/* Header */}
|
|
173
|
+
<div className="flex items-center justify-between px-4 py-3 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
174
|
+
<div className="flex items-center gap-4">
|
|
175
|
+
<Link href="/dashboard/boards">
|
|
176
|
+
<Button variant="ghost" size="sm">
|
|
177
|
+
<ArrowLeft className="h-4 w-4" />
|
|
178
|
+
</Button>
|
|
179
|
+
</Link>
|
|
180
|
+
|
|
181
|
+
<div>
|
|
182
|
+
<div className="flex items-center gap-2">
|
|
183
|
+
<h1 className="text-xl font-semibold">{board.name}</h1>
|
|
184
|
+
{board.status && board.status !== 'active' && (
|
|
185
|
+
<Badge variant="secondary">{board.status}</Badge>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
{board.description && (
|
|
189
|
+
<p className="text-sm text-muted-foreground">{board.description}</p>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<DropdownMenu>
|
|
195
|
+
<DropdownMenuTrigger asChild>
|
|
196
|
+
<Button variant="ghost" size="sm">
|
|
197
|
+
<MoreVertical className="h-4 w-4" />
|
|
198
|
+
</Button>
|
|
199
|
+
</DropdownMenuTrigger>
|
|
200
|
+
<DropdownMenuContent align="end">
|
|
201
|
+
<PermissionGate permission="boards.update">
|
|
202
|
+
<DropdownMenuItem asChild>
|
|
203
|
+
<Link href={`/dashboard/boards/${boardId}/edit`}>
|
|
204
|
+
<Settings className="h-4 w-4 mr-2" />
|
|
205
|
+
Settings
|
|
206
|
+
</Link>
|
|
207
|
+
</DropdownMenuItem>
|
|
208
|
+
</PermissionGate>
|
|
209
|
+
<DropdownMenuSeparator />
|
|
210
|
+
<PermissionGate permission="boards.archive">
|
|
211
|
+
<DropdownMenuItem onClick={handleArchive}>
|
|
212
|
+
<Archive className="h-4 w-4 mr-2" />
|
|
213
|
+
Archive Board
|
|
214
|
+
</DropdownMenuItem>
|
|
215
|
+
</PermissionGate>
|
|
216
|
+
<PermissionGate permission="boards.delete">
|
|
217
|
+
<DropdownMenuItem
|
|
218
|
+
className="text-destructive"
|
|
219
|
+
onClick={handleDelete}
|
|
220
|
+
>
|
|
221
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
222
|
+
Delete Board
|
|
223
|
+
</DropdownMenuItem>
|
|
224
|
+
</PermissionGate>
|
|
225
|
+
</DropdownMenuContent>
|
|
226
|
+
</DropdownMenu>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Kanban Board */}
|
|
230
|
+
<div className="flex-1 overflow-hidden">
|
|
231
|
+
<KanbanBoard boardId={boardId} />
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Productivity Theme - Create Board Page
|
|
5
|
+
*
|
|
6
|
+
* Form for creating a new board.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState } from 'react'
|
|
10
|
+
import Link from 'next/link'
|
|
11
|
+
import { useRouter } from 'next/navigation'
|
|
12
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@nextsparkjs/core/components/ui/card'
|
|
13
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
14
|
+
import { Input } from '@nextsparkjs/core/components/ui/input'
|
|
15
|
+
import { Textarea } from '@nextsparkjs/core/components/ui/textarea'
|
|
16
|
+
import { Label } from '@nextsparkjs/core/components/ui/label'
|
|
17
|
+
import {
|
|
18
|
+
Select,
|
|
19
|
+
SelectContent,
|
|
20
|
+
SelectItem,
|
|
21
|
+
SelectTrigger,
|
|
22
|
+
SelectValue,
|
|
23
|
+
} from '@nextsparkjs/core/components/ui/select'
|
|
24
|
+
import { ArrowLeft, Loader2 } from 'lucide-react'
|
|
25
|
+
import { useToast } from '@nextsparkjs/core/hooks/useToast'
|
|
26
|
+
import { usePermission } from '@nextsparkjs/core/lib/permissions/hooks'
|
|
27
|
+
import { NoPermission } from '@nextsparkjs/core/components/permissions/NoPermission'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get headers with x-team-id for API calls
|
|
31
|
+
*/
|
|
32
|
+
function getTeamHeaders(): HeadersInit {
|
|
33
|
+
const headers: HeadersInit = {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
}
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
const teamId = localStorage.getItem('activeTeamId')
|
|
38
|
+
if (teamId) {
|
|
39
|
+
headers['x-team-id'] = teamId
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return headers
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const colorOptions = [
|
|
46
|
+
{ value: 'blue', label: 'Blue', class: 'bg-blue-500' },
|
|
47
|
+
{ value: 'green', label: 'Green', class: 'bg-green-500' },
|
|
48
|
+
{ value: 'purple', label: 'Purple', class: 'bg-purple-500' },
|
|
49
|
+
{ value: 'orange', label: 'Orange', class: 'bg-orange-500' },
|
|
50
|
+
{ value: 'red', label: 'Red', class: 'bg-red-500' },
|
|
51
|
+
{ value: 'pink', label: 'Pink', class: 'bg-pink-500' },
|
|
52
|
+
{ value: 'gray', label: 'Gray', class: 'bg-gray-500' },
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
export default function CreateBoardPage() {
|
|
56
|
+
const router = useRouter()
|
|
57
|
+
const { toast } = useToast()
|
|
58
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
59
|
+
const [formData, setFormData] = useState({
|
|
60
|
+
name: '',
|
|
61
|
+
description: '',
|
|
62
|
+
color: 'blue',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Permission check
|
|
66
|
+
const hasCreatePermission = usePermission('boards.create')
|
|
67
|
+
|
|
68
|
+
// If user doesn't have permission, show NoPermission component
|
|
69
|
+
if (hasCreatePermission === false) {
|
|
70
|
+
return (
|
|
71
|
+
<NoPermission
|
|
72
|
+
entityName="Board"
|
|
73
|
+
action="create"
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
79
|
+
e.preventDefault()
|
|
80
|
+
|
|
81
|
+
if (!formData.name.trim()) {
|
|
82
|
+
toast({
|
|
83
|
+
title: 'Validation Error',
|
|
84
|
+
description: 'Board name is required.',
|
|
85
|
+
variant: 'destructive',
|
|
86
|
+
})
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setIsSubmitting(true)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch('/api/v1/boards', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: getTeamHeaders(),
|
|
96
|
+
body: JSON.stringify(formData),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const error = await response.json()
|
|
101
|
+
throw new Error(error.message || 'Failed to create board')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await response.json()
|
|
105
|
+
const boardId = data.data?.id || data.id
|
|
106
|
+
|
|
107
|
+
toast({ title: 'Board created successfully!' })
|
|
108
|
+
router.push(`/dashboard/boards/${boardId}`)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Error creating board:', error)
|
|
111
|
+
toast({
|
|
112
|
+
title: 'Error',
|
|
113
|
+
description: error instanceof Error ? error.message : 'Could not create board.',
|
|
114
|
+
variant: 'destructive',
|
|
115
|
+
})
|
|
116
|
+
} finally {
|
|
117
|
+
setIsSubmitting(false)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="p-6 max-w-2xl mx-auto" data-cy="boards-create-page">
|
|
123
|
+
{/* Header */}
|
|
124
|
+
<div className="flex items-center gap-4 mb-6">
|
|
125
|
+
<Link href="/dashboard/boards">
|
|
126
|
+
<Button variant="ghost" size="sm">
|
|
127
|
+
<ArrowLeft className="h-4 w-4" />
|
|
128
|
+
</Button>
|
|
129
|
+
</Link>
|
|
130
|
+
<div>
|
|
131
|
+
<h1 className="text-2xl font-bold">Create Board</h1>
|
|
132
|
+
<p className="text-muted-foreground text-sm">
|
|
133
|
+
Set up a new board to organize your work
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Form */}
|
|
139
|
+
<Card>
|
|
140
|
+
<CardHeader>
|
|
141
|
+
<CardTitle>Board Details</CardTitle>
|
|
142
|
+
</CardHeader>
|
|
143
|
+
<CardContent>
|
|
144
|
+
<form onSubmit={handleSubmit} className="space-y-6" data-cy="boards-form">
|
|
145
|
+
{/* Name */}
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Label htmlFor="name">Name *</Label>
|
|
148
|
+
<Input
|
|
149
|
+
id="name"
|
|
150
|
+
placeholder="Enter board name..."
|
|
151
|
+
value={formData.name}
|
|
152
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
153
|
+
disabled={isSubmitting}
|
|
154
|
+
autoFocus
|
|
155
|
+
data-cy="boards-field-name"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Description */}
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
<Label htmlFor="description">Description</Label>
|
|
162
|
+
<Textarea
|
|
163
|
+
id="description"
|
|
164
|
+
placeholder="Describe what this board is for..."
|
|
165
|
+
value={formData.description}
|
|
166
|
+
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
167
|
+
disabled={isSubmitting}
|
|
168
|
+
rows={3}
|
|
169
|
+
data-cy="boards-field-description"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Color */}
|
|
174
|
+
<div className="space-y-2">
|
|
175
|
+
<Label htmlFor="color">Color</Label>
|
|
176
|
+
<Select
|
|
177
|
+
value={formData.color}
|
|
178
|
+
onValueChange={(value) => setFormData({ ...formData, color: value })}
|
|
179
|
+
disabled={isSubmitting}
|
|
180
|
+
>
|
|
181
|
+
<SelectTrigger id="color" data-cy="boards-field-color">
|
|
182
|
+
<SelectValue placeholder="Select a color" />
|
|
183
|
+
</SelectTrigger>
|
|
184
|
+
<SelectContent>
|
|
185
|
+
{colorOptions.map((color) => (
|
|
186
|
+
<SelectItem key={color.value} value={color.value}>
|
|
187
|
+
<div className="flex items-center gap-2">
|
|
188
|
+
<div className={`w-4 h-4 rounded ${color.class}`} />
|
|
189
|
+
{color.label}
|
|
190
|
+
</div>
|
|
191
|
+
</SelectItem>
|
|
192
|
+
))}
|
|
193
|
+
</SelectContent>
|
|
194
|
+
</Select>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Color Preview */}
|
|
198
|
+
<div className="space-y-2">
|
|
199
|
+
<Label>Preview</Label>
|
|
200
|
+
<div className="border rounded-lg overflow-hidden">
|
|
201
|
+
<div
|
|
202
|
+
className={`h-2 ${colorOptions.find(c => c.value === formData.color)?.class || 'bg-blue-500'}`}
|
|
203
|
+
/>
|
|
204
|
+
<div className="p-4 bg-muted/30">
|
|
205
|
+
<p className="font-medium">{formData.name || 'Board Name'}</p>
|
|
206
|
+
{formData.description && (
|
|
207
|
+
<p className="text-sm text-muted-foreground mt-1">{formData.description}</p>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Actions */}
|
|
214
|
+
<div className="flex items-center gap-4 pt-4">
|
|
215
|
+
<Button type="submit" disabled={isSubmitting} data-cy="boards-form-submit">
|
|
216
|
+
{isSubmitting ? (
|
|
217
|
+
<>
|
|
218
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
219
|
+
Creating...
|
|
220
|
+
</>
|
|
221
|
+
) : (
|
|
222
|
+
'Create Board'
|
|
223
|
+
)}
|
|
224
|
+
</Button>
|
|
225
|
+
<Link href="/dashboard/boards">
|
|
226
|
+
<Button type="button" variant="outline" disabled={isSubmitting} data-cy="boards-form-cancel">
|
|
227
|
+
Cancel
|
|
228
|
+
</Button>
|
|
229
|
+
</Link>
|
|
230
|
+
</div>
|
|
231
|
+
</form>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|