From 1979eaf19aaecb458f49ddf3bbd679658a0fd7ce Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Sun, 27 Sep 2026 12:26:34 +0330 Subject: [PATCH] feat: use mock fallbacks for CMS content --- .env.sample | 1 + src/lib/content-cms.ts | 271 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 src/lib/content-cms.ts diff --git a/.env.sample b/.env.sample index a431a2f..ecee98d 100644 --- a/.env.sample +++ b/.env.sample @@ -1,2 +1,3 @@ NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8000 NEXT_PUBLIC_SITE_URL=http://localhost:8080 +CONTENT_CMS_ENABLED=false diff --git a/src/lib/content-cms.ts b/src/lib/content-cms.ts new file mode 100644 index 0000000..be7c8f9 --- /dev/null +++ b/src/lib/content-cms.ts @@ -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; + 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 = Extract; + +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 = { + 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(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; + const fallback = mock as Record; + 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: Slug): ContentPageFor { + return MOCK_PAGE_CONTENT[slug] as ContentPageFor; +} + +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(path: string): Promise { + 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 { + if (!CMS_ENABLED) return MOCK_SITE_CONTENT; + try { + const content = await requestContent("/site"); + return fillEmptyFieldsWithMock(content, MOCK_SITE_CONTENT); + } catch { + return MOCK_SITE_CONTENT; + } +} + +export async function getCmsPageContent( + slug: Slug, +): Promise> { + const fallback = mockPage(slug); + if (!CMS_ENABLED) return fallback; + try { + const page = await requestContent(`/pages/${slug}`); + return page.slug === slug + ? fillEmptyFieldsWithMock(page as ContentPageFor, fallback) + : fallback; + } catch { + return fallback; + } +}