@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,336 @@
1
+ /**
2
+ * Cards Service
3
+ * Provides data access methods for cards entity.
4
+ */
5
+ import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
6
+
7
+ export interface Card {
8
+ id: string
9
+ title: string
10
+ description: string | null
11
+ listId: string
12
+ boardId: string
13
+ position: number
14
+ dueDate: Date | null
15
+ assigneeId: string | null
16
+ labels: string[]
17
+ isArchived: boolean
18
+ userId: string
19
+ teamId: string
20
+ createdAt: Date
21
+ updatedAt: Date
22
+ }
23
+
24
+ export interface CardListOptions {
25
+ limit?: number
26
+ offset?: number
27
+ orderBy?: string
28
+ orderDir?: 'asc' | 'desc'
29
+ teamId?: string
30
+ listId?: string
31
+ boardId?: string
32
+ assigneeId?: string
33
+ isArchived?: boolean
34
+ }
35
+
36
+ export interface CardListResult {
37
+ cards: Card[]
38
+ total: number
39
+ }
40
+
41
+ export interface CardCreateData {
42
+ title: string
43
+ description?: string | null
44
+ listId: string
45
+ boardId: string
46
+ position?: number
47
+ dueDate?: Date | null
48
+ assigneeId?: string | null
49
+ labels?: string[]
50
+ isArchived?: boolean
51
+ teamId: string
52
+ }
53
+
54
+ export interface CardUpdateData {
55
+ title?: string
56
+ description?: string | null
57
+ listId?: string
58
+ boardId?: string
59
+ position?: number
60
+ dueDate?: Date | null
61
+ assigneeId?: string | null
62
+ labels?: string[]
63
+ isArchived?: boolean
64
+ }
65
+
66
+ interface DbCard {
67
+ id: string
68
+ title: string
69
+ description: string | null
70
+ list_id: string
71
+ board_id: string
72
+ position: number
73
+ due_date: string | null
74
+ assignee_id: string | null
75
+ labels: string
76
+ is_archived: boolean
77
+ user_id: string
78
+ team_id: string
79
+ created_at: string
80
+ updated_at: string
81
+ }
82
+
83
+ function mapDbCard(dbCard: DbCard): Card {
84
+ return {
85
+ id: dbCard.id,
86
+ title: dbCard.title,
87
+ description: dbCard.description,
88
+ listId: dbCard.list_id,
89
+ boardId: dbCard.board_id,
90
+ position: dbCard.position,
91
+ dueDate: dbCard.due_date ? new Date(dbCard.due_date) : null,
92
+ assigneeId: dbCard.assignee_id,
93
+ labels: dbCard.labels ? JSON.parse(dbCard.labels) : [],
94
+ isArchived: dbCard.is_archived,
95
+ userId: dbCard.user_id,
96
+ teamId: dbCard.team_id,
97
+ createdAt: new Date(dbCard.created_at),
98
+ updatedAt: new Date(dbCard.updated_at),
99
+ }
100
+ }
101
+
102
+ export class CardsService {
103
+ /**
104
+ * Get a card by ID with RLS
105
+ */
106
+ static async getById(id: string, userId: string): Promise<Card | null> {
107
+ const result = await queryOneWithRLS<DbCard>(
108
+ userId,
109
+ `SELECT * FROM cards WHERE id = $1`,
110
+ [id]
111
+ )
112
+
113
+ return result ? mapDbCard(result) : null
114
+ }
115
+
116
+ /**
117
+ * List cards with RLS and filtering
118
+ */
119
+ static async list(userId: string, options: CardListOptions = {}): Promise<CardListResult> {
120
+ const {
121
+ limit = 50,
122
+ offset = 0,
123
+ orderBy = 'position',
124
+ orderDir = 'asc',
125
+ teamId,
126
+ listId,
127
+ boardId,
128
+ assigneeId,
129
+ isArchived,
130
+ } = options
131
+
132
+ const conditions: string[] = []
133
+ const params: any[] = []
134
+ let paramIndex = 1
135
+
136
+ if (teamId) {
137
+ conditions.push(`team_id = $${paramIndex++}`)
138
+ params.push(teamId)
139
+ }
140
+
141
+ if (listId) {
142
+ conditions.push(`list_id = $${paramIndex++}`)
143
+ params.push(listId)
144
+ }
145
+
146
+ if (boardId) {
147
+ conditions.push(`board_id = $${paramIndex++}`)
148
+ params.push(boardId)
149
+ }
150
+
151
+ if (assigneeId) {
152
+ conditions.push(`assignee_id = $${paramIndex++}`)
153
+ params.push(assigneeId)
154
+ }
155
+
156
+ if (isArchived !== undefined) {
157
+ conditions.push(`is_archived = $${paramIndex++}`)
158
+ params.push(isArchived)
159
+ }
160
+
161
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
162
+
163
+ // Get total count
164
+ const countResult = await queryOneWithRLS<{ count: string }>(
165
+ userId,
166
+ `SELECT COUNT(*) as count FROM cards ${whereClause}`,
167
+ params
168
+ )
169
+ const total = parseInt(countResult?.count || '0', 10)
170
+
171
+ // Get cards
172
+ const validOrderBy = ['position', 'title', 'due_date', 'created_at', 'updated_at'].includes(orderBy)
173
+ ? orderBy
174
+ : 'position'
175
+ const validOrderDir = orderDir === 'desc' ? 'DESC' : 'ASC'
176
+
177
+ params.push(limit, offset)
178
+ const cards = await queryWithRLS<DbCard>(
179
+ userId,
180
+ `SELECT * FROM cards ${whereClause} ORDER BY ${validOrderBy} ${validOrderDir} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
181
+ params
182
+ )
183
+
184
+ return {
185
+ cards: cards.map(mapDbCard),
186
+ total,
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Get all cards for a specific list
192
+ */
193
+ static async getByList(listId: string, userId: string): Promise<Card[]> {
194
+ const result = await this.list(userId, {
195
+ listId,
196
+ orderBy: 'position',
197
+ orderDir: 'asc',
198
+ isArchived: false,
199
+ })
200
+
201
+ return result.cards
202
+ }
203
+
204
+ /**
205
+ * Create a new card with RLS
206
+ */
207
+ static async create(userId: string, data: CardCreateData): Promise<Card> {
208
+ const {
209
+ title,
210
+ description = null,
211
+ listId,
212
+ boardId,
213
+ position = 0,
214
+ dueDate = null,
215
+ assigneeId = null,
216
+ labels = [],
217
+ isArchived = false,
218
+ teamId,
219
+ } = data
220
+
221
+ const result = await mutateWithRLS<DbCard>(
222
+ userId,
223
+ `INSERT INTO cards (title, description, list_id, board_id, position, due_date, assignee_id, labels, is_archived, user_id, team_id)
224
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
225
+ RETURNING *`,
226
+ [title, description, listId, boardId, position, dueDate, assigneeId, JSON.stringify(labels), isArchived, userId, teamId]
227
+ )
228
+
229
+ if (!result) {
230
+ throw new Error('Failed to create card')
231
+ }
232
+
233
+ return mapDbCard(result)
234
+ }
235
+
236
+ /**
237
+ * Update a card with RLS
238
+ */
239
+ static async update(userId: string, id: string, data: CardUpdateData): Promise<Card> {
240
+ const updates: string[] = []
241
+ const params: any[] = []
242
+ let paramIndex = 1
243
+
244
+ if (data.title !== undefined) {
245
+ updates.push(`title = $${paramIndex++}`)
246
+ params.push(data.title)
247
+ }
248
+
249
+ if (data.description !== undefined) {
250
+ updates.push(`description = $${paramIndex++}`)
251
+ params.push(data.description)
252
+ }
253
+
254
+ if (data.listId !== undefined) {
255
+ updates.push(`list_id = $${paramIndex++}`)
256
+ params.push(data.listId)
257
+ }
258
+
259
+ if (data.boardId !== undefined) {
260
+ updates.push(`board_id = $${paramIndex++}`)
261
+ params.push(data.boardId)
262
+ }
263
+
264
+ if (data.position !== undefined) {
265
+ updates.push(`position = $${paramIndex++}`)
266
+ params.push(data.position)
267
+ }
268
+
269
+ if (data.dueDate !== undefined) {
270
+ updates.push(`due_date = $${paramIndex++}`)
271
+ params.push(data.dueDate)
272
+ }
273
+
274
+ if (data.assigneeId !== undefined) {
275
+ updates.push(`assignee_id = $${paramIndex++}`)
276
+ params.push(data.assigneeId)
277
+ }
278
+
279
+ if (data.labels !== undefined) {
280
+ updates.push(`labels = $${paramIndex++}`)
281
+ params.push(JSON.stringify(data.labels))
282
+ }
283
+
284
+ if (data.isArchived !== undefined) {
285
+ updates.push(`is_archived = $${paramIndex++}`)
286
+ params.push(data.isArchived)
287
+ }
288
+
289
+ if (updates.length === 0) {
290
+ throw new Error('No fields to update')
291
+ }
292
+
293
+ updates.push(`updated_at = NOW()`)
294
+ params.push(id)
295
+
296
+ const result = await mutateWithRLS<DbCard>(
297
+ userId,
298
+ `UPDATE cards SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
299
+ params
300
+ )
301
+
302
+ if (!result) {
303
+ throw new Error('Card not found or access denied')
304
+ }
305
+
306
+ return mapDbCard(result)
307
+ }
308
+
309
+ /**
310
+ * Move a card to a different list with new position
311
+ */
312
+ static async moveToList(
313
+ cardId: string,
314
+ userId: string,
315
+ newListId: string,
316
+ newPosition: number
317
+ ): Promise<Card> {
318
+ return this.update(userId, cardId, {
319
+ listId: newListId,
320
+ position: newPosition,
321
+ })
322
+ }
323
+
324
+ /**
325
+ * Delete a card with RLS
326
+ */
327
+ static async delete(userId: string, id: string): Promise<boolean> {
328
+ const result = await mutateWithRLS<DbCard>(
329
+ userId,
330
+ `DELETE FROM cards WHERE id = $1 RETURNING *`,
331
+ [id]
332
+ )
333
+
334
+ return result !== null
335
+ }
336
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Card Service Types
3
+ *
4
+ * Type definitions for the CardsService.
5
+ * Cards represent individual tasks or items within a list.
6
+ *
7
+ * @module CardsTypes
8
+ */
9
+
10
+ // Card label interface
11
+ export interface CardLabel {
12
+ id: string
13
+ name: string
14
+ color: string
15
+ }
16
+
17
+ // Main entity interface
18
+ export interface Card {
19
+ id: string
20
+ title: string
21
+ description: string | null
22
+ listId: string
23
+ boardId: string | null
24
+ position: number
25
+ dueDate: string | null
26
+ assigneeId: string | null
27
+ labels: CardLabel[]
28
+ isArchived: boolean
29
+ teamId: string
30
+ userId: string
31
+ createdAt: string
32
+ updatedAt: string
33
+ }
34
+
35
+ // List options
36
+ export interface CardListOptions {
37
+ limit?: number
38
+ offset?: number
39
+ teamId?: string
40
+ listId?: string
41
+ boardId?: string
42
+ assigneeId?: string
43
+ isArchived?: boolean
44
+ orderBy?: 'title' | 'position' | 'dueDate' | 'createdAt'
45
+ orderDir?: 'asc' | 'desc'
46
+ }
47
+
48
+ // List result
49
+ export interface CardListResult {
50
+ cards: Card[]
51
+ total: number
52
+ }
53
+
54
+ // Create data (required fields + teamId + optional fields)
55
+ export interface CardCreateData {
56
+ title: string
57
+ listId: string
58
+ teamId: string
59
+ description?: string
60
+ boardId?: string
61
+ position?: number
62
+ dueDate?: string
63
+ assigneeId?: string
64
+ labels?: CardLabel[]
65
+ isArchived?: boolean
66
+ }
67
+
68
+ // Update data (all fields optional)
69
+ export interface CardUpdateData {
70
+ title?: string
71
+ description?: string | null
72
+ listId?: string
73
+ boardId?: string | null
74
+ position?: number
75
+ dueDate?: string | null
76
+ assigneeId?: string | null
77
+ labels?: CardLabel[]
78
+ isArchived?: boolean
79
+ }
@@ -0,0 +1,114 @@
1
+ {
2
+ "title": "Cards",
3
+ "singular": "Card",
4
+ "plural": "Cards",
5
+ "description": "Task cards to track your work",
6
+
7
+ "myCards": "My Cards",
8
+ "allCards": "All Cards",
9
+ "assignedToMe": "Assigned to Me",
10
+
11
+ "fields": {
12
+ "title": {
13
+ "label": "Title",
14
+ "placeholder": "Enter card title...",
15
+ "description": "Card title"
16
+ },
17
+ "description": {
18
+ "label": "Description",
19
+ "placeholder": "Add a more detailed description...",
20
+ "description": "Detailed description of the task"
21
+ },
22
+ "position": {
23
+ "label": "Position",
24
+ "description": "Display order within list"
25
+ },
26
+ "dueDate": {
27
+ "label": "Due Date",
28
+ "placeholder": "Select due date...",
29
+ "description": "When this task is due"
30
+ },
31
+ "labels": {
32
+ "label": "Labels",
33
+ "placeholder": "Select labels...",
34
+ "description": "Tags to categorize this card",
35
+ "options": {
36
+ "urgent": "Urgent",
37
+ "important": "Important",
38
+ "bug": "Bug",
39
+ "feature": "Feature",
40
+ "enhancement": "Enhancement",
41
+ "documentation": "Documentation"
42
+ }
43
+ },
44
+ "assigneeId": {
45
+ "label": "Assignee",
46
+ "placeholder": "Assign to...",
47
+ "description": "Team member assigned to this card"
48
+ },
49
+ "listId": {
50
+ "label": "List",
51
+ "placeholder": "Select list...",
52
+ "description": "Parent list"
53
+ },
54
+ "boardId": {
55
+ "label": "Board",
56
+ "placeholder": "Select board...",
57
+ "description": "Parent board"
58
+ },
59
+ "createdAt": {
60
+ "label": "Created At",
61
+ "description": "When the card was created"
62
+ },
63
+ "updatedAt": {
64
+ "label": "Updated At",
65
+ "description": "When the card was last modified"
66
+ }
67
+ },
68
+
69
+ "actions": {
70
+ "create": "Add Card",
71
+ "edit": "Edit Card",
72
+ "delete": "Delete Card",
73
+ "move": "Move Card",
74
+ "assign": "Assign",
75
+ "unassign": "Unassign",
76
+ "addLabel": "Add Label",
77
+ "removeLabel": "Remove Label",
78
+ "setDueDate": "Set Due Date",
79
+ "removeDueDate": "Remove Due Date"
80
+ },
81
+
82
+ "messages": {
83
+ "created": "Card created successfully",
84
+ "updated": "Card updated successfully",
85
+ "deleted": "Card deleted successfully",
86
+ "moved": "Card moved to {listName}",
87
+ "assigned": "Card assigned to {userName}",
88
+ "unassigned": "Card unassigned",
89
+ "confirmDelete": "Are you sure you want to delete this card?"
90
+ },
91
+
92
+ "empty": {
93
+ "title": "No cards in this list",
94
+ "description": "Add a card to get started.",
95
+ "action": "Add a card"
96
+ },
97
+
98
+ "filters": {
99
+ "all": "All Cards",
100
+ "assignedToMe": "Assigned to Me",
101
+ "unassigned": "Unassigned",
102
+ "overdue": "Overdue",
103
+ "dueThisWeek": "Due This Week",
104
+ "noDueDate": "No Due Date"
105
+ },
106
+
107
+ "dueStatus": {
108
+ "overdue": "Overdue",
109
+ "dueToday": "Due Today",
110
+ "dueTomorrow": "Due Tomorrow",
111
+ "dueThisWeek": "Due This Week"
112
+ }
113
+ }
114
+
@@ -0,0 +1,114 @@
1
+ {
2
+ "title": "Tarjetas",
3
+ "singular": "Tarjeta",
4
+ "plural": "Tarjetas",
5
+ "description": "Tarjetas de tareas para seguir tu trabajo",
6
+
7
+ "myCards": "Mis Tarjetas",
8
+ "allCards": "Todas las Tarjetas",
9
+ "assignedToMe": "Asignadas a Mí",
10
+
11
+ "fields": {
12
+ "title": {
13
+ "label": "Título",
14
+ "placeholder": "Ingresa el título de la tarjeta...",
15
+ "description": "Título de la tarjeta"
16
+ },
17
+ "description": {
18
+ "label": "Descripción",
19
+ "placeholder": "Agrega una descripción más detallada...",
20
+ "description": "Descripción detallada de la tarea"
21
+ },
22
+ "position": {
23
+ "label": "Posición",
24
+ "description": "Orden de visualización dentro de la lista"
25
+ },
26
+ "dueDate": {
27
+ "label": "Fecha Límite",
28
+ "placeholder": "Seleccionar fecha límite...",
29
+ "description": "Cuándo vence esta tarea"
30
+ },
31
+ "labels": {
32
+ "label": "Etiquetas",
33
+ "placeholder": "Seleccionar etiquetas...",
34
+ "description": "Tags para categorizar esta tarjeta",
35
+ "options": {
36
+ "urgent": "Urgente",
37
+ "important": "Importante",
38
+ "bug": "Bug",
39
+ "feature": "Funcionalidad",
40
+ "enhancement": "Mejora",
41
+ "documentation": "Documentación"
42
+ }
43
+ },
44
+ "assigneeId": {
45
+ "label": "Asignado a",
46
+ "placeholder": "Asignar a...",
47
+ "description": "Miembro del equipo asignado a esta tarjeta"
48
+ },
49
+ "listId": {
50
+ "label": "Lista",
51
+ "placeholder": "Seleccionar lista...",
52
+ "description": "Lista padre"
53
+ },
54
+ "boardId": {
55
+ "label": "Tablero",
56
+ "placeholder": "Seleccionar tablero...",
57
+ "description": "Tablero padre"
58
+ },
59
+ "createdAt": {
60
+ "label": "Creado",
61
+ "description": "Cuándo se creó la tarjeta"
62
+ },
63
+ "updatedAt": {
64
+ "label": "Actualizado",
65
+ "description": "Cuándo se modificó por última vez"
66
+ }
67
+ },
68
+
69
+ "actions": {
70
+ "create": "Agregar Tarjeta",
71
+ "edit": "Editar Tarjeta",
72
+ "delete": "Eliminar Tarjeta",
73
+ "move": "Mover Tarjeta",
74
+ "assign": "Asignar",
75
+ "unassign": "Desasignar",
76
+ "addLabel": "Agregar Etiqueta",
77
+ "removeLabel": "Quitar Etiqueta",
78
+ "setDueDate": "Establecer Fecha Límite",
79
+ "removeDueDate": "Quitar Fecha Límite"
80
+ },
81
+
82
+ "messages": {
83
+ "created": "Tarjeta creada exitosamente",
84
+ "updated": "Tarjeta actualizada exitosamente",
85
+ "deleted": "Tarjeta eliminada exitosamente",
86
+ "moved": "Tarjeta movida a {listName}",
87
+ "assigned": "Tarjeta asignada a {userName}",
88
+ "unassigned": "Tarjeta desasignada",
89
+ "confirmDelete": "¿Estás seguro de que quieres eliminar esta tarjeta?"
90
+ },
91
+
92
+ "empty": {
93
+ "title": "No hay tarjetas en esta lista",
94
+ "description": "Agrega una tarjeta para comenzar.",
95
+ "action": "Agregar una tarjeta"
96
+ },
97
+
98
+ "filters": {
99
+ "all": "Todas las Tarjetas",
100
+ "assignedToMe": "Asignadas a Mí",
101
+ "unassigned": "Sin Asignar",
102
+ "overdue": "Vencidas",
103
+ "dueThisWeek": "Vencen Esta Semana",
104
+ "noDueDate": "Sin Fecha Límite"
105
+ },
106
+
107
+ "dueStatus": {
108
+ "overdue": "Vencida",
109
+ "dueToday": "Vence Hoy",
110
+ "dueTomorrow": "Vence Mañana",
111
+ "dueThisWeek": "Vence Esta Semana"
112
+ }
113
+ }
114
+