mirror of
https://git.meshkee.com/Meshkee-Websites/raoufi-group.git
synced 2026-08-11 20:29:33 +04:30
Fix
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { NewsArticleDetail } from "@/components/news/NewsArticleDetail";
|
||||
import { SiteFooter } from "@/components/SiteFooter";
|
||||
import { SiteHeader } from "@/components/SiteHeader";
|
||||
import { getBlogs, toDisplayNewsItem } from "@/lib/api";
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
async function getBlogPosts() {
|
||||
const posts = await getBlogs();
|
||||
return posts.filter((p) => p.type !== "news");
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getBlogPosts();
|
||||
return posts.map((item) => ({ slug: item.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const posts = await getBlogPosts();
|
||||
const item = posts.find((p) => p.slug === slug);
|
||||
if (!item) return {};
|
||||
return { title: item.titleFa || item.title, description: item.abstract || item.titleFa };
|
||||
}
|
||||
|
||||
export default async function BlogDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const posts = await getBlogPosts();
|
||||
const item = posts.find((p) => p.slug === slug);
|
||||
if (!item) notFound();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main>
|
||||
<NewsArticleDetail item={toDisplayNewsItem(item)} basePath="/blog" sectionLabel="بلاگ" />
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,13 +8,16 @@ import { SiteFooter } from "@/components/SiteFooter";
|
||||
import { SiteHeader } from "@/components/SiteHeader";
|
||||
import heroImage from "@/assets/images/hero/hero-2.jpg";
|
||||
import heroImageMobile from "@/assets/images/hero/hero-2-mobile.jpg";
|
||||
import { blog, blogListingPage, newsAndBlogPage } from "@/data/site";
|
||||
import { blogListingPage, newsAndBlogPage } from "@/data/site";
|
||||
import { getBlogs, toDisplayNewsItem } from "@/lib/api";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "بلاگ",
|
||||
description: "فهرست کامل مقالات منتشرشدهی گروه ساختمانی رئوفی.",
|
||||
};
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default async function BlogListPage({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -22,6 +25,8 @@ export default async function BlogListPage({
|
||||
}) {
|
||||
const { page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
const posts = await getBlogs();
|
||||
const blog = posts.filter((p) => p.type !== "news").map(toDisplayNewsItem);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,7 +51,7 @@ export default async function BlogListPage({
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="bg-sand pb-16 lg:pb-24">
|
||||
<section className="bg-sand py-16 lg:py-24">
|
||||
<NewsListing
|
||||
items={blog}
|
||||
page={page}
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
--color-paper: #fbfaf8;
|
||||
--color-sand: #f2eee8;
|
||||
--color-muted: #5c6873; /* brand body grey, carried over from the current site */
|
||||
--color-gold: #b08d57;
|
||||
--color-gold-soft: #cbab7d;
|
||||
--color-gold: #f98223; /* Raoofi brand orange */
|
||||
--color-gold-soft: #fbae70;
|
||||
--color-line: #e2ddd5;
|
||||
|
||||
/* ---- type ---------------------------------------------------------- */
|
||||
|
||||
@@ -10,14 +10,21 @@ import { PageCta } from "@/components/shared/PageCta";
|
||||
import { PageHero } from "@/components/shared/PageHero";
|
||||
import { SiteFooter } from "@/components/SiteFooter";
|
||||
import { SiteHeader } from "@/components/SiteHeader";
|
||||
import { blog, news, newsAndBlogPage } from "@/data/site";
|
||||
import { newsAndBlogPage } from "@/data/site";
|
||||
import { getBlogs, toDisplayNewsItem } from "@/lib/api";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "مقالات و اخبار",
|
||||
description: "آخرین اخبار، رویدادها و مقالات گروه ساختمانی رئوفی.",
|
||||
};
|
||||
|
||||
export default function NewsAndBlogPage() {
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default async function NewsAndBlogPage() {
|
||||
const posts = await getBlogs();
|
||||
const news = posts.filter((p) => p.type === "news").map(toDisplayNewsItem);
|
||||
const blog = posts.filter((p) => p.type !== "news").map(toDisplayNewsItem);
|
||||
|
||||
const hasNews = news.length > 0;
|
||||
const hasBlog = blog.length > 0;
|
||||
const hasNothing = !hasNews && !hasBlog;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { NewsArticleDetail } from "@/components/news/NewsArticleDetail";
|
||||
import { SiteFooter } from "@/components/SiteFooter";
|
||||
import { SiteHeader } from "@/components/SiteHeader";
|
||||
import { getBlogs, toDisplayNewsItem } from "@/lib/api";
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getBlogs("news");
|
||||
return posts.map((item) => ({ slug: item.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const posts = await getBlogs("news");
|
||||
const item = posts.find((p) => p.slug === slug);
|
||||
if (!item) return {};
|
||||
return { title: item.titleFa || item.title, description: item.abstract || item.titleFa };
|
||||
}
|
||||
|
||||
export default async function NewsDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const posts = await getBlogs("news");
|
||||
const item = posts.find((p) => p.slug === slug);
|
||||
if (!item) notFound();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main>
|
||||
<NewsArticleDetail item={toDisplayNewsItem(item)} basePath="/news" sectionLabel="اخبار" />
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,13 +8,16 @@ import { SiteFooter } from "@/components/SiteFooter";
|
||||
import { SiteHeader } from "@/components/SiteHeader";
|
||||
import heroImage from "@/assets/images/hero/hero-1.jpg";
|
||||
import heroImageMobile from "@/assets/images/hero/hero-1-mobile.jpg";
|
||||
import { news, newsAndBlogPage, newsListingPage } from "@/data/site";
|
||||
import { newsAndBlogPage, newsListingPage } from "@/data/site";
|
||||
import { getBlogs, toDisplayNewsItem } from "@/lib/api";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "اخبار",
|
||||
description: "فهرست کامل اخبار و رویدادهای منتشرشدهی گروه ساختمانی رئوفی.",
|
||||
};
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default async function NewsListPage({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -22,6 +25,8 @@ export default async function NewsListPage({
|
||||
}) {
|
||||
const { page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
const posts = await getBlogs("news");
|
||||
const news = posts.map(toDisplayNewsItem);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,7 +51,7 @@ export default async function NewsListPage({
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="bg-sand pb-16 lg:pb-24">
|
||||
<section className="bg-sand py-16 lg:py-24">
|
||||
<NewsListing
|
||||
items={news}
|
||||
page={page}
|
||||
|
||||
+4
-3
@@ -8,7 +8,7 @@ import { Services } from "@/components/Services";
|
||||
import { SiteFooter } from "@/components/SiteFooter";
|
||||
import { SiteHeader } from "@/components/SiteHeader";
|
||||
import { contact, site } from "@/data/site";
|
||||
import { getPortfolios, toDisplayProject } from "@/lib/api";
|
||||
import { getBlogs, getPortfolios, toDisplayNewsItem, toDisplayProject } from "@/lib/api";
|
||||
|
||||
const organizationJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
@@ -29,8 +29,9 @@ const organizationJsonLd = {
|
||||
};
|
||||
|
||||
export default async function HomePage() {
|
||||
const portfolios = await getPortfolios();
|
||||
const [portfolios, newsPosts] = await Promise.all([getPortfolios(), getBlogs("news")]);
|
||||
const displayProjects = portfolios.map(toDisplayProject);
|
||||
const displayNews = newsPosts.slice(0, 3).map(toDisplayNewsItem);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -47,7 +48,7 @@ export default async function HomePage() {
|
||||
<Marquee />
|
||||
<Projects projects={displayProjects} />
|
||||
<Services />
|
||||
<News />
|
||||
<News items={displayNews} />
|
||||
<ContactCta />
|
||||
</main>
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { news } from "@/data/site";
|
||||
import type { DisplayNewsItem } from "@/lib/api";
|
||||
|
||||
export function News({ items }: { items: DisplayNewsItem[] }) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
export function News() {
|
||||
return (
|
||||
<section id="news" className="bg-sand py-16 lg:py-24">
|
||||
<div className="mx-auto max-w-[1400px] px-5 lg:px-10">
|
||||
@@ -41,7 +43,7 @@ export function News() {
|
||||
</div>
|
||||
|
||||
<ul className="mt-16 grid gap-8 md:grid-cols-3 lg:gap-10">
|
||||
{news.map((item, i) => (
|
||||
{items.map((item, i) => (
|
||||
<Reveal as="li" key={item.slug} delay={i * 110}>
|
||||
<Link href={`/news/${item.slug}`} className="group block h-full">
|
||||
<article className="flex h-full flex-col bg-paper">
|
||||
@@ -52,7 +54,6 @@ export function News() {
|
||||
fill
|
||||
quality={80}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
placeholder="blur"
|
||||
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.07]"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-ink/0 transition-colors duration-500 group-hover:bg-ink/20" />
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { ProjectGalleryLightbox } from "@/components/portfolios/ProjectGalleryLightbox";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { PageCta } from "@/components/shared/PageCta";
|
||||
import { PageHero } from "@/components/shared/PageHero";
|
||||
import { newsAndBlogPage } from "@/data/site";
|
||||
import type { DisplayNewsItem } from "@/lib/api";
|
||||
|
||||
/**
|
||||
* Shared detail template for a single news or blog item — same rules as
|
||||
* NewsListing keeps the two archives consistent, this keeps their detail
|
||||
* pages consistent too, and mirrors the portfolio detail page's own
|
||||
* jump-nav + gallery pattern since blog items carry the same gallery field.
|
||||
* Body content is the item's real mainTextHtml from the API; nothing here
|
||||
* is invented, and the gallery section only appears once a post actually
|
||||
* has images to show.
|
||||
*/
|
||||
export function NewsArticleDetail({
|
||||
item,
|
||||
basePath,
|
||||
sectionLabel,
|
||||
}: {
|
||||
item: DisplayNewsItem;
|
||||
basePath: string;
|
||||
sectionLabel: string;
|
||||
}) {
|
||||
const hasGallery = item.gallery.length > 0;
|
||||
|
||||
const navItems = [
|
||||
{ id: "overview", label: "معرفی" },
|
||||
...(hasGallery ? [{ id: "gallery", label: "گالری تصاویر" }] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
image={item.image}
|
||||
caption={item.date}
|
||||
title={item.title}
|
||||
breadcrumb={[
|
||||
{ label: "صفحه اصلی", href: "/" },
|
||||
{ label: "مقالات و اخبار", href: "/news-and-blog" },
|
||||
{ label: sectionLabel, href: basePath },
|
||||
{ label: item.title, href: `${basePath}/${item.slug}` },
|
||||
]}
|
||||
/>
|
||||
|
||||
{navItems.length > 1 && (
|
||||
<nav aria-label="بخشهای صفحه" className="border-b border-line bg-paper">
|
||||
<ul className="no-scrollbar mx-auto flex max-w-[1400px] items-center gap-8 overflow-x-auto px-6 lg:px-12">
|
||||
{navItems.map((navItem) => (
|
||||
<li key={navItem.id}>
|
||||
<a
|
||||
href={`#${navItem.id}`}
|
||||
className="block scroll-mt-24 py-5 text-sm whitespace-nowrap text-muted transition-colors duration-300 hover:text-gold"
|
||||
>
|
||||
{navItem.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<section id="overview" className="scroll-mt-20 bg-paper py-16 lg:py-24">
|
||||
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
|
||||
<Reveal>
|
||||
<div className="mx-auto mb-14 flex max-w-[738px] flex-col items-center gap-3 text-center">
|
||||
<p className="kicker justify-center">{sectionLabel}</p>
|
||||
<time dateTime={item.publishedAt} className="text-xs text-muted">
|
||||
{item.date}
|
||||
</time>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{item.bodyHtml && (
|
||||
<Reveal delay={90}>
|
||||
<div className="cms-body" dangerouslySetInnerHTML={{ __html: item.bodyHtml }} />
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{hasGallery && (
|
||||
<section id="gallery" className="scroll-mt-20 bg-sand py-16 lg:py-24">
|
||||
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
|
||||
<Reveal>
|
||||
<p className="kicker mb-14 justify-center">گالری تصاویر</p>
|
||||
</Reveal>
|
||||
|
||||
<ProjectGalleryLightbox images={item.gallery} alt={item.title} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<PageCta
|
||||
caption={newsAndBlogPage.cta.caption}
|
||||
title={newsAndBlogPage.cta.title}
|
||||
action={newsAndBlogPage.cta.action}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,16 @@ import Link from "next/link";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import type { NewsItem } from "@/data/site";
|
||||
import type { DisplayNewsItem } from "@/lib/api";
|
||||
|
||||
/** Horizontal snap-scroll rail of news/blog cards, matching the site's other carousels. */
|
||||
export function NewsCarousel({ items, basePath }: { items: NewsItem[]; basePath: string }) {
|
||||
export function NewsCarousel({
|
||||
items,
|
||||
basePath,
|
||||
}: {
|
||||
items: DisplayNewsItem[];
|
||||
basePath: string;
|
||||
}) {
|
||||
const trackRef = useRef<HTMLUListElement>(null);
|
||||
const [atStart, setAtStart] = useState(true);
|
||||
const [atEnd, setAtEnd] = useState(false);
|
||||
@@ -92,7 +98,6 @@ export function NewsCarousel({ items, basePath }: { items: NewsItem[]; basePath:
|
||||
fill
|
||||
quality={80}
|
||||
sizes="(max-width: 640px) 82vw, (max-width: 1024px) 52vw, 26rem"
|
||||
placeholder="blur"
|
||||
loading={i < 2 ? "eager" : "lazy"}
|
||||
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.06]"
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
||||
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { Pagination } from "@/components/shared/Pagination";
|
||||
import type { NewsItem } from "@/data/site";
|
||||
import type { DisplayNewsItem } from "@/lib/api";
|
||||
|
||||
const PAGE_SIZE = 9;
|
||||
|
||||
@@ -18,7 +18,7 @@ export function NewsListing({
|
||||
basePath,
|
||||
emptyMessage,
|
||||
}: {
|
||||
items: NewsItem[];
|
||||
items: DisplayNewsItem[];
|
||||
page: number;
|
||||
basePath: string;
|
||||
emptyMessage: string;
|
||||
@@ -44,7 +44,6 @@ export function NewsListing({
|
||||
fill
|
||||
quality={80}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
placeholder="blur"
|
||||
loading={i < 3 ? "eager" : "lazy"}
|
||||
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.07]"
|
||||
/>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function PortfolioBrowser({
|
||||
type="button"
|
||||
onClick={() => setActiveId(ALL_ID)}
|
||||
aria-current={activeId === ALL_ID}
|
||||
className={`relative whitespace-nowrap pb-4 text-sm transition-colors duration-300 ${
|
||||
className={`relative whitespace-nowrap pb-4 text-sm font-bold transition-colors duration-300 ${
|
||||
activeId === ALL_ID ? "text-ink" : "text-muted hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
@@ -70,7 +70,7 @@ export function PortfolioBrowser({
|
||||
type="button"
|
||||
onClick={() => setActiveId(cat.id)}
|
||||
aria-current={activeId === cat.id}
|
||||
className={`relative whitespace-nowrap pb-4 text-sm transition-colors duration-300 ${
|
||||
className={`relative whitespace-nowrap pb-4 text-sm font-bold transition-colors duration-300 ${
|
||||
activeId === cat.id ? "text-ink" : "text-muted hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function Editorial({
|
||||
{title && (
|
||||
<Reveal delay={90}>
|
||||
<h2
|
||||
className={`max-w-[927px] text-[1.9rem] leading-[1.45] font-light sm:text-4xl lg:text-5xl lg:leading-[1.3] ${heading}`}
|
||||
className={`max-w-[927px] text-[1.9rem] leading-[1.45] font-normal sm:text-4xl lg:text-5xl lg:leading-[1.3] ${heading}`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
@@ -14,10 +14,6 @@ import pania from "@/assets/images/projects/pania.jpg";
|
||||
import ghasr3 from "@/assets/images/projects/ghasr-3.jpg";
|
||||
import goldenGhasr from "@/assets/images/projects/golden-ghasr.jpg";
|
||||
|
||||
import yearEnd from "@/assets/images/news/year-end-ceremony.jpg";
|
||||
import elysiumOpening from "@/assets/images/news/elysium-opening.jpg";
|
||||
import hayatFoundation from "@/assets/images/news/hayat-foundation.jpg";
|
||||
|
||||
export const site = {
|
||||
name: "گروه ساختمانی رئوفی",
|
||||
tagline: "سازنده رویاهای شما",
|
||||
@@ -385,33 +381,3 @@ export const blogListingPage = {
|
||||
emptyMessage: "هنوز مقالهای منتشر نشده است.",
|
||||
} as const;
|
||||
|
||||
export type NewsItem = {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
image: StaticImageData;
|
||||
};
|
||||
|
||||
export const news: NewsItem[] = [
|
||||
{
|
||||
slug: "year-end-ceremony",
|
||||
title: "مراسم پایان سال گروه رئوفی",
|
||||
date: "شنبه ۲۲ شهریور ۱۴۰۴",
|
||||
image: yearEnd,
|
||||
},
|
||||
{
|
||||
slug: "elysium-opening",
|
||||
title: "مراسم افتتاحیه برج الیزیوم",
|
||||
date: "سهشنبه ۱۹ فروردین ۱۴۰۴",
|
||||
image: elysiumOpening,
|
||||
},
|
||||
{
|
||||
slug: "hayat-foundation",
|
||||
title: "بتن ریزی فوندانسیون پروژه حیات",
|
||||
date: "شنبه ۱۳ بهمن ۱۴۰۳",
|
||||
image: hayatFoundation,
|
||||
},
|
||||
];
|
||||
|
||||
/** No blog posts published anywhere yet — genuinely empty, not left out. */
|
||||
export const blog: NewsItem[] = [];
|
||||
|
||||
@@ -165,3 +165,96 @@ export function toDisplayProject(item: ApiPortfolioItem): DisplayProject {
|
||||
publishedAt: item.publishedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Blogs / news — same API family as portfolios (same base URL, same list
|
||||
* envelope, a sibling {slug} + {id}/comments pair), so the item shape below
|
||||
* mirrors ApiPortfolioItem. `/blogs` additionally takes a `type` filter with
|
||||
* values "news" | "article" | "blog".
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export type BlogType = "news" | "article" | "blog";
|
||||
|
||||
export type ApiBlogItem = {
|
||||
id: string;
|
||||
businessId: string;
|
||||
title: string;
|
||||
titleFa: string;
|
||||
titleEn: string;
|
||||
slug: string;
|
||||
abstract: string;
|
||||
mainTextHtml: string;
|
||||
status: string;
|
||||
type: BlogType;
|
||||
categoryId: string;
|
||||
categoryName: string;
|
||||
tags: string[];
|
||||
titleImageUrl: string | null;
|
||||
featuredMediaId: string | null;
|
||||
gallery: { mediaId: string; url: string }[];
|
||||
galleryMediaIds: string[];
|
||||
sortOrder: number;
|
||||
publishedAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type BlogsResponse = {
|
||||
items: ApiBlogItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
/** All published blog/news items, every page merged. Optionally filtered by `type`. */
|
||||
export async function getBlogs(type?: BlogType): Promise<ApiBlogItem[]> {
|
||||
const query = type ? `?type=${type}` : "";
|
||||
const first = await apiGet<BlogsResponse>(`/tenants/${SITE_DOMAIN}/blogs${query}`);
|
||||
const pageCount = Math.ceil(first.total / first.pageSize);
|
||||
|
||||
const rest = await Promise.all(
|
||||
Array.from({ length: Math.max(0, pageCount - 1) }, (_, i) =>
|
||||
apiGet<BlogsResponse>(
|
||||
`/tenants/${SITE_DOMAIN}/blogs${query}${query ? "&" : "?"}page=${i + 2}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return [first, ...rest]
|
||||
.flatMap((page) => page.items)
|
||||
.filter((item) => item.status === "published")
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
}
|
||||
|
||||
export async function getBlogBySlug(slug: string): Promise<ApiBlogItem | null> {
|
||||
const items = await getBlogs();
|
||||
return items.find((item) => item.slug === slug) ?? null;
|
||||
}
|
||||
|
||||
export type DisplayNewsItem = {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
image: string;
|
||||
gallery: string[];
|
||||
bodyHtml: string;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
export function toDisplayNewsItem(item: ApiBlogItem): DisplayNewsItem {
|
||||
return {
|
||||
slug: item.slug,
|
||||
title: item.titleFa || item.title,
|
||||
date: dateFormatter.format(new Date(item.publishedAt)),
|
||||
image: item.titleImageUrl ?? FALLBACK_IMAGE,
|
||||
gallery: item.gallery.map((g) => g.url),
|
||||
bodyHtml: stripBrokenEmbedArtifacts(item.mainTextHtml),
|
||||
publishedAt: item.publishedAt,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user