feat: enhance blog post layout and add scroll progress indicator

- Adjusted padding for the blog post thumbnail section.
- Introduced a custom hook for tracking scroll progress.
- Added horizontal scroll progress indicators for both desktop and mobile views in the blog table of contents.
This commit is contained in:
2026-02-05 20:59:38 +03:30
parent e21abe3443
commit 03bf7b2a67
2 changed files with 57 additions and 8 deletions
+2 -2
View File
@@ -68,8 +68,8 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
<>
{/* تصویر شاخص تمام‌عرض بالای صفحه */}
{thumbnailUrl && (
<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">
<div className="pt-13 md:pt-24">
<div className="relative container mx-auto max-w-6xl md: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}
+55 -6
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { ChevronDown, List } from "lucide-react";
import {
@@ -12,6 +12,33 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { TocItem } from "@/lib/blog-utils";
function useScrollProgress() {
const [progress, setProgress] = useState(0);
const update = useCallback(() => {
const { scrollY } = window;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
if (docHeight <= 0) {
setProgress(0);
return;
}
const value = Math.min(100, Math.max(0, (scrollY / docHeight) * 100));
setProgress(value);
}, []);
useEffect(() => {
update();
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
return () => {
window.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
};
}, [update]);
return progress;
}
interface TocGroup {
heading: TocItem;
children: TocItem[];
@@ -54,6 +81,7 @@ export function BlogToc({ items, className }: BlogTocProps) {
const [activeId, setActiveId] = useState<string | null>(items[0]?.id ?? null);
const [mobileOpen, setMobileOpen] = useState(false);
const [openSections, setOpenSections] = useState<Set<string>>(() => new Set());
const scrollProgress = useScrollProgress();
const groups = useMemo(() => groupByLevel2(items), [items]);
@@ -172,15 +200,25 @@ export function BlogToc({ items, className }: BlogTocProps) {
return (
<>
{/* Desktop: sticky sidebar left */}
{/* Desktop: sticky sidebar with scroll progress bar */}
<aside
className={cn(
"hidden lg:block shrink-0",
"hidden lg:block shrink-0 relative",
"sticky top-24 self-start",
className
)}
>
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{/* Horizontal scroll progress indicator */}
<div
className="absolute -top-2 left-0 right-0 h-1 rounded-full bg-muted overflow-hidden"
aria-hidden
>
<div
className="h-full min-w-[2px] bg-primary transition-[width] duration-150 ease-out rounded-full"
style={{ width: `${scrollProgress}%` }}
/>
</div>
<p className="mb-3 text-md ms-2 font-semibold uppercase tracking-wider text-muted-foreground">
فهرست مطالب
</p>
<ScrollArea className="h-full pb-8" dir="rtl">
@@ -188,21 +226,32 @@ export function BlogToc({ items, className }: BlogTocProps) {
</ScrollArea>
</aside>
{/* Mobile: collapsible, sticky when scrolling */}
{/* Mobile: collapsible with horizontal scroll progress bar */}
<Collapsible
open={mobileOpen}
onOpenChange={setMobileOpen}
className={cn(
"lg:hidden w-full sticky top-20 z-10 bg-background",
"lg:hidden w-full sticky top-14 z-10 bg-background",
className
)}
>
<CollapsibleTrigger
className={cn(
"flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm font-medium transition-colors hover:bg-muted/50",
mobileOpen && "rounded-b-none border-b-0"
)}
>
{/* Horizontal scroll progress indicator (mobile) */}
<div
className="absolute right-1 top-1 inset-inline-0 h-1 w-[97%] bg-muted overflow-hidden rounded-full"
aria-hidden
>
<div
className="h-full bg-primary transition-[width] duration-150 ease-out rounded-full"
style={{ width: `${scrollProgress}%` }}
/>
</div>
<span className="flex items-center gap-2">
<List className="size-4 text-muted-foreground" />
فهرست مطالب