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:
@@ -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
@@ -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"
|
||||
}`}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -8,6 +8,9 @@ export const metadata: Metadata = {
|
||||
title: "فیپ",
|
||||
description:
|
||||
"فیپ یک سامانه برای ایجاد و طراحی راهکارهای مناسب برای کسب و کار شما است.",
|
||||
icons: {
|
||||
icon: "/assets/feep-Logo.svg",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
Reference in New Issue
Block a user