@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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categories Entity Fields Configuration
|
|
3
|
+
*
|
|
4
|
+
* Field definitions for blog categories.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
|
|
8
|
+
|
|
9
|
+
export const categoryFields: EntityField[] = [
|
|
10
|
+
// ==========================================
|
|
11
|
+
// BASIC INFORMATION
|
|
12
|
+
// ==========================================
|
|
13
|
+
{
|
|
14
|
+
name: 'name',
|
|
15
|
+
type: 'text',
|
|
16
|
+
required: true,
|
|
17
|
+
display: {
|
|
18
|
+
label: 'Name',
|
|
19
|
+
description: 'The name of the category',
|
|
20
|
+
placeholder: 'Enter category name...',
|
|
21
|
+
showInList: true,
|
|
22
|
+
showInDetail: true,
|
|
23
|
+
showInForm: true,
|
|
24
|
+
order: 1,
|
|
25
|
+
columnWidth: 6,
|
|
26
|
+
},
|
|
27
|
+
api: {
|
|
28
|
+
readOnly: false,
|
|
29
|
+
searchable: true,
|
|
30
|
+
sortable: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'slug',
|
|
35
|
+
type: 'text',
|
|
36
|
+
required: true,
|
|
37
|
+
display: {
|
|
38
|
+
label: 'Slug',
|
|
39
|
+
description: 'URL-friendly version of the name (auto-generated)',
|
|
40
|
+
placeholder: 'category-slug',
|
|
41
|
+
showInList: true,
|
|
42
|
+
showInDetail: true,
|
|
43
|
+
showInForm: true,
|
|
44
|
+
order: 2,
|
|
45
|
+
columnWidth: 6,
|
|
46
|
+
},
|
|
47
|
+
api: {
|
|
48
|
+
readOnly: false,
|
|
49
|
+
searchable: false,
|
|
50
|
+
sortable: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'description',
|
|
55
|
+
type: 'textarea',
|
|
56
|
+
required: false,
|
|
57
|
+
display: {
|
|
58
|
+
label: 'Description',
|
|
59
|
+
description: 'A brief description of this category',
|
|
60
|
+
placeholder: 'Describe what this category is about...',
|
|
61
|
+
showInList: false,
|
|
62
|
+
showInDetail: true,
|
|
63
|
+
showInForm: true,
|
|
64
|
+
order: 3,
|
|
65
|
+
columnWidth: 12,
|
|
66
|
+
},
|
|
67
|
+
api: {
|
|
68
|
+
readOnly: false,
|
|
69
|
+
searchable: true,
|
|
70
|
+
sortable: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// ==========================================
|
|
75
|
+
// TIMESTAMPS
|
|
76
|
+
// ==========================================
|
|
77
|
+
{
|
|
78
|
+
name: 'createdAt',
|
|
79
|
+
type: 'datetime',
|
|
80
|
+
required: false,
|
|
81
|
+
display: {
|
|
82
|
+
label: 'Created At',
|
|
83
|
+
description: 'When the category was created',
|
|
84
|
+
showInList: false,
|
|
85
|
+
showInDetail: true,
|
|
86
|
+
showInForm: false,
|
|
87
|
+
order: 98,
|
|
88
|
+
columnWidth: 6,
|
|
89
|
+
},
|
|
90
|
+
api: {
|
|
91
|
+
readOnly: true,
|
|
92
|
+
searchable: false,
|
|
93
|
+
sortable: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'updatedAt',
|
|
98
|
+
type: 'datetime',
|
|
99
|
+
required: false,
|
|
100
|
+
display: {
|
|
101
|
+
label: 'Updated At',
|
|
102
|
+
description: 'When the category was last modified',
|
|
103
|
+
showInList: false,
|
|
104
|
+
showInDetail: true,
|
|
105
|
+
showInForm: false,
|
|
106
|
+
order: 99,
|
|
107
|
+
columnWidth: 6,
|
|
108
|
+
},
|
|
109
|
+
api: {
|
|
110
|
+
readOnly: true,
|
|
111
|
+
searchable: false,
|
|
112
|
+
sortable: true,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
]
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categories Service
|
|
3
|
+
*
|
|
4
|
+
* Provides data access methods for blog categories.
|
|
5
|
+
* Categories is a private entity - users only see categories in their team.
|
|
6
|
+
*
|
|
7
|
+
* All methods require authentication (use RLS with userId filter).
|
|
8
|
+
*
|
|
9
|
+
* @module CategoriesService
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
13
|
+
|
|
14
|
+
// Category interface
|
|
15
|
+
export interface Category {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
slug: string
|
|
19
|
+
description?: string
|
|
20
|
+
createdAt: string
|
|
21
|
+
updatedAt: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// List options
|
|
25
|
+
export interface CategoryListOptions {
|
|
26
|
+
limit?: number
|
|
27
|
+
offset?: number
|
|
28
|
+
orderBy?: 'name' | 'slug' | 'createdAt'
|
|
29
|
+
orderDir?: 'asc' | 'desc'
|
|
30
|
+
teamId?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// List result
|
|
34
|
+
export interface CategoryListResult {
|
|
35
|
+
categories: Category[]
|
|
36
|
+
total: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create data
|
|
40
|
+
export interface CategoryCreateData {
|
|
41
|
+
name: string
|
|
42
|
+
slug: string
|
|
43
|
+
description?: string
|
|
44
|
+
teamId: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Update data
|
|
48
|
+
export interface CategoryUpdateData {
|
|
49
|
+
name?: string
|
|
50
|
+
slug?: string
|
|
51
|
+
description?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Database row type
|
|
55
|
+
interface DbCategory {
|
|
56
|
+
id: string
|
|
57
|
+
name: string
|
|
58
|
+
slug: string
|
|
59
|
+
description: string | null
|
|
60
|
+
createdAt: string
|
|
61
|
+
updatedAt: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class CategoriesService {
|
|
65
|
+
// ============================================
|
|
66
|
+
// READ METHODS
|
|
67
|
+
// ============================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get a category by ID
|
|
71
|
+
*/
|
|
72
|
+
static async getById(id: string, userId: string): Promise<Category | null> {
|
|
73
|
+
try {
|
|
74
|
+
if (!id?.trim()) throw new Error('Category ID is required')
|
|
75
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
76
|
+
|
|
77
|
+
const category = await queryOneWithRLS<DbCategory>(
|
|
78
|
+
`SELECT id, name, slug, description, "createdAt", "updatedAt"
|
|
79
|
+
FROM categories WHERE id = $1`,
|
|
80
|
+
[id],
|
|
81
|
+
userId
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (!category) return null
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
id: category.id,
|
|
88
|
+
name: category.name,
|
|
89
|
+
slug: category.slug,
|
|
90
|
+
description: category.description ?? undefined,
|
|
91
|
+
createdAt: category.createdAt,
|
|
92
|
+
updatedAt: category.updatedAt,
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('CategoriesService.getById error:', error)
|
|
96
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to fetch category')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a category by slug
|
|
102
|
+
*/
|
|
103
|
+
static async getBySlug(slug: string, userId: string): Promise<Category | null> {
|
|
104
|
+
try {
|
|
105
|
+
if (!slug?.trim()) throw new Error('Slug is required')
|
|
106
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
107
|
+
|
|
108
|
+
const category = await queryOneWithRLS<DbCategory>(
|
|
109
|
+
`SELECT id, name, slug, description, "createdAt", "updatedAt"
|
|
110
|
+
FROM categories WHERE slug = $1`,
|
|
111
|
+
[slug],
|
|
112
|
+
userId
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (!category) return null
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
id: category.id,
|
|
119
|
+
name: category.name,
|
|
120
|
+
slug: category.slug,
|
|
121
|
+
description: category.description ?? undefined,
|
|
122
|
+
createdAt: category.createdAt,
|
|
123
|
+
updatedAt: category.updatedAt,
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('CategoriesService.getBySlug error:', error)
|
|
127
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to fetch category')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List categories with pagination
|
|
133
|
+
*/
|
|
134
|
+
static async list(userId: string, options: CategoryListOptions = {}): Promise<CategoryListResult> {
|
|
135
|
+
try {
|
|
136
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
137
|
+
|
|
138
|
+
const {
|
|
139
|
+
limit = 50,
|
|
140
|
+
offset = 0,
|
|
141
|
+
orderBy = 'name',
|
|
142
|
+
orderDir = 'asc',
|
|
143
|
+
teamId,
|
|
144
|
+
} = options
|
|
145
|
+
|
|
146
|
+
// Build WHERE clause
|
|
147
|
+
const conditions: string[] = []
|
|
148
|
+
const params: unknown[] = []
|
|
149
|
+
let paramIndex = 1
|
|
150
|
+
|
|
151
|
+
if (teamId) {
|
|
152
|
+
conditions.push(`"teamId" = $${paramIndex++}`)
|
|
153
|
+
params.push(teamId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
157
|
+
|
|
158
|
+
// Validate orderBy
|
|
159
|
+
const validOrderBy = ['name', 'slug', 'createdAt'].includes(orderBy) ? orderBy : 'name'
|
|
160
|
+
const validOrderDir = orderDir === 'desc' ? 'DESC' : 'ASC'
|
|
161
|
+
const orderColumnMap: Record<string, string> = {
|
|
162
|
+
name: 'name',
|
|
163
|
+
slug: 'slug',
|
|
164
|
+
createdAt: '"createdAt"',
|
|
165
|
+
}
|
|
166
|
+
const orderColumn = orderColumnMap[validOrderBy] || 'name'
|
|
167
|
+
|
|
168
|
+
// Get total count
|
|
169
|
+
const countResult = await queryWithRLS<{ count: string }>(
|
|
170
|
+
`SELECT COUNT(*)::text as count FROM categories ${whereClause}`,
|
|
171
|
+
params,
|
|
172
|
+
userId
|
|
173
|
+
)
|
|
174
|
+
const total = parseInt(countResult[0]?.count || '0', 10)
|
|
175
|
+
|
|
176
|
+
// Get categories
|
|
177
|
+
params.push(limit, offset)
|
|
178
|
+
const categories = await queryWithRLS<DbCategory>(
|
|
179
|
+
`SELECT id, name, slug, description, "createdAt", "updatedAt"
|
|
180
|
+
FROM categories ${whereClause}
|
|
181
|
+
ORDER BY ${orderColumn} ${validOrderDir}
|
|
182
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
183
|
+
params,
|
|
184
|
+
userId
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
categories: categories.map((cat) => ({
|
|
189
|
+
id: cat.id,
|
|
190
|
+
name: cat.name,
|
|
191
|
+
slug: cat.slug,
|
|
192
|
+
description: cat.description ?? undefined,
|
|
193
|
+
createdAt: cat.createdAt,
|
|
194
|
+
updatedAt: cat.updatedAt,
|
|
195
|
+
})),
|
|
196
|
+
total,
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('CategoriesService.list error:', error)
|
|
200
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to list categories')
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get all categories (no pagination)
|
|
206
|
+
*/
|
|
207
|
+
static async getAll(userId: string): Promise<Category[]> {
|
|
208
|
+
const { categories } = await this.list(userId, { limit: 1000 })
|
|
209
|
+
return categories
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================
|
|
213
|
+
// WRITE METHODS
|
|
214
|
+
// ============================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Create a new category
|
|
218
|
+
*/
|
|
219
|
+
static async create(userId: string, data: CategoryCreateData): Promise<Category> {
|
|
220
|
+
try {
|
|
221
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
222
|
+
if (!data.name?.trim()) throw new Error('Name is required')
|
|
223
|
+
if (!data.slug?.trim()) throw new Error('Slug is required')
|
|
224
|
+
if (!data.teamId?.trim()) throw new Error('Team ID is required')
|
|
225
|
+
|
|
226
|
+
const id = crypto.randomUUID()
|
|
227
|
+
const now = new Date().toISOString()
|
|
228
|
+
|
|
229
|
+
const result = await mutateWithRLS<DbCategory>(
|
|
230
|
+
`INSERT INTO categories (id, "userId", "teamId", name, slug, description, "createdAt", "updatedAt")
|
|
231
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
232
|
+
RETURNING id, name, slug, description, "createdAt", "updatedAt"`,
|
|
233
|
+
[
|
|
234
|
+
id,
|
|
235
|
+
userId,
|
|
236
|
+
data.teamId,
|
|
237
|
+
data.name,
|
|
238
|
+
data.slug,
|
|
239
|
+
data.description || null,
|
|
240
|
+
now,
|
|
241
|
+
now,
|
|
242
|
+
],
|
|
243
|
+
userId
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if (!result.rows[0]) throw new Error('Failed to create category')
|
|
247
|
+
|
|
248
|
+
const category = result.rows[0]
|
|
249
|
+
return {
|
|
250
|
+
id: category.id,
|
|
251
|
+
name: category.name,
|
|
252
|
+
slug: category.slug,
|
|
253
|
+
description: category.description ?? undefined,
|
|
254
|
+
createdAt: category.createdAt,
|
|
255
|
+
updatedAt: category.updatedAt,
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('CategoriesService.create error:', error)
|
|
259
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to create category')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Update an existing category
|
|
265
|
+
*/
|
|
266
|
+
static async update(userId: string, id: string, data: CategoryUpdateData): Promise<Category> {
|
|
267
|
+
try {
|
|
268
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
269
|
+
if (!id?.trim()) throw new Error('Category ID is required')
|
|
270
|
+
|
|
271
|
+
const updates: string[] = []
|
|
272
|
+
const values: unknown[] = []
|
|
273
|
+
let paramIndex = 1
|
|
274
|
+
|
|
275
|
+
if (data.name !== undefined) {
|
|
276
|
+
updates.push(`name = $${paramIndex++}`)
|
|
277
|
+
values.push(data.name)
|
|
278
|
+
}
|
|
279
|
+
if (data.slug !== undefined) {
|
|
280
|
+
updates.push(`slug = $${paramIndex++}`)
|
|
281
|
+
values.push(data.slug)
|
|
282
|
+
}
|
|
283
|
+
if (data.description !== undefined) {
|
|
284
|
+
updates.push(`description = $${paramIndex++}`)
|
|
285
|
+
values.push(data.description || null)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (updates.length === 0) throw new Error('No fields to update')
|
|
289
|
+
|
|
290
|
+
updates.push(`"updatedAt" = $${paramIndex++}`)
|
|
291
|
+
values.push(new Date().toISOString())
|
|
292
|
+
values.push(id)
|
|
293
|
+
|
|
294
|
+
const result = await mutateWithRLS<DbCategory>(
|
|
295
|
+
`UPDATE categories SET ${updates.join(', ')} WHERE id = $${paramIndex}
|
|
296
|
+
RETURNING id, name, slug, description, "createdAt", "updatedAt"`,
|
|
297
|
+
values,
|
|
298
|
+
userId
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if (!result.rows[0]) throw new Error('Category not found or update failed')
|
|
302
|
+
|
|
303
|
+
const category = result.rows[0]
|
|
304
|
+
return {
|
|
305
|
+
id: category.id,
|
|
306
|
+
name: category.name,
|
|
307
|
+
slug: category.slug,
|
|
308
|
+
description: category.description ?? undefined,
|
|
309
|
+
createdAt: category.createdAt,
|
|
310
|
+
updatedAt: category.updatedAt,
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('CategoriesService.update error:', error)
|
|
314
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to update category')
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Delete a category
|
|
320
|
+
*/
|
|
321
|
+
static async delete(userId: string, id: string): Promise<boolean> {
|
|
322
|
+
try {
|
|
323
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
324
|
+
if (!id?.trim()) throw new Error('Category ID is required')
|
|
325
|
+
|
|
326
|
+
const result = await mutateWithRLS(`DELETE FROM categories WHERE id = $1`, [id], userId)
|
|
327
|
+
return result.rowCount > 0
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error('CategoriesService.delete error:', error)
|
|
330
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to delete category')
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categories Service Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the CategoriesService.
|
|
5
|
+
* Categories is a private entity - users only see categories in their team.
|
|
6
|
+
*
|
|
7
|
+
* @module CategoriesTypes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Category entity
|
|
12
|
+
*/
|
|
13
|
+
export interface Category {
|
|
14
|
+
id: string
|
|
15
|
+
name: string
|
|
16
|
+
slug: string
|
|
17
|
+
description?: string
|
|
18
|
+
createdAt: string
|
|
19
|
+
updatedAt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for listing categories
|
|
24
|
+
*/
|
|
25
|
+
export interface CategoryListOptions {
|
|
26
|
+
limit?: number
|
|
27
|
+
offset?: number
|
|
28
|
+
teamId?: string
|
|
29
|
+
orderBy?: 'name' | 'slug' | 'createdAt'
|
|
30
|
+
orderDir?: 'asc' | 'desc'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Result of listing categories with pagination
|
|
35
|
+
*/
|
|
36
|
+
export interface CategoryListResult {
|
|
37
|
+
categories: Category[]
|
|
38
|
+
total: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Data required to create a new category
|
|
43
|
+
*/
|
|
44
|
+
export interface CategoryCreateData {
|
|
45
|
+
name: string
|
|
46
|
+
slug: string
|
|
47
|
+
teamId: string
|
|
48
|
+
description?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Data for updating an existing category
|
|
53
|
+
*/
|
|
54
|
+
export interface CategoryUpdateData {
|
|
55
|
+
name?: string
|
|
56
|
+
slug?: string
|
|
57
|
+
description?: string
|
|
58
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "Category",
|
|
4
|
+
"namePlural": "Categories",
|
|
5
|
+
"description": "Organize your blog posts by category"
|
|
6
|
+
},
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": "Name",
|
|
9
|
+
"slug": "Slug",
|
|
10
|
+
"description": "Description",
|
|
11
|
+
"createdAt": "Created At",
|
|
12
|
+
"updatedAt": "Updated At"
|
|
13
|
+
},
|
|
14
|
+
"actions": {
|
|
15
|
+
"create": "Create Category",
|
|
16
|
+
"edit": "Edit Category",
|
|
17
|
+
"delete": "Delete Category",
|
|
18
|
+
"view": "View Category",
|
|
19
|
+
"list": "List Categories"
|
|
20
|
+
},
|
|
21
|
+
"messages": {
|
|
22
|
+
"created": "Category created successfully",
|
|
23
|
+
"updated": "Category updated successfully",
|
|
24
|
+
"deleted": "Category deleted successfully",
|
|
25
|
+
"notFound": "Category not found",
|
|
26
|
+
"listEmpty": "No categories found. Create your first category to organize your posts."
|
|
27
|
+
},
|
|
28
|
+
"validation": {
|
|
29
|
+
"nameRequired": "Category name is required",
|
|
30
|
+
"slugRequired": "Slug is required",
|
|
31
|
+
"slugInvalid": "Slug must be URL-friendly (lowercase letters, numbers, and hyphens only)"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "Categoría",
|
|
4
|
+
"namePlural": "Categorías",
|
|
5
|
+
"description": "Organiza tus posts por categoría"
|
|
6
|
+
},
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": "Nombre",
|
|
9
|
+
"slug": "Slug",
|
|
10
|
+
"description": "Descripción",
|
|
11
|
+
"createdAt": "Creado el",
|
|
12
|
+
"updatedAt": "Actualizado el"
|
|
13
|
+
},
|
|
14
|
+
"actions": {
|
|
15
|
+
"create": "Crear Categoría",
|
|
16
|
+
"edit": "Editar Categoría",
|
|
17
|
+
"delete": "Eliminar Categoría",
|
|
18
|
+
"view": "Ver Categoría",
|
|
19
|
+
"list": "Listar Categorías"
|
|
20
|
+
},
|
|
21
|
+
"messages": {
|
|
22
|
+
"created": "Categoría creada exitosamente",
|
|
23
|
+
"updated": "Categoría actualizada exitosamente",
|
|
24
|
+
"deleted": "Categoría eliminada exitosamente",
|
|
25
|
+
"notFound": "Categoría no encontrada",
|
|
26
|
+
"listEmpty": "No se encontraron categorías. Crea tu primera categoría para organizar tus posts."
|
|
27
|
+
},
|
|
28
|
+
"validation": {
|
|
29
|
+
"nameRequired": "El nombre de la categoría es requerido",
|
|
30
|
+
"slugRequired": "El slug es requerido",
|
|
31
|
+
"slugInvalid": "El slug debe ser compatible con URLs (solo letras minúsculas, números y guiones)"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Posts",
|
|
3
|
+
"singular": "Post",
|
|
4
|
+
"plural": "Posts",
|
|
5
|
+
"description": "Manage your blog posts",
|
|
6
|
+
|
|
7
|
+
"fields": {
|
|
8
|
+
"title": {
|
|
9
|
+
"label": "Title",
|
|
10
|
+
"placeholder": "Enter a compelling title...",
|
|
11
|
+
"description": "The title of your blog post"
|
|
12
|
+
},
|
|
13
|
+
"slug": {
|
|
14
|
+
"label": "Slug",
|
|
15
|
+
"placeholder": "my-blog-post",
|
|
16
|
+
"description": "URL-friendly version of the title"
|
|
17
|
+
},
|
|
18
|
+
"excerpt": {
|
|
19
|
+
"label": "Excerpt",
|
|
20
|
+
"placeholder": "Write a brief summary of your post...",
|
|
21
|
+
"description": "A short summary shown in post listings"
|
|
22
|
+
},
|
|
23
|
+
"content": {
|
|
24
|
+
"label": "Content",
|
|
25
|
+
"placeholder": "Start writing your post...",
|
|
26
|
+
"description": "The main content of your blog post"
|
|
27
|
+
},
|
|
28
|
+
"featuredImage": {
|
|
29
|
+
"label": "Featured Image",
|
|
30
|
+
"placeholder": "Upload an image...",
|
|
31
|
+
"description": "Main image displayed with the post"
|
|
32
|
+
},
|
|
33
|
+
"category": {
|
|
34
|
+
"label": "Category",
|
|
35
|
+
"placeholder": "e.g., Technology, Travel, Food",
|
|
36
|
+
"description": "Main category for the post"
|
|
37
|
+
},
|
|
38
|
+
"tags": {
|
|
39
|
+
"label": "Tags",
|
|
40
|
+
"placeholder": "Add tags...",
|
|
41
|
+
"description": "Keywords to help readers find this post"
|
|
42
|
+
},
|
|
43
|
+
"status": {
|
|
44
|
+
"label": "Status",
|
|
45
|
+
"placeholder": "Select status...",
|
|
46
|
+
"description": "Publication status",
|
|
47
|
+
"options": {
|
|
48
|
+
"draft": "Draft",
|
|
49
|
+
"published": "Published",
|
|
50
|
+
"scheduled": "Scheduled"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"publishedAt": {
|
|
54
|
+
"label": "Publish Date",
|
|
55
|
+
"placeholder": "Select date...",
|
|
56
|
+
"description": "When the post was/will be published"
|
|
57
|
+
},
|
|
58
|
+
"createdAt": {
|
|
59
|
+
"label": "Created At",
|
|
60
|
+
"description": "When the post was created"
|
|
61
|
+
},
|
|
62
|
+
"updatedAt": {
|
|
63
|
+
"label": "Updated At",
|
|
64
|
+
"description": "When the post was last modified"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
"actions": {
|
|
69
|
+
"create": "New Post",
|
|
70
|
+
"edit": "Edit Post",
|
|
71
|
+
"delete": "Delete Post",
|
|
72
|
+
"publish": "Publish",
|
|
73
|
+
"unpublish": "Unpublish",
|
|
74
|
+
"saveDraft": "Save Draft",
|
|
75
|
+
"preview": "Preview"
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"messages": {
|
|
79
|
+
"created": "Post created successfully",
|
|
80
|
+
"updated": "Post updated successfully",
|
|
81
|
+
"deleted": "Post deleted successfully",
|
|
82
|
+
"published": "Post published successfully",
|
|
83
|
+
"unpublished": "Post unpublished",
|
|
84
|
+
"confirmDelete": "Are you sure you want to delete this post? This action cannot be undone."
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
"empty": {
|
|
88
|
+
"title": "No posts yet",
|
|
89
|
+
"description": "Get started by writing your first blog post.",
|
|
90
|
+
"action": "Write your first post"
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
"filters": {
|
|
94
|
+
"all": "All Posts",
|
|
95
|
+
"published": "Published",
|
|
96
|
+
"draft": "Drafts",
|
|
97
|
+
"scheduled": "Scheduled"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|