@estation/create-cms-site 1.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/list-sections.d.ts +19 -0
- package/dist/tools/list-sections.d.ts.map +1 -0
- package/dist/tools/list-sections.js +152 -0
- package/dist/tools/list-sections.js.map +1 -0
- package/dist/tools/scaffold-project.d.ts +11 -0
- package/dist/tools/scaffold-project.d.ts.map +1 -0
- package/dist/tools/scaffold-project.js +90 -0
- package/dist/tools/scaffold-project.js.map +1 -0
- package/dist/tools/validate-config.d.ts +9 -0
- package/dist/tools/validate-config.d.ts.map +1 -0
- package/dist/tools/validate-config.js +72 -0
- package/dist/tools/validate-config.js.map +1 -0
- package/dist/utils/helpers.d.ts +2 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +5 -0
- package/dist/utils/helpers.js.map +1 -0
- package/package.json +36 -0
- package/template/.env.example +9 -0
- package/template/LIVE-PREVIEW.md +267 -0
- package/template/README.md +210 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +24 -0
- package/template/postcss.config.mjs +8 -0
- package/template/src/app/[slug]/page.tsx +38 -0
- package/template/src/app/api/revalidate/route.ts +34 -0
- package/template/src/app/blog/[slug]/page.tsx +60 -0
- package/template/src/app/blog/page.tsx +68 -0
- package/template/src/app/collections/[uuid]/page.tsx +28 -0
- package/template/src/app/events/[slug]/page.tsx +62 -0
- package/template/src/app/events/page.tsx +70 -0
- package/template/src/app/layout.tsx +27 -0
- package/template/src/app/news/[slug]/page.tsx +66 -0
- package/template/src/app/news/page.tsx +68 -0
- package/template/src/app/page.tsx +40 -0
- package/template/src/app/search/page.tsx +136 -0
- package/template/src/app/sitemap.ts +48 -0
- package/template/src/components/Footer.tsx +47 -0
- package/template/src/components/Navigation.tsx +65 -0
- package/template/src/components/SectionRenderer.tsx +43 -0
- package/template/src/components/cms-preview-listener.tsx +114 -0
- package/template/src/components/sections/CTASection.tsx +32 -0
- package/template/src/components/sections/ContactSection.tsx +74 -0
- package/template/src/components/sections/FAQSection.tsx +48 -0
- package/template/src/components/sections/FeaturesSection.tsx +42 -0
- package/template/src/components/sections/GallerySection.tsx +44 -0
- package/template/src/components/sections/GenericSection.tsx +63 -0
- package/template/src/components/sections/HeroSection.tsx +27 -0
- package/template/src/components/sections/SliderSection.tsx +66 -0
- package/template/src/components/sections/TestimonialsSection.tsx +52 -0
- package/template/src/components/sections/TextSection.tsx +31 -0
- package/template/src/lib/cms-api.ts +103 -0
- package/template/src/lib/content-helpers.ts +18 -0
- package/template/src/lib/types.ts +109 -0
- package/template/src/styles/globals.css +1 -0
- package/template/tsconfig.json +23 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SectionProps } from "@/lib/types";
|
|
2
|
+
import { str } from "@/lib/types";
|
|
3
|
+
|
|
4
|
+
export function TestimonialsSection({ block }: SectionProps) {
|
|
5
|
+
const c = block.content;
|
|
6
|
+
const title = str(c.title);
|
|
7
|
+
|
|
8
|
+
const testimonials: { name: string; quote: string; role?: string }[] = [];
|
|
9
|
+
if (Array.isArray(c.items?.fieldValue)) {
|
|
10
|
+
for (const item of c.items.fieldValue as Record<string, unknown>[]) {
|
|
11
|
+
const name = (item.name as { fieldValue?: string })?.fieldValue || (item.name as string) || "";
|
|
12
|
+
const quote = (item.quote as { fieldValue?: string })?.fieldValue || (item.quote as string) || "";
|
|
13
|
+
const role = (item.role as { fieldValue?: string })?.fieldValue || (item.role as string) || "";
|
|
14
|
+
testimonials.push({ name, quote, role });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Fallback: extract numbered testimonials from flat fields
|
|
19
|
+
if (testimonials.length === 0) {
|
|
20
|
+
let i = 1;
|
|
21
|
+
while (str(c[`testimonial-${i}-title`]) || str(c[`testimonial-${i}-description`])) {
|
|
22
|
+
testimonials.push({
|
|
23
|
+
name: str(c[`testimonial-${i}-title`]),
|
|
24
|
+
quote: str(c[`testimonial-${i}-description`]),
|
|
25
|
+
});
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="py-16 px-6 bg-gray-50">
|
|
32
|
+
<div className="max-w-5xl mx-auto">
|
|
33
|
+
{title && (
|
|
34
|
+
<h2 data-cms-field="title" className="text-3xl font-bold text-center mb-12">
|
|
35
|
+
{title}
|
|
36
|
+
</h2>
|
|
37
|
+
)}
|
|
38
|
+
<div data-cms-field="items" className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
39
|
+
{testimonials.map((t, i) => (
|
|
40
|
+
<blockquote key={i} className="p-6 bg-white rounded-lg border border-gray-200">
|
|
41
|
+
<p className="text-gray-600 italic mb-4">“{t.quote}”</p>
|
|
42
|
+
<footer className="font-medium">
|
|
43
|
+
{t.name}
|
|
44
|
+
{t.role && <span className="text-gray-500 text-sm block">{t.role}</span>}
|
|
45
|
+
</footer>
|
|
46
|
+
</blockquote>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SectionProps } from "@/lib/types";
|
|
2
|
+
import { str } from "@/lib/types";
|
|
3
|
+
|
|
4
|
+
export function TextSection({ block }: SectionProps) {
|
|
5
|
+
const c = block.content;
|
|
6
|
+
const title = str(c.title);
|
|
7
|
+
const subtitle = str(c.subtitle);
|
|
8
|
+
const body = str(c.body);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="py-16 px-6 max-w-3xl mx-auto">
|
|
12
|
+
{title && (
|
|
13
|
+
<h2 data-cms-field="title" className="text-3xl font-bold mb-4">
|
|
14
|
+
{title}
|
|
15
|
+
</h2>
|
|
16
|
+
)}
|
|
17
|
+
{subtitle && (
|
|
18
|
+
<p data-cms-field="subtitle" className="text-lg text-gray-600 mb-6">
|
|
19
|
+
{subtitle}
|
|
20
|
+
</p>
|
|
21
|
+
)}
|
|
22
|
+
{body && (
|
|
23
|
+
<div
|
|
24
|
+
data-cms-field="body"
|
|
25
|
+
className="prose prose-gray max-w-none"
|
|
26
|
+
dangerouslySetInnerHTML={{ __html: body }}
|
|
27
|
+
/>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PageWithBlocksResponse,
|
|
3
|
+
ContentBlock,
|
|
4
|
+
PageComposition,
|
|
5
|
+
PaginatedResponse,
|
|
6
|
+
Collection,
|
|
7
|
+
SearchResultItem,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
const CMS_API_URL = process.env.CMS_API_URL || "";
|
|
11
|
+
const CMS_API_TOKEN = process.env.CMS_API_TOKEN || "";
|
|
12
|
+
|
|
13
|
+
interface ApiResponse<T> {
|
|
14
|
+
status: string;
|
|
15
|
+
message: string;
|
|
16
|
+
data: T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function cmsFetch<T>(path: string, tags?: string[]): Promise<T> {
|
|
20
|
+
const url = `${CMS_API_URL}${path}`;
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
headers: {
|
|
25
|
+
"X-API-TOKEN": CMS_API_TOKEN,
|
|
26
|
+
},
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
next: {
|
|
29
|
+
revalidate: 60,
|
|
30
|
+
tags: tags || [],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`CMS API error: ${res.status} ${res.statusText} for ${path}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const json: ApiResponse<T> = await res.json();
|
|
40
|
+
return json.data;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getPageBySlug(slug: string): Promise<PageWithBlocksResponse> {
|
|
44
|
+
return cmsFetch<PageWithBlocksResponse>(
|
|
45
|
+
`/public/content/pages/slug/${encodeURIComponent(slug)}`,
|
|
46
|
+
[`page-${slug}`]
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getBlocksByTags(tags: string[]): Promise<ContentBlock[]> {
|
|
51
|
+
const query = tags.map(encodeURIComponent).join(",");
|
|
52
|
+
return cmsFetch<ContentBlock[]>(
|
|
53
|
+
`/public/content/blocks/by-tags?tags=${query}`,
|
|
54
|
+
tags.map((t) => `tag-${t}`)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getBlocksByTagPaginated(
|
|
59
|
+
tag: string,
|
|
60
|
+
page = 1,
|
|
61
|
+
size = 20
|
|
62
|
+
): Promise<PaginatedResponse<ContentBlock>> {
|
|
63
|
+
return cmsFetch<PaginatedResponse<ContentBlock>>(
|
|
64
|
+
`/public/content/blocks/by-tags?tags=${encodeURIComponent(tag)}&page=${page}&size=${size}`,
|
|
65
|
+
[`tag-${tag}`]
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function getBlockByUUID(uuid: string): Promise<ContentBlock> {
|
|
70
|
+
return cmsFetch<ContentBlock>(
|
|
71
|
+
`/public/content/blocks/${encodeURIComponent(uuid)}`,
|
|
72
|
+
[`block-${uuid}`]
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function getAllPages(): Promise<PageComposition[]> {
|
|
77
|
+
return cmsFetch<PageComposition[]>("/public/content/pages", ["all-pages"]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function executeCollection(uuid: string): Promise<ContentBlock[]> {
|
|
81
|
+
return cmsFetch<ContentBlock[]>(
|
|
82
|
+
`/public/content/collections/${encodeURIComponent(uuid)}/execute`,
|
|
83
|
+
[`collection-${uuid}`]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function getCollections(): Promise<Collection[]> {
|
|
88
|
+
return cmsFetch<Collection[]>("/public/content/collections", ["collections"]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function searchContent(
|
|
92
|
+
query: string,
|
|
93
|
+
type?: string,
|
|
94
|
+
page = 1,
|
|
95
|
+
size = 20
|
|
96
|
+
): Promise<PaginatedResponse<SearchResultItem>> {
|
|
97
|
+
const params = new URLSearchParams({ q: query, page: String(page), size: String(size) });
|
|
98
|
+
if (type) params.set("type", type);
|
|
99
|
+
return cmsFetch<PaginatedResponse<SearchResultItem>>(
|
|
100
|
+
`/public/content/search?${params.toString()}`,
|
|
101
|
+
["search"]
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ContentBlock } from "./types";
|
|
2
|
+
import { str } from "./types";
|
|
3
|
+
|
|
4
|
+
export function findBlockBySlug(blocks: ContentBlock[], slug: string): ContentBlock | undefined {
|
|
5
|
+
return blocks.find((b) => str(b.content.slug) === slug);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatDate(dateStr: string): string {
|
|
9
|
+
try {
|
|
10
|
+
return new Date(dateStr).toLocaleDateString("en-US", {
|
|
11
|
+
year: "numeric",
|
|
12
|
+
month: "long",
|
|
13
|
+
day: "numeric",
|
|
14
|
+
});
|
|
15
|
+
} catch {
|
|
16
|
+
return dateStr;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface ContentField {
|
|
2
|
+
fieldType: string;
|
|
3
|
+
fieldValue: string | ListItem[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Safely extract a string value from a content field. */
|
|
7
|
+
export function str(field: ContentField | undefined, fallback = ""): string {
|
|
8
|
+
if (!field || typeof field.fieldValue !== "string") return fallback;
|
|
9
|
+
return field.fieldValue || fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Safely extract a list value from a content field. */
|
|
13
|
+
export function list(field: any, fallback: any[] = []): any[] {
|
|
14
|
+
if (!field?.fieldValue) return fallback;
|
|
15
|
+
|
|
16
|
+
return field.fieldValue.map((item: any) => {
|
|
17
|
+
const flattened: Record<string, any> = {};
|
|
18
|
+
|
|
19
|
+
Object.entries(item).forEach(([key, value]: any) => {
|
|
20
|
+
if (key === "id") return;
|
|
21
|
+
flattened[key] = value?.fieldValue ?? value;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return flattened;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ListItem {
|
|
29
|
+
id: string;
|
|
30
|
+
[fieldName: string]: ContentField | string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ContentBlock {
|
|
34
|
+
uuid: string;
|
|
35
|
+
tenant_uuid: string;
|
|
36
|
+
type: string;
|
|
37
|
+
name: string;
|
|
38
|
+
tags: string[];
|
|
39
|
+
content: Record<string, ContentField>;
|
|
40
|
+
metadata: Record<string, unknown>;
|
|
41
|
+
version: number;
|
|
42
|
+
is_published: boolean;
|
|
43
|
+
created_at: string;
|
|
44
|
+
updated_at: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PageComposition {
|
|
48
|
+
uuid: string;
|
|
49
|
+
tenant_uuid: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
title: string;
|
|
52
|
+
description: string;
|
|
53
|
+
blocks: string[];
|
|
54
|
+
layout: string;
|
|
55
|
+
tags: string[];
|
|
56
|
+
metadata: Record<string, unknown>;
|
|
57
|
+
is_published: boolean;
|
|
58
|
+
published_at: string | null;
|
|
59
|
+
created_at: string;
|
|
60
|
+
updated_at: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PageWithBlocksResponse {
|
|
64
|
+
page: PageComposition;
|
|
65
|
+
blocks: Record<string, ContentBlock>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SectionProps {
|
|
69
|
+
block: ContentBlock;
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CMSPreviewMessage {
|
|
74
|
+
type: "cms-preview-update";
|
|
75
|
+
blockTag: string;
|
|
76
|
+
fieldName: string;
|
|
77
|
+
fieldType: string;
|
|
78
|
+
value: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CMSPreviewHighlightMessage {
|
|
82
|
+
type: "cms-preview-highlight";
|
|
83
|
+
blockTag: string;
|
|
84
|
+
fieldName: string;
|
|
85
|
+
active: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PaginatedResponse<T> {
|
|
89
|
+
page: number;
|
|
90
|
+
size: number;
|
|
91
|
+
total: number;
|
|
92
|
+
total_pages: number;
|
|
93
|
+
data: T[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface Collection {
|
|
97
|
+
uuid: string;
|
|
98
|
+
name: string;
|
|
99
|
+
query: string;
|
|
100
|
+
order_by: string;
|
|
101
|
+
order_dir: string;
|
|
102
|
+
limit: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SearchResultItem {
|
|
106
|
+
type: "block" | "page";
|
|
107
|
+
block?: ContentBlock;
|
|
108
|
+
page?: PageComposition;
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|