@stacksjs/defaults 0.70.88 → 0.70.90
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/app/Actions/Blog/BlogDestroyAction.ts +25 -0
- package/app/Actions/Blog/BlogIndexAction.ts +18 -0
- package/app/Actions/Blog/BlogShowAction.ts +25 -0
- package/app/Actions/Blog/BlogStoreAction.ts +21 -0
- package/app/Actions/Blog/BlogUpdateAction.ts +23 -0
- package/app/Actions/Blog/payload.ts +56 -0
- package/app/Actions/Cms/PostStoreAction.ts +10 -5
- package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +38 -0
- package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +53 -39
- package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +61 -0
- package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +30 -0
- package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +40 -17
- package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +58 -0
- package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +30 -0
- package/app/Actions/Dashboard/Content/CommentIndexAction.ts +46 -31
- package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +48 -0
- package/app/Actions/Dashboard/Content/PageDestroyAction.ts +25 -0
- package/app/Actions/Dashboard/Content/PageIndexAction.ts +43 -15
- package/app/Actions/Dashboard/Content/PageStoreAction.ts +49 -0
- package/app/Actions/Dashboard/Content/PostDestroyAction.ts +37 -0
- package/app/Actions/Dashboard/Content/PostIndexAction.ts +64 -83
- package/app/Actions/Dashboard/Content/PostStoreAction.ts +50 -0
- package/app/Actions/Dashboard/Content/PostUpdateAction.ts +54 -0
- package/app/Actions/Dashboard/Content/TagDestroyAction.ts +30 -0
- package/app/Actions/Dashboard/Content/TagIndexAction.ts +39 -13
- package/app/Actions/Dashboard/Content/TagStoreAction.ts +60 -0
- package/app/Actions/Dashboard/Content/comment-input.ts +35 -0
- package/app/Actions/Dashboard/Content/content-input.ts +65 -0
- package/app/Actions/Dashboard/Content/post-input.ts +54 -0
- package/app/Models/Content/Post.ts +4 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/FileViewer/MediaCard.stx +119 -0
- package/resources/components/Dashboard/FileViewer/MediaInfoPanel.stx +143 -0
- package/resources/components/Dashboard/SidebarModern.stx +1 -0
- package/resources/functions/dashboard/files-tree.ts +162 -0
- package/resources/functions/dashboard/sidebar.ts +1 -0
|
@@ -1,41 +1,56 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { db } from '@stacksjs/database'
|
|
3
|
+
import { normalizeCommentStatus } from './comment-input'
|
|
3
4
|
|
|
5
|
+
interface CommentRow {
|
|
6
|
+
id: number
|
|
7
|
+
author_name: string | null
|
|
8
|
+
author_email: string | null
|
|
9
|
+
content: string | null
|
|
10
|
+
body: string | null
|
|
11
|
+
post_title: string | null
|
|
12
|
+
status: string | null
|
|
13
|
+
is_approved: number | null
|
|
14
|
+
created_at: string | null
|
|
15
|
+
updated_at: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `GET /api/dashboard/comments` — backs `views/dashboard/content/comments/index.stx`.
|
|
20
|
+
*
|
|
21
|
+
* Reads the `comments` table via `db`. The previous `Comment.orderBy(...)` call
|
|
22
|
+
* threw on every request (the ORM model exposes no query methods) and the catch
|
|
23
|
+
* turned that into an empty list, so a broken read looked like a CMS with no
|
|
24
|
+
* comments. It also mapped `author` / `email` / `ip` / `post_id`, none of which
|
|
25
|
+
* this table has — the columns are `author_name`, `author_email`, `ip_address`,
|
|
26
|
+
* and there is no post foreign key, only the denormalized `post_title`.
|
|
27
|
+
*
|
|
28
|
+
* Column names are returned as-is; the page normalizes them client-side.
|
|
29
|
+
*/
|
|
4
30
|
export default new Action({
|
|
5
31
|
name: 'CommentIndexAction',
|
|
6
|
-
description: 'Returns comments
|
|
32
|
+
description: 'Returns CMS comments for the dashboard.',
|
|
7
33
|
method: 'GET',
|
|
34
|
+
apiResponse: true,
|
|
8
35
|
async handle() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
id: Number(c.get('id')),
|
|
15
|
-
author: String(c.get('author') || c.get('name') || ''),
|
|
16
|
-
email: String(c.get('email') || ''),
|
|
17
|
-
content: String(c.get('content') || c.get('body') || ''),
|
|
18
|
-
status: String(c.get('status') || 'pending'),
|
|
19
|
-
date: String(c.get('created_at') || ''),
|
|
20
|
-
postTitle: String(c.get('post_title') || ''),
|
|
21
|
-
postId: Number(c.get('post_id') || 0),
|
|
22
|
-
ip: String(c.get('ip') || ''),
|
|
23
|
-
}))
|
|
36
|
+
const rows = await db
|
|
37
|
+
.selectFrom('comments')
|
|
38
|
+
.selectAll()
|
|
39
|
+
.orderBy('created_at', 'desc')
|
|
40
|
+
.execute() as unknown as CommentRow[]
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
const comments = rows.map(row => ({
|
|
43
|
+
id: Number(row.id),
|
|
44
|
+
author_name: String(row.author_name || ''),
|
|
45
|
+
author_email: String(row.author_email || ''),
|
|
46
|
+
content: String(row.content || row.body || ''),
|
|
47
|
+
post_title: String(row.post_title || ''),
|
|
48
|
+
status: normalizeCommentStatus(row.status),
|
|
49
|
+
is_approved: Boolean(row.is_approved),
|
|
50
|
+
created_at: row.created_at || null,
|
|
51
|
+
updated_at: row.updated_at || null,
|
|
52
|
+
}))
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
comments,
|
|
31
|
-
stats: { total: totalCount, pending, approved, spam },
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return {
|
|
36
|
-
comments: [],
|
|
37
|
-
stats: { total: 0, pending: 0, approved: 0, spam: 0 },
|
|
38
|
-
}
|
|
39
|
-
}
|
|
54
|
+
return { comments }
|
|
40
55
|
},
|
|
41
56
|
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { COMMENT_STATUSES, parseCommentStatus } from './comment-input'
|
|
6
|
+
import { findRow, rowExists, rowId, timestamp } from './content-input'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `PATCH /api/dashboard/comments/{id}` — moderates a comment from the dashboard.
|
|
10
|
+
*
|
|
11
|
+
* Status is the only writable field: the page's approve/spam buttons are the
|
|
12
|
+
* only callers, and comment text belongs to the reader who wrote it.
|
|
13
|
+
*
|
|
14
|
+
* `is_approved` is kept in step with `status` so the two never disagree — the
|
|
15
|
+
* table carries both, and the page falls back to `is_approved` when a row has
|
|
16
|
+
* no status.
|
|
17
|
+
*/
|
|
18
|
+
export default new Action({
|
|
19
|
+
name: 'CommentUpdateAction',
|
|
20
|
+
description: 'Updates a CMS comment status from the dashboard.',
|
|
21
|
+
method: 'PATCH',
|
|
22
|
+
async handle(request: RequestInstance) {
|
|
23
|
+
const id = rowId(request)
|
|
24
|
+
|
|
25
|
+
if (!id)
|
|
26
|
+
return response.json({ message: 'A valid comment id is required.' }, 422)
|
|
27
|
+
|
|
28
|
+
const status = parseCommentStatus(request.get('status'))
|
|
29
|
+
|
|
30
|
+
if (!status)
|
|
31
|
+
return response.json({ message: `Status must be one of: ${COMMENT_STATUSES.join(', ')}.` }, 422)
|
|
32
|
+
|
|
33
|
+
if (!await rowExists('comments', id))
|
|
34
|
+
return response.json({ message: 'Comment not found.' }, 404)
|
|
35
|
+
|
|
36
|
+
await db
|
|
37
|
+
.updateTable('comments')
|
|
38
|
+
.set({
|
|
39
|
+
status,
|
|
40
|
+
is_approved: status === 'approved' ? 1 : 0,
|
|
41
|
+
updated_at: timestamp(),
|
|
42
|
+
} as any)
|
|
43
|
+
.where('id', '=', id)
|
|
44
|
+
.execute()
|
|
45
|
+
|
|
46
|
+
return response.json(await findRow('comments', id))
|
|
47
|
+
},
|
|
48
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { rowExists, rowId } from './content-input'
|
|
6
|
+
|
|
7
|
+
/** `DELETE /api/dashboard/pages/{id}` — deletes a CMS page from the dashboard. */
|
|
8
|
+
export default new Action({
|
|
9
|
+
name: 'PageDestroyAction',
|
|
10
|
+
description: 'Deletes a CMS page from the dashboard.',
|
|
11
|
+
method: 'DELETE',
|
|
12
|
+
async handle(request: RequestInstance) {
|
|
13
|
+
const id = rowId(request)
|
|
14
|
+
|
|
15
|
+
if (!id)
|
|
16
|
+
return response.json({ message: 'A valid page id is required.' }, 422)
|
|
17
|
+
|
|
18
|
+
if (!await rowExists('pages', id))
|
|
19
|
+
return response.json({ message: 'Page not found.' }, 404)
|
|
20
|
+
|
|
21
|
+
await db.deleteFrom('pages').where('id', '=', id).execute()
|
|
22
|
+
|
|
23
|
+
return response.json({ message: 'Page deleted.', id })
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -1,25 +1,53 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { db } from '@stacksjs/database'
|
|
3
3
|
|
|
4
|
+
interface PageRow {
|
|
5
|
+
id: number
|
|
6
|
+
title: string | null
|
|
7
|
+
template: string | null
|
|
8
|
+
views: number | null
|
|
9
|
+
conversions: number | null
|
|
10
|
+
published_at: string | null
|
|
11
|
+
author_id: number | null
|
|
12
|
+
created_at: string | null
|
|
13
|
+
updated_at: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `GET /api/dashboard/pages` — backs `views/dashboard/content/pages/index.stx`.
|
|
18
|
+
*
|
|
19
|
+
* Reads the `pages` table via `db`. The previous `Page.orderBy(...)` call threw
|
|
20
|
+
* on every request (the ORM model exposes no query methods) and the catch
|
|
21
|
+
* turned that into an empty list, so a broken read looked like a CMS with no
|
|
22
|
+
* pages. It also mapped `slug` and `status`, which this table does not have —
|
|
23
|
+
* a page is a title, a template, and its traffic counters.
|
|
24
|
+
*
|
|
25
|
+
* Ordered newest-first because the page's Newest tile reads the first row.
|
|
26
|
+
*/
|
|
4
27
|
export default new Action({
|
|
5
28
|
name: 'PageIndexAction',
|
|
6
|
-
description: 'Returns pages
|
|
29
|
+
description: 'Returns CMS pages for the dashboard.',
|
|
7
30
|
method: 'GET',
|
|
31
|
+
apiResponse: true,
|
|
8
32
|
async handle() {
|
|
9
|
-
|
|
10
|
-
|
|
33
|
+
const rows = await db
|
|
34
|
+
.selectFrom('pages')
|
|
35
|
+
.selectAll()
|
|
36
|
+
.orderBy('created_at', 'desc')
|
|
37
|
+
.execute() as unknown as PageRow[]
|
|
11
38
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
39
|
+
const pages = rows.map(row => ({
|
|
40
|
+
id: Number(row.id),
|
|
41
|
+
title: String(row.title || ''),
|
|
42
|
+
template: String(row.template || 'default'),
|
|
43
|
+
views: Number(row.views || 0),
|
|
44
|
+
conversions: Number(row.conversions || 0),
|
|
45
|
+
published_at: row.published_at || null,
|
|
46
|
+
author_id: row.author_id ?? null,
|
|
47
|
+
created_at: row.created_at || null,
|
|
48
|
+
updated_at: row.updated_at || null,
|
|
49
|
+
}))
|
|
18
50
|
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return { pages: [] }
|
|
23
|
-
}
|
|
51
|
+
return { pages }
|
|
24
52
|
},
|
|
25
53
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { findRow, insertedId, str, timestamp } from './content-input'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `POST /api/dashboard/pages` — creates a CMS page from the dashboard.
|
|
9
|
+
*
|
|
10
|
+
* No `uuid` is written: unlike `posts` / `authors` / `tags`, the `pages` table
|
|
11
|
+
* has no uuid column even though the model declares the `useUuid` trait.
|
|
12
|
+
*
|
|
13
|
+
* `published_at` stays null — the dialog has no publish control, and the page's
|
|
14
|
+
* Published column falls back to `created_at`.
|
|
15
|
+
*/
|
|
16
|
+
export default new Action({
|
|
17
|
+
name: 'PageStoreAction',
|
|
18
|
+
description: 'Creates a CMS page from the dashboard.',
|
|
19
|
+
method: 'POST',
|
|
20
|
+
async handle(request: RequestInstance) {
|
|
21
|
+
const title = str(request.get('title')).trim()
|
|
22
|
+
const template = str(request.get('template')).trim() || 'default'
|
|
23
|
+
|
|
24
|
+
if (!title)
|
|
25
|
+
return response.json({ message: 'Title is required.' }, 422)
|
|
26
|
+
|
|
27
|
+
const now = timestamp()
|
|
28
|
+
|
|
29
|
+
const result = await db
|
|
30
|
+
.insertInto('pages')
|
|
31
|
+
.values({
|
|
32
|
+
title,
|
|
33
|
+
template,
|
|
34
|
+
views: 0,
|
|
35
|
+
conversions: 0,
|
|
36
|
+
published_at: null,
|
|
37
|
+
created_at: now,
|
|
38
|
+
updated_at: now,
|
|
39
|
+
} as any)
|
|
40
|
+
.executeTakeFirst()
|
|
41
|
+
|
|
42
|
+
const id = insertedId(result)
|
|
43
|
+
|
|
44
|
+
if (!id)
|
|
45
|
+
return response.json({ message: 'Could not create page.' }, 500)
|
|
46
|
+
|
|
47
|
+
return response.json(await findRow('pages', id), 201)
|
|
48
|
+
},
|
|
49
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `DELETE /api/dashboard/posts/{id}` — deletes a CMS post from the dashboard.
|
|
8
|
+
*
|
|
9
|
+
* A plain row delete: this schema has no `categorizable_models` /
|
|
10
|
+
* `taggable_models` pivot tables, so there is nothing to cascade to. (The
|
|
11
|
+
* `@stacksjs/cms` `posts.destroy()` helper assumes those tables exist, which is
|
|
12
|
+
* why this action talks to `db` directly instead.)
|
|
13
|
+
*/
|
|
14
|
+
export default new Action({
|
|
15
|
+
name: 'PostDestroyAction',
|
|
16
|
+
description: 'Deletes a CMS post from the dashboard.',
|
|
17
|
+
method: 'DELETE',
|
|
18
|
+
async handle(request: RequestInstance) {
|
|
19
|
+
const id = Number(request.getParam('id'))
|
|
20
|
+
|
|
21
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
22
|
+
return response.json({ message: 'A valid post id is required.' }, 422)
|
|
23
|
+
|
|
24
|
+
const existing = await db
|
|
25
|
+
.selectFrom('posts')
|
|
26
|
+
.select(['id'])
|
|
27
|
+
.where('id', '=', id)
|
|
28
|
+
.executeTakeFirst()
|
|
29
|
+
|
|
30
|
+
if (!existing)
|
|
31
|
+
return response.json({ message: 'Post not found.' }, 404)
|
|
32
|
+
|
|
33
|
+
await db.deleteFrom('posts').where('id', '=', id).execute()
|
|
34
|
+
|
|
35
|
+
return response.json({ message: 'Post deleted.', id })
|
|
36
|
+
},
|
|
37
|
+
})
|
|
@@ -1,102 +1,83 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { db } from '@stacksjs/database'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
interface PostRow {
|
|
5
|
+
id: number
|
|
6
|
+
title: string
|
|
7
|
+
excerpt: string | null
|
|
8
|
+
content: string | null
|
|
9
|
+
poster: string | null
|
|
10
|
+
status: string | null
|
|
11
|
+
views: number | null
|
|
12
|
+
published_at: string | null
|
|
13
|
+
created_at: string | null
|
|
14
|
+
updated_at: string | null
|
|
15
|
+
author_id: number | null
|
|
16
|
+
is_featured: number | null
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
// `status` is stored lowercase (the posts table has a CHECK constraint on
|
|
20
|
+
// 'published' | 'draft' | 'archived'), but older rows and hand-written seeds
|
|
21
|
+
// have been seen with 'Draft'/'Published'. Normalize on read so the dashboard
|
|
22
|
+
// filter and badge styling only ever deal with one casing.
|
|
23
|
+
function normalizeStatus(status: string | null): string {
|
|
24
|
+
const value = String(status || 'draft').toLowerCase()
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{ id: 2, name: 'JavaScript', slug: 'javascript' },
|
|
28
|
-
{ id: 3, name: 'Vue', slug: 'vue' },
|
|
29
|
-
{ id: 4, name: 'TypeScript', slug: 'typescript' },
|
|
30
|
-
]
|
|
26
|
+
return value === 'published' || value === 'archived' ? value : 'draft'
|
|
27
|
+
}
|
|
31
28
|
|
|
32
29
|
function counts(posts: Array<{ status: string }>) {
|
|
33
30
|
return {
|
|
34
|
-
publishedCount: posts.filter(p => p.status
|
|
35
|
-
draftCount: posts.filter(p => p.status
|
|
36
|
-
|
|
31
|
+
publishedCount: posts.filter(p => p.status === 'published').length,
|
|
32
|
+
draftCount: posts.filter(p => p.status === 'draft').length,
|
|
33
|
+
archivedCount: posts.filter(p => p.status === 'archived').length,
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
37
|
+
/**
|
|
38
|
+
* `GET /api/dashboard/posts` — backs `views/dashboard/content/posts/index.stx`.
|
|
39
|
+
*
|
|
40
|
+
* Reads straight from the `posts` table via `db` rather than the ORM model:
|
|
41
|
+
* `Post` from `@stacksjs/orm` exposes no query methods today, so the previous
|
|
42
|
+
* `Post.orderBy(...)` call threw on every request.
|
|
43
|
+
*
|
|
44
|
+
* There is deliberately no mock-data fallback. This action used to catch every
|
|
45
|
+
* error and serve placeholder rows, which made a page that was 404ing against
|
|
46
|
+
* an unregistered endpoint look like it was working. A failure here must
|
|
47
|
+
* surface as a failure.
|
|
48
|
+
*/
|
|
40
49
|
export default new Action({
|
|
41
50
|
name: 'PostIndexAction',
|
|
42
|
-
description: 'Returns posts
|
|
51
|
+
description: 'Returns CMS posts for the dashboard.',
|
|
43
52
|
method: 'GET',
|
|
53
|
+
apiResponse: true,
|
|
44
54
|
async handle() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
// Empty result from a real ORM still falls through to mock data so
|
|
53
|
-
// the dev dashboard always has something to render.
|
|
54
|
-
if (!allPosts || allPosts.length === 0) {
|
|
55
|
-
return {
|
|
56
|
-
posts: mockPosts,
|
|
57
|
-
categories: mockCategories,
|
|
58
|
-
tags: mockTags,
|
|
59
|
-
...counts(mockPosts),
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const posts = allPosts.map((p, idx) => ({
|
|
64
|
-
id: Number(p.get('id') || idx + 1),
|
|
65
|
-
title: String(p.get('title') || ''),
|
|
66
|
-
excerpt: String(p.get('excerpt') || ''),
|
|
67
|
-
slug: String(p.get('slug') || ''),
|
|
68
|
-
category: String(p.get('category') || ''),
|
|
69
|
-
tags: Array.isArray(p.get('tags')) ? p.get('tags') as string[] : [],
|
|
70
|
-
author: String(p.get('author') || ''),
|
|
71
|
-
views: Number(p.get('views') || 0),
|
|
72
|
-
comments: Number(p.get('comments') || 0),
|
|
73
|
-
published: String(p.get('published_at') || p.get('published') || p.get('created_at') || ''),
|
|
74
|
-
status: String(p.get('status') || 'Draft'),
|
|
75
|
-
featured: Boolean(p.get('featured')),
|
|
76
|
-
poster: String(p.get('poster') || ''),
|
|
77
|
-
}))
|
|
55
|
+
const rows = await db
|
|
56
|
+
.selectFrom('posts')
|
|
57
|
+
.selectAll()
|
|
58
|
+
.orderBy('created_at', 'desc')
|
|
59
|
+
.execute() as unknown as PostRow[]
|
|
78
60
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
const posts = rows.map(row => ({
|
|
62
|
+
id: Number(row.id),
|
|
63
|
+
title: String(row.title || ''),
|
|
64
|
+
excerpt: String(row.excerpt || ''),
|
|
65
|
+
content: String(row.content || ''),
|
|
66
|
+
poster: String(row.poster || ''),
|
|
67
|
+
status: normalizeStatus(row.status),
|
|
68
|
+
views: Number(row.views || 0),
|
|
69
|
+
published_at: row.published_at || null,
|
|
70
|
+
created_at: row.created_at || null,
|
|
71
|
+
updated_at: row.updated_at || null,
|
|
72
|
+
author_id: row.author_id ?? null,
|
|
73
|
+
featured: Boolean(row.is_featured),
|
|
74
|
+
}))
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}))
|
|
76
|
+
const [categories, tags] = await Promise.all([
|
|
77
|
+
db.selectFrom('categories').select(['id', 'name', 'slug']).execute(),
|
|
78
|
+
db.selectFrom('tags').select(['id', 'name', 'slug']).execute(),
|
|
79
|
+
])
|
|
90
80
|
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return {
|
|
95
|
-
posts: mockPosts,
|
|
96
|
-
categories: mockCategories,
|
|
97
|
-
tags: mockTags,
|
|
98
|
-
...counts(mockPosts),
|
|
99
|
-
}
|
|
100
|
-
}
|
|
81
|
+
return { posts, categories, tags, ...counts(posts) }
|
|
101
82
|
},
|
|
102
83
|
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { randomUUIDv7 } from 'bun'
|
|
6
|
+
import { findPost, insertedId, postPayload, publishedAtFor, timestamp } from './post-input'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `POST /api/dashboard/posts` — creates a CMS post from the dashboard.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately does not reuse `Actions/Cms/PostStoreAction`: that action runs
|
|
12
|
+
* the `Post` model validation, which rejects the empty `excerpt` / `poster`
|
|
13
|
+
* that the dashboard dialog legitimately submits for a bare draft.
|
|
14
|
+
*/
|
|
15
|
+
export default new Action({
|
|
16
|
+
name: 'PostStoreAction',
|
|
17
|
+
description: 'Creates a CMS post from the dashboard.',
|
|
18
|
+
method: 'POST',
|
|
19
|
+
async handle(request: RequestInstance) {
|
|
20
|
+
const payload = postPayload(request)
|
|
21
|
+
|
|
22
|
+
if (!payload.title)
|
|
23
|
+
return response.json({ message: 'Title is required.' }, 422)
|
|
24
|
+
|
|
25
|
+
const now = timestamp()
|
|
26
|
+
|
|
27
|
+
const result = await db
|
|
28
|
+
.insertInto('posts')
|
|
29
|
+
.values({
|
|
30
|
+
uuid: randomUUIDv7(),
|
|
31
|
+
title: payload.title,
|
|
32
|
+
excerpt: payload.excerpt,
|
|
33
|
+
content: payload.content,
|
|
34
|
+
poster: payload.poster,
|
|
35
|
+
status: payload.status,
|
|
36
|
+
views: 0,
|
|
37
|
+
published_at: publishedAtFor(payload.status, null, now),
|
|
38
|
+
created_at: now,
|
|
39
|
+
updated_at: now,
|
|
40
|
+
} as any)
|
|
41
|
+
.executeTakeFirst()
|
|
42
|
+
|
|
43
|
+
const id = insertedId(result)
|
|
44
|
+
|
|
45
|
+
if (!id)
|
|
46
|
+
return response.json({ message: 'Could not create post.' }, 500)
|
|
47
|
+
|
|
48
|
+
return response.json(await findPost(id), 201)
|
|
49
|
+
},
|
|
50
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { findPost, postPayload, publishedAtFor, timestamp } from './post-input'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `PATCH /api/dashboard/posts/{id}` — updates a CMS post from the dashboard.
|
|
9
|
+
*
|
|
10
|
+
* The dashboard dialog always submits the full post, so every writable column
|
|
11
|
+
* is replaced. `published_at` is derived from the incoming status rather than
|
|
12
|
+
* accepted from the body — see `publishedAtFor`.
|
|
13
|
+
*/
|
|
14
|
+
export default new Action({
|
|
15
|
+
name: 'PostUpdateAction',
|
|
16
|
+
description: 'Updates a CMS post from the dashboard.',
|
|
17
|
+
method: 'PATCH',
|
|
18
|
+
async handle(request: RequestInstance) {
|
|
19
|
+
const id = Number(request.getParam('id'))
|
|
20
|
+
|
|
21
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
22
|
+
return response.json({ message: 'A valid post id is required.' }, 422)
|
|
23
|
+
|
|
24
|
+
const payload = postPayload(request)
|
|
25
|
+
|
|
26
|
+
if (!payload.title)
|
|
27
|
+
return response.json({ message: 'Title is required.' }, 422)
|
|
28
|
+
|
|
29
|
+
const existing = await db
|
|
30
|
+
.selectFrom('posts')
|
|
31
|
+
.select(['id', 'published_at'])
|
|
32
|
+
.where('id', '=', id)
|
|
33
|
+
.executeTakeFirst() as { id: number, published_at: string | null } | undefined
|
|
34
|
+
|
|
35
|
+
if (!existing)
|
|
36
|
+
return response.json({ message: 'Post not found.' }, 404)
|
|
37
|
+
|
|
38
|
+
await db
|
|
39
|
+
.updateTable('posts')
|
|
40
|
+
.set({
|
|
41
|
+
title: payload.title,
|
|
42
|
+
excerpt: payload.excerpt,
|
|
43
|
+
content: payload.content,
|
|
44
|
+
poster: payload.poster,
|
|
45
|
+
status: payload.status,
|
|
46
|
+
published_at: publishedAtFor(payload.status, existing.published_at, timestamp()),
|
|
47
|
+
updated_at: timestamp(),
|
|
48
|
+
} as any)
|
|
49
|
+
.where('id', '=', id)
|
|
50
|
+
.execute()
|
|
51
|
+
|
|
52
|
+
return response.json(await findPost(id))
|
|
53
|
+
},
|
|
54
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { rowExists, rowId } from './content-input'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `DELETE /api/dashboard/tags/{id}` — deletes a CMS tag.
|
|
9
|
+
*
|
|
10
|
+
* A plain row delete: this schema has no `taggable_models` pivot table, so
|
|
11
|
+
* there is nothing to cascade to.
|
|
12
|
+
*/
|
|
13
|
+
export default new Action({
|
|
14
|
+
name: 'TagDestroyAction',
|
|
15
|
+
description: 'Deletes a CMS tag from the dashboard.',
|
|
16
|
+
method: 'DELETE',
|
|
17
|
+
async handle(request: RequestInstance) {
|
|
18
|
+
const id = rowId(request)
|
|
19
|
+
|
|
20
|
+
if (!id)
|
|
21
|
+
return response.json({ message: 'A valid tag id is required.' }, 422)
|
|
22
|
+
|
|
23
|
+
if (!await rowExists('tags', id))
|
|
24
|
+
return response.json({ message: 'Tag not found.' }, 404)
|
|
25
|
+
|
|
26
|
+
await db.deleteFrom('tags').where('id', '=', id).execute()
|
|
27
|
+
|
|
28
|
+
return response.json({ message: 'Tag deleted.', id })
|
|
29
|
+
},
|
|
30
|
+
})
|