@arch-cadre/blog-module 1.0.7 → 1.0.8
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/dist/actions/index.d.ts +38 -5
- package/dist/actions/index.js +149 -0
- package/dist/components/BlogStatsWidget.d.ts +1 -2
- package/dist/components/BlogStatsWidget.js +17 -0
- package/dist/components/RecentCommentsWidget.d.ts +1 -2
- package/dist/components/RecentCommentsWidget.js +24 -0
- package/dist/components/RecentPostsWidget.d.ts +1 -2
- package/dist/components/RecentPostsWidget.js +24 -0
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/button.js +33 -0
- package/dist/components/ui/card.js +12 -0
- package/dist/components/ui/input.js +8 -0
- package/dist/components/ui/table.js +16 -0
- package/dist/components/ui/textarea.js +8 -0
- package/dist/index.js +98 -0
- package/dist/lib/{utils.mjs → utils.js} +1 -1
- package/dist/lib/validation.d.ts +2 -2
- package/dist/lib/validation.js +11 -0
- package/dist/navigation.js +21 -0
- package/dist/routes.js +55 -0
- package/dist/schema.js +60 -0
- package/dist/ui/views.d.ts +5 -6
- package/dist/ui/views.js +119 -0
- package/package.json +10 -17
- package/dist/actions/index.cjs +0 -158
- package/dist/actions/index.mjs +0 -121
- package/dist/components/BlogStatsWidget.cjs +0 -45
- package/dist/components/BlogStatsWidget.mjs +0 -13
- package/dist/components/RecentCommentsWidget.cjs +0 -47
- package/dist/components/RecentCommentsWidget.mjs +0 -28
- package/dist/components/RecentPostsWidget.cjs +0 -47
- package/dist/components/RecentPostsWidget.mjs +0 -28
- package/dist/components/ui/button.cjs +0 -53
- package/dist/components/ui/button.mjs +0 -44
- package/dist/components/ui/card.cjs +0 -46
- package/dist/components/ui/card.mjs +0 -35
- package/dist/components/ui/input.cjs +0 -23
- package/dist/components/ui/input.mjs +0 -20
- package/dist/components/ui/table.cjs +0 -66
- package/dist/components/ui/table.mjs +0 -59
- package/dist/components/ui/textarea.cjs +0 -21
- package/dist/components/ui/textarea.mjs +0 -19
- package/dist/index.cjs +0 -100
- package/dist/index.mjs +0 -95
- package/dist/intl.d.ts +0 -7
- package/dist/lib/utils.cjs +0 -11
- package/dist/lib/validation.cjs +0 -16
- package/dist/lib/validation.mjs +0 -10
- package/dist/navigation.cjs +0 -23
- package/dist/navigation.mjs +0 -21
- package/dist/routes.cjs +0 -74
- package/dist/routes.mjs +0 -68
- package/dist/schema.cjs +0 -62
- package/dist/schema.mjs +0 -53
- package/dist/styles/globals.css +0 -1
- package/dist/ui/views.cjs +0 -448
- package/dist/ui/views.mjs +0 -232
package/dist/actions/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
-
import { commentSchema, postSchema } from "../lib/validation";
|
|
2
|
+
import { commentSchema, postSchema } from "../lib/validation.js";
|
|
3
3
|
export declare function createPost(data: z.infer<typeof postSchema>): Promise<{
|
|
4
4
|
success: boolean;
|
|
5
5
|
error?: undefined;
|
|
@@ -7,10 +7,43 @@ export declare function createPost(data: z.infer<typeof postSchema>): Promise<{
|
|
|
7
7
|
success: boolean;
|
|
8
8
|
error: any;
|
|
9
9
|
}>;
|
|
10
|
-
export declare function getPosts(): Promise<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
export declare function getPosts(): Promise<{
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
content: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
author: {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
}[]>;
|
|
20
|
+
export declare function getPostBySlug(slug: string): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
slug: string;
|
|
24
|
+
content: string;
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
author: {
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
export declare function getPostById(id: string): Promise<{
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
slug: string;
|
|
34
|
+
content: string;
|
|
35
|
+
authorId: string;
|
|
36
|
+
createdAt: Date;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function getComments(postId: string): Promise<{
|
|
39
|
+
id: string;
|
|
40
|
+
content: string;
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
author: {
|
|
43
|
+
name: string;
|
|
44
|
+
image: string | null;
|
|
45
|
+
};
|
|
46
|
+
}[]>;
|
|
14
47
|
export declare function createComment(data: z.infer<typeof commentSchema>): Promise<{
|
|
15
48
|
success: boolean;
|
|
16
49
|
error?: undefined;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { db, getCurrentSession, userTable } from "@arch-cadre/core/server";
|
|
3
|
+
import { desc, eq } from "drizzle-orm";
|
|
4
|
+
import { revalidatePath } from "next/cache";
|
|
5
|
+
import { commentSchema, postSchema } from "../lib/validation.js";
|
|
6
|
+
import { commentsTable, postsTable } from "../schema.js";
|
|
7
|
+
// --- Posts ---
|
|
8
|
+
export async function createPost(data) {
|
|
9
|
+
try {
|
|
10
|
+
const { user } = await getCurrentSession();
|
|
11
|
+
if (!user)
|
|
12
|
+
throw new Error("Unauthorized");
|
|
13
|
+
const validated = postSchema.parse(data);
|
|
14
|
+
await db.insert(postsTable).values({
|
|
15
|
+
...validated,
|
|
16
|
+
authorId: user.id,
|
|
17
|
+
});
|
|
18
|
+
revalidatePath("/blog");
|
|
19
|
+
return { success: true };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error("[BlogModule:Actions] createPost failed:", error);
|
|
23
|
+
return { success: false, error: error.message };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function getPosts() {
|
|
27
|
+
try {
|
|
28
|
+
return await db
|
|
29
|
+
.select({
|
|
30
|
+
id: postsTable.id,
|
|
31
|
+
title: postsTable.title,
|
|
32
|
+
slug: postsTable.slug,
|
|
33
|
+
content: postsTable.content,
|
|
34
|
+
createdAt: postsTable.createdAt,
|
|
35
|
+
author: {
|
|
36
|
+
name: userTable.name,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
.from(postsTable)
|
|
40
|
+
.innerJoin(userTable, eq(postsTable.authorId, userTable.id))
|
|
41
|
+
.orderBy(desc(postsTable.createdAt));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error("[BlogModule:Actions] getPosts failed:", error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function getPostBySlug(slug) {
|
|
49
|
+
try {
|
|
50
|
+
const [post] = await db
|
|
51
|
+
.select({
|
|
52
|
+
id: postsTable.id,
|
|
53
|
+
title: postsTable.title,
|
|
54
|
+
slug: postsTable.slug,
|
|
55
|
+
content: postsTable.content,
|
|
56
|
+
createdAt: postsTable.createdAt,
|
|
57
|
+
author: {
|
|
58
|
+
name: userTable.name,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
.from(postsTable)
|
|
62
|
+
.innerJoin(userTable, eq(postsTable.authorId, userTable.id))
|
|
63
|
+
.where(eq(postsTable.slug, slug));
|
|
64
|
+
return post || null;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error("[BlogModule:Actions] getPostBySlug failed:", error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function getPostById(id) {
|
|
72
|
+
try {
|
|
73
|
+
const [post] = await db
|
|
74
|
+
.select()
|
|
75
|
+
.from(postsTable)
|
|
76
|
+
.where(eq(postsTable.id, id));
|
|
77
|
+
return post || null;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error("[BlogModule:Actions] getPostById failed:", error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// --- Comments ---
|
|
85
|
+
export async function getComments(postId) {
|
|
86
|
+
try {
|
|
87
|
+
return await db
|
|
88
|
+
.select({
|
|
89
|
+
id: commentsTable.id,
|
|
90
|
+
content: commentsTable.content,
|
|
91
|
+
createdAt: commentsTable.createdAt,
|
|
92
|
+
author: {
|
|
93
|
+
name: userTable.name,
|
|
94
|
+
image: userTable.image,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
.from(commentsTable)
|
|
98
|
+
.innerJoin(userTable, eq(commentsTable.authorId, userTable.id))
|
|
99
|
+
.where(eq(commentsTable.postId, postId))
|
|
100
|
+
.orderBy(desc(commentsTable.createdAt));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error("[BlogModule:Actions] getComments failed:", error);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function createComment(data) {
|
|
108
|
+
try {
|
|
109
|
+
const { user } = await getCurrentSession();
|
|
110
|
+
if (!user)
|
|
111
|
+
throw new Error("Must be logged in to comment");
|
|
112
|
+
const validated = commentSchema.parse(data);
|
|
113
|
+
await db.insert(commentsTable).values({
|
|
114
|
+
content: validated.content,
|
|
115
|
+
postId: validated.postId,
|
|
116
|
+
authorId: user.id,
|
|
117
|
+
});
|
|
118
|
+
revalidatePath(`/blog`);
|
|
119
|
+
return { success: true };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error("[BlogModule:Actions] createComment failed:", error);
|
|
123
|
+
return { success: false, error: error.message };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function deletePost(id) {
|
|
127
|
+
try {
|
|
128
|
+
await db.delete(postsTable).where(eq(postsTable.id, id));
|
|
129
|
+
revalidatePath("/blog");
|
|
130
|
+
return { success: true };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error("[BlogModule:Actions] deletePost failed:", error);
|
|
134
|
+
return { success: false, error: error.message };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function updatePost(id, data) {
|
|
138
|
+
try {
|
|
139
|
+
const validated = postSchema.parse(data);
|
|
140
|
+
await db.update(postsTable).set(validated).where(eq(postsTable.id, id));
|
|
141
|
+
revalidatePath("/blog");
|
|
142
|
+
revalidatePath(`/blog/${validated.slug}`);
|
|
143
|
+
return { success: true };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error("[BlogModule:Actions] updatePost failed:", error);
|
|
147
|
+
return { success: false, error: error.message };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export default function BlogStatsWidget(): Promise<React.JSX.Element>;
|
|
1
|
+
export default function BlogStatsWidget(): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { db } from "@arch-cadre/core/server";
|
|
3
|
+
import { getTranslation } from "@arch-cadre/intl/server";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@arch-cadre/ui";
|
|
5
|
+
import { sql } from "drizzle-orm";
|
|
6
|
+
import { FileText, MessageSquare } from "lucide-react";
|
|
7
|
+
import { commentsTable, postsTable } from "../schema.js";
|
|
8
|
+
export default async function BlogStatsWidget() {
|
|
9
|
+
const { t } = await getTranslation();
|
|
10
|
+
const [postsCount] = await db
|
|
11
|
+
.select({ count: sql `count(*)` })
|
|
12
|
+
.from(postsTable);
|
|
13
|
+
const [commentsCount] = await db
|
|
14
|
+
.select({ count: sql `count(*)` })
|
|
15
|
+
.from(commentsTable);
|
|
16
|
+
return (_jsxs("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-2", children: [_jsx(CardTitle, { className: "text-sm font-medium", children: t("Total Posts") }), _jsx(FileText, { className: "h-4 w-4 text-muted-foreground" })] }), _jsx(CardContent, { children: _jsx("div", { className: "text-2xl font-bold", children: postsCount?.count || 0 }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-2", children: [_jsx(CardTitle, { className: "text-sm font-medium", children: t("Total Comments") }), _jsx(MessageSquare, { className: "h-4 w-4 text-muted-foreground" })] }), _jsx(CardContent, { children: _jsx("div", { className: "text-2xl font-bold", children: commentsCount?.count || 0 }) })] })] }));
|
|
17
|
+
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export default function RecentCommentsWidget(): Promise<React.JSX.Element>;
|
|
1
|
+
export default function RecentCommentsWidget(): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { db, userTable } from "@arch-cadre/core/server";
|
|
3
|
+
import { getTranslation } from "@arch-cadre/intl/server";
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@arch-cadre/ui";
|
|
5
|
+
import { desc, eq } from "drizzle-orm";
|
|
6
|
+
import { commentsTable } from "../schema.js";
|
|
7
|
+
export default async function RecentCommentsWidget() {
|
|
8
|
+
const { t } = await getTranslation();
|
|
9
|
+
const comments = await db
|
|
10
|
+
.select({
|
|
11
|
+
id: commentsTable.id,
|
|
12
|
+
content: commentsTable.content,
|
|
13
|
+
createdAt: commentsTable.createdAt,
|
|
14
|
+
author: {
|
|
15
|
+
name: userTable.name,
|
|
16
|
+
image: userTable.image,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
.from(commentsTable)
|
|
20
|
+
.leftJoin(userTable, eq(commentsTable.authorId, userTable.id))
|
|
21
|
+
.orderBy(desc(commentsTable.createdAt))
|
|
22
|
+
.limit(5);
|
|
23
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: t("Recent Comments") }), _jsx(CardDescription, { children: t("What readers are saying about your posts.") })] }), _jsxs(CardContent, { className: "grid gap-8", children: [comments.map((comment) => (_jsxs("div", { className: "flex items-center gap-4", children: [_jsxs(Avatar, { className: "h-9 w-9", children: [_jsx(AvatarImage, { src: comment.author?.image || "", alt: "Avatar" }), _jsx(AvatarFallback, { children: comment.author?.name?.substring(0, 2).toUpperCase() || "CM" })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx("p", { className: "text-sm font-medium leading-none line-clamp-1", children: comment.content }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [comment.author?.name || t("Anonymous"), " \u2022", " ", new Date(comment.createdAt).toLocaleDateString()] })] })] }, comment.id))), comments.length === 0 && (_jsx("div", { className: "text-center py-4 text-muted-foreground", children: t("No comments yet.") }))] })] }));
|
|
24
|
+
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export default function RecentPostsWidget(): Promise<React.JSX.Element>;
|
|
1
|
+
export default function RecentPostsWidget(): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { db, userTable } from "@arch-cadre/core/server";
|
|
3
|
+
import { getTranslation } from "@arch-cadre/intl/server";
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@arch-cadre/ui";
|
|
5
|
+
import { desc, eq } from "drizzle-orm";
|
|
6
|
+
import { postsTable } from "../schema.js";
|
|
7
|
+
export default async function RecentPostsWidget() {
|
|
8
|
+
const { t } = await getTranslation();
|
|
9
|
+
const posts = await db
|
|
10
|
+
.select({
|
|
11
|
+
id: postsTable.id,
|
|
12
|
+
title: postsTable.title,
|
|
13
|
+
createdAt: postsTable.createdAt,
|
|
14
|
+
author: {
|
|
15
|
+
name: userTable.name,
|
|
16
|
+
image: userTable.image,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
.from(postsTable)
|
|
20
|
+
.leftJoin(userTable, eq(postsTable.authorId, userTable.id))
|
|
21
|
+
.orderBy(desc(postsTable.createdAt))
|
|
22
|
+
.limit(5);
|
|
23
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: t("Recent Posts") }), _jsx(CardDescription, { children: t("The latest articles published on your blog.") })] }), _jsxs(CardContent, { className: "grid gap-8", children: [posts.map((post) => (_jsxs("div", { className: "flex items-center gap-4", children: [_jsxs(Avatar, { className: "h-9 w-9", children: [_jsx(AvatarImage, { src: post.author?.image || "", alt: "Avatar" }), _jsx(AvatarFallback, { children: post.author?.name?.substring(0, 2).toUpperCase() || "AU" })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx("p", { className: "text-sm font-medium leading-none", children: post.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: new Date(post.createdAt).toLocaleDateString() })] })] }, post.id))), posts.length === 0 && (_jsx("div", { className: "text-center py-4 text-muted-foreground", children: t("No posts found.") }))] })] }));
|
|
24
|
+
}
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
4
|
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
|
|
5
5
|
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
|
6
|
-
} & import("class-variance-authority/
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
8
|
asChild?: boolean;
|
|
9
9
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
const buttonVariants = cva("inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
10
|
+
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
11
|
+
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
12
|
+
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
13
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
14
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default: "h-9 px-4 py-2",
|
|
18
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
19
|
+
lg: "h-10 rounded-md px-8",
|
|
20
|
+
icon: "size-9",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
variant: "default",
|
|
25
|
+
size: "default",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
29
|
+
const Comp = asChild ? Slot : "button";
|
|
30
|
+
return (_jsx(Comp, { className: cn(buttonVariants({ variant, size, className })), ref: ref, ...props }));
|
|
31
|
+
});
|
|
32
|
+
Button.displayName = "Button";
|
|
33
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
const Card = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("rounded-xl border bg-card text-card-foreground shadow", className), ...props })));
|
|
5
|
+
Card.displayName = "Card";
|
|
6
|
+
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("flex flex-col space-y-1.5 p-6", className), ...props })));
|
|
7
|
+
CardHeader.displayName = "CardHeader";
|
|
8
|
+
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("font-semibold leading-none tracking-tight", className), ...props })));
|
|
9
|
+
CardTitle.displayName = "CardTitle";
|
|
10
|
+
const CardContent = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("p-6 pt-0", className), ...props })));
|
|
11
|
+
CardContent.displayName = "CardContent";
|
|
12
|
+
export { Card, CardHeader, CardTitle, CardContent };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
|
5
|
+
return (_jsx("input", { type: type, className: cn("flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", className), ref: ref, ...props }));
|
|
6
|
+
});
|
|
7
|
+
Input.displayName = "Input";
|
|
8
|
+
export { Input };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
const Table = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { className: "relative w-full overflow-auto", children: _jsx("table", { ref: ref, className: cn("w-full caption-bottom text-sm", className), ...props }) })));
|
|
5
|
+
Table.displayName = "Table";
|
|
6
|
+
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (_jsx("thead", { ref: ref, className: cn("[&_tr]:border-b", className), ...props })));
|
|
7
|
+
TableHeader.displayName = "TableHeader";
|
|
8
|
+
const TableBody = React.forwardRef(({ className, ...props }, ref) => (_jsx("tbody", { ref: ref, className: cn("[&_tr:last-child]:border-0", className), ...props })));
|
|
9
|
+
TableBody.displayName = "TableBody";
|
|
10
|
+
const TableRow = React.forwardRef(({ className, ...props }, ref) => (_jsx("tr", { ref: ref, className: cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className), ...props })));
|
|
11
|
+
TableRow.displayName = "TableRow";
|
|
12
|
+
const TableHead = React.forwardRef(({ className, ...props }, ref) => (_jsx("th", { ref: ref, className: cn("h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className), ...props })));
|
|
13
|
+
TableHead.displayName = "TableHead";
|
|
14
|
+
const TableCell = React.forwardRef(({ className, ...props }, ref) => (_jsx("td", { ref: ref, className: cn("p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className), ...props })));
|
|
15
|
+
TableCell.displayName = "TableCell";
|
|
16
|
+
export { Table, TableHeader, TableBody, TableHead, TableRow, TableCell };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
|
5
|
+
return (_jsx("textarea", { className: cn("flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", className), ref: ref, ...props }));
|
|
6
|
+
});
|
|
7
|
+
Textarea.displayName = "Textarea";
|
|
8
|
+
export { Textarea };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { assignPermissionToRole, createPermission, db, getRoles, permissionsTable, } from "@arch-cadre/core/server";
|
|
2
|
+
import { inArray, sql } from "drizzle-orm";
|
|
3
|
+
import manifest from "../manifest.json" with { type: "json" };
|
|
4
|
+
import BlogStatsWidget from "./components/BlogStatsWidget.js";
|
|
5
|
+
import RecentCommentsWidget from "./components/RecentCommentsWidget.js";
|
|
6
|
+
import RecentPostsWidget from "./components/RecentPostsWidget.js";
|
|
7
|
+
import { navigation } from "./navigation.js";
|
|
8
|
+
import { privateRoutes, publicRoutes } from "./routes.js";
|
|
9
|
+
const BLOG_PERMISSIONS = [
|
|
10
|
+
{ name: "post:create", description: "Allow creating blog posts" },
|
|
11
|
+
{ name: "post:update", description: "Allow updating blog posts" },
|
|
12
|
+
{ name: "post:delete", description: "Allow deleting blog posts" },
|
|
13
|
+
{ name: "comment:create", description: "Allow creating comments" },
|
|
14
|
+
{ name: "comment:update", description: "Allow updating comments" },
|
|
15
|
+
{ name: "comment:delete", description: "Allow deleting comments" },
|
|
16
|
+
];
|
|
17
|
+
// --- Module Definition ---
|
|
18
|
+
const blogModule = {
|
|
19
|
+
manifest: manifest,
|
|
20
|
+
init: async () => {
|
|
21
|
+
console.log("[BlogModule] ready.");
|
|
22
|
+
},
|
|
23
|
+
widgets: [
|
|
24
|
+
{
|
|
25
|
+
id: "blog-stats",
|
|
26
|
+
name: "Blog Stats",
|
|
27
|
+
area: "dashboard-stats",
|
|
28
|
+
component: BlogStatsWidget,
|
|
29
|
+
priority: 20,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "recent-posts",
|
|
33
|
+
name: "Recent Posts",
|
|
34
|
+
area: "dashboard-main",
|
|
35
|
+
component: RecentPostsWidget,
|
|
36
|
+
priority: 20,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "recent-comments",
|
|
40
|
+
name: "Recent Comments",
|
|
41
|
+
area: "dashboard-main",
|
|
42
|
+
component: RecentCommentsWidget,
|
|
43
|
+
priority: 30,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
onEnable: async () => {
|
|
47
|
+
console.log("[BlogModule] enabling and registering permissions...");
|
|
48
|
+
try {
|
|
49
|
+
// 1. Create permissions
|
|
50
|
+
for (const perm of BLOG_PERMISSIONS) {
|
|
51
|
+
await createPermission(perm.name, perm.description);
|
|
52
|
+
}
|
|
53
|
+
// 2. Assign to admin role
|
|
54
|
+
const roles = await getRoles();
|
|
55
|
+
const adminRole = roles.find((r) => r.name === "admin");
|
|
56
|
+
if (adminRole) {
|
|
57
|
+
const blogPermNames = BLOG_PERMISSIONS.map((p) => p.name);
|
|
58
|
+
const blogPerms = await db
|
|
59
|
+
.select()
|
|
60
|
+
.from(permissionsTable)
|
|
61
|
+
.where(inArray(permissionsTable.name, blogPermNames));
|
|
62
|
+
for (const p of blogPerms) {
|
|
63
|
+
await assignPermissionToRole(adminRole.id, p.id);
|
|
64
|
+
}
|
|
65
|
+
console.log("[BlogModule] Permissions assigned to admin role.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error("[BlogModule] Error during permission registration:", error);
|
|
70
|
+
}
|
|
71
|
+
console.log("[BlogModule] enabled.");
|
|
72
|
+
},
|
|
73
|
+
onDisable: async () => {
|
|
74
|
+
console.log("[Blog] onDisable: Cleaning up tables and permissions...");
|
|
75
|
+
try {
|
|
76
|
+
// 1. Remove permissions (cascades to role-permission mappings)
|
|
77
|
+
const blogPermNames = BLOG_PERMISSIONS.map((p) => p.name);
|
|
78
|
+
await db
|
|
79
|
+
.delete(permissionsTable)
|
|
80
|
+
.where(inArray(permissionsTable.name, blogPermNames));
|
|
81
|
+
console.log("[BlogModule] Permissions and mappings removed.");
|
|
82
|
+
// 2. Drop tables
|
|
83
|
+
const tables = ["blog_posts", "blog_comments"];
|
|
84
|
+
for (const table of tables) {
|
|
85
|
+
await db.execute(sql.raw(`DROP TABLE IF EXISTS ${table} CASCADE`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
console.error("[Blog] onDisable Error:", e);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
routes: {
|
|
93
|
+
public: publicRoutes,
|
|
94
|
+
private: privateRoutes,
|
|
95
|
+
},
|
|
96
|
+
navigation: navigation,
|
|
97
|
+
};
|
|
98
|
+
export default blogModule;
|
package/dist/lib/validation.d.ts
CHANGED
|
@@ -5,12 +5,12 @@ export declare const postSchema: z.ZodObject<{
|
|
|
5
5
|
slug: z.ZodString;
|
|
6
6
|
}, "strip", z.ZodTypeAny, {
|
|
7
7
|
title: string;
|
|
8
|
-
content: string;
|
|
9
8
|
slug: string;
|
|
9
|
+
content: string;
|
|
10
10
|
}, {
|
|
11
11
|
title: string;
|
|
12
|
-
content: string;
|
|
13
12
|
slug: string;
|
|
13
|
+
content: string;
|
|
14
14
|
}>;
|
|
15
15
|
export declare const commentSchema: z.ZodObject<{
|
|
16
16
|
content: z.ZodString;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// --- Validation Schemas ---
|
|
3
|
+
export const postSchema = z.object({
|
|
4
|
+
title: z.string().min(3, "Title must be at least 3 characters"),
|
|
5
|
+
content: z.string().min(10, "Content must be at least 10 characters"),
|
|
6
|
+
slug: z.string().min(3, "Slug is required"),
|
|
7
|
+
});
|
|
8
|
+
export const commentSchema = z.object({
|
|
9
|
+
content: z.string().min(2, "Comment is too short"),
|
|
10
|
+
postId: z.string().uuid(),
|
|
11
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { i18n } from "@arch-cadre/intl";
|
|
2
|
+
export const navigation = {
|
|
3
|
+
public: [
|
|
4
|
+
{
|
|
5
|
+
title: i18n("Blog"),
|
|
6
|
+
url: "/blog",
|
|
7
|
+
icon: "solar:pen-2-broken",
|
|
8
|
+
},
|
|
9
|
+
],
|
|
10
|
+
admin: {
|
|
11
|
+
[i18n("CMS")]: [
|
|
12
|
+
{
|
|
13
|
+
title: i18n("Blog Manager"),
|
|
14
|
+
url: "/blog",
|
|
15
|
+
icon: "solar:posts-carousel-vertical-broken",
|
|
16
|
+
roles: ["admin"],
|
|
17
|
+
permissions: ["post:create", "post:update", "post:delete"],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
};
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getCurrentSession } from "@arch-cadre/core/server";
|
|
3
|
+
import { getComments, getPostById, getPostBySlug, getPosts } from "./actions/index.js";
|
|
4
|
+
import { BlogAdminPage, BlogListPage, CreatePostForm, EditPostForm, PostDetailPage, } from "./ui/views.js";
|
|
5
|
+
export const publicRoutes = [
|
|
6
|
+
{
|
|
7
|
+
path: "/blog",
|
|
8
|
+
component: async () => {
|
|
9
|
+
const posts = await getPosts();
|
|
10
|
+
return _jsx(BlogListPage, { posts: posts });
|
|
11
|
+
},
|
|
12
|
+
auth: false,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
path: "/blog/:slug",
|
|
16
|
+
component: async ({ params }) => {
|
|
17
|
+
const { slug } = await params;
|
|
18
|
+
const post = await getPostBySlug(slug);
|
|
19
|
+
const comments = post ? await getComments(post.id) : [];
|
|
20
|
+
const session = await getCurrentSession();
|
|
21
|
+
return (_jsx(PostDetailPage, { post: post, comments: comments, currentUser: session?.user }));
|
|
22
|
+
},
|
|
23
|
+
auth: false,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
export const privateRoutes = [
|
|
27
|
+
{
|
|
28
|
+
path: "/blog",
|
|
29
|
+
component: async () => {
|
|
30
|
+
const posts = await getPosts();
|
|
31
|
+
return _jsx(BlogAdminPage, { posts: posts });
|
|
32
|
+
},
|
|
33
|
+
auth: true,
|
|
34
|
+
roles: ["admin"],
|
|
35
|
+
permissions: ["post:create", "post:update", "post:delete"],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: "/blog/new",
|
|
39
|
+
component: CreatePostForm,
|
|
40
|
+
auth: true,
|
|
41
|
+
roles: ["admin"],
|
|
42
|
+
permissions: ["post:create"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
path: "/blog/edit/:id",
|
|
46
|
+
component: async ({ params }) => {
|
|
47
|
+
const { id } = await params;
|
|
48
|
+
const post = await getPostById(id);
|
|
49
|
+
return _jsx(EditPostForm, { post: post });
|
|
50
|
+
},
|
|
51
|
+
auth: true,
|
|
52
|
+
roles: ["admin"],
|
|
53
|
+
permissions: ["post:update"],
|
|
54
|
+
},
|
|
55
|
+
];
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { userTable } from "@arch-cadre/core";
|
|
2
|
+
import { defineRelations } from "drizzle-orm";
|
|
3
|
+
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
4
|
+
export const postsTable = pgTable("blog_posts", {
|
|
5
|
+
id: text("id")
|
|
6
|
+
.primaryKey()
|
|
7
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
8
|
+
title: text("title").notNull(),
|
|
9
|
+
slug: text("slug").unique().notNull(),
|
|
10
|
+
content: text("content").notNull(),
|
|
11
|
+
authorId: text("author_id")
|
|
12
|
+
.references(() => userTable.id, { onDelete: "cascade" })
|
|
13
|
+
.notNull(),
|
|
14
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
15
|
+
});
|
|
16
|
+
export const commentsTable = pgTable("blog_comments", {
|
|
17
|
+
id: text("id")
|
|
18
|
+
.primaryKey()
|
|
19
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
20
|
+
postId: text("post_id")
|
|
21
|
+
.references(() => postsTable.id, { onDelete: "cascade" })
|
|
22
|
+
.notNull(),
|
|
23
|
+
authorId: text("author_id")
|
|
24
|
+
.references(() => userTable.id, { onDelete: "cascade" })
|
|
25
|
+
.notNull(),
|
|
26
|
+
content: text("content").notNull(),
|
|
27
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
28
|
+
});
|
|
29
|
+
export const blogSchema = {
|
|
30
|
+
postsTable,
|
|
31
|
+
commentsTable,
|
|
32
|
+
};
|
|
33
|
+
export const relations = defineRelations({
|
|
34
|
+
user: userTable,
|
|
35
|
+
post: postsTable,
|
|
36
|
+
comment: commentsTable,
|
|
37
|
+
}, (r) => ({
|
|
38
|
+
user: {
|
|
39
|
+
posts: r.many.post({
|
|
40
|
+
from: r.user.id,
|
|
41
|
+
to: r.post.authorId,
|
|
42
|
+
}),
|
|
43
|
+
comments: r.many.comment({
|
|
44
|
+
from: r.user.id,
|
|
45
|
+
to: r.comment.authorId,
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
post: {
|
|
49
|
+
comments: r.many.comment({
|
|
50
|
+
from: r.post.id,
|
|
51
|
+
to: r.comment.postId,
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
comment: {
|
|
55
|
+
post: r.one.post({
|
|
56
|
+
from: r.comment.postId,
|
|
57
|
+
to: r.post.id,
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
}));
|