@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.
Files changed (64) hide show
  1. package/README.md +65 -0
  2. package/about.md +93 -0
  3. package/api/authors/[username]/route.ts +150 -0
  4. package/api/authors/route.ts +63 -0
  5. package/api/posts/public/route.ts +151 -0
  6. package/components/ExportPostsButton.tsx +102 -0
  7. package/components/ImportPostsDialog.tsx +284 -0
  8. package/components/PostsToolbar.tsx +24 -0
  9. package/components/editor/FeaturedImageUpload.tsx +185 -0
  10. package/components/editor/WysiwygEditor.tsx +340 -0
  11. package/components/index.ts +4 -0
  12. package/components/public/AuthorBio.tsx +105 -0
  13. package/components/public/AuthorCard.tsx +130 -0
  14. package/components/public/BlogFooter.tsx +185 -0
  15. package/components/public/BlogNavbar.tsx +201 -0
  16. package/components/public/PostCard.tsx +306 -0
  17. package/components/public/ReadingProgress.tsx +70 -0
  18. package/components/public/RelatedPosts.tsx +78 -0
  19. package/config/app.config.ts +200 -0
  20. package/config/billing.config.ts +146 -0
  21. package/config/dashboard.config.ts +333 -0
  22. package/config/dev.config.ts +48 -0
  23. package/config/features.config.ts +196 -0
  24. package/config/flows.config.ts +333 -0
  25. package/config/permissions.config.ts +101 -0
  26. package/config/theme.config.ts +128 -0
  27. package/entities/categories/categories.config.ts +60 -0
  28. package/entities/categories/categories.fields.ts +115 -0
  29. package/entities/categories/categories.service.ts +333 -0
  30. package/entities/categories/categories.types.ts +58 -0
  31. package/entities/categories/messages/en.json +33 -0
  32. package/entities/categories/messages/es.json +33 -0
  33. package/entities/posts/messages/en.json +100 -0
  34. package/entities/posts/messages/es.json +100 -0
  35. package/entities/posts/migrations/001_posts_table.sql +110 -0
  36. package/entities/posts/migrations/002_add_featured.sql +19 -0
  37. package/entities/posts/migrations/003_post_categories_pivot.sql +47 -0
  38. package/entities/posts/posts.config.ts +61 -0
  39. package/entities/posts/posts.fields.ts +234 -0
  40. package/entities/posts/posts.service.ts +464 -0
  41. package/entities/posts/posts.types.ts +80 -0
  42. package/lib/selectors.ts +179 -0
  43. package/messages/en.json +113 -0
  44. package/messages/es.json +113 -0
  45. package/migrations/002_author_profile_fields.sql +37 -0
  46. package/migrations/003_categories_table.sql +90 -0
  47. package/migrations/999_sample_data.sql +412 -0
  48. package/migrations/999_theme_sample_data.sql +1070 -0
  49. package/package.json +18 -0
  50. package/permissions-matrix.md +63 -0
  51. package/styles/article.css +333 -0
  52. package/styles/components.css +204 -0
  53. package/styles/globals.css +327 -0
  54. package/styles/theme.css +167 -0
  55. package/templates/(public)/author/[username]/page.tsx +247 -0
  56. package/templates/(public)/authors/page.tsx +161 -0
  57. package/templates/(public)/layout.tsx +44 -0
  58. package/templates/(public)/page.tsx +276 -0
  59. package/templates/(public)/posts/[slug]/page.tsx +342 -0
  60. package/templates/dashboard/(main)/page.tsx +385 -0
  61. package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +529 -0
  62. package/templates/dashboard/(main)/posts/[id]/page.tsx +33 -0
  63. package/templates/dashboard/(main)/posts/create/page.tsx +353 -0
  64. 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
+