improvement changelog pages

This commit is contained in:
Wildan Nursahidan
2025-05-12 19:06:24 +07:00
parent 6aeb790cb8
commit ba2963ae41
8 changed files with 117 additions and 94 deletions

View File

@@ -4,6 +4,8 @@ import { formatDate2, stringToDate } from "@/lib/utils";
import { getMetadata } from "@/app/layout"; import { getMetadata } from "@/app/layout";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { AuroraText } from "@/components/ui/aurora";
import { ShineBorder } from "@/components/ui/shine-border";
import docuConfig from "@/docu.json"; import docuConfig from "@/docu.json";
export const metadata = getMetadata({ export const metadata = getMetadata({
@@ -16,16 +18,17 @@ export default async function BlogIndexPage() {
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime() (a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
); );
return ( return (
<div className="w-full mx-auto flex flex-col gap-1 sm:min-h-[91vh] min-h-[88vh] py-2"> <div className="flex flex-col items-center justify-center px-2 py-8 text-center sm:py-36">
<div className="mb-7 flex flex-col gap-2"> <div className="w-full max-w-[800px] pb-8">
<h1 className="text-2xl font-extrabold"> <AuroraText className="text-lg"># Stay Informed, Stay Ahead</AuroraText>
<h1 className="mb-4 text-2xl font-bold sm:text-5xl">
Blog Posts Blog Posts
</h1> </h1>
<p className="text-lg text-muted-foreground mt-2"> <p className="mb-8 sm:text-xl text-muted-foreground">
Discover the latest updates, tutorials, and insights on {meta.title}. Explore updates, tips, and deep dives from the {meta.title}.
</p> </p>
</div> </div>
<div className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 sm:gap-8 gap-4 mb-5"> <div className="text-left grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 sm:gap-8 gap-4 my-6">
{blogs.map((blog) => ( {blogs.map((blog) => (
<BlogCard {...blog} slug={blog.slug} key={blog.slug} /> <BlogCard {...blog} slug={blog.slug} key={blog.slug} />
))} ))}
@@ -45,9 +48,8 @@ function BlogCard({
return ( return (
<Link <Link
href={`/blog/${slug}`} href={`/blog/${slug}`}
className="flex flex-col gap-2 items-start border rounded-md py-5 px-3 min-h-[400px]" className="flex flex-col gap-2 items-start border rounded-md max-h-[420px] min-h-[420px]"
> >
<h3 className="text-md font-semibold -mt-1 pr-7">{title}</h3>
<div className="w-full"> <div className="w-full">
<Image <Image
src={cover} src={cover}
@@ -55,11 +57,14 @@ function BlogCard({
width={400} width={400}
height={150} height={150}
quality={80} quality={80}
className="w-full rounded-md object-cover h-[180px] border" className="w-full rounded-md object-cover h-[200px]"
/> />
</div> </div>
<p className="text-sm text-muted-foreground">{description}</p> <div className="flex flex-col items-start px-3 py-3 gap-2 mb-auto">
<div className="flex items-center justify-between w-full mt-auto"> <h3 className="text-md font-semibold line-clamp-2">{title}</h3>
<p className="text-sm text-muted-foreground line-clamp-3">{description}</p>
</div>
<div className="flex items-center justify-between w-full px-3 mb-6">
<p className="text-[13px] text-muted-foreground"> <p className="text-[13px] text-muted-foreground">
Published on {formatDate2(date)} Published on {formatDate2(date)}
</p> </p>

View File

@@ -2,42 +2,23 @@ import { Suspense } from "react";
import { getChangelogEntries } from "@/lib/changelog"; import { getChangelogEntries } from "@/lib/changelog";
import { VersionEntry } from "@/components/changelog/version-entry"; import { VersionEntry } from "@/components/changelog/version-entry";
import { VersionToc } from "@/components/changelog/version-toc"; import { VersionToc } from "@/components/changelog/version-toc";
import { getMetadata } from "@/app/layout";
import docuConfig from "@/docu.json";
import { FloatingVersionToc } from "@/components/changelog/floating-version"; import { FloatingVersionToc } from "@/components/changelog/floating-version";
export const metadata = getMetadata({
title: "Changelog",
description: "Latest updates and improvements to DocuBook",
image: "release-note.png",
});
export default async function ChangelogPage() { export default async function ChangelogPage() {
const entries = await getChangelogEntries(); const entries = await getChangelogEntries();
const { meta } = docuConfig;
return (
<div className="flex flex-col w-full">
<div className="border-b">
<div className="py-8">
<h1 className="text-2xl font-extrabold">Changelog</h1>
<p className="text-lg text-muted-foreground mt-2">
Latest updates and improvements to {meta.title}
</p>
</div>
</div>
<div className="md:container py-8"> return (
<div className="flex items-start gap-8"> <div className="flex items-start">
<Suspense fallback={<div className="lg:flex hidden flex-[1.5] min-w-[238px]" />}> <Suspense fallback={<div className="lg:flex hidden flex-[1.5]" />}>
<VersionToc <VersionToc
versions={entries.map(({ version, date }) => ({ version, date }))} versions={entries.map(({ version, date }) => ({ version, date }))}
/> />
</Suspense> </Suspense>
<main className="flex-1 lg:flex-[5.25] min-w-0"> <main className="flex-1 md:flex-[5.25] min-w-0 max-w-[800px]">
<div className="relative"> <div className="relative">
<div className="absolute left-0 top-0 h-full w-px bg-border lg:block hidden" /> <div className="absolute left-0 top-0 h-full w-px bg-border md:block hidden" />
<div className="lg:pl-12 pl-0 lg:pt-8"> <div className="md:px-12 md:py-8 max-md:py-10">
{entries.map((entry, index) => ( {entries.map((entry, index) => (
<section <section
id={`version-${entry.version}`} id={`version-${entry.version}`}
@@ -50,8 +31,6 @@ export default async function ChangelogPage() {
</div> </div>
</div> </div>
</main> </main>
</div>
</div>
{/* Floating TOC for smaller screens */} {/* Floating TOC for smaller screens */}
{entries.length > 0 && ( {entries.length > 0 && (
<FloatingVersionToc <FloatingVersionToc

View File

@@ -193,18 +193,19 @@ export default function PlaygroundPage() {
const before = markdown.substring(0, start); const before = markdown.substring(0, start);
const after = markdown.substring(end); const after = markdown.substring(end);
// Menambahkan satu baris kosong sebelum dan sesudah komponen const needsLeadingNewline = before && !before.endsWith('\n\n') ? '\n\n' : '';
const newText = `${before}${text}\n${after}`; const needsTrailingNewline = after && !after.startsWith('\n\n') ? '\n\n' : '';
const newText = `${before}${needsLeadingNewline}${text}${needsTrailingNewline}${after}`;
setMarkdown(newText); setMarkdown(newText);
requestAnimationFrame(() => { requestAnimationFrame(() => {
textArea.focus(); textArea.focus();
const newPosition = start + text.length + 1; const newPosition = before.length + needsLeadingNewline.length + text.length + 1;
textArea.setSelectionRange(newPosition, newPosition); textArea.setSelectionRange(newPosition, newPosition);
}); });
}; };
if (isMobile) { if (isMobile) {
return <MobileMessage />; return <MobileMessage />;
} }

View File

@@ -51,7 +51,7 @@ export function FloatingVersionToc({ versions }: FloatingVersionTocProps) {
}; };
return ( return (
<div className="fixed bottom-4 right-4 lg:hidden z-50"> <div className="fixed bottom-4 right-4 md:hidden z-50">
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" className="rounded-full shadow-lg px-4 py-2 flex items-center gap-2"> <Button variant="outline" className="rounded-full shadow-lg px-4 py-2 flex items-center gap-2">

View File

@@ -2,8 +2,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cn, formatDate2 } from "@/lib/utils"; import { cn, formatDate2 } from "@/lib/utils";
import { History } from "lucide-react"; import { History, PanelLeftOpen, PanelLeftClose } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
interface VersionTocProps { interface VersionTocProps {
versions: Array<{ versions: Array<{
@@ -14,33 +15,30 @@ interface VersionTocProps {
export function VersionToc({ versions }: VersionTocProps) { export function VersionToc({ versions }: VersionTocProps) {
const [activeId, setActiveId] = useState<string | null>(null); const [activeId, setActiveId] = useState<string | null>(null);
const [collapsed, setCollapsed] = useState(false);
useEffect(() => { useEffect(() => {
// Handle initial hash
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
if (hash) { if (hash) {
setActiveId(hash); setActiveId(hash);
} }
// Set up intersection observer
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
const id = entry.target.id; const id = entry.target.id;
setActiveId(id); setActiveId(id);
// Use pushState instead of replaceState to maintain history window.history.pushState(null, "", `#${id}`);
window.history.pushState(null, '', `#${id}`);
} }
}); });
}, },
{ {
threshold: 0.2, threshold: 0.2,
rootMargin: '-20% 0px -60% 0px' rootMargin: "-20% 0px -60% 0px",
} }
); );
// Observe version elements
versions.forEach(({ version }) => { versions.forEach(({ version }) => {
const element = document.getElementById(`v${version}`); const element = document.getElementById(`v${version}`);
if (element) observer.observe(element); if (element) observer.observe(element);
@@ -50,13 +48,35 @@ export function VersionToc({ versions }: VersionTocProps) {
}, [versions]); }, [versions]);
return ( return (
<aside className="lg:flex hidden toc flex-[1.5] min-w-[238px] pt-8 sticky top-16 h-[calc(100vh-4rem)]"> <aside
<div className="flex flex-col gap-2 w-full"> className={cn(
"sticky top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300 z-20 hidden md:flex",
collapsed ? "w-[48px]" : "w-[250px]"
)}
>
{/* Toggle Button */}
<div className="absolute top-0 right-0 py-2 px-0 ml-6 z-30">
<Button
size="icon"
variant="outline"
className="hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
onClick={() => setCollapsed((prev) => !prev)}
>
{collapsed ? <PanelLeftOpen size={18} /> : <PanelLeftClose size={18} />}
</Button>
</div>
{/* Content */}
{!collapsed && (
<div className="flex flex-col gap-2 w-full pt-8 pr-2">
<div className="flex mb-2">
<h2 className="font-semibold text-lg">Changelog</h2>
</div>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<History className="w-4 h-4" /> <History className="w-4 h-4" />
<h3 className="font-medium text-sm">Version History</h3> <h3 className="font-medium text-sm">Version History</h3>
</div> </div>
<ScrollArea className="h-full"> <ScrollArea className="h-full pr-2">
<div className="flex flex-col gap-1.5 text-sm dark:text-stone-300/85 text-stone-800 pr-4"> <div className="flex flex-col gap-1.5 text-sm dark:text-stone-300/85 text-stone-800 pr-4">
{versions.map(({ version, date }) => ( {versions.map(({ version, date }) => (
<a <a
@@ -70,9 +90,9 @@ export function VersionToc({ versions }: VersionTocProps) {
e.preventDefault(); e.preventDefault();
const element = document.getElementById(`v${version}`); const element = document.getElementById(`v${version}`);
if (element) { if (element) {
element.scrollIntoView({ behavior: 'smooth' }); element.scrollIntoView({ behavior: "smooth" });
setActiveId(`v${version}`); setActiveId(`v${version}`);
window.history.pushState(null, '', `#v${version}`); window.history.pushState(null, "", `#v${version}`);
} }
}} }}
> >
@@ -85,6 +105,7 @@ export function VersionToc({ versions }: VersionTocProps) {
</div> </div>
</ScrollArea> </ScrollArea>
</div> </div>
)}
</aside> </aside>
); );
} }

View File

@@ -9,7 +9,7 @@ import {
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { Logo, NavMenu } from "@/components/navbar"; import { Logo, NavMenu } from "@/components/navbar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AlignLeftIcon, ArrowLeftFromLine, ArrowRightFromLine } from "lucide-react"; import { AlignLeftIcon, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { FooterButtons } from "@/components/footer"; import { FooterButtons } from "@/components/footer";
import { DialogTitle } from "@/components/ui/dialog"; import { DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
@@ -22,7 +22,7 @@ export function Leftbar() {
return ( return (
<aside <aside
className={`sticky lg:flex hidden top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300 className={`sticky lg:flex hidden top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300
${collapsed ? "w-[0px]" : "w-[250px]"} flex flex-col pr-2`} ${collapsed ? "w-[48px]" : "w-[250px]"} flex flex-col pr-2`}
> >
{/* Toggle Button */} {/* Toggle Button */}
<div className="absolute top-0 right-0 py-2 px-0 ml-6 z-10"> <div className="absolute top-0 right-0 py-2 px-0 ml-6 z-10">
@@ -33,9 +33,9 @@ export function Leftbar() {
onClick={() => setCollapsed((prev) => !prev)} onClick={() => setCollapsed((prev) => !prev)}
> >
{collapsed ? ( {collapsed ? (
<ArrowRightFromLine size={18} /> <PanelLeftOpen size={18} />
) : ( ) : (
<ArrowLeftFromLine size={18} /> <PanelLeftClose size={18} />
)} )}
</Button> </Button>
</div> </div>

View File

@@ -119,6 +119,22 @@ pre>code {
background-color: transparent !important; background-color: transparent !important;
} }
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
@layer utilities { @layer utilities {
.animate-shine { .animate-shine {
--animate-shine: shine var(--duration) infinite linear; --animate-shine: shine var(--duration) infinite linear;

View File

@@ -9,6 +9,7 @@ const config = {
"./src/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}",
], ],
prefix: "", prefix: "",
safelist: ["line-clamp-3","line-clam-2"],
theme: { theme: {
container: { container: {
center: true, center: true,