@nextsparkjs/theme-blog 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 +65 -0
- package/about.md +93 -0
- package/api/authors/[username]/route.ts +150 -0
- package/api/authors/route.ts +63 -0
- package/api/posts/public/route.ts +151 -0
- package/components/ExportPostsButton.tsx +102 -0
- package/components/ImportPostsDialog.tsx +284 -0
- package/components/PostsToolbar.tsx +24 -0
- package/components/editor/FeaturedImageUpload.tsx +185 -0
- package/components/editor/WysiwygEditor.tsx +340 -0
- package/components/index.ts +4 -0
- package/components/public/AuthorBio.tsx +105 -0
- package/components/public/AuthorCard.tsx +130 -0
- package/components/public/BlogFooter.tsx +185 -0
- package/components/public/BlogNavbar.tsx +201 -0
- package/components/public/PostCard.tsx +306 -0
- package/components/public/ReadingProgress.tsx +70 -0
- package/components/public/RelatedPosts.tsx +78 -0
- package/config/app.config.ts +200 -0
- package/config/billing.config.ts +146 -0
- package/config/dashboard.config.ts +333 -0
- package/config/dev.config.ts +48 -0
- package/config/features.config.ts +196 -0
- package/config/flows.config.ts +333 -0
- package/config/permissions.config.ts +101 -0
- package/config/theme.config.ts +128 -0
- package/entities/categories/categories.config.ts +60 -0
- package/entities/categories/categories.fields.ts +115 -0
- package/entities/categories/categories.service.ts +333 -0
- package/entities/categories/categories.types.ts +58 -0
- package/entities/categories/messages/en.json +33 -0
- package/entities/categories/messages/es.json +33 -0
- package/entities/posts/messages/en.json +100 -0
- package/entities/posts/messages/es.json +100 -0
- package/entities/posts/migrations/001_posts_table.sql +110 -0
- package/entities/posts/migrations/002_add_featured.sql +19 -0
- package/entities/posts/migrations/003_post_categories_pivot.sql +47 -0
- package/entities/posts/posts.config.ts +61 -0
- package/entities/posts/posts.fields.ts +234 -0
- package/entities/posts/posts.service.ts +464 -0
- package/entities/posts/posts.types.ts +80 -0
- package/lib/selectors.ts +179 -0
- package/messages/en.json +113 -0
- package/messages/es.json +113 -0
- package/migrations/002_author_profile_fields.sql +37 -0
- package/migrations/003_categories_table.sql +90 -0
- package/migrations/999_sample_data.sql +412 -0
- package/migrations/999_theme_sample_data.sql +1070 -0
- package/package.json +18 -0
- package/permissions-matrix.md +63 -0
- package/styles/article.css +333 -0
- package/styles/components.css +204 -0
- package/styles/globals.css +327 -0
- package/styles/theme.css +167 -0
- package/templates/(public)/author/[username]/page.tsx +247 -0
- package/templates/(public)/authors/page.tsx +161 -0
- package/templates/(public)/layout.tsx +44 -0
- package/templates/(public)/page.tsx +276 -0
- package/templates/(public)/posts/[slug]/page.tsx +342 -0
- package/templates/dashboard/(main)/page.tsx +385 -0
- package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +529 -0
- package/templates/dashboard/(main)/posts/[id]/page.tsx +33 -0
- package/templates/dashboard/(main)/posts/create/page.tsx +353 -0
- package/templates/dashboard/(main)/posts/page.tsx +833 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Posts Service
|
|
3
|
+
*
|
|
4
|
+
* Provides data access methods for blog posts.
|
|
5
|
+
* Posts is a private entity - users only see posts in their team.
|
|
6
|
+
*
|
|
7
|
+
* All methods require authentication (use RLS with userId filter).
|
|
8
|
+
*
|
|
9
|
+
* @module PostsService
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
13
|
+
|
|
14
|
+
// Post status type
|
|
15
|
+
export type PostStatus = 'draft' | 'published' | 'scheduled'
|
|
16
|
+
|
|
17
|
+
// Post interface
|
|
18
|
+
export interface Post {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
slug: string
|
|
22
|
+
excerpt?: string
|
|
23
|
+
content: string
|
|
24
|
+
featuredImage?: string
|
|
25
|
+
featured?: boolean
|
|
26
|
+
status: PostStatus
|
|
27
|
+
publishedAt?: string
|
|
28
|
+
createdAt: string
|
|
29
|
+
updatedAt: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// List options
|
|
33
|
+
export interface PostListOptions {
|
|
34
|
+
limit?: number
|
|
35
|
+
offset?: number
|
|
36
|
+
status?: PostStatus
|
|
37
|
+
featured?: boolean
|
|
38
|
+
orderBy?: 'title' | 'publishedAt' | 'createdAt' | 'updatedAt'
|
|
39
|
+
orderDir?: 'asc' | 'desc'
|
|
40
|
+
teamId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// List result
|
|
44
|
+
export interface PostListResult {
|
|
45
|
+
posts: Post[]
|
|
46
|
+
total: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create data
|
|
50
|
+
export interface PostCreateData {
|
|
51
|
+
title: string
|
|
52
|
+
slug: string
|
|
53
|
+
excerpt?: string
|
|
54
|
+
content: string
|
|
55
|
+
featuredImage?: string
|
|
56
|
+
featured?: boolean
|
|
57
|
+
status?: PostStatus
|
|
58
|
+
publishedAt?: string
|
|
59
|
+
teamId: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Update data
|
|
63
|
+
export interface PostUpdateData {
|
|
64
|
+
title?: string
|
|
65
|
+
slug?: string
|
|
66
|
+
excerpt?: string
|
|
67
|
+
content?: string
|
|
68
|
+
featuredImage?: string
|
|
69
|
+
featured?: boolean
|
|
70
|
+
status?: PostStatus
|
|
71
|
+
publishedAt?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Database row type
|
|
75
|
+
interface DbPost {
|
|
76
|
+
id: string
|
|
77
|
+
title: string
|
|
78
|
+
slug: string
|
|
79
|
+
excerpt: string | null
|
|
80
|
+
content: string
|
|
81
|
+
featuredImage: string | null
|
|
82
|
+
featured: boolean | null
|
|
83
|
+
status: PostStatus
|
|
84
|
+
publishedAt: string | null
|
|
85
|
+
createdAt: string
|
|
86
|
+
updatedAt: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class PostsService {
|
|
90
|
+
// ============================================
|
|
91
|
+
// READ METHODS
|
|
92
|
+
// ============================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get a post by ID
|
|
96
|
+
*/
|
|
97
|
+
static async getById(id: string, userId: string): Promise<Post | null> {
|
|
98
|
+
try {
|
|
99
|
+
if (!id?.trim()) throw new Error('Post ID is required')
|
|
100
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
101
|
+
|
|
102
|
+
const post = await queryOneWithRLS<DbPost>(
|
|
103
|
+
`SELECT id, title, slug, excerpt, content, "featuredImage", featured, status, "publishedAt", "createdAt", "updatedAt"
|
|
104
|
+
FROM posts WHERE id = $1`,
|
|
105
|
+
[id],
|
|
106
|
+
userId
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (!post) return null
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
id: post.id,
|
|
113
|
+
title: post.title,
|
|
114
|
+
slug: post.slug,
|
|
115
|
+
excerpt: post.excerpt ?? undefined,
|
|
116
|
+
content: post.content,
|
|
117
|
+
featuredImage: post.featuredImage ?? undefined,
|
|
118
|
+
featured: post.featured ?? undefined,
|
|
119
|
+
status: post.status,
|
|
120
|
+
publishedAt: post.publishedAt ?? undefined,
|
|
121
|
+
createdAt: post.createdAt,
|
|
122
|
+
updatedAt: post.updatedAt,
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('PostsService.getById error:', error)
|
|
126
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to fetch post')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get a post by slug
|
|
132
|
+
*/
|
|
133
|
+
static async getBySlug(slug: string, userId: string): Promise<Post | null> {
|
|
134
|
+
try {
|
|
135
|
+
if (!slug?.trim()) throw new Error('Slug is required')
|
|
136
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
137
|
+
|
|
138
|
+
const post = await queryOneWithRLS<DbPost>(
|
|
139
|
+
`SELECT id, title, slug, excerpt, content, "featuredImage", featured, status, "publishedAt", "createdAt", "updatedAt"
|
|
140
|
+
FROM posts WHERE slug = $1`,
|
|
141
|
+
[slug],
|
|
142
|
+
userId
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (!post) return null
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
id: post.id,
|
|
149
|
+
title: post.title,
|
|
150
|
+
slug: post.slug,
|
|
151
|
+
excerpt: post.excerpt ?? undefined,
|
|
152
|
+
content: post.content,
|
|
153
|
+
featuredImage: post.featuredImage ?? undefined,
|
|
154
|
+
featured: post.featured ?? undefined,
|
|
155
|
+
status: post.status,
|
|
156
|
+
publishedAt: post.publishedAt ?? undefined,
|
|
157
|
+
createdAt: post.createdAt,
|
|
158
|
+
updatedAt: post.updatedAt,
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('PostsService.getBySlug error:', error)
|
|
162
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to fetch post')
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* List posts with pagination and filtering
|
|
168
|
+
*/
|
|
169
|
+
static async list(userId: string, options: PostListOptions = {}): Promise<PostListResult> {
|
|
170
|
+
try {
|
|
171
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
172
|
+
|
|
173
|
+
const {
|
|
174
|
+
limit = 10,
|
|
175
|
+
offset = 0,
|
|
176
|
+
status,
|
|
177
|
+
featured,
|
|
178
|
+
orderBy = 'createdAt',
|
|
179
|
+
orderDir = 'desc',
|
|
180
|
+
teamId,
|
|
181
|
+
} = options
|
|
182
|
+
|
|
183
|
+
// Build WHERE clause
|
|
184
|
+
const conditions: string[] = []
|
|
185
|
+
const params: unknown[] = []
|
|
186
|
+
let paramIndex = 1
|
|
187
|
+
|
|
188
|
+
if (status) {
|
|
189
|
+
conditions.push(`status = $${paramIndex++}`)
|
|
190
|
+
params.push(status)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (featured !== undefined) {
|
|
194
|
+
conditions.push(`featured = $${paramIndex++}`)
|
|
195
|
+
params.push(featured)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (teamId) {
|
|
199
|
+
conditions.push(`"teamId" = $${paramIndex++}`)
|
|
200
|
+
params.push(teamId)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
204
|
+
|
|
205
|
+
// Validate orderBy
|
|
206
|
+
const validOrderBy = ['title', 'publishedAt', 'createdAt', 'updatedAt'].includes(orderBy) ? orderBy : 'createdAt'
|
|
207
|
+
const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
|
|
208
|
+
const orderColumnMap: Record<string, string> = {
|
|
209
|
+
title: 'title',
|
|
210
|
+
publishedAt: '"publishedAt"',
|
|
211
|
+
createdAt: '"createdAt"',
|
|
212
|
+
updatedAt: '"updatedAt"',
|
|
213
|
+
}
|
|
214
|
+
const orderColumn = orderColumnMap[validOrderBy] || '"createdAt"'
|
|
215
|
+
|
|
216
|
+
// Get total count
|
|
217
|
+
const countResult = await queryWithRLS<{ count: string }>(
|
|
218
|
+
`SELECT COUNT(*)::text as count FROM posts ${whereClause}`,
|
|
219
|
+
params,
|
|
220
|
+
userId
|
|
221
|
+
)
|
|
222
|
+
const total = parseInt(countResult[0]?.count || '0', 10)
|
|
223
|
+
|
|
224
|
+
// Get posts
|
|
225
|
+
params.push(limit, offset)
|
|
226
|
+
const posts = await queryWithRLS<DbPost>(
|
|
227
|
+
`SELECT id, title, slug, excerpt, content, "featuredImage", featured, status, "publishedAt", "createdAt", "updatedAt"
|
|
228
|
+
FROM posts ${whereClause}
|
|
229
|
+
ORDER BY ${orderColumn} ${validOrderDir}
|
|
230
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
231
|
+
params,
|
|
232
|
+
userId
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
posts: posts.map((post) => ({
|
|
237
|
+
id: post.id,
|
|
238
|
+
title: post.title,
|
|
239
|
+
slug: post.slug,
|
|
240
|
+
excerpt: post.excerpt ?? undefined,
|
|
241
|
+
content: post.content,
|
|
242
|
+
featuredImage: post.featuredImage ?? undefined,
|
|
243
|
+
featured: post.featured ?? undefined,
|
|
244
|
+
status: post.status,
|
|
245
|
+
publishedAt: post.publishedAt ?? undefined,
|
|
246
|
+
createdAt: post.createdAt,
|
|
247
|
+
updatedAt: post.updatedAt,
|
|
248
|
+
})),
|
|
249
|
+
total,
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('PostsService.list error:', error)
|
|
253
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to list posts')
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get published posts (for public display)
|
|
259
|
+
*/
|
|
260
|
+
static async getPublished(userId: string, limit = 10, offset = 0): Promise<PostListResult> {
|
|
261
|
+
return this.list(userId, {
|
|
262
|
+
status: 'published',
|
|
263
|
+
limit,
|
|
264
|
+
offset,
|
|
265
|
+
orderBy: 'publishedAt',
|
|
266
|
+
orderDir: 'desc',
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get featured posts
|
|
272
|
+
*/
|
|
273
|
+
static async getFeatured(userId: string, limit = 5): Promise<Post[]> {
|
|
274
|
+
const { posts } = await this.list(userId, {
|
|
275
|
+
status: 'published',
|
|
276
|
+
featured: true,
|
|
277
|
+
limit,
|
|
278
|
+
orderBy: 'publishedAt',
|
|
279
|
+
orderDir: 'desc',
|
|
280
|
+
})
|
|
281
|
+
return posts
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================
|
|
285
|
+
// WRITE METHODS
|
|
286
|
+
// ============================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create a new post
|
|
290
|
+
*/
|
|
291
|
+
static async create(userId: string, data: PostCreateData): Promise<Post> {
|
|
292
|
+
try {
|
|
293
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
294
|
+
if (!data.title?.trim()) throw new Error('Title is required')
|
|
295
|
+
if (!data.slug?.trim()) throw new Error('Slug is required')
|
|
296
|
+
if (!data.content?.trim()) throw new Error('Content is required')
|
|
297
|
+
if (!data.teamId?.trim()) throw new Error('Team ID is required')
|
|
298
|
+
|
|
299
|
+
const id = crypto.randomUUID()
|
|
300
|
+
const now = new Date().toISOString()
|
|
301
|
+
|
|
302
|
+
const result = await mutateWithRLS<DbPost>(
|
|
303
|
+
`INSERT INTO posts (id, "userId", "teamId", title, slug, excerpt, content, "featuredImage", featured, status, "publishedAt", "createdAt", "updatedAt")
|
|
304
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
305
|
+
RETURNING id, title, slug, excerpt, content, "featuredImage", featured, status, "publishedAt", "createdAt", "updatedAt"`,
|
|
306
|
+
[
|
|
307
|
+
id,
|
|
308
|
+
userId,
|
|
309
|
+
data.teamId,
|
|
310
|
+
data.title,
|
|
311
|
+
data.slug,
|
|
312
|
+
data.excerpt || null,
|
|
313
|
+
data.content,
|
|
314
|
+
data.featuredImage || null,
|
|
315
|
+
data.featured || false,
|
|
316
|
+
data.status || 'draft',
|
|
317
|
+
data.publishedAt || null,
|
|
318
|
+
now,
|
|
319
|
+
now,
|
|
320
|
+
],
|
|
321
|
+
userId
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if (!result.rows[0]) throw new Error('Failed to create post')
|
|
325
|
+
|
|
326
|
+
const post = result.rows[0]
|
|
327
|
+
return {
|
|
328
|
+
id: post.id,
|
|
329
|
+
title: post.title,
|
|
330
|
+
slug: post.slug,
|
|
331
|
+
excerpt: post.excerpt ?? undefined,
|
|
332
|
+
content: post.content,
|
|
333
|
+
featuredImage: post.featuredImage ?? undefined,
|
|
334
|
+
featured: post.featured ?? undefined,
|
|
335
|
+
status: post.status,
|
|
336
|
+
publishedAt: post.publishedAt ?? undefined,
|
|
337
|
+
createdAt: post.createdAt,
|
|
338
|
+
updatedAt: post.updatedAt,
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error('PostsService.create error:', error)
|
|
342
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to create post')
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Update an existing post
|
|
348
|
+
*/
|
|
349
|
+
static async update(userId: string, id: string, data: PostUpdateData): Promise<Post> {
|
|
350
|
+
try {
|
|
351
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
352
|
+
if (!id?.trim()) throw new Error('Post ID is required')
|
|
353
|
+
|
|
354
|
+
const updates: string[] = []
|
|
355
|
+
const values: unknown[] = []
|
|
356
|
+
let paramIndex = 1
|
|
357
|
+
|
|
358
|
+
if (data.title !== undefined) {
|
|
359
|
+
updates.push(`title = $${paramIndex++}`)
|
|
360
|
+
values.push(data.title)
|
|
361
|
+
}
|
|
362
|
+
if (data.slug !== undefined) {
|
|
363
|
+
updates.push(`slug = $${paramIndex++}`)
|
|
364
|
+
values.push(data.slug)
|
|
365
|
+
}
|
|
366
|
+
if (data.excerpt !== undefined) {
|
|
367
|
+
updates.push(`excerpt = $${paramIndex++}`)
|
|
368
|
+
values.push(data.excerpt || null)
|
|
369
|
+
}
|
|
370
|
+
if (data.content !== undefined) {
|
|
371
|
+
updates.push(`content = $${paramIndex++}`)
|
|
372
|
+
values.push(data.content)
|
|
373
|
+
}
|
|
374
|
+
if (data.featuredImage !== undefined) {
|
|
375
|
+
updates.push(`"featuredImage" = $${paramIndex++}`)
|
|
376
|
+
values.push(data.featuredImage || null)
|
|
377
|
+
}
|
|
378
|
+
if (data.featured !== undefined) {
|
|
379
|
+
updates.push(`featured = $${paramIndex++}`)
|
|
380
|
+
values.push(data.featured)
|
|
381
|
+
}
|
|
382
|
+
if (data.status !== undefined) {
|
|
383
|
+
updates.push(`status = $${paramIndex++}`)
|
|
384
|
+
values.push(data.status)
|
|
385
|
+
}
|
|
386
|
+
if (data.publishedAt !== undefined) {
|
|
387
|
+
updates.push(`"publishedAt" = $${paramIndex++}`)
|
|
388
|
+
values.push(data.publishedAt || null)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (updates.length === 0) throw new Error('No fields to update')
|
|
392
|
+
|
|
393
|
+
updates.push(`"updatedAt" = $${paramIndex++}`)
|
|
394
|
+
values.push(new Date().toISOString())
|
|
395
|
+
values.push(id)
|
|
396
|
+
|
|
397
|
+
const result = await mutateWithRLS<DbPost>(
|
|
398
|
+
`UPDATE posts SET ${updates.join(', ')} WHERE id = $${paramIndex}
|
|
399
|
+
RETURNING id, title, slug, excerpt, content, "featuredImage", featured, status, "publishedAt", "createdAt", "updatedAt"`,
|
|
400
|
+
values,
|
|
401
|
+
userId
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if (!result.rows[0]) throw new Error('Post not found or update failed')
|
|
405
|
+
|
|
406
|
+
const post = result.rows[0]
|
|
407
|
+
return {
|
|
408
|
+
id: post.id,
|
|
409
|
+
title: post.title,
|
|
410
|
+
slug: post.slug,
|
|
411
|
+
excerpt: post.excerpt ?? undefined,
|
|
412
|
+
content: post.content,
|
|
413
|
+
featuredImage: post.featuredImage ?? undefined,
|
|
414
|
+
featured: post.featured ?? undefined,
|
|
415
|
+
status: post.status,
|
|
416
|
+
publishedAt: post.publishedAt ?? undefined,
|
|
417
|
+
createdAt: post.createdAt,
|
|
418
|
+
updatedAt: post.updatedAt,
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error('PostsService.update error:', error)
|
|
422
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to update post')
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Delete a post
|
|
428
|
+
*/
|
|
429
|
+
static async delete(userId: string, id: string): Promise<boolean> {
|
|
430
|
+
try {
|
|
431
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
432
|
+
if (!id?.trim()) throw new Error('Post ID is required')
|
|
433
|
+
|
|
434
|
+
const result = await mutateWithRLS(`DELETE FROM posts WHERE id = $1`, [id], userId)
|
|
435
|
+
return result.rowCount > 0
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error('PostsService.delete error:', error)
|
|
438
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to delete post')
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ============================================
|
|
443
|
+
// PUBLISHING METHODS
|
|
444
|
+
// ============================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Publish a post
|
|
448
|
+
*/
|
|
449
|
+
static async publish(userId: string, id: string): Promise<Post> {
|
|
450
|
+
return this.update(userId, id, {
|
|
451
|
+
status: 'published',
|
|
452
|
+
publishedAt: new Date().toISOString(),
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Unpublish a post (revert to draft)
|
|
458
|
+
*/
|
|
459
|
+
static async unpublish(userId: string, id: string): Promise<Post> {
|
|
460
|
+
return this.update(userId, id, {
|
|
461
|
+
status: 'draft',
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Posts Service Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the PostsService.
|
|
5
|
+
* Posts is a private entity - users only see posts in their team.
|
|
6
|
+
*
|
|
7
|
+
* @module PostsTypes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Post status values
|
|
12
|
+
*/
|
|
13
|
+
export type PostStatus = 'draft' | 'published' | 'scheduled'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Post entity
|
|
17
|
+
*/
|
|
18
|
+
export interface Post {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
slug: string
|
|
22
|
+
excerpt?: string
|
|
23
|
+
content: string
|
|
24
|
+
featuredImage?: string
|
|
25
|
+
featured?: boolean
|
|
26
|
+
status: PostStatus
|
|
27
|
+
publishedAt?: string
|
|
28
|
+
createdAt: string
|
|
29
|
+
updatedAt: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for listing posts
|
|
34
|
+
*/
|
|
35
|
+
export interface PostListOptions {
|
|
36
|
+
limit?: number
|
|
37
|
+
offset?: number
|
|
38
|
+
status?: PostStatus
|
|
39
|
+
featured?: boolean
|
|
40
|
+
teamId?: string
|
|
41
|
+
orderBy?: 'title' | 'publishedAt' | 'createdAt' | 'updatedAt'
|
|
42
|
+
orderDir?: 'asc' | 'desc'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Result of listing posts with pagination
|
|
47
|
+
*/
|
|
48
|
+
export interface PostListResult {
|
|
49
|
+
posts: Post[]
|
|
50
|
+
total: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Data required to create a new post
|
|
55
|
+
*/
|
|
56
|
+
export interface PostCreateData {
|
|
57
|
+
title: string
|
|
58
|
+
slug: string
|
|
59
|
+
content: string
|
|
60
|
+
teamId: string
|
|
61
|
+
excerpt?: string
|
|
62
|
+
featuredImage?: string
|
|
63
|
+
featured?: boolean
|
|
64
|
+
status?: PostStatus
|
|
65
|
+
publishedAt?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Data for updating an existing post
|
|
70
|
+
*/
|
|
71
|
+
export interface PostUpdateData {
|
|
72
|
+
title?: string
|
|
73
|
+
slug?: string
|
|
74
|
+
excerpt?: string
|
|
75
|
+
content?: string
|
|
76
|
+
featuredImage?: string
|
|
77
|
+
featured?: boolean
|
|
78
|
+
status?: PostStatus
|
|
79
|
+
publishedAt?: string
|
|
80
|
+
}
|