@navneet_25/tempjs 2.0.0 → 3.0.0
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 +66 -18
- package/cli/brand-manager.js +183 -96
- package/cli/config.js +9 -1
- package/cli/doctor.js +393 -0
- package/cli/fetch-core-modules.js +130 -0
- package/cli/index.js +141 -10
- package/cli/info.js +28 -1
- package/cli/init-options.js +209 -0
- package/cli/module-manager.js +168 -0
- package/cli/parse-args.js +21 -0
- package/cli/update.js +15 -0
- package/cli/version-manager.js +518 -0
- package/package.json +27 -4
- package/packages/core/modules/README.md +72 -0
- package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
- package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
- package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
- package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
- package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
- package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
- package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
- package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
- package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
- package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
- package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
- package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
- package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
- package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
- package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
- package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
- package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
- package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
- package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
- package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
- package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
- package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
- package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
- package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
- package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
- package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
- package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
- package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
- package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
- package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
- package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
- package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
- package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
- package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
- package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
- package/packages/core/modules/seo/app/robots.ts +30 -0
- package/packages/core/modules/seo/app/sitemap.ts +9 -0
- package/packages/core/modules/seo/lib/seo/index.ts +5 -0
- package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
- package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
- package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
- package/packages/core/modules/seo/lib/seo/types.ts +30 -0
- package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
- package/packages/core/modules.json +140 -0
- package/packages/core/prisma/schema.prisma +33 -0
- package/scripts/module-installer-core.mjs +657 -0
- package/scripts/template-modules-admin.mjs +366 -0
- package/scripts/template-modules-home.mjs +120 -0
- package/templates.json +163 -8
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ComposeBlock, ComposeDocument } from "./types";
|
|
2
|
+
import { ComposeHeading, ComposeExplain, ComposeParagraph, ComposeList, ComposeQuote } from "./blocks/text-blocks";
|
|
3
|
+
import {
|
|
4
|
+
ComposeCode,
|
|
5
|
+
ComposeCommand,
|
|
6
|
+
ComposeCallout,
|
|
7
|
+
ComposeDivider,
|
|
8
|
+
ComposeImage,
|
|
9
|
+
} from "./blocks/media-blocks";
|
|
10
|
+
|
|
11
|
+
export function ComposeBlockView({ block }: { block: ComposeBlock }) {
|
|
12
|
+
switch (block.type) {
|
|
13
|
+
case "heading":
|
|
14
|
+
return <ComposeHeading block={block} />;
|
|
15
|
+
case "explain":
|
|
16
|
+
return <ComposeExplain block={block} />;
|
|
17
|
+
case "paragraph":
|
|
18
|
+
return <ComposeParagraph block={block} />;
|
|
19
|
+
case "code":
|
|
20
|
+
return <ComposeCode block={block} />;
|
|
21
|
+
case "command":
|
|
22
|
+
return <ComposeCommand block={block} />;
|
|
23
|
+
case "callout":
|
|
24
|
+
return <ComposeCallout block={block} />;
|
|
25
|
+
case "divider":
|
|
26
|
+
return <ComposeDivider />;
|
|
27
|
+
case "list":
|
|
28
|
+
return <ComposeList block={block} />;
|
|
29
|
+
case "quote":
|
|
30
|
+
return <ComposeQuote block={block} />;
|
|
31
|
+
case "image":
|
|
32
|
+
return <ComposeImage block={block} />;
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Interpreter: renders a Compose document as React components.
|
|
40
|
+
*/
|
|
41
|
+
export function ComposeBlogRenderer({
|
|
42
|
+
document,
|
|
43
|
+
className,
|
|
44
|
+
}: {
|
|
45
|
+
document: ComposeDocument;
|
|
46
|
+
className?: string;
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<article className={className ?? "compose-blog max-w-3xl"}>
|
|
50
|
+
{document.blocks.map((block, index) => (
|
|
51
|
+
<ComposeBlockView key={index} block={block} />
|
|
52
|
+
))}
|
|
53
|
+
</article>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComposeBlock,
|
|
3
|
+
ComposeBlockType,
|
|
4
|
+
ComposeCalloutBlock,
|
|
5
|
+
ComposeCodeBlock,
|
|
6
|
+
ComposeCommandBlock,
|
|
7
|
+
ComposeDividerBlock,
|
|
8
|
+
ComposeExplainBlock,
|
|
9
|
+
ComposeHeadingBlock,
|
|
10
|
+
ComposeImageBlock,
|
|
11
|
+
ComposeListBlock,
|
|
12
|
+
ComposeParagraphBlock,
|
|
13
|
+
ComposeQuoteBlock,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
export type ComposeFieldInput = "text" | "textarea" | "select" | "toggle" | "tags";
|
|
17
|
+
|
|
18
|
+
export interface ComposeFieldDef {
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
input: ComposeFieldInput;
|
|
23
|
+
options?: { value: string; label: string }[];
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ComposeBlockDefinition {
|
|
29
|
+
type: ComposeBlockType;
|
|
30
|
+
label: string;
|
|
31
|
+
description: string;
|
|
32
|
+
icon: string;
|
|
33
|
+
createDefault: () => ComposeBlock;
|
|
34
|
+
fields: ComposeFieldDef[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const COMPOSE_BLOCK_REGISTRY: Record<ComposeBlockType, ComposeBlockDefinition> = {
|
|
38
|
+
heading: {
|
|
39
|
+
type: "heading",
|
|
40
|
+
label: "Heading",
|
|
41
|
+
description: "Title, section header, or subheader with style variants.",
|
|
42
|
+
icon: "H",
|
|
43
|
+
createDefault: (): ComposeHeadingBlock => ({
|
|
44
|
+
type: "heading",
|
|
45
|
+
level: "h2",
|
|
46
|
+
variant: "default",
|
|
47
|
+
text: "",
|
|
48
|
+
}),
|
|
49
|
+
fields: [
|
|
50
|
+
{ key: "text", label: "Text", input: "text", required: true, placeholder: "Section heading" },
|
|
51
|
+
{
|
|
52
|
+
key: "level",
|
|
53
|
+
label: "Level",
|
|
54
|
+
input: "select",
|
|
55
|
+
options: [
|
|
56
|
+
{ value: "title", label: "Page title" },
|
|
57
|
+
{ value: "h2", label: "Section (H2)" },
|
|
58
|
+
{ value: "h3", label: "Subsection (H3)" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "variant",
|
|
63
|
+
label: "Design",
|
|
64
|
+
input: "select",
|
|
65
|
+
options: [
|
|
66
|
+
{ value: "default", label: "Default" },
|
|
67
|
+
{ value: "accent", label: "Accent color" },
|
|
68
|
+
{ value: "serif", label: "Serif display" },
|
|
69
|
+
{ value: "underline-bar", label: "Underline bar" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
explain: {
|
|
75
|
+
type: "explain",
|
|
76
|
+
label: "Explanation",
|
|
77
|
+
description: "Rich text with optional highlight and underline phrases.",
|
|
78
|
+
icon: "¶",
|
|
79
|
+
createDefault: (): ComposeExplainBlock => ({
|
|
80
|
+
type: "explain",
|
|
81
|
+
text: "",
|
|
82
|
+
variant: "default",
|
|
83
|
+
highlight: [],
|
|
84
|
+
underline: [],
|
|
85
|
+
}),
|
|
86
|
+
fields: [
|
|
87
|
+
{
|
|
88
|
+
key: "text",
|
|
89
|
+
label: "Body text",
|
|
90
|
+
input: "textarea",
|
|
91
|
+
required: true,
|
|
92
|
+
placeholder: "Write your explanation…",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "variant",
|
|
96
|
+
label: "Style",
|
|
97
|
+
input: "select",
|
|
98
|
+
options: [
|
|
99
|
+
{ value: "default", label: "Default" },
|
|
100
|
+
{ value: "lead", label: "Lead paragraph" },
|
|
101
|
+
{ value: "muted", label: "Muted" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "highlight",
|
|
106
|
+
label: "Highlight phrases",
|
|
107
|
+
input: "tags",
|
|
108
|
+
description: "Exact substrings in the text to highlight (comma-separated).",
|
|
109
|
+
placeholder: "important term, keyword",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: "underline",
|
|
113
|
+
label: "Underline phrases",
|
|
114
|
+
input: "tags",
|
|
115
|
+
description: "Exact substrings to underline.",
|
|
116
|
+
placeholder: "term to underline",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
paragraph: {
|
|
121
|
+
type: "paragraph",
|
|
122
|
+
label: "Paragraph",
|
|
123
|
+
description: "Simple body paragraph.",
|
|
124
|
+
icon: "P",
|
|
125
|
+
createDefault: (): ComposeParagraphBlock => ({ type: "paragraph", text: "" }),
|
|
126
|
+
fields: [
|
|
127
|
+
{ key: "text", label: "Text", input: "textarea", required: true },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
code: {
|
|
131
|
+
type: "code",
|
|
132
|
+
label: "Code block",
|
|
133
|
+
description: "Syntax-friendly code snippet.",
|
|
134
|
+
icon: "</>",
|
|
135
|
+
createDefault: (): ComposeCodeBlock => ({
|
|
136
|
+
type: "code",
|
|
137
|
+
code: "",
|
|
138
|
+
language: "typescript",
|
|
139
|
+
showLineNumbers: false,
|
|
140
|
+
}),
|
|
141
|
+
fields: [
|
|
142
|
+
{ key: "code", label: "Code", input: "textarea", required: true },
|
|
143
|
+
{
|
|
144
|
+
key: "language",
|
|
145
|
+
label: "Language",
|
|
146
|
+
input: "select",
|
|
147
|
+
options: [
|
|
148
|
+
{ value: "typescript", label: "TypeScript" },
|
|
149
|
+
{ value: "javascript", label: "JavaScript" },
|
|
150
|
+
{ value: "bash", label: "Bash" },
|
|
151
|
+
{ value: "json", label: "JSON" },
|
|
152
|
+
{ value: "text", label: "Plain text" },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{ key: "showLineNumbers", label: "Show line numbers", input: "toggle" },
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
command: {
|
|
159
|
+
type: "command",
|
|
160
|
+
label: "Command",
|
|
161
|
+
description: "Terminal command with copy-friendly styling (like docsite).",
|
|
162
|
+
icon: "$",
|
|
163
|
+
createDefault: (): ComposeCommandBlock => ({
|
|
164
|
+
type: "command",
|
|
165
|
+
command: "",
|
|
166
|
+
title: "",
|
|
167
|
+
description: "",
|
|
168
|
+
}),
|
|
169
|
+
fields: [
|
|
170
|
+
{ key: "command", label: "Command", input: "textarea", required: true, placeholder: "pnpm dev" },
|
|
171
|
+
{ key: "title", label: "Title (optional)", input: "text" },
|
|
172
|
+
{ key: "description", label: "Description (optional)", input: "textarea" },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
callout: {
|
|
176
|
+
type: "callout",
|
|
177
|
+
label: "Callout",
|
|
178
|
+
description: "Info, tip, or warning box.",
|
|
179
|
+
icon: "!",
|
|
180
|
+
createDefault: (): ComposeCalloutBlock => ({
|
|
181
|
+
type: "callout",
|
|
182
|
+
variant: "info",
|
|
183
|
+
title: "",
|
|
184
|
+
text: "",
|
|
185
|
+
}),
|
|
186
|
+
fields: [
|
|
187
|
+
{
|
|
188
|
+
key: "variant",
|
|
189
|
+
label: "Variant",
|
|
190
|
+
input: "select",
|
|
191
|
+
options: [
|
|
192
|
+
{ value: "info", label: "Info" },
|
|
193
|
+
{ value: "tip", label: "Tip" },
|
|
194
|
+
{ value: "warning", label: "Warning" },
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
{ key: "title", label: "Title", input: "text" },
|
|
198
|
+
{ key: "text", label: "Body", input: "textarea", required: true },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
divider: {
|
|
202
|
+
type: "divider",
|
|
203
|
+
label: "Divider",
|
|
204
|
+
description: "Visual separator between sections.",
|
|
205
|
+
icon: "—",
|
|
206
|
+
createDefault: (): ComposeDividerBlock => ({ type: "divider" }),
|
|
207
|
+
fields: [],
|
|
208
|
+
},
|
|
209
|
+
list: {
|
|
210
|
+
type: "list",
|
|
211
|
+
label: "List",
|
|
212
|
+
description: "Bulleted or numbered list of items.",
|
|
213
|
+
icon: "•",
|
|
214
|
+
createDefault: (): ComposeListBlock => ({
|
|
215
|
+
type: "list",
|
|
216
|
+
style: "bullet",
|
|
217
|
+
items: [],
|
|
218
|
+
}),
|
|
219
|
+
fields: [
|
|
220
|
+
{
|
|
221
|
+
key: "style",
|
|
222
|
+
label: "Style",
|
|
223
|
+
input: "select",
|
|
224
|
+
options: [
|
|
225
|
+
{ value: "bullet", label: "Bulleted" },
|
|
226
|
+
{ value: "number", label: "Numbered" },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "items",
|
|
231
|
+
label: "Items",
|
|
232
|
+
input: "tags",
|
|
233
|
+
description: "One item per comma-separated entry.",
|
|
234
|
+
placeholder: "First point, Second point",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
quote: {
|
|
239
|
+
type: "quote",
|
|
240
|
+
label: "Quote",
|
|
241
|
+
description: "Blockquote with optional attribution.",
|
|
242
|
+
icon: "❝",
|
|
243
|
+
createDefault: (): ComposeQuoteBlock => ({
|
|
244
|
+
type: "quote",
|
|
245
|
+
text: "",
|
|
246
|
+
attribution: "",
|
|
247
|
+
}),
|
|
248
|
+
fields: [
|
|
249
|
+
{ key: "text", label: "Quote text", input: "textarea", required: true },
|
|
250
|
+
{ key: "attribution", label: "Attribution (optional)", input: "text", placeholder: "Author name" },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
image: {
|
|
254
|
+
type: "image",
|
|
255
|
+
label: "Image",
|
|
256
|
+
description: "Image with alt text and optional caption.",
|
|
257
|
+
icon: "🖼",
|
|
258
|
+
createDefault: (): ComposeImageBlock => ({
|
|
259
|
+
type: "image",
|
|
260
|
+
src: "",
|
|
261
|
+
alt: "",
|
|
262
|
+
caption: "",
|
|
263
|
+
}),
|
|
264
|
+
fields: [
|
|
265
|
+
{ key: "src", label: "Image URL or path", input: "text", required: true, placeholder: "/assets/photo.jpg" },
|
|
266
|
+
{ key: "alt", label: "Alt text", input: "text", required: true },
|
|
267
|
+
{ key: "caption", label: "Caption (optional)", input: "text" },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export const COMPOSE_BLOCK_TYPES = Object.keys(COMPOSE_BLOCK_REGISTRY) as ComposeBlockType[];
|
|
273
|
+
|
|
274
|
+
export function getComposeBlockDefinition(type: ComposeBlockType): ComposeBlockDefinition {
|
|
275
|
+
return COMPOSE_BLOCK_REGISTRY[type];
|
|
276
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Blog Engine — declarative JSON document types.
|
|
3
|
+
* First engine variant: block-based composition (not WYSIWYG).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const COMPOSE_ENGINE_ID = "compose" as const;
|
|
7
|
+
|
|
8
|
+
export type ComposeHeadingLevel = "title" | "h2" | "h3";
|
|
9
|
+
export type ComposeHeadingVariant = "default" | "accent" | "serif" | "underline-bar";
|
|
10
|
+
|
|
11
|
+
export type ComposeExplainVariant = "default" | "lead" | "muted";
|
|
12
|
+
|
|
13
|
+
export type ComposeCalloutVariant = "info" | "tip" | "warning";
|
|
14
|
+
|
|
15
|
+
export type ComposeListStyle = "bullet" | "number";
|
|
16
|
+
|
|
17
|
+
export interface ComposeHeadingBlock {
|
|
18
|
+
type: "heading";
|
|
19
|
+
level: ComposeHeadingLevel;
|
|
20
|
+
variant: ComposeHeadingVariant;
|
|
21
|
+
text: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ComposeExplainBlock {
|
|
25
|
+
type: "explain";
|
|
26
|
+
text: string;
|
|
27
|
+
variant?: ComposeExplainVariant;
|
|
28
|
+
/** Substrings in `text` rendered with highlight styling */
|
|
29
|
+
highlight?: string[];
|
|
30
|
+
/** Substrings in `text` rendered with underline styling */
|
|
31
|
+
underline?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ComposeParagraphBlock {
|
|
35
|
+
type: "paragraph";
|
|
36
|
+
text: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ComposeCodeBlock {
|
|
40
|
+
type: "code";
|
|
41
|
+
code: string;
|
|
42
|
+
language?: string;
|
|
43
|
+
showLineNumbers?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ComposeCommandBlock {
|
|
47
|
+
type: "command";
|
|
48
|
+
command: string;
|
|
49
|
+
title?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ComposeCalloutBlock {
|
|
54
|
+
type: "callout";
|
|
55
|
+
variant: ComposeCalloutVariant;
|
|
56
|
+
title?: string;
|
|
57
|
+
text: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ComposeDividerBlock {
|
|
61
|
+
type: "divider";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ComposeListBlock {
|
|
65
|
+
type: "list";
|
|
66
|
+
style: ComposeListStyle;
|
|
67
|
+
items: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ComposeQuoteBlock {
|
|
71
|
+
type: "quote";
|
|
72
|
+
text: string;
|
|
73
|
+
attribution?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ComposeImageBlock {
|
|
77
|
+
type: "image";
|
|
78
|
+
src: string;
|
|
79
|
+
alt: string;
|
|
80
|
+
caption?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ComposeBlock =
|
|
84
|
+
| ComposeHeadingBlock
|
|
85
|
+
| ComposeExplainBlock
|
|
86
|
+
| ComposeParagraphBlock
|
|
87
|
+
| ComposeCodeBlock
|
|
88
|
+
| ComposeCommandBlock
|
|
89
|
+
| ComposeCalloutBlock
|
|
90
|
+
| ComposeDividerBlock
|
|
91
|
+
| ComposeListBlock
|
|
92
|
+
| ComposeQuoteBlock
|
|
93
|
+
| ComposeImageBlock;
|
|
94
|
+
|
|
95
|
+
export type ComposeBlockType = ComposeBlock["type"];
|
|
96
|
+
|
|
97
|
+
export interface ComposeDocument {
|
|
98
|
+
engine: typeof COMPOSE_ENGINE_ID;
|
|
99
|
+
version: 1;
|
|
100
|
+
blocks: ComposeBlock[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface BlogPostMeta {
|
|
104
|
+
id: string;
|
|
105
|
+
slug: string;
|
|
106
|
+
title: string;
|
|
107
|
+
excerpt?: string | null;
|
|
108
|
+
coverImage?: string | null;
|
|
109
|
+
engine: string;
|
|
110
|
+
published: boolean;
|
|
111
|
+
publishedAt?: string | null;
|
|
112
|
+
createdAt: string;
|
|
113
|
+
updatedAt: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface BlogPostWithContent extends BlogPostMeta {
|
|
117
|
+
content: ComposeDocument;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createEmptyComposeDocument(): ComposeDocument {
|
|
121
|
+
return { engine: COMPOSE_ENGINE_ID, version: 1, blocks: [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isComposeDocument(value: unknown): value is ComposeDocument {
|
|
125
|
+
if (!value || typeof value !== "object") return false;
|
|
126
|
+
const doc = value as ComposeDocument;
|
|
127
|
+
return doc.engine === COMPOSE_ENGINE_ID && doc.version === 1 && Array.isArray(doc.blocks);
|
|
128
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerDynamicSitemapProvider } from "@/lib/seo/sitemap";
|
|
2
|
+
import type { SitemapPathConfig } from "@/lib/seo/types";
|
|
3
|
+
import { blogComposeService } from "@/lib/features/blog-compose";
|
|
4
|
+
|
|
5
|
+
registerDynamicSitemapProvider(async (): Promise<SitemapPathConfig[]> => {
|
|
6
|
+
try {
|
|
7
|
+
const posts = await blogComposeService.listPublic();
|
|
8
|
+
return posts.map((post) => ({
|
|
9
|
+
path: `/blog/${post.slug}`,
|
|
10
|
+
changeFrequency: "weekly",
|
|
11
|
+
priority: 0.7,
|
|
12
|
+
lastModified: post.publishedAt ?? post.updatedAt,
|
|
13
|
+
}));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("[sitemap] failed to list blog posts:", error);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
});
|
package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { blogComposeService, BlogComposeService } from "./blog-compose.service";
|
|
3
|
+
|
|
4
|
+
export class BlogComposeController {
|
|
5
|
+
private service: BlogComposeService;
|
|
6
|
+
|
|
7
|
+
constructor(service: BlogComposeService = blogComposeService) {
|
|
8
|
+
this.service = service;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async listPublic(): Promise<NextResponse> {
|
|
12
|
+
try {
|
|
13
|
+
const data = await this.service.listPublic();
|
|
14
|
+
return NextResponse.json({ success: true, data }, { status: 200 });
|
|
15
|
+
} catch (error: unknown) {
|
|
16
|
+
const message = error instanceof Error ? error.message : "Failed to fetch blog posts";
|
|
17
|
+
return NextResponse.json({ success: false, error: message }, { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async listAll(): Promise<NextResponse> {
|
|
22
|
+
try {
|
|
23
|
+
const data = await this.service.listAll();
|
|
24
|
+
return NextResponse.json({ success: true, data }, { status: 200 });
|
|
25
|
+
} catch (error: unknown) {
|
|
26
|
+
const message = error instanceof Error ? error.message : "Failed to fetch blog posts";
|
|
27
|
+
return NextResponse.json({ success: false, error: message }, { status: 500 });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async create(req: Request): Promise<NextResponse> {
|
|
32
|
+
try {
|
|
33
|
+
const body = await req.json();
|
|
34
|
+
const item = await this.service.create({
|
|
35
|
+
title: body.title,
|
|
36
|
+
slug: body.slug,
|
|
37
|
+
excerpt: body.excerpt,
|
|
38
|
+
coverImage: body.coverImage,
|
|
39
|
+
content: body.content,
|
|
40
|
+
published: body.published === true,
|
|
41
|
+
});
|
|
42
|
+
return NextResponse.json({ success: true, data: item }, { status: 201 });
|
|
43
|
+
} catch (error: unknown) {
|
|
44
|
+
const message = error instanceof Error ? error.message : "Failed to create blog post";
|
|
45
|
+
return NextResponse.json({ success: false, error: message }, { status: 400 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async update(req: Request, params: { id: string }): Promise<NextResponse> {
|
|
50
|
+
try {
|
|
51
|
+
const body = await req.json();
|
|
52
|
+
const item = await this.service.update(params.id, {
|
|
53
|
+
title: body.title,
|
|
54
|
+
slug: body.slug,
|
|
55
|
+
excerpt: body.excerpt,
|
|
56
|
+
coverImage: body.coverImage,
|
|
57
|
+
content: body.content,
|
|
58
|
+
published: body.published,
|
|
59
|
+
});
|
|
60
|
+
return NextResponse.json({ success: true, data: item }, { status: 200 });
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
const message = error instanceof Error ? error.message : "Failed to update blog post";
|
|
63
|
+
return NextResponse.json({ success: false, error: message }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async delete(params: { id: string }): Promise<NextResponse> {
|
|
68
|
+
try {
|
|
69
|
+
const item = await this.service.delete(params.id);
|
|
70
|
+
return NextResponse.json({ success: true, data: item }, { status: 200 });
|
|
71
|
+
} catch (error: unknown) {
|
|
72
|
+
const message = error instanceof Error ? error.message : "Failed to delete blog post";
|
|
73
|
+
return NextResponse.json({ success: false, error: message }, { status: 400 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const blogComposeController = new BlogComposeController();
|
package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { BlogPost, Prisma } from "@prisma/client";
|
|
2
|
+
import { prisma } from "@/lib/db";
|
|
3
|
+
import type { BlogPostInput, BlogPostUpdate } from "./blog-compose.types";
|
|
4
|
+
|
|
5
|
+
export class BlogComposeRepository {
|
|
6
|
+
async listPublished(): Promise<BlogPost[]> {
|
|
7
|
+
return prisma.blogPost.findMany({
|
|
8
|
+
where: { published: true },
|
|
9
|
+
orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }],
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async listAll(): Promise<BlogPost[]> {
|
|
14
|
+
return prisma.blogPost.findMany({
|
|
15
|
+
orderBy: [{ updatedAt: "desc" }],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getById(id: string): Promise<BlogPost | null> {
|
|
20
|
+
return prisma.blogPost.findUnique({ where: { id } });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async getBySlug(slug: string, publishedOnly = true): Promise<BlogPost | null> {
|
|
24
|
+
return prisma.blogPost.findFirst({
|
|
25
|
+
where: publishedOnly ? { slug, published: true } : { slug },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async create(data: BlogPostInput & { slug: string }): Promise<BlogPost> {
|
|
30
|
+
const now = data.published ? new Date() : null;
|
|
31
|
+
return prisma.blogPost.create({
|
|
32
|
+
data: {
|
|
33
|
+
title: data.title,
|
|
34
|
+
slug: data.slug,
|
|
35
|
+
excerpt: data.excerpt,
|
|
36
|
+
coverImage: data.coverImage,
|
|
37
|
+
engine: data.engine ?? "compose",
|
|
38
|
+
contentJson: data.content as unknown as Prisma.InputJsonValue,
|
|
39
|
+
published: data.published ?? false,
|
|
40
|
+
publishedAt: now,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async update(id: string, data: BlogPostUpdate & { slug?: string }): Promise<BlogPost> {
|
|
46
|
+
const existing = await this.getById(id);
|
|
47
|
+
if (!existing) throw new Error("Blog post not found.");
|
|
48
|
+
|
|
49
|
+
let publishedAt = existing.publishedAt;
|
|
50
|
+
if (data.published === true && !existing.published) {
|
|
51
|
+
publishedAt = new Date();
|
|
52
|
+
}
|
|
53
|
+
if (data.published === false) {
|
|
54
|
+
publishedAt = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const patch: Prisma.BlogPostUpdateInput = {
|
|
58
|
+
title: data.title,
|
|
59
|
+
slug: data.slug,
|
|
60
|
+
excerpt: data.excerpt,
|
|
61
|
+
coverImage: data.coverImage,
|
|
62
|
+
published: data.published,
|
|
63
|
+
publishedAt,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (data.content) {
|
|
67
|
+
patch.contentJson = data.content as unknown as Prisma.InputJsonValue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return prisma.blogPost.update({ where: { id }, data: patch });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async delete(id: string): Promise<BlogPost> {
|
|
74
|
+
return prisma.blogPost.delete({ where: { id } });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const blogComposeRepository = new BlogComposeRepository();
|