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:
@@ -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
@@ -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
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const links = [
|
||||
href: "/about",
|
||||
},
|
||||
{
|
||||
title: "بلاگ",
|
||||
title: "دانشنامه",
|
||||
href: "/blog",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
@@ -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()}`;
|
||||
|
||||
Reference in New Issue
Block a user