feat: use mock fallbacks for CMS content
This commit is contained in:
271
src/lib/content-cms.ts
Normal file
271
src/lib/content-cms.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import "server-only";
|
||||
|
||||
import { apiBaseUrl } from "@/lib/site";
|
||||
|
||||
export type ContentPageSlug = "home" | "about" | "events" | "blog";
|
||||
|
||||
export type ContentImage = {
|
||||
url: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
export type ContentSeo = {
|
||||
title: string;
|
||||
description: string;
|
||||
image: ContentImage | null;
|
||||
};
|
||||
|
||||
export type SiteContent = {
|
||||
official_name: string;
|
||||
short_name: string;
|
||||
faculty_subline: string;
|
||||
description: string;
|
||||
logo: ContentImage | null;
|
||||
navigation: Record<ContentPageSlug, string>;
|
||||
contacts: {
|
||||
email: string;
|
||||
phone: string;
|
||||
telegram: string;
|
||||
instagram: string;
|
||||
website: string;
|
||||
};
|
||||
address: string;
|
||||
footer_copy: string;
|
||||
seo: ContentSeo;
|
||||
};
|
||||
|
||||
type PageBase = {
|
||||
title: string;
|
||||
intro: string;
|
||||
seo: ContentSeo;
|
||||
};
|
||||
|
||||
export type HomeContentPage = PageBase & {
|
||||
slug: "home";
|
||||
hero_heading: string;
|
||||
hero_body: string;
|
||||
hero_image: ContentImage | null;
|
||||
hero_action_label: string;
|
||||
featured_events_heading: string;
|
||||
featured_events_intro: string;
|
||||
featured_events_link_label: string;
|
||||
featured_posts_heading: string;
|
||||
featured_posts_intro: string;
|
||||
featured_posts_link_label: string;
|
||||
participation_heading: string;
|
||||
participation_body: string;
|
||||
participation_image: ContentImage | null;
|
||||
participation_action_label: string;
|
||||
};
|
||||
|
||||
export type AboutContentItem = {
|
||||
kind: string;
|
||||
title: string;
|
||||
text: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type AboutContentPage = PageBase & {
|
||||
slug: "about";
|
||||
identity_heading: string;
|
||||
identity_body: string;
|
||||
mission_heading: string;
|
||||
mission_body: string;
|
||||
activities_heading: string;
|
||||
activities_body: string;
|
||||
values_heading: string;
|
||||
values_body: string;
|
||||
committees_heading: string;
|
||||
committees_body: string;
|
||||
affiliations_heading: string;
|
||||
affiliations_body: string;
|
||||
faq_heading: string;
|
||||
contact_heading: string;
|
||||
items: AboutContentItem[];
|
||||
};
|
||||
|
||||
export type EventsContentPage = PageBase & { slug: "events" };
|
||||
export type BlogContentPage = PageBase & { slug: "blog" };
|
||||
export type ListingContentPage = EventsContentPage | BlogContentPage;
|
||||
|
||||
export type CmsContentPage = HomeContentPage | AboutContentPage | ListingContentPage;
|
||||
|
||||
const CMS_ENABLED = process.env.CONTENT_CMS_ENABLED === "true";
|
||||
|
||||
export const isContentCmsEnabled = () => CMS_ENABLED;
|
||||
|
||||
type ContentPageFor<Slug extends ContentPageSlug> = Extract<CmsContentPage, { slug: Slug }>;
|
||||
|
||||
const MOCK_SITE_CONTENT: SiteContent = {
|
||||
official_name: "نام انجمن نمونه",
|
||||
short_name: "انجمن نمونه",
|
||||
faculty_subline: "دانشکده نمونه",
|
||||
description: "این متن نمونه است و پس از ورود محتوای اصلی از پنل مدیریت جایگزین میشود.",
|
||||
logo: null,
|
||||
navigation: { home: "خانه", about: "درباره ما", events: "رویدادها", blog: "بلاگ" },
|
||||
contacts: {
|
||||
email: "contact@example.invalid",
|
||||
phone: "0000000000",
|
||||
telegram: "https://example.invalid/telegram",
|
||||
instagram: "https://example.invalid/instagram",
|
||||
website: "https://example.invalid",
|
||||
},
|
||||
address: "نشانی نمونه",
|
||||
footer_copy: "متن نمونه پابرگ؛ محتوای نهایی را از پنل مدیریت وارد کنید.",
|
||||
seo: {
|
||||
title: "عنوان نمونه سایت",
|
||||
description: "توضیحات نمونه برای موتورهای جستوجو.",
|
||||
image: null,
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_PAGE_CONTENT: Record<ContentPageSlug, CmsContentPage> = {
|
||||
home: {
|
||||
slug: "home",
|
||||
title: "عنوان نمونه صفحه اصلی",
|
||||
intro: "این متن نمونه صفحه اصلی است و پس از ورود محتوای اصلی جایگزین میشود.",
|
||||
seo: {
|
||||
title: "عنوان نمونه صفحه اصلی",
|
||||
description: "توضیحات نمونه صفحه اصلی برای موتورهای جستوجو.",
|
||||
image: null,
|
||||
},
|
||||
hero_heading: "عنوان نمونه صفحه اصلی",
|
||||
hero_body: "توضیحات نمونه برای معرفی انجمن و فعالیتهای آن.",
|
||||
hero_image: null,
|
||||
hero_action_label: "مشاهده نمونهها",
|
||||
featured_events_heading: "رویدادهای نمونه",
|
||||
featured_events_intro: "توضیحات نمونه برای بخش رویدادها.",
|
||||
featured_events_link_label: "همه رویدادها",
|
||||
featured_posts_heading: "نوشتههای نمونه",
|
||||
featured_posts_intro: "توضیحات نمونه برای بخش نوشتهها.",
|
||||
featured_posts_link_label: "همه نوشتهها",
|
||||
participation_heading: "به ما بپیوندید",
|
||||
participation_body: "این متن نمونه راههای مشارکت را توضیح میدهد.",
|
||||
participation_image: null,
|
||||
participation_action_label: "اطلاعات بیشتر",
|
||||
},
|
||||
about: {
|
||||
slug: "about",
|
||||
title: "عنوان نمونه صفحه درباره ما",
|
||||
intro: "این متن نمونه معرفی انجمن است و بعداً از پنل مدیریت قابل ویرایش است.",
|
||||
seo: {
|
||||
title: "عنوان نمونه صفحه درباره ما",
|
||||
description: "توضیحات نمونه صفحه درباره ما برای موتورهای جستوجو.",
|
||||
image: null,
|
||||
},
|
||||
identity_heading: "معرفی نمونه",
|
||||
identity_body: "اطلاعات نمونه درباره هویت انجمن.",
|
||||
mission_heading: "ماموریت نمونه",
|
||||
mission_body: "توضیحات نمونه درباره ماموریت انجمن.",
|
||||
activities_heading: "فعالیتهای نمونه",
|
||||
activities_body: "توضیحات نمونه درباره فعالیتها.",
|
||||
values_heading: "ارزشهای نمونه",
|
||||
values_body: "یادگیری، همکاری و مسئولیتپذیری نمونه.",
|
||||
committees_heading: "کمیتههای نمونه",
|
||||
committees_body: "اطلاعات نمونه درباره کمیتهها.",
|
||||
affiliations_heading: "همکاریهای نمونه",
|
||||
affiliations_body: "اطلاعات نمونه درباره گروههای همکار.",
|
||||
faq_heading: "پرسشهای نمونه",
|
||||
contact_heading: "راههای ارتباطی نمونه",
|
||||
items: [
|
||||
{ kind: "mission", title: "ماموریت نمونه", text: "این متن نمونه برای توضیح ماموریت انجمن است.", position: 0 },
|
||||
{ kind: "activities", title: "فعالیت نمونه", text: "این متن نمونه یکی از فعالیتهای انجمن را معرفی میکند.", position: 0 },
|
||||
{ kind: "values", title: "ارزش نمونه", text: "این متن نمونه یکی از ارزشهای انجمن را توضیح میدهد.", position: 0 },
|
||||
{ kind: "committees", title: "کمیته نمونه", text: "این متن نمونه برای معرفی یک کمیته است.", position: 0 },
|
||||
{ kind: "affiliations", title: "همکاری نمونه", text: "این متن نمونه برای معرفی یک گروه همکار است.", position: 0 },
|
||||
{ kind: "faq", title: "پرسش نمونه", text: "پاسخ نمونه؛ اطلاعات نهایی را از پنل مدیریت وارد کنید.", position: 0 },
|
||||
],
|
||||
},
|
||||
events: {
|
||||
slug: "events",
|
||||
title: "عنوان نمونه رویدادها",
|
||||
intro: "این متن نمونه برای معرفی فهرست رویدادهاست.",
|
||||
seo: {
|
||||
title: "عنوان نمونه رویدادها",
|
||||
description: "توضیحات نمونه صفحه رویدادها برای موتورهای جستوجو.",
|
||||
image: null,
|
||||
},
|
||||
},
|
||||
blog: {
|
||||
slug: "blog",
|
||||
title: "عنوان نمونه بلاگ",
|
||||
intro: "این متن نمونه برای معرفی نوشتههای بلاگ است.",
|
||||
seo: {
|
||||
title: "عنوان نمونه بلاگ",
|
||||
description: "توضیحات نمونه صفحه بلاگ برای موتورهای جستوجو.",
|
||||
image: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function fillEmptyFieldsWithMock<T>(content: T, mock: T): T {
|
||||
if (content === null || content === undefined) return mock;
|
||||
if (typeof content === "string") return (content.trim() ? content : mock) as T;
|
||||
if (Array.isArray(content)) return (content.length ? content : mock) as T;
|
||||
if (
|
||||
typeof content === "object" &&
|
||||
mock !== null &&
|
||||
typeof mock === "object" &&
|
||||
!Array.isArray(mock)
|
||||
) {
|
||||
const current = content as Record<string, unknown>;
|
||||
const fallback = mock as Record<string, unknown>;
|
||||
return {
|
||||
...current,
|
||||
...Object.fromEntries(
|
||||
Object.entries(fallback).map(([key, mockValue]) => [
|
||||
key,
|
||||
key in current ? fillEmptyFieldsWithMock(current[key], mockValue) : mockValue,
|
||||
]),
|
||||
),
|
||||
} as T;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function mockPage<Slug extends ContentPageSlug>(slug: Slug): ContentPageFor<Slug> {
|
||||
return MOCK_PAGE_CONTENT[slug] as ContentPageFor<Slug>;
|
||||
}
|
||||
|
||||
export class ContentCmsError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(path: string, status: number) {
|
||||
super(`Content CMS request failed for ${path}: ${status}`);
|
||||
this.name = "ContentCmsError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestContent<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${apiBaseUrl}/api/v1/content${path}`, { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new ContentCmsError(path, response.status);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
export async function getCmsSiteContent(): Promise<SiteContent> {
|
||||
if (!CMS_ENABLED) return MOCK_SITE_CONTENT;
|
||||
try {
|
||||
const content = await requestContent<SiteContent>("/site");
|
||||
return fillEmptyFieldsWithMock(content, MOCK_SITE_CONTENT);
|
||||
} catch {
|
||||
return MOCK_SITE_CONTENT;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCmsPageContent<Slug extends ContentPageSlug>(
|
||||
slug: Slug,
|
||||
): Promise<ContentPageFor<Slug>> {
|
||||
const fallback = mockPage(slug);
|
||||
if (!CMS_ENABLED) return fallback;
|
||||
try {
|
||||
const page = await requestContent<CmsContentPage>(`/pages/${slug}`);
|
||||
return page.slug === slug
|
||||
? fillEmptyFieldsWithMock(page as ContentPageFor<Slug>, fallback)
|
||||
: fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user