feat: enhance blog functionality with new components and styles

- Added styles for WordPress table and button blocks in globals.css.
- Updated layout.tsx to include a favicon.
- Improved blog page layout and search functionality, including new search and filter icons.
- Implemented related posts feature in blog post pages with new BlogBlockquote and BlogPullquote components.
- Enhanced blog card display with a new variant for smaller cards.
- Updated blog collapse sections for better user interaction.
- Added new utility functions for extracting block data from WordPress content.
This commit is contained in:
2026-02-05 17:58:28 +03:30
parent bfb967fb43
commit aa6f3c9b35
13 changed files with 386 additions and 69 deletions
+33 -5
View File
@@ -2,14 +2,18 @@ import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
import { notFound } from "next/navigation";
import { getPostBySlug, getFeaturedImageUrl } from "@/lib/wp-api";
import { getPostBySlug, getFeaturedImageUrl, getRelatedPosts } from "@/lib/wp-api";
import { formatDateShamsi } from "@/lib/format-date";
import { ChevronRight } from "lucide-react";
import type { CollapseBlockPayload, BlockquoteBlockPayload } from "@/lib/blog-blocks";
import { parseContentHeadings, getReadingTimeMinutes } from "@/lib/blog-utils";
import { BlogToc } from "@/components/blog-toc";
import { BlogPostMeta } from "@/components/blog-post-meta";
import { BlogCollapseSection } from "@/components/blog-collapse-section";
import { BlogBlockquote } from "@/components/blog-blockquote";
import { BlogPullquote } from "@/components/blog-pullquote";
import { ScrollToTop } from "@/components/scroll-to-top";
import { BlogPostCard } from "@/components/blog-post-card";
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
@@ -39,6 +43,13 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
}
if (!post) notFound();
let relatedPosts: Awaited<ReturnType<typeof getRelatedPosts>> = [];
try {
relatedPosts = await getRelatedPosts(post.id, post.categories ?? [], 3);
} catch {
// show no related section on API error
}
const date = post.date ? formatDateShamsi(post.date) : null;
const modifiedDate = post.modified
? formatDateShamsi(post.modified)
@@ -57,8 +68,8 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
<>
{/* تصویر شاخص تمام‌عرض بالای صفحه */}
{thumbnailUrl && (
<div className="pt-28 md:pt-36">
<div className="relative w-full aspect-[21/5] min-h-[10rem] md:min-h-[10rem] bg-muted">
<div className="pt-10 md:pt-24">
<div className="relative container mx-auto max-w-6xl rounded-xl overflow-hidden shadow-sm w-full aspect-[5/2] min-h-[10rem] md:min-h-[10rem] bg-muted">
<Image
src={thumbnailUrl}
alt={altText}
@@ -114,11 +125,12 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
) : seg.blockType === "collapse" ? (
<BlogCollapseSection
key={i}
section={seg.payload}
section={seg.payload as CollapseBlockPayload}
idPrefix={`blog-${slug}-${i}`}
/>
) : seg.blockType === "blockquote" ? (
<BlogBlockquote key={i} payload={seg.payload as BlockquoteBlockPayload} />
) : (
/* سایر بلاک‌ها (blockquote, code, ...) بعداً با کامپوننت خودشان رندر می‌شوند */
null
)
)}
@@ -127,6 +139,22 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
</div>
</div>
</section>
{relatedPosts.length > 0 && (
<section className="bg-muted border-t border-b">
<div className="mx-auto max-w-6xl p-4">
<h2 className="text-xl text-center mb-6 md:mb-8">
مقالات مرتبط
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{relatedPosts.map((p) => (
<BlogPostCard key={p.id} post={p} variant="tiny" />
))}
</div>
</div>
</section>
)}
<ScrollToTop />
</>
);
+35 -33
View File
@@ -4,7 +4,7 @@ import { getPosts, getCategories } from "@/lib/wp-api";
import { BlogPostCard } from "@/components/blog-post-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ChevronRight, ChevronLeft } from "lucide-react";
import { ChevronRight, ChevronLeft, Search, X } from "lucide-react";
const PER_PAGE = 9;
@@ -37,7 +37,6 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
getCategories(),
]);
} catch (e) {
console.log(e)
return (
<section className="bg-background pt-32 pb-20 md:pt-40 md:pb-28">
<div className="mx-auto max-w-4xl px-6 text-center">
@@ -64,53 +63,56 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
return (
<>
{/* Hero + search & filters in one section */}
<section className="bg-background pt-32 pb-10 md:pt-40 md:pb-14">
<div className="mx-auto max-w-6xl px-6">
<div className="text-center space-y-8">
<div className="space-y-3">
<h1 className="text-4xl font-bold md:text-5xl lg:text-6xl leading-tight tracking-normal">
{/* Hero + search & filters — compact */}
<section className="bg-background pt-24 pb-6 md:pt-28 md:pb-8">
<div className="mx-auto max-w-3xl px-4 sm:px-6">
<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-lg md:text-xl leading-relaxed text-muted-foreground max-w-2xl mx-auto">
<p className="text-sm md:text-base text-muted-foreground max-w-xl mx-auto">
مقالات و مطالب فیپ در زمینه یکپارچه‌سازی، اتوماسیون و بهره‌وری سازمانی.
</p>
</div>
<div className="flex flex-col gap-5 max-w-2xl mx-auto">
<div className="flex flex-col gap-4">
<form
method="get"
action="/blog"
className="flex flex-col sm:flex-row gap-2 sm:gap-2 w-full"
className="flex gap-2 w-full"
>
<input type="hidden" name="category" value={categoryId ?? ""} />
<Input
type="search"
name="search"
placeholder="جستجو در مطالب..."
defaultValue={search}
className="flex-1 w-full sm:min-w-[14rem] text-base"
aria-label="جستجو در مطالب"
/>
<div className="flex gap-2 flex-shrink-0">
<Button type="submit" size="default" className="flex-1 sm:flex-none">
جستجو
</Button>
{hasFilters && (
<Link href="/blog" className="flex-1 sm:flex-none">
<Button type="button" variant="outline" size="default" className="w-full">
پاک کردن فیلتر
</Button>
</Link>
)}
<div className="relative flex-1 flex items-center">
<Search className="absolute right-3 size-4 text-muted-foreground pointer-events-none" aria-hidden />
<Input
type="search"
name="search"
placeholder="جستجو در مطالب..."
defaultValue={search}
className="w-full pr-10 pl-3 h-9 text-sm"
aria-label="جستجو در مطالب"
/>
</div>
<Button type="submit" size="default" className="h-9 px-4 shrink-0">
جستجو
</Button>
</form>
{hasFilters && (
<Link
href="/blog"
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<X className="size-3.5" />
پاک کردن فیلتر
</Link>
)}
{categories.length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2">
<div className="flex flex-wrap items-center justify-center gap-1.5">
<Link
href="/blog"
className={`text-sm rounded-full px-4 py-2 transition-colors ${!categoryId
className={`text-xs rounded-full px-3 py-1.5 transition-colors ${!categoryId
? "bg-primary text-primary-foreground shadow-sm"
: "bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
@@ -121,7 +123,7 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
<Link
key={cat.id}
href={`/blog${buildQuery({ search, category: String(cat.id), page: 1 })}`}
className={`text-sm rounded-full px-4 py-2 transition-colors ${categoryId === cat.id
className={`text-xs rounded-full px-3 py-1.5 transition-colors ${categoryId === cat.id
? "bg-primary text-primary-foreground shadow-sm"
: "bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
+65
View File
@@ -231,3 +231,68 @@
transition: all 0.3s ease;
}
}
/* WordPress table block (figure.wp-block-table > table) */
.wp-block-table {
margin: 1.5rem 0;
overflow-x: auto;
border-radius: var(--radius);
}
.wp-block-table table {
width: 100%;
border-collapse: collapse;
font-size: 0.9375rem;
}
.wp-block-table table.has-fixed-layout {
table-layout: fixed;
}
.wp-block-table th,
.wp-block-table td {
padding: 0.75rem 1rem;
border: 1px solid var(--border);
text-align: right;
min-width: 850px;
}
.wp-block-table tbody tr:first-child th,
.wp-block-table tbody tr:first-child td {
background: var(--muted);
font-weight: 600;
color: var(--foreground);
}
.wp-block-table tbody tr:not(:first-child):hover td {
background: var(--muted);
}
.wp-block-table thead th {
background: var(--muted);
font-weight: 600;
color: var(--foreground);
}
.wp-block-button > .wp-element-button {
@apply inline-flex bg-primary text-primary-foreground px-4 py-2 rounded-md hover:bg-primary/80 hover:text-primary-foreground/80 cursor-pointer no-underline;
}
.wp-block-button {
@apply inline-flex;
}
.wp-block-button.is-style-outline > .wp-element-button {
@apply bg-transparent text-foreground border border-2 border-primary hover:text-primary cursor-pointer no-underline;
}
.cta-block {
@apply flex flex-col gap-4 border rounded-md border-primary/50 p-4 items-center my-4 shadow-sm bg-primary/5 text-primary;
}
.wp-block-image {
@apply rounded-md overflow-hidden shadow-sm;
}
+3
View File
@@ -8,6 +8,9 @@ export const metadata: Metadata = {
title: "فیپ",
description:
"فیپ یک سامانه برای ایجاد و طراحی راهکارهای مناسب برای کسب و کار شما است.",
icons: {
icon: "/assets/feep-Logo.svg",
},
};
export default function RootLayout({