@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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Productivity Theme Configuration
|
|
3
|
+
*
|
|
4
|
+
* A Trello-style task management app with boards, lists, and cards.
|
|
5
|
+
* Collaborative mode: owner can invite team members.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ThemeConfig } from '@nextsparkjs/core/types/theme'
|
|
9
|
+
|
|
10
|
+
export const productivityThemeConfig: ThemeConfig = {
|
|
11
|
+
name: 'productivity',
|
|
12
|
+
displayName: 'Productivity',
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
description: 'A collaborative task management app with boards, lists, and cards',
|
|
15
|
+
author: 'NextSpark Team',
|
|
16
|
+
|
|
17
|
+
plugins: [],
|
|
18
|
+
|
|
19
|
+
styles: {
|
|
20
|
+
globals: 'globals.css',
|
|
21
|
+
components: 'components.css',
|
|
22
|
+
variables: {
|
|
23
|
+
'--spacing-xs': '0.125rem',
|
|
24
|
+
'--spacing-sm': '0.25rem',
|
|
25
|
+
'--spacing-md': '0.5rem',
|
|
26
|
+
'--spacing-lg': '1rem',
|
|
27
|
+
'--spacing-xl': '1.5rem',
|
|
28
|
+
'--spacing-2xl': '2rem'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Modern, clean productivity aesthetic
|
|
33
|
+
config: {
|
|
34
|
+
colors: {
|
|
35
|
+
background: 'oklch(0.98 0.005 240)',
|
|
36
|
+
foreground: 'oklch(0.2 0.02 260)',
|
|
37
|
+
card: 'oklch(1.0 0 0)',
|
|
38
|
+
'card-foreground': 'oklch(0.2 0.02 260)',
|
|
39
|
+
popover: 'oklch(1.0 0 0)',
|
|
40
|
+
'popover-foreground': 'oklch(0.2 0.02 260)',
|
|
41
|
+
// Blue primary for productivity focus
|
|
42
|
+
primary: 'oklch(0.55 0.2 250)',
|
|
43
|
+
'primary-foreground': 'oklch(1.0 0 0)',
|
|
44
|
+
secondary: 'oklch(0.95 0.01 240)',
|
|
45
|
+
'secondary-foreground': 'oklch(0.3 0.02 260)',
|
|
46
|
+
muted: 'oklch(0.96 0.005 240)',
|
|
47
|
+
'muted-foreground': 'oklch(0.45 0.015 260)',
|
|
48
|
+
accent: 'oklch(0.92 0.03 200)',
|
|
49
|
+
'accent-foreground': 'oklch(0.2 0.02 260)',
|
|
50
|
+
destructive: 'oklch(0.55 0.22 25)',
|
|
51
|
+
'destructive-foreground': 'oklch(1.0 0 0)',
|
|
52
|
+
border: 'oklch(0.9 0.01 240)',
|
|
53
|
+
input: 'oklch(0.9 0.01 240)',
|
|
54
|
+
ring: 'oklch(0.55 0.2 250)',
|
|
55
|
+
|
|
56
|
+
// Chart colors for analytics
|
|
57
|
+
'chart-1': 'oklch(0.55 0.2 250)',
|
|
58
|
+
'chart-2': 'oklch(0.6 0.18 150)',
|
|
59
|
+
'chart-3': 'oklch(0.65 0.15 50)',
|
|
60
|
+
'chart-4': 'oklch(0.5 0.2 300)',
|
|
61
|
+
'chart-5': 'oklch(0.55 0.15 30)',
|
|
62
|
+
|
|
63
|
+
// Sidebar - slightly tinted
|
|
64
|
+
sidebar: 'oklch(0.97 0.01 250)',
|
|
65
|
+
'sidebar-foreground': 'oklch(0.2 0.02 260)',
|
|
66
|
+
'sidebar-primary': 'oklch(0.55 0.2 250)',
|
|
67
|
+
'sidebar-primary-foreground': 'oklch(1.0 0 0)',
|
|
68
|
+
'sidebar-accent': 'oklch(0.92 0.03 200)',
|
|
69
|
+
'sidebar-accent-foreground': 'oklch(0.2 0.02 260)',
|
|
70
|
+
'sidebar-border': 'oklch(0.9 0.01 240)',
|
|
71
|
+
'sidebar-ring': 'oklch(0.55 0.2 250)'
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
fonts: {
|
|
75
|
+
sans: 'Inter, system-ui, sans-serif',
|
|
76
|
+
serif: 'Georgia, serif',
|
|
77
|
+
mono: 'JetBrains Mono, monospace'
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
spacing: {
|
|
81
|
+
radius: '0.75rem',
|
|
82
|
+
'radius-sm': '0.5rem',
|
|
83
|
+
'radius-md': '0.625rem',
|
|
84
|
+
'radius-lg': '0.75rem',
|
|
85
|
+
'radius-xl': '1rem'
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
breakpoints: {
|
|
89
|
+
'shadow-2xs': '0 1px 2px 0 rgb(0 0 0 / 0.03)',
|
|
90
|
+
'shadow-xs': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
91
|
+
'shadow-sm': '0 1px 3px 0 rgb(0 0 0 / 0.1)',
|
|
92
|
+
shadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
93
|
+
'shadow-md': '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
|
94
|
+
'shadow-lg': '0 10px 15px -3px rgb(0 0 0 / 0.1)',
|
|
95
|
+
'shadow-xl': '0 20px 25px -5px rgb(0 0 0 / 0.1)',
|
|
96
|
+
'shadow-2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
components: {
|
|
101
|
+
overrides: {},
|
|
102
|
+
custom: {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default productivityThemeConfig
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boards Entity Configuration
|
|
3
|
+
*
|
|
4
|
+
* Trello-style boards for organizing lists and cards.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LayoutDashboard } from 'lucide-react'
|
|
8
|
+
import type { EntityConfig } from '@nextsparkjs/core/lib/entities/types'
|
|
9
|
+
import { boardFields } from './boards.fields'
|
|
10
|
+
|
|
11
|
+
export const boardEntityConfig: EntityConfig = {
|
|
12
|
+
// ==========================================
|
|
13
|
+
// 1. BASIC IDENTIFICATION
|
|
14
|
+
// ==========================================
|
|
15
|
+
slug: 'boards',
|
|
16
|
+
enabled: true,
|
|
17
|
+
names: {
|
|
18
|
+
singular: 'board',
|
|
19
|
+
plural: 'Boards'
|
|
20
|
+
},
|
|
21
|
+
icon: LayoutDashboard,
|
|
22
|
+
|
|
23
|
+
// ==========================================
|
|
24
|
+
// 2. ACCESS AND SCOPE CONFIGURATION
|
|
25
|
+
// ==========================================
|
|
26
|
+
access: {
|
|
27
|
+
public: false, // Private boards
|
|
28
|
+
api: true,
|
|
29
|
+
metadata: false,
|
|
30
|
+
shared: true // Shared within team
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// ==========================================
|
|
34
|
+
// 3. UI/UX FEATURES
|
|
35
|
+
// ==========================================
|
|
36
|
+
ui: {
|
|
37
|
+
dashboard: {
|
|
38
|
+
showInMenu: true,
|
|
39
|
+
showInTopbar: true
|
|
40
|
+
},
|
|
41
|
+
public: {
|
|
42
|
+
hasArchivePage: false,
|
|
43
|
+
hasSinglePage: false
|
|
44
|
+
},
|
|
45
|
+
features: {
|
|
46
|
+
searchable: true,
|
|
47
|
+
sortable: true,
|
|
48
|
+
filterable: true,
|
|
49
|
+
bulkOperations: false,
|
|
50
|
+
importExport: false
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// ==========================================
|
|
55
|
+
// FIELDS
|
|
56
|
+
// ==========================================
|
|
57
|
+
fields: boardFields,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default boardEntityConfig
|
|
61
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boards Entity Fields Configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
|
|
6
|
+
|
|
7
|
+
export const boardFields: EntityField[] = [
|
|
8
|
+
{
|
|
9
|
+
name: 'name',
|
|
10
|
+
type: 'text',
|
|
11
|
+
required: true,
|
|
12
|
+
display: {
|
|
13
|
+
label: 'Name',
|
|
14
|
+
description: 'Board name',
|
|
15
|
+
placeholder: 'Enter board name...',
|
|
16
|
+
showInList: true,
|
|
17
|
+
showInDetail: true,
|
|
18
|
+
showInForm: true,
|
|
19
|
+
order: 1,
|
|
20
|
+
columnWidth: 12,
|
|
21
|
+
},
|
|
22
|
+
api: {
|
|
23
|
+
readOnly: false,
|
|
24
|
+
searchable: true,
|
|
25
|
+
sortable: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'description',
|
|
30
|
+
type: 'textarea',
|
|
31
|
+
required: false,
|
|
32
|
+
display: {
|
|
33
|
+
label: 'Description',
|
|
34
|
+
description: 'Optional board description',
|
|
35
|
+
placeholder: 'Describe what this board is for...',
|
|
36
|
+
showInList: false,
|
|
37
|
+
showInDetail: true,
|
|
38
|
+
showInForm: true,
|
|
39
|
+
order: 2,
|
|
40
|
+
columnWidth: 12,
|
|
41
|
+
},
|
|
42
|
+
api: {
|
|
43
|
+
readOnly: false,
|
|
44
|
+
searchable: true,
|
|
45
|
+
sortable: false,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'color',
|
|
50
|
+
type: 'select',
|
|
51
|
+
required: false,
|
|
52
|
+
defaultValue: 'blue',
|
|
53
|
+
options: [
|
|
54
|
+
{ value: 'blue', label: 'Blue' },
|
|
55
|
+
{ value: 'green', label: 'Green' },
|
|
56
|
+
{ value: 'purple', label: 'Purple' },
|
|
57
|
+
{ value: 'orange', label: 'Orange' },
|
|
58
|
+
{ value: 'red', label: 'Red' },
|
|
59
|
+
{ value: 'pink', label: 'Pink' },
|
|
60
|
+
{ value: 'gray', label: 'Gray' },
|
|
61
|
+
],
|
|
62
|
+
display: {
|
|
63
|
+
label: 'Color',
|
|
64
|
+
description: 'Board background color',
|
|
65
|
+
placeholder: 'Select color...',
|
|
66
|
+
showInList: true,
|
|
67
|
+
showInDetail: true,
|
|
68
|
+
showInForm: true,
|
|
69
|
+
order: 3,
|
|
70
|
+
columnWidth: 6,
|
|
71
|
+
},
|
|
72
|
+
api: {
|
|
73
|
+
readOnly: false,
|
|
74
|
+
searchable: false,
|
|
75
|
+
sortable: false,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'archived',
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
required: false,
|
|
82
|
+
defaultValue: false,
|
|
83
|
+
display: {
|
|
84
|
+
label: 'Archived',
|
|
85
|
+
description: 'Archive this board',
|
|
86
|
+
showInList: false,
|
|
87
|
+
showInDetail: true,
|
|
88
|
+
showInForm: true,
|
|
89
|
+
order: 4,
|
|
90
|
+
columnWidth: 6,
|
|
91
|
+
},
|
|
92
|
+
api: {
|
|
93
|
+
readOnly: false,
|
|
94
|
+
searchable: false,
|
|
95
|
+
sortable: true,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'position',
|
|
100
|
+
type: 'number',
|
|
101
|
+
required: false,
|
|
102
|
+
defaultValue: 0,
|
|
103
|
+
display: {
|
|
104
|
+
label: 'Position',
|
|
105
|
+
description: 'Display order',
|
|
106
|
+
showInList: false,
|
|
107
|
+
showInDetail: false,
|
|
108
|
+
showInForm: false,
|
|
109
|
+
order: 5,
|
|
110
|
+
},
|
|
111
|
+
api: {
|
|
112
|
+
readOnly: false,
|
|
113
|
+
searchable: false,
|
|
114
|
+
sortable: true,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'createdAt',
|
|
119
|
+
type: 'datetime',
|
|
120
|
+
required: false,
|
|
121
|
+
display: {
|
|
122
|
+
label: 'Created At',
|
|
123
|
+
description: 'When the board was created',
|
|
124
|
+
showInList: true,
|
|
125
|
+
showInDetail: true,
|
|
126
|
+
showInForm: false,
|
|
127
|
+
order: 98,
|
|
128
|
+
},
|
|
129
|
+
api: {
|
|
130
|
+
readOnly: true,
|
|
131
|
+
searchable: false,
|
|
132
|
+
sortable: true,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'updatedAt',
|
|
137
|
+
type: 'datetime',
|
|
138
|
+
required: false,
|
|
139
|
+
display: {
|
|
140
|
+
label: 'Updated At',
|
|
141
|
+
description: 'When the board was last modified',
|
|
142
|
+
showInList: false,
|
|
143
|
+
showInDetail: true,
|
|
144
|
+
showInForm: false,
|
|
145
|
+
order: 99,
|
|
146
|
+
},
|
|
147
|
+
api: {
|
|
148
|
+
readOnly: true,
|
|
149
|
+
searchable: false,
|
|
150
|
+
sortable: true,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boards Service
|
|
3
|
+
* Provides data access methods for boards entity.
|
|
4
|
+
*/
|
|
5
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
6
|
+
|
|
7
|
+
export interface Board {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
description: string | null
|
|
11
|
+
color: string | null
|
|
12
|
+
isArchived: boolean
|
|
13
|
+
position: number
|
|
14
|
+
userId: string
|
|
15
|
+
teamId: string
|
|
16
|
+
createdAt: Date
|
|
17
|
+
updatedAt: Date
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BoardListOptions {
|
|
21
|
+
limit?: number
|
|
22
|
+
offset?: number
|
|
23
|
+
orderBy?: string
|
|
24
|
+
orderDir?: 'asc' | 'desc'
|
|
25
|
+
teamId?: string
|
|
26
|
+
isArchived?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BoardListResult {
|
|
30
|
+
boards: Board[]
|
|
31
|
+
total: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BoardCreateData {
|
|
35
|
+
name: string
|
|
36
|
+
description?: string | null
|
|
37
|
+
color?: string | null
|
|
38
|
+
isArchived?: boolean
|
|
39
|
+
position?: number
|
|
40
|
+
teamId: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BoardUpdateData {
|
|
44
|
+
name?: string
|
|
45
|
+
description?: string | null
|
|
46
|
+
color?: string | null
|
|
47
|
+
isArchived?: boolean
|
|
48
|
+
position?: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DbBoard {
|
|
52
|
+
id: string
|
|
53
|
+
name: string
|
|
54
|
+
description: string | null
|
|
55
|
+
color: string | null
|
|
56
|
+
is_archived: boolean
|
|
57
|
+
position: number
|
|
58
|
+
user_id: string
|
|
59
|
+
team_id: string
|
|
60
|
+
created_at: string
|
|
61
|
+
updated_at: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function mapDbBoard(dbBoard: DbBoard): Board {
|
|
65
|
+
return {
|
|
66
|
+
id: dbBoard.id,
|
|
67
|
+
name: dbBoard.name,
|
|
68
|
+
description: dbBoard.description,
|
|
69
|
+
color: dbBoard.color,
|
|
70
|
+
isArchived: dbBoard.is_archived,
|
|
71
|
+
position: dbBoard.position,
|
|
72
|
+
userId: dbBoard.user_id,
|
|
73
|
+
teamId: dbBoard.team_id,
|
|
74
|
+
createdAt: new Date(dbBoard.created_at),
|
|
75
|
+
updatedAt: new Date(dbBoard.updated_at),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class BoardsService {
|
|
80
|
+
/**
|
|
81
|
+
* Get a board by ID with RLS
|
|
82
|
+
*/
|
|
83
|
+
static async getById(id: string, userId: string): Promise<Board | null> {
|
|
84
|
+
const result = await queryOneWithRLS<DbBoard>(
|
|
85
|
+
userId,
|
|
86
|
+
`SELECT * FROM boards WHERE id = $1`,
|
|
87
|
+
[id]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return result ? mapDbBoard(result) : null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* List boards with RLS and filtering
|
|
95
|
+
*/
|
|
96
|
+
static async list(userId: string, options: BoardListOptions = {}): Promise<BoardListResult> {
|
|
97
|
+
const {
|
|
98
|
+
limit = 50,
|
|
99
|
+
offset = 0,
|
|
100
|
+
orderBy = 'position',
|
|
101
|
+
orderDir = 'asc',
|
|
102
|
+
teamId,
|
|
103
|
+
isArchived,
|
|
104
|
+
} = options
|
|
105
|
+
|
|
106
|
+
const conditions: string[] = []
|
|
107
|
+
const params: any[] = []
|
|
108
|
+
let paramIndex = 1
|
|
109
|
+
|
|
110
|
+
if (teamId) {
|
|
111
|
+
conditions.push(`team_id = $${paramIndex++}`)
|
|
112
|
+
params.push(teamId)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isArchived !== undefined) {
|
|
116
|
+
conditions.push(`is_archived = $${paramIndex++}`)
|
|
117
|
+
params.push(isArchived)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
121
|
+
|
|
122
|
+
// Get total count
|
|
123
|
+
const countResult = await queryOneWithRLS<{ count: string }>(
|
|
124
|
+
userId,
|
|
125
|
+
`SELECT COUNT(*) as count FROM boards ${whereClause}`,
|
|
126
|
+
params
|
|
127
|
+
)
|
|
128
|
+
const total = parseInt(countResult?.count || '0', 10)
|
|
129
|
+
|
|
130
|
+
// Get boards
|
|
131
|
+
const validOrderBy = ['position', 'name', 'created_at', 'updated_at'].includes(orderBy)
|
|
132
|
+
? orderBy
|
|
133
|
+
: 'position'
|
|
134
|
+
const validOrderDir = orderDir === 'desc' ? 'DESC' : 'ASC'
|
|
135
|
+
|
|
136
|
+
params.push(limit, offset)
|
|
137
|
+
const boards = await queryWithRLS<DbBoard>(
|
|
138
|
+
userId,
|
|
139
|
+
`SELECT * FROM boards ${whereClause} ORDER BY ${validOrderBy} ${validOrderDir} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
140
|
+
params
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
boards: boards.map(mapDbBoard),
|
|
145
|
+
total,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get boards ordered by position
|
|
151
|
+
*/
|
|
152
|
+
static async getByPosition(userId: string, teamId?: string): Promise<Board[]> {
|
|
153
|
+
const result = await this.list(userId, {
|
|
154
|
+
orderBy: 'position',
|
|
155
|
+
orderDir: 'asc',
|
|
156
|
+
teamId,
|
|
157
|
+
isArchived: false,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return result.boards
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a new board with RLS
|
|
165
|
+
*/
|
|
166
|
+
static async create(userId: string, data: BoardCreateData): Promise<Board> {
|
|
167
|
+
const {
|
|
168
|
+
name,
|
|
169
|
+
description = null,
|
|
170
|
+
color = null,
|
|
171
|
+
isArchived = false,
|
|
172
|
+
position = 0,
|
|
173
|
+
teamId,
|
|
174
|
+
} = data
|
|
175
|
+
|
|
176
|
+
const result = await mutateWithRLS<DbBoard>(
|
|
177
|
+
userId,
|
|
178
|
+
`INSERT INTO boards (name, description, color, is_archived, position, user_id, team_id)
|
|
179
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
180
|
+
RETURNING *`,
|
|
181
|
+
[name, description, color, isArchived, position, userId, teamId]
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if (!result) {
|
|
185
|
+
throw new Error('Failed to create board')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return mapDbBoard(result)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Update a board with RLS
|
|
193
|
+
*/
|
|
194
|
+
static async update(userId: string, id: string, data: BoardUpdateData): Promise<Board> {
|
|
195
|
+
const updates: string[] = []
|
|
196
|
+
const params: any[] = []
|
|
197
|
+
let paramIndex = 1
|
|
198
|
+
|
|
199
|
+
if (data.name !== undefined) {
|
|
200
|
+
updates.push(`name = $${paramIndex++}`)
|
|
201
|
+
params.push(data.name)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (data.description !== undefined) {
|
|
205
|
+
updates.push(`description = $${paramIndex++}`)
|
|
206
|
+
params.push(data.description)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (data.color !== undefined) {
|
|
210
|
+
updates.push(`color = $${paramIndex++}`)
|
|
211
|
+
params.push(data.color)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (data.isArchived !== undefined) {
|
|
215
|
+
updates.push(`is_archived = $${paramIndex++}`)
|
|
216
|
+
params.push(data.isArchived)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (data.position !== undefined) {
|
|
220
|
+
updates.push(`position = $${paramIndex++}`)
|
|
221
|
+
params.push(data.position)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (updates.length === 0) {
|
|
225
|
+
throw new Error('No fields to update')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
updates.push(`updated_at = NOW()`)
|
|
229
|
+
params.push(id)
|
|
230
|
+
|
|
231
|
+
const result = await mutateWithRLS<DbBoard>(
|
|
232
|
+
userId,
|
|
233
|
+
`UPDATE boards SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
|
|
234
|
+
params
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if (!result) {
|
|
238
|
+
throw new Error('Board not found or access denied')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return mapDbBoard(result)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Delete a board with RLS
|
|
246
|
+
*/
|
|
247
|
+
static async delete(userId: string, id: string): Promise<boolean> {
|
|
248
|
+
const result = await mutateWithRLS<DbBoard>(
|
|
249
|
+
userId,
|
|
250
|
+
`DELETE FROM boards WHERE id = $1 RETURNING *`,
|
|
251
|
+
[id]
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return result !== null
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board Service Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the BoardsService.
|
|
5
|
+
* Boards represent workspaces that contain lists and cards for project organization.
|
|
6
|
+
*
|
|
7
|
+
* @module BoardsTypes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Main entity interface
|
|
11
|
+
export interface Board {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
description: string | null
|
|
15
|
+
color: string | null
|
|
16
|
+
isArchived: boolean
|
|
17
|
+
position: number
|
|
18
|
+
teamId: string
|
|
19
|
+
userId: string
|
|
20
|
+
createdAt: string
|
|
21
|
+
updatedAt: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// List options
|
|
25
|
+
export interface BoardListOptions {
|
|
26
|
+
limit?: number
|
|
27
|
+
offset?: number
|
|
28
|
+
teamId?: string
|
|
29
|
+
isArchived?: boolean
|
|
30
|
+
orderBy?: 'name' | 'position' | 'createdAt'
|
|
31
|
+
orderDir?: 'asc' | 'desc'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// List result
|
|
35
|
+
export interface BoardListResult {
|
|
36
|
+
boards: Board[]
|
|
37
|
+
total: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create data (required fields + teamId + optional fields)
|
|
41
|
+
export interface BoardCreateData {
|
|
42
|
+
name: string
|
|
43
|
+
teamId: string
|
|
44
|
+
description?: string
|
|
45
|
+
color?: string
|
|
46
|
+
isArchived?: boolean
|
|
47
|
+
position?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Update data (all fields optional)
|
|
51
|
+
export interface BoardUpdateData {
|
|
52
|
+
name?: string
|
|
53
|
+
description?: string | null
|
|
54
|
+
color?: string | null
|
|
55
|
+
isArchived?: boolean
|
|
56
|
+
position?: number
|
|
57
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Boards",
|
|
3
|
+
"singular": "Board",
|
|
4
|
+
"plural": "Boards",
|
|
5
|
+
"description": "Organize your work into boards",
|
|
6
|
+
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": {
|
|
9
|
+
"label": "Name",
|
|
10
|
+
"placeholder": "Enter board name...",
|
|
11
|
+
"description": "Board name"
|
|
12
|
+
},
|
|
13
|
+
"description": {
|
|
14
|
+
"label": "Description",
|
|
15
|
+
"placeholder": "Describe what this board is for...",
|
|
16
|
+
"description": "Optional board description"
|
|
17
|
+
},
|
|
18
|
+
"color": {
|
|
19
|
+
"label": "Color",
|
|
20
|
+
"placeholder": "Select color...",
|
|
21
|
+
"description": "Board background color",
|
|
22
|
+
"options": {
|
|
23
|
+
"blue": "Blue",
|
|
24
|
+
"green": "Green",
|
|
25
|
+
"purple": "Purple",
|
|
26
|
+
"orange": "Orange",
|
|
27
|
+
"red": "Red",
|
|
28
|
+
"pink": "Pink",
|
|
29
|
+
"gray": "Gray"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"archived": {
|
|
33
|
+
"label": "Archived",
|
|
34
|
+
"description": "Archive this board"
|
|
35
|
+
},
|
|
36
|
+
"position": {
|
|
37
|
+
"label": "Position",
|
|
38
|
+
"description": "Display order"
|
|
39
|
+
},
|
|
40
|
+
"createdAt": {
|
|
41
|
+
"label": "Created At",
|
|
42
|
+
"description": "When the board was created"
|
|
43
|
+
},
|
|
44
|
+
"updatedAt": {
|
|
45
|
+
"label": "Updated At",
|
|
46
|
+
"description": "When the board was last modified"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
"actions": {
|
|
51
|
+
"create": "New Board",
|
|
52
|
+
"edit": "Edit Board",
|
|
53
|
+
"delete": "Delete Board",
|
|
54
|
+
"archive": "Archive Board",
|
|
55
|
+
"unarchive": "Unarchive Board",
|
|
56
|
+
"duplicate": "Duplicate Board"
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
"messages": {
|
|
60
|
+
"created": "Board created successfully",
|
|
61
|
+
"updated": "Board updated successfully",
|
|
62
|
+
"deleted": "Board deleted successfully",
|
|
63
|
+
"archived": "Board archived",
|
|
64
|
+
"unarchived": "Board unarchived",
|
|
65
|
+
"confirmDelete": "Are you sure you want to delete this board? All lists and cards will be permanently removed."
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
"empty": {
|
|
69
|
+
"title": "No boards yet",
|
|
70
|
+
"description": "Create your first board to start organizing your work.",
|
|
71
|
+
"action": "Create your first board"
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
"filters": {
|
|
75
|
+
"all": "All Boards",
|
|
76
|
+
"active": "Active",
|
|
77
|
+
"archived": "Archived"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|