feat: tip component
This commit is contained in:
@@ -5,12 +5,13 @@ import { notFound } from "next/navigation";
|
|||||||
import { getPostBySlug, getFeaturedImageUrl, getRelatedPosts } from "@/lib/wp-api";
|
import { getPostBySlug, getFeaturedImageUrl, getRelatedPosts } from "@/lib/wp-api";
|
||||||
import { formatDateShamsi } from "@/lib/format-date";
|
import { formatDateShamsi } from "@/lib/format-date";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import type { CollapseBlockPayload, BlockquoteBlockPayload } from "@/lib/blog-blocks";
|
import type { CollapseBlockPayload, BlockquoteBlockPayload, TipBlockPayload } from "@/lib/blog-blocks";
|
||||||
import { parseContentHeadings, getReadingTimeMinutes } from "@/lib/blog-utils";
|
import { parseContentHeadings, getReadingTimeMinutes } from "@/lib/blog-utils";
|
||||||
import { BlogToc } from "@/components/blog-toc";
|
import { BlogToc } from "@/components/blog-toc";
|
||||||
import { BlogPostMeta } from "@/components/blog-post-meta";
|
import { BlogPostMeta } from "@/components/blog-post-meta";
|
||||||
import { BlogCollapseSection } from "@/components/blog-collapse-section";
|
import { BlogCollapseSection } from "@/components/blog-collapse-section";
|
||||||
import { BlogBlockquote } from "@/components/blog-blockquote";
|
import { BlogBlockquote } from "@/components/blog-blockquote";
|
||||||
|
import { BlogTip } from "@/components/blog-tip";
|
||||||
import { BlogPullquote } from "@/components/blog-pullquote";
|
import { BlogPullquote } from "@/components/blog-pullquote";
|
||||||
import { ScrollToTop } from "@/components/scroll-to-top";
|
import { ScrollToTop } from "@/components/scroll-to-top";
|
||||||
import { BlogPostCard } from "@/components/blog-post-card";
|
import { BlogPostCard } from "@/components/blog-post-card";
|
||||||
@@ -88,7 +89,7 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
|
|||||||
<div className="lg:grid lg:grid-cols-[16rem_1fr] lg:gap-10">
|
<div className="lg:grid lg:grid-cols-[16rem_1fr] lg:gap-10">
|
||||||
<BlogToc items={toc} className="mb-6 lg:mb-0" />
|
<BlogToc items={toc} className="mb-6 lg:mb-0" />
|
||||||
|
|
||||||
<div className="min-w-0 max-w-3xl space-y-0">
|
<div className="min-w-0 space-y-0">
|
||||||
<div className="pb-8 md:pb-12">
|
<div className="pb-8 md:pb-12">
|
||||||
<nav className="mb-6" aria-label="مسیر">
|
<nav className="mb-6" aria-label="مسیر">
|
||||||
<Link
|
<Link
|
||||||
@@ -130,6 +131,8 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
|
|||||||
/>
|
/>
|
||||||
) : seg.blockType === "blockquote" ? (
|
) : seg.blockType === "blockquote" ? (
|
||||||
<BlogBlockquote key={i} payload={seg.payload as BlockquoteBlockPayload} />
|
<BlogBlockquote key={i} payload={seg.payload as BlockquoteBlockPayload} />
|
||||||
|
) : seg.blockType === "tip" ? (
|
||||||
|
<BlogTip key={i} payload={seg.payload as TipBlockPayload} />
|
||||||
) : (
|
) : (
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { TipBlockPayload } from "@/lib/blog-blocks";
|
||||||
|
import { Info, AlertTriangle, AlertCircle } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface BlogTipProps {
|
||||||
|
payload: TipBlockPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantConfig = {
|
||||||
|
info: {
|
||||||
|
icon: Info,
|
||||||
|
className:
|
||||||
|
"border-primary/40 bg-primary/5 text-foreground [&_strong]:text-primary",
|
||||||
|
iconClassName: "text-primary",
|
||||||
|
},
|
||||||
|
warn: {
|
||||||
|
icon: AlertTriangle,
|
||||||
|
className:
|
||||||
|
"border-amber-500/50 bg-amber-500/10 text-foreground [&_strong]:text-amber-600 dark:[&_strong]:text-amber-400",
|
||||||
|
iconClassName: "text-amber-500",
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
icon: AlertCircle,
|
||||||
|
className:
|
||||||
|
"border-destructive/50 bg-destructive/10 text-foreground [&_strong]:text-destructive",
|
||||||
|
iconClassName: "text-destructive",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function BlogTip({ payload }: BlogTipProps) {
|
||||||
|
const { variant, content } = payload;
|
||||||
|
if (!content.trim()) return null;
|
||||||
|
|
||||||
|
const config = variantConfig[variant ?? 'info'];
|
||||||
|
const Icon = config.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"my-6 flex gap-3 rounded-xl border p-4 pr-4 shadow-sm",
|
||||||
|
config.className
|
||||||
|
)}
|
||||||
|
role="note"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={cn("mt-0.5 size-8 shrink-0", config.iconClassName)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="blog-tip-content min-w-0 text-sm leading-relaxed [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_a]:text-primary [&_a]:underline hover:[&_a]:no-underline"
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+30
-2
@@ -12,7 +12,7 @@ import type { Element } from "domhandler";
|
|||||||
// ─── انواع بلاک (برای گسترش بعدی) ───────────────────────────────────────────
|
// ─── انواع بلاک (برای گسترش بعدی) ───────────────────────────────────────────
|
||||||
|
|
||||||
/** نام نوع بلاک؛ با افزودن بلاک جدید این یونیون را گسترش دهید. */
|
/** نام نوع بلاک؛ با افزودن بلاک جدید این یونیون را گسترش دهید. */
|
||||||
export type BlockType = "collapse" | "blockquote" | "pullquote" | "code";
|
export type BlockType = "collapse" | "blockquote" | "pullquote" | "tip" | "code";
|
||||||
|
|
||||||
/** Payload بلاک collapse (سوالات متداول / accordion). */
|
/** Payload بلاک collapse (سوالات متداول / accordion). */
|
||||||
export interface CollapseBlockPayload {
|
export interface CollapseBlockPayload {
|
||||||
@@ -27,13 +27,21 @@ export interface BlockquoteBlockPayload {
|
|||||||
citation?: string;
|
citation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Payload بلاک tip (نکته / اینفو با variant: info, warn, danger). */
|
||||||
|
export interface TipBlockPayload {
|
||||||
|
variant: "info" | "warn" | "danger";
|
||||||
|
/** HTML محتوای داخل بلاک (عنوان + متن). */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Payload بلاک code — بعداً پیادهسازی میشود. */
|
/** Payload بلاک code — بعداً پیادهسازی میشود. */
|
||||||
// export interface CodeBlockPayload { ... }
|
// export interface CodeBlockPayload { ... }
|
||||||
|
|
||||||
/** یونیون همهٔ payloadها برای تایپامن رندر. */
|
/** یونیون همهٔ payloadها برای تایپامن رندر. */
|
||||||
export type BlockPayload =
|
export type BlockPayload =
|
||||||
| CollapseBlockPayload
|
| CollapseBlockPayload
|
||||||
| BlockquoteBlockPayload;
|
| BlockquoteBlockPayload
|
||||||
|
| TipBlockPayload;
|
||||||
// | CodeBlockPayload
|
// | CodeBlockPayload
|
||||||
|
|
||||||
// ─── سگمنت خروجی برای لایه رندر ─────────────────────────────────────────────
|
// ─── سگمنت خروجی برای لایه رندر ─────────────────────────────────────────────
|
||||||
@@ -143,6 +151,20 @@ function extractPullquotePayload(
|
|||||||
return { content, citation };
|
return { content, citation };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* استخراج از بلاک tip وردپرس: div.wp-block-group.tip با کلاس tip-info | tip-warn | tip-danger.
|
||||||
|
*/
|
||||||
|
function extractTipPayload($: cheerio.CheerioAPI, el: Element): TipBlockPayload {
|
||||||
|
const $el = $(el);
|
||||||
|
const classList = ($el.attr("class") ?? "").split(/\s+/);
|
||||||
|
let variant: "info" | "warn" | "danger" = "info";
|
||||||
|
if (classList.includes("tip-danger")) variant = "danger";
|
||||||
|
else if (classList.includes("tip-warn")) variant = "warn";
|
||||||
|
else if (classList.includes("tip-info")) variant = "info";
|
||||||
|
const content = ($el.html() ?? "").trim();
|
||||||
|
return { variant, content };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ثبت همهٔ بلاکهای شناختهشده.
|
* ثبت همهٔ بلاکهای شناختهشده.
|
||||||
* برای بلاکهای جدید (مثلاً blockquote, code) فقط یک مورد جدید به این آرایه اضافه کنید.
|
* برای بلاکهای جدید (مثلاً blockquote, code) فقط یک مورد جدید به این آرایه اضافه کنید.
|
||||||
@@ -168,6 +190,11 @@ export const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||||||
selector: "figure.wp-block-pullquote",
|
selector: "figure.wp-block-pullquote",
|
||||||
extract: extractPullquotePayload as BlockDefinition["extract"],
|
extract: extractPullquotePayload as BlockDefinition["extract"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
blockType: "tip",
|
||||||
|
selector: "div.wp-block-group.tip",
|
||||||
|
extract: extractTipPayload as BlockDefinition["extract"],
|
||||||
|
},
|
||||||
// { blockType: "code", selector: "pre.wp-block-code", extract: extractCodePayload },
|
// { blockType: "code", selector: "pre.wp-block-code", extract: extractCodePayload },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -215,6 +242,7 @@ export function discoverBlocks(html: string): DiscoveredContent {
|
|||||||
collapse: [],
|
collapse: [],
|
||||||
blockquote: [],
|
blockquote: [],
|
||||||
pullquote: [],
|
pullquote: [],
|
||||||
|
tip: [],
|
||||||
code: [],
|
code: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user