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:
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import type { BlockquoteBlockPayload } from "@/lib/blog-blocks";
|
||||
import { Quote } from "lucide-react";
|
||||
|
||||
interface BlogBlockquoteProps {
|
||||
payload: BlockquoteBlockPayload;
|
||||
}
|
||||
|
||||
export function BlogBlockquote({ payload }: BlogBlockquoteProps) {
|
||||
const { content, citation } = payload;
|
||||
if (!content.trim()) return null;
|
||||
|
||||
return (
|
||||
<figure className="my-8 not-prose">
|
||||
<blockquote className="relative rounded-xl border border-border bg-muted/40 px-6 py-2 pr-14 text-foreground shadow-sm">
|
||||
<Quote
|
||||
className="absolute right-4 top-4 size-8 text-muted-foreground/40"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="blog-blockquote-content text-lg leading-relaxed text-justify [&_p]:my-2 [&_a]:text-primary [&_a]:underline hover:[&_a]:no-underline"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
{citation ? (
|
||||
<footer className="mt-4 border-t border-border/60 pt-4">
|
||||
<cite className="text-sm text-muted-foreground not-italic">
|
||||
— {citation}
|
||||
</cite>
|
||||
</footer>
|
||||
) : null}
|
||||
</blockquote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function BlogCard({
|
||||
<Link href={`/blog/${slug}`} className="group block h-full">
|
||||
<Card className="h-full flex flex-col overflow-hidden transition-all duration-300 hover:shadow-lg hover:-translate-y-1 dark:border-transparent">
|
||||
{/* Image with 3:2 aspect ratio */}
|
||||
<div className="relative aspect-[3/2] w-full overflow-hidden bg-muted">
|
||||
<div className="relative aspect-[5/3] w-full overflow-hidden bg-muted">
|
||||
<Image
|
||||
src={image}
|
||||
alt={title}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import type { CollapseBlockPayload } from "@/lib/blog-blocks";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface BlogCollapseSectionProps {
|
||||
section: CollapseBlockPayload;
|
||||
@@ -14,6 +15,7 @@ interface BlogCollapseSectionProps {
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
|
||||
export function BlogCollapseSection({
|
||||
section,
|
||||
idPrefix = "collapse",
|
||||
@@ -21,24 +23,34 @@ export function BlogCollapseSection({
|
||||
if (!section.items.length) return null;
|
||||
|
||||
return (
|
||||
<div className="my-8 space-y-4 border rounded-md p-6">
|
||||
<div className="my-8 space-y-4 border rounded-xl p-6">
|
||||
{section.title ? (
|
||||
<h2 className="text-2xl font-bold !mt-0 mb-4 scroll-mt-24">
|
||||
{section.title}
|
||||
</h2>
|
||||
) : null}
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<Accordion type="single" collapsible className="w-full gap-1">
|
||||
{section.items.map((item, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`${idPrefix}-${i}`}
|
||||
className="border-b last:border-b-0 !m-0"
|
||||
className="border-none !m-0 hover:bg-muted has-[[data-state=open]]:bg-muted px-4 rounded-md"
|
||||
>
|
||||
<AccordionTrigger className="text-right hover:no-underline py-4 [&>svg]:shrink-0">
|
||||
{item.question}
|
||||
<AccordionTrigger className="group/trigger text-right hover:no-underline cursor-pointer py-4 [&>svg]:shrink-0">
|
||||
<span className="flex gap-1 items-center">
|
||||
<Icon
|
||||
icon="lucide:circle-small"
|
||||
className="size-5 group-data-[state=open]/trigger:hidden"
|
||||
/>
|
||||
<Icon
|
||||
icon="lucide:dot"
|
||||
className="size-5 hidden group-data-[state=open]/trigger:inline-block"
|
||||
/>
|
||||
{item.question}
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground leading-relaxed text-justify">
|
||||
{item.answer}
|
||||
<div dangerouslySetInnerHTML={{ __html: item.answer }} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
|
||||
@@ -9,12 +9,67 @@ function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, "").trim();
|
||||
}
|
||||
|
||||
export function BlogPostCard({ post }: { post: WpPost }) {
|
||||
type BlogPostCardVariant = "default" | "tiny";
|
||||
|
||||
export function BlogPostCard({
|
||||
post,
|
||||
variant = "default",
|
||||
}: {
|
||||
post: WpPost;
|
||||
variant?: BlogPostCardVariant;
|
||||
}) {
|
||||
const excerpt = stripHtml(post.excerpt.rendered) || stripHtml(post.content.rendered).slice(0, 160);
|
||||
const modifiedDate = post.modified ? formatDateShamsi(post.modified) : null;
|
||||
const authorName = post._embedded?.author?.[0]?.name ?? null;
|
||||
const thumbnailUrl = getFeaturedImageUrl(post, "medium");
|
||||
|
||||
if (variant === "tiny") {
|
||||
return (
|
||||
<Link href={`/blog/${encodeURIComponent(post.slug)}`} className="block h-full">
|
||||
<Card className="h-full overflow-hidden transition-all rounded-md duration-300 hover:shadow-md flex flex-col p-2">
|
||||
<CardContent className="p-2 grid grid-cols-3 gap-4">
|
||||
{thumbnailUrl ? (
|
||||
<div className="relative rounded-md aspect-[4/3] w-full overflow-hidden bg-muted shrink-0 col-span-1">
|
||||
<Image
|
||||
src={thumbnailUrl}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative rounded-md flex aspect-[4/3] w-full items-center justify-center overflow-hidden bg-muted shrink-0"
|
||||
aria-hidden
|
||||
>
|
||||
<ImageIcon className="size-8 text-muted-foreground/50" />
|
||||
</div>
|
||||
)}
|
||||
<div className="col-span-2">
|
||||
<h4 className="text-sm font-semibold leading-snug line-clamp-2">
|
||||
{post.title.rendered}
|
||||
</h4>
|
||||
<div className="flex flex-wrap w-full items-center gap-x-4 gap-y-1 text-xs text-muted-foreground bg-muted inline-flex p-1 mt-2 rounded-sm">
|
||||
{authorName && (
|
||||
<span className="inline-flex flex-1 items-center gap-1.5">
|
||||
<User className="size-3.5 shrink-0" aria-hidden />
|
||||
{authorName}
|
||||
</span>
|
||||
)}
|
||||
{modifiedDate && (
|
||||
<time dateTime={post.modified}>
|
||||
{modifiedDate}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={`/blog/${encodeURIComponent(post.slug)}`} className="block h-full">
|
||||
<Card className="h-full overflow-hidden transition-all duration-300 hover:-translate-y-1 hover:shadow-lg pt-0 gap-1">
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import type { BlockquoteBlockPayload } from "@/lib/blog-blocks";
|
||||
|
||||
interface BlogPullquoteProps {
|
||||
payload: BlockquoteBlockPayload;
|
||||
}
|
||||
|
||||
export function BlogPullquote({ payload }: BlogPullquoteProps) {
|
||||
const { content, citation } = payload;
|
||||
if (!content.trim()) return null;
|
||||
|
||||
return (
|
||||
<figure className="my-10 not-prose">
|
||||
<blockquote className="relative rounded-xl border border-border bg-muted/40 px-6 py-5 text-center text-foreground shadow-sm">
|
||||
<div
|
||||
className=""
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
{citation ? (
|
||||
<footer className="mt-4">
|
||||
<cite className="text-sm text-muted-foreground not-italic">
|
||||
— {citation}
|
||||
</cite>
|
||||
</footer>
|
||||
) : null}
|
||||
</blockquote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -176,7 +176,7 @@ export function BlogToc({ items, className }: BlogTocProps) {
|
||||
<aside
|
||||
className={cn(
|
||||
"hidden lg:block shrink-0",
|
||||
"sticky top-24 self-start max-h-[calc(100vh-6rem)]",
|
||||
"sticky top-24 self-start",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
+4
-14
@@ -10,11 +10,10 @@ import { ThemeToggle } from '@/components/theme-toggle'
|
||||
|
||||
const menuItems = [
|
||||
{ name: 'صفحه اصلی', href: '/' },
|
||||
{ name: 'درباره ما', href: '/about' },
|
||||
{ name: 'راه حلها', href: '/solutions' },
|
||||
{ name: 'بلاگ', href: '/blog' },
|
||||
{ name: 'قیمتگذاری', href: '/pricing' },
|
||||
{ name: 'وبلاگ', href: '/blog' },
|
||||
{ name: 'راه حلها', href: '/solutions' },
|
||||
// { name: 'قیمتگذاری', href: '/pricing' },
|
||||
{ name: 'درباره ما', href: '/about' },
|
||||
]
|
||||
|
||||
function isLinkActive(pathname: string, href: string): boolean {
|
||||
@@ -104,16 +103,7 @@ export const HeroHeader = () => {
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-3 sm:flex-row sm:gap-3 sm:space-y-0 md:w-fit">
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className={cn(isScrolled ? 'lg:inline-flex' : 'hidden', 'transition-all duration-300 hover:scale-105')}>
|
||||
<Link href="tel:09162949770">
|
||||
<span>تماس با فیپ</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user