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 { SiteHeader } from "@/components/SiteHeader";
|
||||||
import heroImage from "@/assets/images/hero/hero-2.jpg";
|
import heroImage from "@/assets/images/hero/hero-2.jpg";
|
||||||
import heroImageMobile from "@/assets/images/hero/hero-2-mobile.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 = {
|
export const metadata: Metadata = {
|
||||||
title: "بلاگ",
|
title: "بلاگ",
|
||||||
description: "فهرست کامل مقالات منتشرشدهی گروه ساختمانی رئوفی.",
|
description: "فهرست کامل مقالات منتشرشدهی گروه ساختمانی رئوفی.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export default async function BlogListPage({
|
export default async function BlogListPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@@ -22,6 +25,8 @@ export default async function BlogListPage({
|
|||||||
}) {
|
}) {
|
||||||
const { page: pageParam } = await searchParams;
|
const { page: pageParam } = await searchParams;
|
||||||
const page = Math.max(1, Number(pageParam) || 1);
|
const page = Math.max(1, Number(pageParam) || 1);
|
||||||
|
const posts = await getBlogs();
|
||||||
|
const blog = posts.filter((p) => p.type !== "news").map(toDisplayNewsItem);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -46,7 +51,7 @@ export default async function BlogListPage({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="bg-sand pb-16 lg:pb-24">
|
<section className="bg-sand py-16 lg:py-24">
|
||||||
<NewsListing
|
<NewsListing
|
||||||
items={blog}
|
items={blog}
|
||||||
page={page}
|
page={page}
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@
|
|||||||
--color-paper: #fbfaf8;
|
--color-paper: #fbfaf8;
|
||||||
--color-sand: #f2eee8;
|
--color-sand: #f2eee8;
|
||||||
--color-muted: #5c6873; /* brand body grey, carried over from the current site */
|
--color-muted: #5c6873; /* brand body grey, carried over from the current site */
|
||||||
--color-gold: #b08d57;
|
--color-gold: #f98223; /* Raoofi brand orange */
|
||||||
--color-gold-soft: #cbab7d;
|
--color-gold-soft: #fbae70;
|
||||||
--color-line: #e2ddd5;
|
--color-line: #e2ddd5;
|
||||||
|
|
||||||
/* ---- type ---------------------------------------------------------- */
|
/* ---- type ---------------------------------------------------------- */
|
||||||
|
|||||||
@@ -10,14 +10,21 @@ import { PageCta } from "@/components/shared/PageCta";
|
|||||||
import { PageHero } from "@/components/shared/PageHero";
|
import { PageHero } from "@/components/shared/PageHero";
|
||||||
import { SiteFooter } from "@/components/SiteFooter";
|
import { SiteFooter } from "@/components/SiteFooter";
|
||||||
import { SiteHeader } from "@/components/SiteHeader";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "مقالات و اخبار",
|
title: "مقالات و اخبار",
|
||||||
description: "آخرین اخبار، رویدادها و مقالات گروه ساختمانی رئوفی.",
|
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 hasNews = news.length > 0;
|
||||||
const hasBlog = blog.length > 0;
|
const hasBlog = blog.length > 0;
|
||||||
const hasNothing = !hasNews && !hasBlog;
|
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 { SiteHeader } from "@/components/SiteHeader";
|
||||||
import heroImage from "@/assets/images/hero/hero-1.jpg";
|
import heroImage from "@/assets/images/hero/hero-1.jpg";
|
||||||
import heroImageMobile from "@/assets/images/hero/hero-1-mobile.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 = {
|
export const metadata: Metadata = {
|
||||||
title: "اخبار",
|
title: "اخبار",
|
||||||
description: "فهرست کامل اخبار و رویدادهای منتشرشدهی گروه ساختمانی رئوفی.",
|
description: "فهرست کامل اخبار و رویدادهای منتشرشدهی گروه ساختمانی رئوفی.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export default async function NewsListPage({
|
export default async function NewsListPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@@ -22,6 +25,8 @@ export default async function NewsListPage({
|
|||||||
}) {
|
}) {
|
||||||
const { page: pageParam } = await searchParams;
|
const { page: pageParam } = await searchParams;
|
||||||
const page = Math.max(1, Number(pageParam) || 1);
|
const page = Math.max(1, Number(pageParam) || 1);
|
||||||
|
const posts = await getBlogs("news");
|
||||||
|
const news = posts.map(toDisplayNewsItem);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -46,7 +51,7 @@ export default async function NewsListPage({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="bg-sand pb-16 lg:pb-24">
|
<section className="bg-sand py-16 lg:py-24">
|
||||||
<NewsListing
|
<NewsListing
|
||||||
items={news}
|
items={news}
|
||||||
page={page}
|
page={page}
|
||||||
|
|||||||
+4
-3
@@ -8,7 +8,7 @@ import { Services } from "@/components/Services";
|
|||||||
import { SiteFooter } from "@/components/SiteFooter";
|
import { SiteFooter } from "@/components/SiteFooter";
|
||||||
import { SiteHeader } from "@/components/SiteHeader";
|
import { SiteHeader } from "@/components/SiteHeader";
|
||||||
import { contact, site } from "@/data/site";
|
import { contact, site } from "@/data/site";
|
||||||
import { getPortfolios, toDisplayProject } from "@/lib/api";
|
import { getBlogs, getPortfolios, toDisplayNewsItem, toDisplayProject } from "@/lib/api";
|
||||||
|
|
||||||
const organizationJsonLd = {
|
const organizationJsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -29,8 +29,9 @@ const organizationJsonLd = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const portfolios = await getPortfolios();
|
const [portfolios, newsPosts] = await Promise.all([getPortfolios(), getBlogs("news")]);
|
||||||
const displayProjects = portfolios.map(toDisplayProject);
|
const displayProjects = portfolios.map(toDisplayProject);
|
||||||
|
const displayNews = newsPosts.slice(0, 3).map(toDisplayNewsItem);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -47,7 +48,7 @@ export default async function HomePage() {
|
|||||||
<Marquee />
|
<Marquee />
|
||||||
<Projects projects={displayProjects} />
|
<Projects projects={displayProjects} />
|
||||||
<Services />
|
<Services />
|
||||||
<News />
|
<News items={displayNews} />
|
||||||
<ContactCta />
|
<ContactCta />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { Reveal } from "@/components/Reveal";
|
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 (
|
return (
|
||||||
<section id="news" className="bg-sand py-16 lg:py-24">
|
<section id="news" className="bg-sand py-16 lg:py-24">
|
||||||
<div className="mx-auto max-w-[1400px] px-5 lg:px-10">
|
<div className="mx-auto max-w-[1400px] px-5 lg:px-10">
|
||||||
@@ -41,7 +43,7 @@ export function News() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="mt-16 grid gap-8 md:grid-cols-3 lg:gap-10">
|
<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}>
|
<Reveal as="li" key={item.slug} delay={i * 110}>
|
||||||
<Link href={`/news/${item.slug}`} className="group block h-full">
|
<Link href={`/news/${item.slug}`} className="group block h-full">
|
||||||
<article className="flex h-full flex-col bg-paper">
|
<article className="flex h-full flex-col bg-paper">
|
||||||
@@ -52,7 +54,6 @@ export function News() {
|
|||||||
fill
|
fill
|
||||||
quality={80}
|
quality={80}
|
||||||
sizes="(max-width: 768px) 100vw, 33vw"
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
placeholder="blur"
|
|
||||||
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.07]"
|
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" />
|
<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 { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Reveal } from "@/components/Reveal";
|
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. */
|
/** 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 trackRef = useRef<HTMLUListElement>(null);
|
||||||
const [atStart, setAtStart] = useState(true);
|
const [atStart, setAtStart] = useState(true);
|
||||||
const [atEnd, setAtEnd] = useState(false);
|
const [atEnd, setAtEnd] = useState(false);
|
||||||
@@ -92,7 +98,6 @@ export function NewsCarousel({ items, basePath }: { items: NewsItem[]; basePath:
|
|||||||
fill
|
fill
|
||||||
quality={80}
|
quality={80}
|
||||||
sizes="(max-width: 640px) 82vw, (max-width: 1024px) 52vw, 26rem"
|
sizes="(max-width: 640px) 82vw, (max-width: 1024px) 52vw, 26rem"
|
||||||
placeholder="blur"
|
|
||||||
loading={i < 2 ? "eager" : "lazy"}
|
loading={i < 2 ? "eager" : "lazy"}
|
||||||
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.06]"
|
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 { Reveal } from "@/components/Reveal";
|
||||||
import { Pagination } from "@/components/shared/Pagination";
|
import { Pagination } from "@/components/shared/Pagination";
|
||||||
import type { NewsItem } from "@/data/site";
|
import type { DisplayNewsItem } from "@/lib/api";
|
||||||
|
|
||||||
const PAGE_SIZE = 9;
|
const PAGE_SIZE = 9;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export function NewsListing({
|
|||||||
basePath,
|
basePath,
|
||||||
emptyMessage,
|
emptyMessage,
|
||||||
}: {
|
}: {
|
||||||
items: NewsItem[];
|
items: DisplayNewsItem[];
|
||||||
page: number;
|
page: number;
|
||||||
basePath: string;
|
basePath: string;
|
||||||
emptyMessage: string;
|
emptyMessage: string;
|
||||||
@@ -44,7 +44,6 @@ export function NewsListing({
|
|||||||
fill
|
fill
|
||||||
quality={80}
|
quality={80}
|
||||||
sizes="(max-width: 768px) 100vw, 33vw"
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
placeholder="blur"
|
|
||||||
loading={i < 3 ? "eager" : "lazy"}
|
loading={i < 3 ? "eager" : "lazy"}
|
||||||
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.07]"
|
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.07]"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function PortfolioBrowser({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveId(ALL_ID)}
|
onClick={() => setActiveId(ALL_ID)}
|
||||||
aria-current={activeId === 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"
|
activeId === ALL_ID ? "text-ink" : "text-muted hover:text-ink"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -70,7 +70,7 @@ export function PortfolioBrowser({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveId(cat.id)}
|
onClick={() => setActiveId(cat.id)}
|
||||||
aria-current={activeId === 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"
|
activeId === cat.id ? "text-ink" : "text-muted hover:text-ink"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function Editorial({
|
|||||||
{title && (
|
{title && (
|
||||||
<Reveal delay={90}>
|
<Reveal delay={90}>
|
||||||
<h2
|
<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}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ import pania from "@/assets/images/projects/pania.jpg";
|
|||||||
import ghasr3 from "@/assets/images/projects/ghasr-3.jpg";
|
import ghasr3 from "@/assets/images/projects/ghasr-3.jpg";
|
||||||
import goldenGhasr from "@/assets/images/projects/golden-ghasr.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 = {
|
export const site = {
|
||||||
name: "گروه ساختمانی رئوفی",
|
name: "گروه ساختمانی رئوفی",
|
||||||
tagline: "سازنده رویاهای شما",
|
tagline: "سازنده رویاهای شما",
|
||||||
@@ -385,33 +381,3 @@ export const blogListingPage = {
|
|||||||
emptyMessage: "هنوز مقالهای منتشر نشده است.",
|
emptyMessage: "هنوز مقالهای منتشر نشده است.",
|
||||||
} as const;
|
} 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,
|
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