@estation/create-cms-site 2.5.2 → 2.7.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/package.json +1 -1
- package/template/src/app/[locale]/[slug]/page.tsx +1 -1
- package/template/src/app/[locale]/blog/[slug]/page.tsx +7 -7
- package/template/src/app/[locale]/blog/page.tsx +7 -7
- package/template/src/app/[locale]/collections/[uuid]/page.tsx +1 -1
- package/template/src/app/[locale]/content/[typeName]/[slug]/page.tsx +87 -0
- package/template/src/app/[locale]/content/[typeName]/page.tsx +77 -0
- package/template/src/app/[locale]/events/[slug]/page.tsx +9 -9
- package/template/src/app/[locale]/events/page.tsx +8 -8
- package/template/src/app/[locale]/news/[slug]/page.tsx +8 -8
- package/template/src/app/[locale]/news/page.tsx +7 -7
- package/template/src/app/[locale]/page.tsx +1 -1
- package/template/src/components/SectionRenderer.tsx +2 -2
- package/template/src/components/sections/CTASection.tsx +5 -5
- package/template/src/components/sections/ContactSection.tsx +6 -6
- package/template/src/components/sections/FAQSection.tsx +2 -2
- package/template/src/components/sections/FeaturesSection.tsx +3 -3
- package/template/src/components/sections/GallerySection.tsx +2 -2
- package/template/src/components/sections/GenericSection.tsx +1 -1
- package/template/src/components/sections/HeroSection.tsx +4 -4
- package/template/src/components/sections/SliderSection.tsx +1 -1
- package/template/src/components/sections/TestimonialsSection.tsx +2 -2
- package/template/src/components/sections/TextSection.tsx +4 -4
- package/template/src/lib/cms-api.ts +47 -0
- package/template/src/lib/types.ts +37 -5
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@ export default async function DynamicPage({ params }: PageProps) {
|
|
|
25
25
|
const data = await getPageBySlug(slug, locale);
|
|
26
26
|
const orderedBlocks = getOrderedBlocks(data.page.blocks, data.blocks);
|
|
27
27
|
|
|
28
|
-
return <SectionRenderer blocks={orderedBlocks} />;
|
|
28
|
+
return <SectionRenderer blocks={orderedBlocks} locale={locale} />;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function getOrderedBlocks(
|
|
@@ -14,8 +14,8 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
14
14
|
const block = findBlockBySlug(blocks, slug);
|
|
15
15
|
if (!block) return { title: "Post Not Found" };
|
|
16
16
|
return {
|
|
17
|
-
title: str(block.content.title, block.name),
|
|
18
|
-
description: str(block.content.excerpt),
|
|
17
|
+
title: str(block.content.title, locale, block.name),
|
|
18
|
+
description: str(block.content.excerpt, locale),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -26,11 +26,11 @@ export default async function BlogDetailPage({ params }: PageProps) {
|
|
|
26
26
|
|
|
27
27
|
if (!block) notFound();
|
|
28
28
|
|
|
29
|
-
const title = str(block.content.title, block.name);
|
|
30
|
-
const author = str(block.content.author);
|
|
31
|
-
const date = str(block.content.publishDate) || block.created_at;
|
|
32
|
-
const featuredImage = str(block.content.featuredImage);
|
|
33
|
-
const content = str(block.content.content);
|
|
29
|
+
const title = str(block.content.title, locale, block.name);
|
|
30
|
+
const author = str(block.content.author, locale);
|
|
31
|
+
const date = str(block.content.publishDate, locale) || block.created_at;
|
|
32
|
+
const featuredImage = str(block.content.featuredImage, locale);
|
|
33
|
+
const content = str(block.content.content, locale);
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
36
|
<article className="py-16 px-6">
|
|
@@ -22,8 +22,8 @@ export default async function BlogListingPage({ params }: PageProps) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const sorted = [...blocks].sort((a, b) => {
|
|
25
|
-
const da = str(a.content.publishDate) || a.created_at;
|
|
26
|
-
const db = str(b.content.publishDate) || b.created_at;
|
|
25
|
+
const da = str(a.content.publishDate, locale) || a.created_at;
|
|
26
|
+
const db = str(b.content.publishDate, locale) || b.created_at;
|
|
27
27
|
return new Date(db).getTime() - new Date(da).getTime();
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -36,11 +36,11 @@ export default async function BlogListingPage({ params }: PageProps) {
|
|
|
36
36
|
) : (
|
|
37
37
|
<div className="space-y-8">
|
|
38
38
|
{sorted.map((block) => {
|
|
39
|
-
const slug = str(block.content.slug);
|
|
40
|
-
const title = str(block.content.title, block.name);
|
|
41
|
-
const excerpt = str(block.content.excerpt);
|
|
42
|
-
const featuredImage = str(block.content.featuredImage);
|
|
43
|
-
const date = str(block.content.publishDate) || block.created_at;
|
|
39
|
+
const slug = str(block.content.slug, locale);
|
|
40
|
+
const title = str(block.content.title, locale, block.name);
|
|
41
|
+
const excerpt = str(block.content.excerpt, locale);
|
|
42
|
+
const featuredImage = str(block.content.featuredImage, locale);
|
|
43
|
+
const date = str(block.content.publishDate, locale) || block.created_at;
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<article key={block.uuid} className="border-b pb-8">
|
|
@@ -20,7 +20,7 @@ export default async function CollectionPage({ params }: PageProps) {
|
|
|
20
20
|
{blocks.length === 0 ? (
|
|
21
21
|
<p className="text-gray-500 text-center">No items in this collection.</p>
|
|
22
22
|
) : (
|
|
23
|
-
<SectionRenderer blocks={blocks} />
|
|
23
|
+
<SectionRenderer blocks={blocks} locale={locale} />
|
|
24
24
|
)}
|
|
25
25
|
</div>
|
|
26
26
|
</div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getContentBySlug } from "@/lib/cms-api";
|
|
2
|
+
import { str } from "@/lib/types";
|
|
3
|
+
import type { Metadata } from "next";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
interface PageProps {
|
|
7
|
+
params: Promise<{ locale: string; typeName: string; slug: string }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
11
|
+
const { typeName, slug, locale } = await params;
|
|
12
|
+
try {
|
|
13
|
+
const block = await getContentBySlug(typeName, slug);
|
|
14
|
+
return {
|
|
15
|
+
title: str(block.content?.title, locale, block.name),
|
|
16
|
+
description: str(block.content?.excerpt || block.content?.description, locale),
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
return { title: typeName };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function ContentDetailPage({ params }: PageProps) {
|
|
24
|
+
const { locale, typeName, slug } = await params;
|
|
25
|
+
|
|
26
|
+
let block;
|
|
27
|
+
try {
|
|
28
|
+
block = await getContentBySlug(typeName, slug);
|
|
29
|
+
} catch {
|
|
30
|
+
notFound();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!block) notFound();
|
|
34
|
+
|
|
35
|
+
const c = block.content || {};
|
|
36
|
+
const title = str(c.title, locale, block.name);
|
|
37
|
+
const image = str(c.featuredImage || c.image);
|
|
38
|
+
const body = str(c.content || c.body || c.description, locale);
|
|
39
|
+
const author = str(c.author, locale);
|
|
40
|
+
const date = str(c.publishDate || c.startDate) || block.created_at;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="py-16 px-6">
|
|
44
|
+
<div className="max-w-3xl mx-auto">
|
|
45
|
+
<a href={`/${locale}/content/${typeName}`} className="text-sm text-gray-500 hover:underline mb-6 block">
|
|
46
|
+
← Back to {typeName}
|
|
47
|
+
</a>
|
|
48
|
+
|
|
49
|
+
{image && (
|
|
50
|
+
<img src={image} alt={title} className="w-full h-64 object-cover rounded-xl mb-8" />
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
<h1 data-cms-field="title" className="text-4xl font-bold">{title}</h1>
|
|
54
|
+
|
|
55
|
+
{(author || date) && (
|
|
56
|
+
<p className="text-sm text-gray-500 mt-3">
|
|
57
|
+
{author && <span data-cms-field="author">By {author}</span>}
|
|
58
|
+
{author && date && " · "}
|
|
59
|
+
{date && <time>{new Date(date).toLocaleDateString()}</time>}
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{body && (
|
|
64
|
+
<div
|
|
65
|
+
data-cms-field="content"
|
|
66
|
+
className="prose prose-gray max-w-none mt-8"
|
|
67
|
+
dangerouslySetInnerHTML={{ __html: body }}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{/* Render all other text fields */}
|
|
72
|
+
{Object.entries(c).map(([key, field]) => {
|
|
73
|
+
if (["title", "slug", "content", "body", "description", "featuredImage", "image", "author", "publishDate", "startDate", "status", "tags"].includes(key)) return null;
|
|
74
|
+
const val = str(field, locale);
|
|
75
|
+
if (!val) return null;
|
|
76
|
+
const label = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
|
|
77
|
+
return (
|
|
78
|
+
<div key={key} className="mt-6">
|
|
79
|
+
<h3 className="text-sm font-medium text-gray-500 uppercase">{label}</h3>
|
|
80
|
+
<p data-cms-field={key} className="mt-1">{val}</p>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getContentByType } from "@/lib/cms-api";
|
|
2
|
+
import { str } from "@/lib/types";
|
|
3
|
+
import type { Metadata } from "next";
|
|
4
|
+
|
|
5
|
+
interface PageProps {
|
|
6
|
+
params: Promise<{ locale: string; typeName: string }>;
|
|
7
|
+
searchParams: Promise<{ page?: string }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
11
|
+
const { typeName } = await params;
|
|
12
|
+
const label = typeName.charAt(0).toUpperCase() + typeName.slice(1);
|
|
13
|
+
return { title: label, description: `Browse ${label}` };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function ContentListPage({ params, searchParams }: PageProps) {
|
|
17
|
+
const { locale, typeName } = await params;
|
|
18
|
+
const { page: pageParam } = await searchParams;
|
|
19
|
+
const page = parseInt(pageParam || "1", 10);
|
|
20
|
+
|
|
21
|
+
let result;
|
|
22
|
+
try {
|
|
23
|
+
result = await getContentByType(typeName, page, 20);
|
|
24
|
+
} catch {
|
|
25
|
+
result = { data: [], total: 0, page: 1, size: 20, total_pages: 0 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const blocks = result.data || [];
|
|
29
|
+
const label = typeName.charAt(0).toUpperCase() + typeName.slice(1);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="py-16 px-6">
|
|
33
|
+
<div className="max-w-4xl mx-auto">
|
|
34
|
+
<h1 className="text-4xl font-bold mb-10">{label}</h1>
|
|
35
|
+
{blocks.length === 0 ? (
|
|
36
|
+
<p className="text-gray-500">No {typeName} content yet.</p>
|
|
37
|
+
) : (
|
|
38
|
+
<div className="space-y-8">
|
|
39
|
+
{blocks.map((block) => {
|
|
40
|
+
const slug = str(block.content?.slug);
|
|
41
|
+
const title = str(block.content?.title, locale, block.name);
|
|
42
|
+
const excerpt = str(block.content?.excerpt || block.content?.description, locale);
|
|
43
|
+
const image = str(block.content?.featuredImage || block.content?.image);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<article key={block.uuid} className="border-b pb-8">
|
|
47
|
+
{image && (
|
|
48
|
+
<img src={image} alt={title} className="w-full h-48 object-cover rounded-lg mb-4" />
|
|
49
|
+
)}
|
|
50
|
+
<h2 className="text-2xl font-semibold">
|
|
51
|
+
{slug ? (
|
|
52
|
+
<a href={`/${locale}/content/${typeName}/${slug}`} className="hover:underline">{title}</a>
|
|
53
|
+
) : title}
|
|
54
|
+
</h2>
|
|
55
|
+
{excerpt && <p className="text-gray-600 mt-2">{excerpt}</p>}
|
|
56
|
+
</article>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Pagination */}
|
|
63
|
+
{result.total_pages > 1 && (
|
|
64
|
+
<div className="flex justify-center gap-2 mt-10">
|
|
65
|
+
{page > 1 && (
|
|
66
|
+
<a href={`/${locale}/content/${typeName}?page=${page - 1}`} className="px-4 py-2 border rounded hover:bg-gray-50">Previous</a>
|
|
67
|
+
)}
|
|
68
|
+
<span className="px-4 py-2 text-gray-500">Page {page} of {result.total_pages}</span>
|
|
69
|
+
{page < result.total_pages && (
|
|
70
|
+
<a href={`/${locale}/content/${typeName}?page=${page + 1}`} className="px-4 py-2 border rounded hover:bg-gray-50">Next</a>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -14,8 +14,8 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
14
14
|
const block = findBlockBySlug(blocks, slug);
|
|
15
15
|
if (!block) return { title: "Event Not Found" };
|
|
16
16
|
return {
|
|
17
|
-
title: str(block.content.title, block.name),
|
|
18
|
-
description: str(block.content.description),
|
|
17
|
+
title: str(block.content.title, locale, block.name),
|
|
18
|
+
description: str(block.content.description, locale),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -26,13 +26,13 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|
|
26
26
|
|
|
27
27
|
if (!block) notFound();
|
|
28
28
|
|
|
29
|
-
const title = str(block.content.title, block.name);
|
|
30
|
-
const description = str(block.content.description);
|
|
31
|
-
const location = str(block.content.location);
|
|
32
|
-
const startDate = str(block.content.startDate);
|
|
33
|
-
const endDate = str(block.content.endDate);
|
|
34
|
-
const organizer = str(block.content.organizer);
|
|
35
|
-
const content = str(block.content.content);
|
|
29
|
+
const title = str(block.content.title, locale, block.name);
|
|
30
|
+
const description = str(block.content.description, locale);
|
|
31
|
+
const location = str(block.content.location, locale);
|
|
32
|
+
const startDate = str(block.content.startDate, locale);
|
|
33
|
+
const endDate = str(block.content.endDate, locale);
|
|
34
|
+
const organizer = str(block.content.organizer, locale);
|
|
35
|
+
const content = str(block.content.content, locale);
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
38
|
<article className="py-16 px-6">
|
|
@@ -22,8 +22,8 @@ export default async function EventsListingPage({ params }: PageProps) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const sorted = [...blocks].sort((a, b) => {
|
|
25
|
-
const da = str(a.content.startDate) || a.created_at;
|
|
26
|
-
const db = str(b.content.startDate) || b.created_at;
|
|
25
|
+
const da = str(a.content.startDate, locale) || a.created_at;
|
|
26
|
+
const db = str(b.content.startDate, locale) || b.created_at;
|
|
27
27
|
return new Date(da).getTime() - new Date(db).getTime();
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -36,12 +36,12 @@ export default async function EventsListingPage({ params }: PageProps) {
|
|
|
36
36
|
) : (
|
|
37
37
|
<div className="space-y-8">
|
|
38
38
|
{sorted.map((block) => {
|
|
39
|
-
const slug = str(block.content.slug);
|
|
40
|
-
const title = str(block.content.title, block.name);
|
|
41
|
-
const description = str(block.content.description);
|
|
42
|
-
const location = str(block.content.location);
|
|
43
|
-
const startDate = str(block.content.startDate);
|
|
44
|
-
const endDate = str(block.content.endDate);
|
|
39
|
+
const slug = str(block.content.slug, locale);
|
|
40
|
+
const title = str(block.content.title, locale, block.name);
|
|
41
|
+
const description = str(block.content.description, locale);
|
|
42
|
+
const location = str(block.content.location, locale);
|
|
43
|
+
const startDate = str(block.content.startDate, locale);
|
|
44
|
+
const endDate = str(block.content.endDate, locale);
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
47
|
<article key={block.uuid} className="border-b pb-8">
|
|
@@ -14,8 +14,8 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
14
14
|
const block = findBlockBySlug(blocks, slug);
|
|
15
15
|
if (!block) return { title: "Article Not Found" };
|
|
16
16
|
return {
|
|
17
|
-
title: str(block.content.title, block.name),
|
|
18
|
-
description: str(block.content.excerpt),
|
|
17
|
+
title: str(block.content.title, locale, block.name),
|
|
18
|
+
description: str(block.content.excerpt, locale),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -26,12 +26,12 @@ export default async function NewsDetailPage({ params }: PageProps) {
|
|
|
26
26
|
|
|
27
27
|
if (!block) notFound();
|
|
28
28
|
|
|
29
|
-
const title = str(block.content.title, block.name);
|
|
30
|
-
const author = str(block.content.author);
|
|
31
|
-
const category = str(block.content.category);
|
|
32
|
-
const date = str(block.content.publishDate) || block.created_at;
|
|
33
|
-
const featuredImage = str(block.content.featuredImage);
|
|
34
|
-
const content = str(block.content.content);
|
|
29
|
+
const title = str(block.content.title, locale, block.name);
|
|
30
|
+
const author = str(block.content.author, locale);
|
|
31
|
+
const category = str(block.content.category, locale);
|
|
32
|
+
const date = str(block.content.publishDate, locale) || block.created_at;
|
|
33
|
+
const featuredImage = str(block.content.featuredImage, locale);
|
|
34
|
+
const content = str(block.content.content, locale);
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<article className="py-16 px-6">
|
|
@@ -22,8 +22,8 @@ export default async function NewsListingPage({ params }: PageProps) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const sorted = [...blocks].sort((a, b) => {
|
|
25
|
-
const da = str(a.content.publishDate) || a.created_at;
|
|
26
|
-
const db = str(b.content.publishDate) || b.created_at;
|
|
25
|
+
const da = str(a.content.publishDate, locale) || a.created_at;
|
|
26
|
+
const db = str(b.content.publishDate, locale) || b.created_at;
|
|
27
27
|
return new Date(db).getTime() - new Date(da).getTime();
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -36,11 +36,11 @@ export default async function NewsListingPage({ params }: PageProps) {
|
|
|
36
36
|
) : (
|
|
37
37
|
<div className="space-y-8">
|
|
38
38
|
{sorted.map((block) => {
|
|
39
|
-
const slug = str(block.content.slug);
|
|
40
|
-
const title = str(block.content.title, block.name);
|
|
41
|
-
const excerpt = str(block.content.excerpt);
|
|
42
|
-
const category = str(block.content.category);
|
|
43
|
-
const date = str(block.content.publishDate) || block.created_at;
|
|
39
|
+
const slug = str(block.content.slug, locale);
|
|
40
|
+
const title = str(block.content.title, locale, block.name);
|
|
41
|
+
const excerpt = str(block.content.excerpt, locale);
|
|
42
|
+
const category = str(block.content.category, locale);
|
|
43
|
+
const date = str(block.content.publishDate, locale) || block.created_at;
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<article key={block.uuid} className="border-b pb-8">
|
|
@@ -25,7 +25,7 @@ export default async function Home({ params }: PageProps) {
|
|
|
25
25
|
try {
|
|
26
26
|
const data = await getPageBySlug("index", locale);
|
|
27
27
|
const orderedBlocks = getOrderedBlocks(data.page.blocks, data.blocks);
|
|
28
|
-
return <SectionRenderer blocks={orderedBlocks} />;
|
|
28
|
+
return <SectionRenderer blocks={orderedBlocks} locale={locale} />;
|
|
29
29
|
} catch {
|
|
30
30
|
return (
|
|
31
31
|
<div className="py-20 px-6 text-center">
|
|
@@ -26,7 +26,7 @@ const SECTION_MAP: Record<string, React.FC<SectionProps>> = {
|
|
|
26
26
|
form: ContactSection,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export function SectionRenderer({ blocks }: { blocks: ContentBlock[] }) {
|
|
29
|
+
export function SectionRenderer({ blocks, locale }: { blocks: ContentBlock[]; locale?: string }) {
|
|
30
30
|
return (
|
|
31
31
|
<>
|
|
32
32
|
{blocks.map((block) => {
|
|
@@ -34,7 +34,7 @@ export function SectionRenderer({ blocks }: { blocks: ContentBlock[] }) {
|
|
|
34
34
|
const Component = SECTION_MAP[tag] || GenericSection;
|
|
35
35
|
return (
|
|
36
36
|
<section key={block.uuid} data-cms-block={tag}>
|
|
37
|
-
<Component block={block} />
|
|
37
|
+
<Component block={block} locale={locale} />
|
|
38
38
|
</section>
|
|
39
39
|
);
|
|
40
40
|
})}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function CTASection({ block }: SectionProps) {
|
|
4
|
+
export function CTASection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const description = str(c.description);
|
|
7
|
-
const buttonText = str(c.buttonText);
|
|
6
|
+
const description = str(c.description, locale);
|
|
7
|
+
const buttonText = str(c.buttonText, locale);
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
10
|
<div className="py-20 px-6 bg-gray-900 text-white text-center">
|
|
11
11
|
<div className="max-w-2xl mx-auto">
|
|
12
12
|
<h2 data-cms-field="title" className="text-3xl font-bold mb-4">
|
|
13
|
-
{str(c.title, "Ready to get started?")}
|
|
13
|
+
{str(c.title, locale, "Ready to get started?")}
|
|
14
14
|
</h2>
|
|
15
15
|
{description && (
|
|
16
16
|
<p data-cms-field="description" className="text-lg text-gray-300 mb-8">
|
|
@@ -20,7 +20,7 @@ export function CTASection({ block }: SectionProps) {
|
|
|
20
20
|
{buttonText && (
|
|
21
21
|
<a
|
|
22
22
|
data-cms-field="buttonText"
|
|
23
|
-
href={str(c.buttonLink, "#")}
|
|
23
|
+
href={str(c.buttonLink, locale, "#")}
|
|
24
24
|
className="inline-block px-8 py-3 bg-white text-gray-900 rounded-lg font-medium hover:bg-gray-100 transition-colors"
|
|
25
25
|
>
|
|
26
26
|
{buttonText}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function ContactSection({ block }: SectionProps) {
|
|
4
|
+
export function ContactSection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const title = str(c.title, "Contact Us");
|
|
7
|
-
const description = str(c.description);
|
|
8
|
-
const email = str(c.email);
|
|
9
|
-
const phone = str(c.phone);
|
|
10
|
-
const address = str(c.address);
|
|
6
|
+
const title = str(c.title, locale, "Contact Us");
|
|
7
|
+
const description = str(c.description, locale);
|
|
8
|
+
const email = str(c.email, locale);
|
|
9
|
+
const phone = str(c.phone, locale);
|
|
10
|
+
const address = str(c.address, locale);
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
13
|
<div className="py-16 px-6">
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function FAQSection({ block }: SectionProps) {
|
|
4
|
+
export function FAQSection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const title = str(c.title);
|
|
6
|
+
const title = str(c.title, locale);
|
|
7
7
|
|
|
8
8
|
const faqs: { question: string; answer: string }[] = [];
|
|
9
9
|
if (Array.isArray(c.items?.fieldValue)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function FeaturesSection({ block }: SectionProps) {
|
|
4
|
+
export function FeaturesSection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const title = str(c.title);
|
|
7
|
-
const description = str(c.description);
|
|
6
|
+
const title = str(c.title, locale);
|
|
7
|
+
const description = str(c.description, locale);
|
|
8
8
|
|
|
9
9
|
const features: { title: string; description: string }[] = [];
|
|
10
10
|
if (Array.isArray(c.items?.fieldValue)) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { SectionProps, ListItem } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function GallerySection({ block }: SectionProps) {
|
|
4
|
+
export function GallerySection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const title = str(c.title);
|
|
6
|
+
const title = str(c.title, locale);
|
|
7
7
|
const items = (Array.isArray(c.items?.fieldValue) ? c.items.fieldValue : []) as ListItem[];
|
|
8
8
|
|
|
9
9
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
|
|
3
|
-
export function GenericSection({ block }: SectionProps) {
|
|
3
|
+
export function GenericSection({ block, locale }: SectionProps) {
|
|
4
4
|
const c = block.content;
|
|
5
5
|
const entries = Object.entries(c).filter(
|
|
6
6
|
([, field]) => field?.fieldValue !== undefined && field?.fieldValue !== null && field?.fieldValue !== ""
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function HeroSection({ block }: SectionProps) {
|
|
4
|
+
export function HeroSection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const buttonText = str(c.buttonText);
|
|
6
|
+
const buttonText = str(c.buttonText, locale);
|
|
7
7
|
|
|
8
8
|
return (
|
|
9
9
|
<div className="py-20 px-6 text-center">
|
|
10
10
|
<h1 data-cms-field="header" className="text-4xl md:text-5xl font-bold tracking-tight">
|
|
11
|
-
{str(c.header, "Hero Title")}
|
|
11
|
+
{str(c.header, locale, "Hero Title")}
|
|
12
12
|
</h1>
|
|
13
13
|
<p data-cms-field="description" className="mt-4 text-lg text-gray-600 max-w-2xl mx-auto">
|
|
14
|
-
{str(c.description, "Hero description goes here.")}
|
|
14
|
+
{str(c.description, locale, "Hero description goes here.")}
|
|
15
15
|
</p>
|
|
16
16
|
{buttonText && (
|
|
17
17
|
<a
|
|
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import type { SectionProps, ListItem } from "@/lib/types";
|
|
5
5
|
import { str } from "@/lib/types";
|
|
6
6
|
|
|
7
|
-
export function SliderSection({ block }: SectionProps) {
|
|
7
|
+
export function SliderSection({ block, locale }: SectionProps) {
|
|
8
8
|
const c = block.content;
|
|
9
9
|
const items = (Array.isArray(c.items?.fieldValue) ? c.items.fieldValue : []) as ListItem[];
|
|
10
10
|
const [current, setCurrent] = useState(0);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function TestimonialsSection({ block }: SectionProps) {
|
|
4
|
+
export function TestimonialsSection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const title = str(c.title);
|
|
6
|
+
const title = str(c.title, locale);
|
|
7
7
|
|
|
8
8
|
const testimonials: { name: string; quote: string; role?: string }[] = [];
|
|
9
9
|
if (Array.isArray(c.items?.fieldValue)) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { SectionProps } from "@/lib/types";
|
|
2
2
|
import { str } from "@/lib/types";
|
|
3
3
|
|
|
4
|
-
export function TextSection({ block }: SectionProps) {
|
|
4
|
+
export function TextSection({ block, locale }: SectionProps) {
|
|
5
5
|
const c = block.content;
|
|
6
|
-
const title = str(c.title);
|
|
7
|
-
const subtitle = str(c.subtitle);
|
|
8
|
-
const body = str(c.body);
|
|
6
|
+
const title = str(c.title, locale);
|
|
7
|
+
const subtitle = str(c.subtitle, locale);
|
|
8
|
+
const body = str(c.body, locale);
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
11
|
<div className="py-16 px-6 max-w-3xl mx-auto">
|
|
@@ -93,6 +93,53 @@ export async function getCollections(): Promise<Collection[]> {
|
|
|
93
93
|
return cmsFetch<Collection[]>("/public/content/collections", ["collections"]);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// ── v2 API functions ──────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
const CMS_V2_URL = CMS_API_URL.replace("/api/v1", "/api/v2");
|
|
99
|
+
|
|
100
|
+
async function cmsV2Fetch<T>(path: string, tags?: string[]): Promise<T> {
|
|
101
|
+
const url = `${CMS_V2_URL}${path}`;
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
headers: { "X-API-TOKEN": CMS_API_TOKEN },
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
next: { revalidate: 60, tags: tags || [] },
|
|
108
|
+
});
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
if (!res.ok) throw new Error(`CMS v2 API error: ${res.status} for ${path}`);
|
|
111
|
+
const json = await res.json();
|
|
112
|
+
return json.data ?? json;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** List content items of a specific type (paginated). */
|
|
116
|
+
export async function getContentByType(
|
|
117
|
+
typeName: string,
|
|
118
|
+
page = 1,
|
|
119
|
+
size = 20,
|
|
120
|
+
filters?: Record<string, string>
|
|
121
|
+
): Promise<PaginatedResponse<ContentBlock>> {
|
|
122
|
+
const params = new URLSearchParams({ page: String(page), size: String(size) });
|
|
123
|
+
if (filters) {
|
|
124
|
+
for (const [k, v] of Object.entries(filters)) params.set(`filter.${k}`, v);
|
|
125
|
+
}
|
|
126
|
+
return cmsV2Fetch<PaginatedResponse<ContentBlock>>(
|
|
127
|
+
`/public/content/${encodeURIComponent(typeName)}?${params}`,
|
|
128
|
+
[`content-${typeName}`]
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Get a single content item by its slug. */
|
|
133
|
+
export async function getContentBySlug(
|
|
134
|
+
typeName: string,
|
|
135
|
+
slug: string
|
|
136
|
+
): Promise<ContentBlock> {
|
|
137
|
+
return cmsV2Fetch<ContentBlock>(
|
|
138
|
+
`/public/content/${encodeURIComponent(typeName)}/slug/${encodeURIComponent(slug)}`,
|
|
139
|
+
[`content-${typeName}-${slug}`]
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
export async function searchContent(
|
|
97
144
|
query: string,
|
|
98
145
|
type?: string,
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
export interface ContentField {
|
|
2
2
|
fieldType: string;
|
|
3
|
-
fieldValue: string | ListItem[];
|
|
3
|
+
fieldValue: string | Record<string, string> | ListItem[];
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// Known locale codes for detecting locale vs fallback string
|
|
7
|
+
const LOCALE_CODE_SET = new Set(["en","es","fr","de","zh","ja","ko","pt","ar","ku","tr","it","ru","hi","fa","he","ur","nl","sv","pl","th","vi","id","ms","fil","bn","ta","te","ml","kn"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Safely extract a string value from a content field.
|
|
11
|
+
* Handles both plain strings and multilingual locale maps.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* str(field) → returns default locale value
|
|
15
|
+
* str(field, "en") → returns English value
|
|
16
|
+
* str(field, "ar") → returns Arabic value
|
|
17
|
+
* str(field, "ar", "fallback") → returns Arabic or "fallback"
|
|
18
|
+
* str(field, "Fallback Text") → returns value or "Fallback Text" (backwards compat)
|
|
19
|
+
*/
|
|
20
|
+
export function str(field: ContentField | undefined, localeOrFallback?: string, fallback = ""): string {
|
|
21
|
+
if (!field) return fallback || localeOrFallback || "";
|
|
22
|
+
|
|
23
|
+
const fv = field.fieldValue;
|
|
24
|
+
|
|
25
|
+
// Determine if second arg is a locale code or a fallback string
|
|
26
|
+
const isLocale = localeOrFallback ? LOCALE_CODE_SET.has(localeOrFallback) : false;
|
|
27
|
+
const locale = isLocale ? localeOrFallback : undefined;
|
|
28
|
+
const fb = isLocale ? fallback : (localeOrFallback || fallback);
|
|
29
|
+
|
|
30
|
+
// Plain string (old format or non-translatable field)
|
|
31
|
+
if (typeof fv === "string") return fv || fb;
|
|
32
|
+
|
|
33
|
+
// Multilingual locale map: { en: "Hello", ar: "مرحبا" }
|
|
34
|
+
if (fv && typeof fv === "object" && !Array.isArray(fv)) {
|
|
35
|
+
const localeMap = fv as Record<string, string>;
|
|
36
|
+
if (locale && localeMap[locale]) return localeMap[locale];
|
|
37
|
+
return localeMap["en"] || Object.values(localeMap)[0] || fb;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fb;
|
|
10
41
|
}
|
|
11
42
|
|
|
12
43
|
/** Safely extract a list value from a content field. */
|
|
@@ -94,6 +125,7 @@ export interface PageWithBlocksResponse {
|
|
|
94
125
|
|
|
95
126
|
export interface SectionProps {
|
|
96
127
|
block: ContentBlock;
|
|
128
|
+
locale?: string;
|
|
97
129
|
className?: string;
|
|
98
130
|
}
|
|
99
131
|
|