feat(blog): refresh post detail layout
This commit is contained in:
@@ -12,14 +12,12 @@ type BlogPostActionsProps = {
|
||||
slug: string;
|
||||
initialLikes: number;
|
||||
initialSaves: number;
|
||||
initialComments: number;
|
||||
};
|
||||
|
||||
export default function BlogPostActions({
|
||||
slug,
|
||||
initialLikes,
|
||||
initialSaves,
|
||||
initialComments,
|
||||
}: BlogPostActionsProps) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [loadingAction, setLoadingAction] = useState<"like" | "save" | null>(null);
|
||||
@@ -28,7 +26,7 @@ export default function BlogPostActions({
|
||||
saved: false,
|
||||
likes_count: initialLikes,
|
||||
saves_count: initialSaves,
|
||||
comments_count: initialComments,
|
||||
comments_count: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -67,7 +65,7 @@ export default function BlogPostActions({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 pt-4" dir="rtl">
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 border-t border-border/70 pt-6" dir="rtl">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -92,10 +90,10 @@ export default function BlogPostActions({
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
size="icon"
|
||||
onClick={toggleSave}
|
||||
disabled={!isAuthenticated || Boolean(loadingAction)}
|
||||
className="gap-2 rounded-full border border-border/60 bg-background/80 px-5 shadow-sm backdrop-blur hover:bg-amber-50 hover:text-amber-600 dark:hover:bg-amber-950/30"
|
||||
className="rounded-full border border-border/60 bg-background/80 shadow-sm backdrop-blur hover:bg-amber-50 hover:text-amber-600 dark:hover:bg-amber-950/30"
|
||||
aria-label="ذخیره نوشته"
|
||||
>
|
||||
{loadingAction === "save" ? (
|
||||
@@ -108,7 +106,6 @@ export default function BlogPostActions({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span>ذخیره</span>
|
||||
</Button>
|
||||
{!isAuthenticated ? (
|
||||
<span className="basis-full text-center text-xs text-muted-foreground">
|
||||
|
||||
75
src/components/BlogTableOfContents.tsx
Normal file
75
src/components/BlogTableOfContents.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { MarkdownHeading } from "@/lib/markdown-headings";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Props = {
|
||||
headings: MarkdownHeading[];
|
||||
};
|
||||
|
||||
export default function BlogTableOfContents({ headings }: Props) {
|
||||
const [activeId, setActiveId] = useState(headings[0]?.id ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (!headings.length) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
|
||||
if (visible?.target.id) {
|
||||
setActiveId(visible.target.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0px -65% 0px",
|
||||
threshold: [0, 1],
|
||||
},
|
||||
);
|
||||
|
||||
headings.forEach((heading) => {
|
||||
const element = document.getElementById(heading.id);
|
||||
if (element) observer.observe(element);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [headings]);
|
||||
|
||||
if (!headings.length) {
|
||||
return <p className="text-sm leading-7 text-muted-foreground">برای این نوشته فهرست محتوا ثبت نشده است.</p>;
|
||||
}
|
||||
|
||||
const scrollToHeading = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
window.history.replaceState(null, "", `#${id}`);
|
||||
setActiveId(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="space-y-1 text-sm">
|
||||
{headings.map((heading) => {
|
||||
const active = activeId === heading.id;
|
||||
return (
|
||||
<button
|
||||
key={heading.id}
|
||||
type="button"
|
||||
onClick={() => scrollToHeading(heading.id)}
|
||||
className={cn(
|
||||
"block w-full rounded-2xl px-3 py-2 text-right leading-6 transition",
|
||||
active
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-primary/10 hover:text-primary",
|
||||
)}
|
||||
style={{ paddingRight: `${(heading.level - 1) * 0.85 + 0.75}rem` }}
|
||||
>
|
||||
{heading.text}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user