feat: add featured posts grid and update blog terminology

- Introduced a new FeaturedPostsGrid component to display featured posts on the homepage.
- Updated the Home component to fetch and render featured posts from the WordPress API.
- Changed instances of "بلاگ" to "دانشنامه" across various components for consistent terminology.
This commit is contained in:
2026-02-08 20:33:07 +03:30
parent 0e2ea9b85b
commit f7a163c4aa
7 changed files with 208 additions and 8 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
className="text-muted-foreground hover:text-foreground text-sm inline-flex items-center gap-1 transition-colors"
>
<ChevronRight className="size-4" aria-hidden />
بلاگ
دانشنامه
</Link>
</nav>
<h1
+2 -2
View File
@@ -9,7 +9,7 @@ import { ChevronRight, ChevronLeft, Search, X } from "lucide-react";
const PER_PAGE = 9;
export const metadata: Metadata = {
title: "بلاگ",
title: "دانشنامه",
description: "مقالات و مطالب فیپ در زمینه یکپارچه‌سازی، اتوماسیون و بهره‌وری سازمانی.",
};
@@ -69,7 +69,7 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
<div className="text-center space-y-5">
<div className="space-y-1.5">
<h1 className="text-3xl font-bold md:text-4xl tracking-tight">
بلاگ
دانشنامه
</h1>
<p className="text-sm md:text-base text-muted-foreground max-w-xl mx-auto">
مقالات و مطالب فیپ در زمینه یکپارچه‌سازی، اتوماسیون و بهره‌وری سازمانی.
+21 -2
View File
@@ -1,14 +1,33 @@
import ContentSection from "@/components/content-1";
import Features from "@/components/features-12";
import { FeaturedPostsGrid } from "@/components/featured-posts-grid";
import { getPosts } from "@/lib/wp-api";
import { HeroHeader } from "@/components/header";
import HeroSection from "@/components/hero-section";
import Image from "next/image";
export default function Home() {
const FEATURED_CATEGORY_ID = 3;
const FEATURED_POSTS_COUNT = 4;
export default async function Home() {
let featuredPosts: Awaited<ReturnType<typeof getPosts>>["posts"] = [];
try {
const result = await getPosts({
categoryId: FEATURED_CATEGORY_ID,
perPage: FEATURED_POSTS_COUNT,
orderby: "date",
order: "desc",
});
featuredPosts = result.posts;
} catch {
// بدون پست هم صفحه رندر می‌شود
}
return (
<>
<HeroSection />
<Features />
<ContentSection />
<FeaturedPostsGrid posts={featuredPosts} />
</>
);
}
+176
View File
@@ -0,0 +1,176 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { ChevronLeft, ImageIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { getFeaturedImageUrl, type WpPost } from "@/lib/wp-api";
import { formatDateShamsi } from "@/lib/format-date";
import { cn } from "@/lib/utils";
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, "").trim();
}
/** کارت با تصویر به‌صورت پس‌زمینه و عنوان + توضیح در پایین با overlay */
function PostCard({
post,
size = "default",
}: {
post: WpPost;
size?: "large" | "default";
}) {
const excerpt =
stripHtml(post.excerpt.rendered) ||
stripHtml(post.content.rendered).slice(0, size === "large" ? 280 : 120);
const date = post.date ? formatDateShamsi(post.date) : null;
const authorName = post._embedded?.author?.[0]?.name ?? null;
const thumbnailUrl = getFeaturedImageUrl(post, size === "large" ? "medium_large" : "medium");
const href = `/blog/${encodeURIComponent(post.slug)}`;
return (
<Link
href={href}
className={cn(
"group relative flex overflow-hidden rounded-xl border border-border shadow-sm transition-all duration-300 hover:shadow-lg hover:border-border/80",
size === "large" ? "min-h-[420px]" : "min-h-[200px]"
)}
>
{/* پس‌زمینه: تصویر تمام کارت */}
{thumbnailUrl ? (
<Image
src={thumbnailUrl}
alt=""
fill
className="object-cover transition-transform duration-500 ease-out group-hover:scale-105"
sizes={size === "large" ? "50vw, 600px" : "25vw, 300px"}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center bg-muted" aria-hidden>
<ImageIcon className="size-12 text-muted-foreground/40" />
</div>
)}
{/* گرادیان شفاف و متن در پایین */}
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent" />
<div
className="absolute bottom-0 left-0 right-0 p-4 text-white transition-all duration-300 md:-bottom-16 md:p-5 md:[mask-image:linear-gradient(to_bottom,black_0%,transparent)] md:[mask-size:100%_100%] group-hover:bottom-0 group-hover:[mask-image:none]"
>
<h3
className={cn(
"font-bold leading-tight line-clamp-2 drop-shadow-sm",
size === "large" ? "text-lg md:text-xl" : "text-base"
)}
dangerouslySetInnerHTML={{ __html: post.title.rendered }}
/>
<p
className={cn(
"mt-2 text-white/90 leading-relaxed line-clamp-2",
size === "large" ? "text-sm md:text-base line-clamp-3" : "text-sm"
)}
>
{excerpt}
</p>
</div>
</Link>
);
}
/** کارت ردیفی: تصویر کنار عنوان، بدون توضیح */
function PostCardRow({ post }: { post: WpPost }) {
const date = post.date ? formatDateShamsi(post.date) : null;
const authorName = post._embedded?.author?.[0]?.name ?? null;
const thumbnailUrl = getFeaturedImageUrl(post, "medium");
const href = `/blog/${encodeURIComponent(post.slug)}`;
return (
<Link
href={href}
className="group flex min-h-[70px] overflow-hidden rounded-xl transition-all duration-300"
>
{/* نیمه عنوان + نویسنده */}
<div className="flex flex-1 flex-col justify-center p-4">
<h3
className="font-bold leading-tight text-foreground line-clamp-2 text-base"
dangerouslySetInnerHTML={{ __html: post.title.rendered }}
/>
</div>
</Link>
);
}
const DEFAULT_TITLE = "از دانشنامه";
const DEFAULT_CAPTION =
"از داده تا بینش؛ یادداشت‌ها و تجربه‌هایی که مسیر شما را روشن‌تر می‌کنند.";
const DEFAULT_CTA_LABEL = "مشاهده همه مقالات";
const DEFAULT_CTA_HREF = "/blog";
interface FeaturedPostsGridProps {
posts: WpPost[];
/** عنوان بخش */
title?: string;
/** کپشن زیر عنوان */
caption?: string;
/** متن دکمه CTA */
ctaLabel?: string;
/** لینک CTA (پیش‌فرض: /blog) */
ctaHref?: string;
}
export function FeaturedPostsGrid({
posts,
title = DEFAULT_TITLE,
caption = DEFAULT_CAPTION,
ctaLabel = DEFAULT_CTA_LABEL,
ctaHref = DEFAULT_CTA_HREF,
}: FeaturedPostsGridProps) {
if (!posts.length) return null;
const [first, second, third, fourth] = posts;
return (
<section className="bg-background py-16 md:py-24" aria-label={title}>
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<header className="mb-10 text-center md:mb-12">
<h2 className="text-2xl font-bold text-foreground md:text-3xl">
{title}
</h2>
<p className="mt-3 max-w-2xl mx-auto text-muted-foreground text-sm leading-relaxed md:text-base">
{caption}
</p>
</header>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 lg:gap-5">
{/* کارت بزرگ: در RTL سمت چپ، در LTR سمت راست */}
{first && (
<div className="lg:col-span-2 lg:min-h-0">
<PostCard post={first} size="large" />
</div>
)}
{/* سه کارت کوچک: یک بالا، دو پایین */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:order-1 lg:grid-cols-1 lg:gap-5">
{second && (
<div className="min-h-[200px]">
<PostCard post={second} size="default" />
</div>
)}
{third && (
<div className="min-h-[120px]">
<PostCard post={third} size="default" />
</div>
)}
</div>
</div>
<div className="mt-12 flex flex-col items-center justify-center gap-3 text-center md:mt-16">
<p className="text-sm text-muted-foreground">
مطالب بیشتر در دانشنامه
</p>
<Button asChild size="lg" variant="outline" className="rounded-full px-6 font-medium shadow-sm transition-all hover:shadow-md">
<Link href={ctaHref} className="inline-flex items-center gap-2">
{ctaLabel}
<ChevronLeft className="size-4" aria-hidden />
</Link>
</Button>
</div>
</div>
</section>
);
}
+1 -1
View File
@@ -7,7 +7,7 @@ const links = [
href: "/about",
},
{
title: "بلاگ",
title: "دانشنامه",
href: "/blog",
},
{
+1 -1
View File
@@ -10,7 +10,7 @@ import { ThemeToggle } from '@/components/theme-toggle'
const menuItems = [
{ name: 'صفحه اصلی', href: '/' },
{ name: 'بلاگ', href: '/blog' },
{ name: 'دانشنامه', href: '/blog' },
{ name: 'راه حل‌ها', href: '/solutions' },
// { name: 'قیمت‌گذاری', href: '/pricing' },
{ name: 'درباره ما', href: '/about' },
+6 -1
View File
@@ -71,6 +71,9 @@ export interface GetPostsOptions {
perPage?: number;
search?: string;
categoryId?: number;
/** ترتیب بر اساس: date, modified, title, ... در وردپرس. برای اولویت دستی (menu_order) پلاگین/فیلتر در وردپرس لازم است. */
orderby?: string;
order?: "asc" | "desc";
}
async function fetchWithTimeout(
@@ -96,12 +99,14 @@ async function fetchWithTimeout(
export async function getPosts(
options: GetPostsOptions = {}
): Promise<GetPostsResult> {
const { page = 1, perPage = 9, search, categoryId } = options;
const { page = 1, perPage = 9, search, categoryId, orderby, order } = options;
const params = new URLSearchParams();
params.set("page", String(page));
params.set("per_page", String(perPage));
if (search?.trim()) params.set("search", search.trim());
if (categoryId) params.set("categories", String(categoryId));
if (orderby) params.set("orderby", orderby);
if (order) params.set("order", order);
params.set("_embed", "1");
const url = `${WP_API_BASE}/posts?${params.toString()}`;