improvement changelog pages
This commit is contained in:
@@ -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>
|
||||||
Blog Posts
|
<h1 className="mb-4 text-2xl font-bold sm:text-5xl">
|
||||||
|
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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -187,23 +187,24 @@ export default function PlaygroundPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const insertAtCursor = (textArea: HTMLTextAreaElement, text: string) => {
|
const insertAtCursor = (textArea: HTMLTextAreaElement, text: string) => {
|
||||||
const start = textArea.selectionStart;
|
const start = textArea.selectionStart;
|
||||||
const end = textArea.selectionEnd;
|
const end = textArea.selectionEnd;
|
||||||
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 />;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,41 +48,64 @@ 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(
|
||||||
<div className="flex items-center gap-2 mb-2">
|
"sticky top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300 z-20 hidden md:flex",
|
||||||
<History className="w-4 h-4" />
|
collapsed ? "w-[48px]" : "w-[250px]"
|
||||||
<h3 className="font-medium text-sm">Version History</h3>
|
)}
|
||||||
</div>
|
>
|
||||||
<ScrollArea className="h-full">
|
{/* Toggle Button */}
|
||||||
<div className="flex flex-col gap-1.5 text-sm dark:text-stone-300/85 text-stone-800 pr-4">
|
<div className="absolute top-0 right-0 py-2 px-0 ml-6 z-30">
|
||||||
{versions.map(({ version, date }) => (
|
<Button
|
||||||
<a
|
size="icon"
|
||||||
key={version}
|
variant="outline"
|
||||||
href={`#v${version}`}
|
className="hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
|
||||||
className={cn(
|
onClick={() => setCollapsed((prev) => !prev)}
|
||||||
"hover:text-foreground transition-colors py-1",
|
>
|
||||||
activeId === `v${version}` && "font-medium text-primary"
|
{collapsed ? <PanelLeftOpen size={18} /> : <PanelLeftClose size={18} />}
|
||||||
)}
|
</Button>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const element = document.getElementById(`v${version}`);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
setActiveId(`v${version}`);
|
|
||||||
window.history.pushState(null, '', `#v${version}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
v{version}
|
|
||||||
<span className="text-xs text-muted-foreground ml-2">
|
|
||||||
{formatDate2(date)}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
</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">
|
||||||
|
<History className="w-4 h-4" />
|
||||||
|
<h3 className="font-medium text-sm">Version History</h3>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
{versions.map(({ version, date }) => (
|
||||||
|
<a
|
||||||
|
key={version}
|
||||||
|
href={`#v${version}`}
|
||||||
|
className={cn(
|
||||||
|
"hover:text-foreground transition-colors py-1",
|
||||||
|
activeId === `v${version}` && "font-medium text-primary"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const element = document.getElementById(`v${version}`);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: "smooth" });
|
||||||
|
setActiveId(`v${version}`);
|
||||||
|
window.history.pushState(null, "", `#v${version}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
v{version}
|
||||||
|
<span className="text-xs text-muted-foreground ml-2">
|
||||||
|
{formatDate2(date)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user