new create

This commit is contained in:
amirhosein.ashourloo
2026-08-09 15:35:49 +03:30
parent 6207bb8431
commit 95c3c74276
11 changed files with 678 additions and 8 deletions
+4 -1
View File
@@ -5,10 +5,13 @@ const nextConfig: NextConfig = {
formats: ["image/avif", "image/webp"],
// Every quality value used across the page. Required from Next.js 16 on.
qualities: [70, 72, 75, 80, 82, 88, 90],
// Portfolio media served live from the meshkee API.
// Portfolio media served live from the meshkee API. The storage host has
// already changed once mid-project (parminstorage.ir -> parspack.net) —
// keep both around since either could be what a given item still points to.
remotePatterns: [
{ protocol: "https", hostname: "meshkee-storage.sas.amin.parminstorage.ir" },
{ protocol: "https", hostname: "cdn.meshkee.com" },
{ protocol: "https", hostname: "c804387.parspack.net" },
],
},
};
+68
View File
@@ -0,0 +1,68 @@
import type { Metadata } from "next";
import { NewsListing } from "@/components/news/NewsListing";
import { Editorial } from "@/components/shared/Editorial";
import { PageCta } from "@/components/shared/PageCta";
import { PageHero } from "@/components/shared/PageHero";
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";
export const metadata: Metadata = {
title: "بلاگ",
description: "فهرست کامل مقالات منتشرشده‌ی گروه ساختمانی رئوفی.",
};
export default async function BlogListPage({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}) {
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
return (
<>
<SiteHeader />
<main>
<PageHero
image={heroImage}
imageMobile={heroImageMobile}
caption={blogListingPage.hero.caption}
title={blogListingPage.hero.title}
breadcrumb={blogListingPage.hero.breadcrumb}
/>
<section className="bg-paper">
<Editorial
caption={blogListingPage.overview.caption}
captionLatin={blogListingPage.overview.captionLatin}
title={blogListingPage.overview.title}
body={blogListingPage.overview.body}
compact
/>
</section>
<section className="bg-sand pb-16 lg:pb-24">
<NewsListing
items={blog}
page={page}
basePath="/blog"
emptyMessage={blogListingPage.emptyMessage}
/>
</section>
<PageCta
caption={newsAndBlogPage.cta.caption}
title={newsAndBlogPage.cta.title}
action={newsAndBlogPage.cta.action}
/>
</main>
<SiteFooter />
</>
);
}
+112
View File
@@ -0,0 +1,112 @@
import type { Metadata } from "next";
import Link from "next/link";
import heroImage from "@/assets/images/hero/hero-1.jpg";
import heroImageMobile from "@/assets/images/hero/hero-1-mobile.jpg";
import { Reveal } from "@/components/Reveal";
import { NewsCarousel } from "@/components/news/NewsCarousel";
import { Editorial } from "@/components/shared/Editorial";
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";
export const metadata: Metadata = {
title: "مقالات و اخبار",
description: "آخرین اخبار، رویدادها و مقالات گروه ساختمانی رئوفی.",
};
export default function NewsAndBlogPage() {
const hasNews = news.length > 0;
const hasBlog = blog.length > 0;
const hasNothing = !hasNews && !hasBlog;
return (
<>
<SiteHeader />
<main>
<PageHero
image={heroImage}
imageMobile={heroImageMobile}
caption={newsAndBlogPage.hero.caption}
title={newsAndBlogPage.hero.title}
breadcrumb={newsAndBlogPage.hero.breadcrumb}
/>
<section className="bg-paper">
<Editorial
caption={newsAndBlogPage.overview.caption}
captionLatin={newsAndBlogPage.overview.captionLatin}
title={newsAndBlogPage.overview.title}
body={newsAndBlogPage.overview.body}
compact
/>
</section>
{hasNothing && (
<section className="bg-sand py-16 lg:py-24">
<p className="text-center text-muted">{newsAndBlogPage.emptyMessage}</p>
</section>
)}
{hasNews && (
<section className="bg-sand py-16 lg:py-24">
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<p className="kicker mb-10 justify-center">
{newsAndBlogPage.news.caption}
<span className="latin">{newsAndBlogPage.news.captionLatin}</span>
</p>
</Reveal>
</div>
<NewsCarousel items={news} basePath="/news" />
<div className="mx-auto mt-10 max-w-[1400px] px-6 text-center lg:px-12">
<Link
href="/news"
className="inline-block rounded-full bg-gold px-9 py-3.5 text-white transition-colors duration-300 hover:bg-ink"
>
{newsAndBlogPage.news.action}
</Link>
</div>
</section>
)}
{hasBlog && (
<section className={`py-16 lg:py-24 ${hasNews ? "bg-paper" : "bg-sand"}`}>
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<p className="kicker mb-10 justify-center">
{newsAndBlogPage.blog.caption}
<span className="latin">{newsAndBlogPage.blog.captionLatin}</span>
</p>
</Reveal>
</div>
<NewsCarousel items={blog} basePath="/blog" />
<div className="mx-auto mt-10 max-w-[1400px] px-6 text-center lg:px-12">
<Link
href="/blog"
className="inline-block rounded-full bg-gold px-9 py-3.5 text-white transition-colors duration-300 hover:bg-ink"
>
{newsAndBlogPage.blog.action}
</Link>
</div>
</section>
)}
<PageCta
caption={newsAndBlogPage.cta.caption}
title={newsAndBlogPage.cta.title}
action={newsAndBlogPage.cta.action}
/>
</main>
<SiteFooter />
</>
);
}
+68
View File
@@ -0,0 +1,68 @@
import type { Metadata } from "next";
import { NewsListing } from "@/components/news/NewsListing";
import { Editorial } from "@/components/shared/Editorial";
import { PageCta } from "@/components/shared/PageCta";
import { PageHero } from "@/components/shared/PageHero";
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";
export const metadata: Metadata = {
title: "اخبار",
description: "فهرست کامل اخبار و رویدادهای منتشرشده‌ی گروه ساختمانی رئوفی.",
};
export default async function NewsListPage({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}) {
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
return (
<>
<SiteHeader />
<main>
<PageHero
image={heroImage}
imageMobile={heroImageMobile}
caption={newsListingPage.hero.caption}
title={newsListingPage.hero.title}
breadcrumb={newsListingPage.hero.breadcrumb}
/>
<section className="bg-paper">
<Editorial
caption={newsListingPage.overview.caption}
captionLatin={newsListingPage.overview.captionLatin}
title={newsListingPage.overview.title}
body={newsListingPage.overview.body}
compact
/>
</section>
<section className="bg-sand pb-16 lg:pb-24">
<NewsListing
items={news}
page={page}
basePath="/news"
emptyMessage={newsListingPage.emptyMessage}
/>
</section>
<PageCta
caption={newsAndBlogPage.cta.caption}
title={newsAndBlogPage.cta.title}
action={newsAndBlogPage.cta.action}
/>
</main>
<SiteFooter />
</>
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ export function News() {
<Reveal delay={140}>
<Link
href="/news"
href="/news-and-blog"
className="group flex items-center gap-3 border-b border-ink/20 pb-2 text-ink transition-colors duration-300 hover:border-gold hover:text-gold"
>
آرشیو مقالات و اخبار
+130
View File
@@ -0,0 +1,130 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { Reveal } from "@/components/Reveal";
import type { NewsItem } from "@/data/site";
/** Horizontal snap-scroll rail of news/blog cards, matching the site's other carousels. */
export function NewsCarousel({ items, basePath }: { items: NewsItem[]; basePath: string }) {
const trackRef = useRef<HTMLUListElement>(null);
const [atStart, setAtStart] = useState(true);
const [atEnd, setAtEnd] = useState(false);
const syncEdges = useCallback(() => {
const el = trackRef.current;
if (!el) return;
const travelled = Math.abs(el.scrollLeft);
const max = el.scrollWidth - el.clientWidth;
setAtStart(travelled < 8);
setAtEnd(travelled >= max - 8);
}, []);
useEffect(() => {
syncEdges();
const el = trackRef.current;
if (!el) return;
el.addEventListener("scroll", syncEdges, { passive: true });
window.addEventListener("resize", syncEdges);
return () => {
el.removeEventListener("scroll", syncEdges);
window.removeEventListener("resize", syncEdges);
};
}, [syncEdges]);
const step = (direction: "next" | "prev") => {
const el = trackRef.current;
if (!el) return;
const card = el.querySelector("li");
const amount = card ? card.getBoundingClientRect().width + 24 : el.clientWidth * 0.8;
el.scrollBy({ left: direction === "next" ? -amount : amount, behavior: "smooth" });
};
const showArrows = items.length > 3;
return (
<div>
{showArrows && (
<div className="mx-auto mb-6 flex max-w-[1400px] justify-end gap-2 px-6 lg:px-12">
<button
type="button"
onClick={() => step("prev")}
disabled={atStart}
aria-label="اخبار قبلی"
className="flex h-11 w-11 items-center justify-center rounded-full border border-line text-ink transition-colors duration-300 hover:border-gold hover:bg-gold hover:text-white disabled:pointer-events-none disabled:opacity-30"
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path d="M5 12h14m0 0-6-6m6 6-6 6" stroke="currentColor" strokeWidth="1.6" />
</svg>
</button>
<button
type="button"
onClick={() => step("next")}
disabled={atEnd}
aria-label="اخبار بعدی"
className="flex h-11 w-11 items-center justify-center rounded-full border border-line text-ink transition-colors duration-300 hover:border-gold hover:bg-gold hover:text-white disabled:pointer-events-none disabled:opacity-30"
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.6" />
</svg>
</button>
</div>
)}
<ul
ref={trackRef}
className="no-scrollbar flex snap-x snap-mandatory gap-6 overflow-x-auto scroll-smooth px-6 pb-2 lg:gap-8 lg:px-12"
>
{items.map((item, i) => (
<li
key={item.slug}
className="w-[82vw] shrink-0 snap-start sm:w-[52vw] lg:w-[34vw] xl:w-[26rem]"
>
<Reveal delay={(i % 4) * 80}>
<Link href={`${basePath}/${item.slug}`} className="group block h-full">
<article className="flex h-full flex-col bg-paper">
<div className="relative aspect-[16/11] overflow-hidden">
<Image
src={item.image}
alt={item.title}
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]"
/>
<div className="absolute inset-0 bg-ink/0 transition-colors duration-500 group-hover:bg-ink/20" />
</div>
<div className="flex flex-1 flex-col p-7 lg:p-8">
<time className="flex items-center gap-2.5 text-[0.8rem] text-muted">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5 text-gold">
<rect x="3.5" y="5" width="17" height="15.5" rx="2" stroke="currentColor" strokeWidth="1.4" />
<path d="M3.5 10h17M8 3.5V6M16 3.5V6" stroke="currentColor" strokeWidth="1.4" />
</svg>
{item.date}
</time>
<h3 className="mt-4 text-[1.1rem] leading-[1.75] font-bold text-ink transition-colors duration-300 group-hover:text-gold">
{item.title}
</h3>
<span className="mt-auto flex items-center gap-2 pt-7 text-sm text-muted transition-colors duration-300 group-hover:text-gold">
ادامه مطلب
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-x-1">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.8" />
</svg>
</span>
</div>
</article>
</Link>
</Reveal>
</li>
))}
</ul>
</div>
);
}
+88
View File
@@ -0,0 +1,88 @@
import Image from "next/image";
import Link from "next/link";
import { Reveal } from "@/components/Reveal";
import { Pagination } from "@/components/shared/Pagination";
import type { NewsItem } from "@/data/site";
const PAGE_SIZE = 9;
/**
* Grid + pagination for a full news or blog archive. Used by both /news and
* /blog so the two keep identical rules — only the item list, base path and
* empty-state copy differ between them.
*/
export function NewsListing({
items,
page,
basePath,
emptyMessage,
}: {
items: NewsItem[];
page: number;
basePath: string;
emptyMessage: string;
}) {
const totalPages = Math.max(1, Math.ceil(items.length / PAGE_SIZE));
const currentPage = Math.min(totalPages, Math.max(1, page));
const pageItems = items.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
return (
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
{pageItems.length === 0 ? (
<p className="py-16 text-center text-muted">{emptyMessage}</p>
) : (
<ul className="grid gap-8 md:grid-cols-3 lg:gap-10">
{pageItems.map((item, i) => (
<Reveal as="li" key={item.slug} delay={(i % 6) * 70}>
<Link href={`${basePath}/${item.slug}`} className="group block h-full">
<article className="flex h-full flex-col bg-paper">
<div className="relative aspect-[16/11] overflow-hidden">
<Image
src={item.image}
alt={item.title}
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]"
/>
<div className="absolute inset-0 bg-ink/0 transition-colors duration-500 group-hover:bg-ink/20" />
</div>
<div className="flex flex-1 flex-col p-7 lg:p-8">
<time className="flex items-center gap-2.5 text-[0.8rem] text-muted">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5 text-gold">
<rect x="3.5" y="5" width="17" height="15.5" rx="2" stroke="currentColor" strokeWidth="1.4" />
<path d="M3.5 10h17M8 3.5V6M16 3.5V6" stroke="currentColor" strokeWidth="1.4" />
</svg>
{item.date}
</time>
<h3 className="mt-4 text-[1.22rem] leading-[1.75] font-bold text-ink transition-colors duration-300 group-hover:text-gold">
{item.title}
</h3>
<span className="mt-auto flex items-center gap-2 pt-7 text-sm text-muted transition-colors duration-300 group-hover:text-gold">
ادامه مطلب
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-x-1">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.8" />
</svg>
</span>
</div>
</article>
</Link>
</Reveal>
))}
</ul>
)}
<Pagination
page={currentPage}
totalPages={totalPages}
hrefFor={(p) => `${basePath}?page=${p}`}
/>
</div>
);
}
+16 -3
View File
@@ -2,12 +2,14 @@
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Reveal } from "@/components/Reveal";
import { Pagination } from "@/components/shared/Pagination";
import type { DisplayProject, PortfolioCategory } from "@/lib/api";
const ALL_ID = "__all__";
const PAGE_SIZE = 9;
/**
* Category tabs above a grid — no search, click a tab to filter in place.
@@ -25,6 +27,7 @@ export function PortfolioBrowser({
categories: PortfolioCategory[];
}) {
const [activeId, setActiveId] = useState(ALL_ID);
const [page, setPage] = useState(1);
const showTabs = categories.length > 1;
const filtered = useMemo(
@@ -32,6 +35,12 @@ export function PortfolioBrowser({
[projects, activeId],
);
// Switching category with a stale page number could land on an empty page.
useEffect(() => setPage(1), [activeId]);
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const pageItems = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
return (
<div>
{showTabs && (
@@ -84,8 +93,8 @@ export function PortfolioBrowser({
پروژهای در این دسته یافت نشد.
</p>
) : (
<ul className="mx-auto grid max-w-[1400px] gap-6 px-6 pb-24 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:px-12 lg:pb-32">
{filtered.map((project, i) => (
<ul className="mx-auto grid max-w-[1400px] gap-6 px-6 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:px-12">
{pageItems.map((project, i) => (
<Reveal as="li" key={project.slug} delay={(i % 6) * 70}>
<Link href={`/portfolios/${project.slug}`} className="group block">
<div className="relative aspect-[3/4] overflow-hidden bg-ink">
@@ -126,6 +135,10 @@ export function PortfolioBrowser({
))}
</ul>
)}
<div className="mx-auto max-w-[1400px] px-6 pb-24 lg:px-12 lg:pb-32">
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
</div>
</div>
);
}
@@ -115,7 +115,7 @@ export function ProjectGalleryLightbox({ images, alt }: { images: string[]; alt:
showPrev();
}}
aria-label="تصویر قبلی"
className="absolute end-3 top-1/2 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full text-white/80 transition-colors hover:bg-white/10 hover:text-white sm:end-6"
className="absolute start-3 top-1/2 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full text-white/80 transition-colors hover:bg-white/10 hover:text-white sm:start-6"
>
<ChevronIcon direction="prev" className="h-6 w-6" />
</button>
@@ -126,7 +126,7 @@ export function ProjectGalleryLightbox({ images, alt }: { images: string[]; alt:
showNext();
}}
aria-label="تصویر بعدی"
className="absolute start-3 top-1/2 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full text-white/80 transition-colors hover:bg-white/10 hover:text-white sm:start-6"
className="absolute end-3 top-1/2 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full text-white/80 transition-colors hover:bg-white/10 hover:text-white sm:end-6"
>
<ChevronIcon direction="next" className="h-6 w-6" />
</button>
+94
View File
@@ -0,0 +1,94 @@
import Link from "next/link";
function ChevronIcon({ direction, className }: { direction: "next" | "prev"; className?: string }) {
const d = direction === "next" ? "M19 12H5m0 0 6-6m-6 6 6 6" : "M5 12h14m0 0-6-6m6 6-6 6";
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path d={d} stroke="currentColor" strokeWidth="1.6" />
</svg>
);
}
type PaginationProps = {
page: number;
totalPages: number;
} & (
| { hrefFor: (page: number) => string; onPageChange?: never }
| { onPageChange: (page: number) => void; hrefFor?: never }
);
/**
* Numbered pagination, RTL-aware. Works two ways: give it `hrefFor` for a
* plain server-rendered listing (real links, works without JS), or
* `onPageChange` for a page that already manages its own list client-side
* (e.g. alongside a category filter).
*
* Renders nothing when everything already fits on one page — there's no
* reason to show page controls for content that doesn't need paging yet.
*/
export function Pagination({ page, totalPages, hrefFor, onPageChange }: PaginationProps) {
if (totalPages <= 1) return null;
const prevPage = Math.max(1, page - 1);
const nextPage = Math.min(totalPages, page + 1);
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
const controlClass = (current: boolean, disabled = false) =>
`flex h-11 min-w-11 items-center justify-center rounded-full px-3 text-sm transition-colors duration-300 ${
current
? "bg-gold text-white"
: disabled
? "pointer-events-none text-muted/30"
: "text-ink hover:bg-sand"
}`;
const renderItem = (target: number, content: React.ReactNode, ariaLabel: string, current = false) => {
if (hrefFor) {
return (
<Link
key={ariaLabel}
href={hrefFor(target)}
aria-label={ariaLabel}
aria-current={current}
className={controlClass(current)}
>
{content}
</Link>
);
}
return (
<button
key={ariaLabel}
type="button"
onClick={() => onPageChange(target)}
aria-label={ariaLabel}
aria-current={current}
className={controlClass(current)}
>
{content}
</button>
);
};
return (
<nav aria-label="صفحه‌بندی" className="mt-14 flex items-center justify-center gap-2 lg:mt-20">
{page === 1 ? (
<span className={controlClass(false, true)}>
<ChevronIcon direction="prev" className="h-4 w-4" />
</span>
) : (
renderItem(prevPage, <ChevronIcon direction="prev" className="h-4 w-4" />, "صفحه قبل")
)}
{pages.map((p) => renderItem(p, p, `صفحه ${p}`, p === page))}
{page === totalPages ? (
<span className={controlClass(false, true)}>
<ChevronIcon direction="next" className="h-4 w-4" />
</span>
) : (
renderItem(nextPage, <ChevronIcon direction="next" className="h-4 w-4" />, "صفحه بعد")
)}
</nav>
);
}
+95 -1
View File
@@ -31,7 +31,7 @@ export const nav = [
{ label: "صفحه اصلی", href: "/" },
{ label: "خدمات ما", href: "/our-services" },
{ label: "پروژه ها", href: "/portfolios" },
{ label: "مقالات و اخبار", href: "/news" },
{ label: "مقالات و اخبار", href: "/news-and-blog" },
{ label: "درباره ما", href: "/aboutus" },
{ label: "تماس با ما", href: "/contactus" },
] as const;
@@ -294,6 +294,97 @@ export const projectVideos: Record<string, string> = {
"item-old-589": "https://www.aparat.com/v/dnem1b3",
};
/* ---------------------------------------------------------------------------
* News & blog
*
* The meshkee API has a real `/blogs` endpoint with a `type` filter
* (news/article/blog), but it currently returns zero items and the docs
* don't spell out the item's field names — nothing to verify a live
* integration against yet. `news` below is real, published content (scraped
* from the live site, not invented); `blog` is genuinely empty because the
* client has no blog content anywhere yet, not because it was left out.
* Wire both up to live `/blogs?type=...` fetches once there's real content
* to check the field shape against.
* ------------------------------------------------------------------------- */
export const newsAndBlogPage = {
hero: {
caption: "NEWS & MEDIA",
title: "مقالات و اخبار",
breadcrumb: [
{ label: "صفحه اصلی", href: "/" },
{ label: "مقالات و اخبار", href: "/news-and-blog" },
],
},
overview: {
caption: "مقالات و اخبار",
captionLatin: "NEWS & MEDIA",
title: "آخرین رویدادهای گروه رئوفی",
body: [
"از افتتاحیه‌ی پروژه‌ها تا مراسم و بروزرسانی‌های ساخت‌وساز؛ در ادامه تازه‌ترین اخبار و مقالات منتشرشده را می‌بینید.",
],
},
news: {
caption: "اخبار",
captionLatin: "NEWS",
action: "مشاهده همه اخبار",
},
blog: {
caption: "بلاگ",
captionLatin: "BLOG",
action: "مشاهده همه مقالات",
},
emptyMessage: "هیچ مقاله یا خبری ثبت نشده است.",
cta: {
caption: "STAY UPDATED",
title: "از پروژه‌های تازه‌ی رئوفی باخبر شوید",
action: "تماس با ما",
},
} as const;
export const newsListingPage = {
hero: {
caption: "NEWS",
title: "اخبار",
breadcrumb: [
{ label: "صفحه اصلی", href: "/" },
{ label: "مقالات و اخبار", href: "/news-and-blog" },
{ label: "اخبار", href: "/news" },
],
},
overview: {
caption: "اخبار",
captionLatin: "NEWS",
title: "آرشیو اخبار",
body: ["فهرست کامل اخبار و رویدادهای منتشرشده‌ی گروه رئوفی."],
},
emptyMessage: "هنوز خبری منتشر نشده است.",
} as const;
export const blogListingPage = {
hero: {
caption: "BLOG",
title: "بلاگ",
breadcrumb: [
{ label: "صفحه اصلی", href: "/" },
{ label: "مقالات و اخبار", href: "/news-and-blog" },
{ label: "بلاگ", href: "/blog" },
],
},
overview: {
caption: "بلاگ",
captionLatin: "BLOG",
title: "آرشیو مقالات",
body: ["فهرست کامل مقالات منتشرشده‌ی گروه رئوفی."],
},
emptyMessage: "هنوز مقاله‌ای منتشر نشده است.",
} as const;
export type NewsItem = {
slug: string;
title: string;
@@ -321,3 +412,6 @@ export const news: NewsItem[] = [
image: hayatFoundation,
},
];
/** No blog posts published anywhere yet — genuinely empty, not left out. */
export const blog: NewsItem[] = [];