improvement changelog pages
This commit is contained in:
@@ -4,6 +4,8 @@ import { formatDate2, stringToDate } from "@/lib/utils";
|
||||
import { getMetadata } from "@/app/layout";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { AuroraText } from "@/components/ui/aurora";
|
||||
import { ShineBorder } from "@/components/ui/shine-border";
|
||||
import docuConfig from "@/docu.json";
|
||||
|
||||
export const metadata = getMetadata({
|
||||
@@ -16,16 +18,17 @@ export default async function BlogIndexPage() {
|
||||
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
|
||||
);
|
||||
return (
|
||||
<div className="w-full mx-auto flex flex-col gap-1 sm:min-h-[91vh] min-h-[88vh] py-2">
|
||||
<div className="mb-7 flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-extrabold">
|
||||
<div className="flex flex-col items-center justify-center px-2 py-8 text-center sm:py-36">
|
||||
<div className="w-full max-w-[800px] pb-8">
|
||||
<AuroraText className="text-lg"># Stay Informed, Stay Ahead</AuroraText>
|
||||
<h1 className="mb-4 text-2xl font-bold sm:text-5xl">
|
||||
Blog Posts
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground mt-2">
|
||||
Discover the latest updates, tutorials, and insights on {meta.title}.
|
||||
<p className="mb-8 sm:text-xl text-muted-foreground">
|
||||
Explore updates, tips, and deep dives from the {meta.title}.
|
||||
</p>
|
||||
</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) => (
|
||||
<BlogCard {...blog} slug={blog.slug} key={blog.slug} />
|
||||
))}
|
||||
@@ -45,9 +48,8 @@ function BlogCard({
|
||||
return (
|
||||
<Link
|
||||
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">
|
||||
<Image
|
||||
src={cover}
|
||||
@@ -55,11 +57,14 @@ function BlogCard({
|
||||
width={400}
|
||||
height={150}
|
||||
quality={80}
|
||||
className="w-full rounded-md object-cover h-[180px] border"
|
||||
className="w-full rounded-md object-cover h-[200px]"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
<div className="flex items-center justify-between w-full mt-auto">
|
||||
<div className="flex flex-col items-start px-3 py-3 gap-2 mb-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">
|
||||
Published on {formatDate2(date)}
|
||||
</p>
|
||||
|
||||
@@ -2,42 +2,23 @@ import { Suspense } from "react";
|
||||
import { getChangelogEntries } from "@/lib/changelog";
|
||||
import { VersionEntry } from "@/components/changelog/version-entry";
|
||||
import { VersionToc } from "@/components/changelog/version-toc";
|
||||
import { getMetadata } from "@/app/layout";
|
||||
import docuConfig from "@/docu.json";
|
||||
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() {
|
||||
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">
|
||||
<div className="flex items-start gap-8">
|
||||
<Suspense fallback={<div className="lg:flex hidden flex-[1.5] min-w-[238px]" />}>
|
||||
return (
|
||||
<div className="flex items-start">
|
||||
<Suspense fallback={<div className="lg:flex hidden flex-[1.5]" />}>
|
||||
<VersionToc
|
||||
versions={entries.map(({ version, date }) => ({ version, date }))}
|
||||
/>
|
||||
</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="absolute left-0 top-0 h-full w-px bg-border lg:block hidden" />
|
||||
<div className="lg:pl-12 pl-0 lg:pt-8">
|
||||
<div className="absolute left-0 top-0 h-full w-px bg-border md:block hidden" />
|
||||
<div className="md:px-12 md:py-8 max-md:py-10">
|
||||
{entries.map((entry, index) => (
|
||||
<section
|
||||
id={`version-${entry.version}`}
|
||||
@@ -50,8 +31,6 @@ export default async function ChangelogPage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
{/* Floating TOC for smaller screens */}
|
||||
{entries.length > 0 && (
|
||||
<FloatingVersionToc
|
||||
|
||||
@@ -193,18 +193,19 @@ export default function PlaygroundPage() {
|
||||
const before = markdown.substring(0, start);
|
||||
const after = markdown.substring(end);
|
||||
|
||||
// Menambahkan satu baris kosong sebelum dan sesudah komponen
|
||||
const newText = `${before}${text}\n${after}`;
|
||||
const needsLeadingNewline = before && !before.endsWith('\n\n') ? '\n\n' : '';
|
||||
const needsTrailingNewline = after && !after.startsWith('\n\n') ? '\n\n' : '';
|
||||
|
||||
const newText = `${before}${needsLeadingNewline}${text}${needsTrailingNewline}${after}`;
|
||||
setMarkdown(newText);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
textArea.focus();
|
||||
const newPosition = start + text.length + 1;
|
||||
const newPosition = before.length + needsLeadingNewline.length + text.length + 1;
|
||||
textArea.setSelectionRange(newPosition, newPosition);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (isMobile) {
|
||||
return <MobileMessage />;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function FloatingVersionToc({ versions }: FloatingVersionTocProps) {
|
||||
};
|
||||
|
||||
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}>
|
||||
<PopoverTrigger asChild>
|
||||
<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 { 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 { Button } from "@/components/ui/button";
|
||||
|
||||
interface VersionTocProps {
|
||||
versions: Array<{
|
||||
@@ -14,33 +15,30 @@ interface VersionTocProps {
|
||||
|
||||
export function VersionToc({ versions }: VersionTocProps) {
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Handle initial hash
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) {
|
||||
setActiveId(hash);
|
||||
}
|
||||
|
||||
// Set up intersection observer
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.id;
|
||||
setActiveId(id);
|
||||
// Use pushState instead of replaceState to maintain history
|
||||
window.history.pushState(null, '', `#${id}`);
|
||||
window.history.pushState(null, "", `#${id}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.2,
|
||||
rootMargin: '-20% 0px -60% 0px'
|
||||
rootMargin: "-20% 0px -60% 0px",
|
||||
}
|
||||
);
|
||||
|
||||
// Observe version elements
|
||||
versions.forEach(({ version }) => {
|
||||
const element = document.getElementById(`v${version}`);
|
||||
if (element) observer.observe(element);
|
||||
@@ -50,13 +48,35 @@ export function VersionToc({ versions }: VersionTocProps) {
|
||||
}, [versions]);
|
||||
|
||||
return (
|
||||
<aside className="lg:flex hidden toc flex-[1.5] min-w-[238px] pt-8 sticky top-16 h-[calc(100vh-4rem)]">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<aside
|
||||
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">
|
||||
<History className="w-4 h-4" />
|
||||
<h3 className="font-medium text-sm">Version History</h3>
|
||||
</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">
|
||||
{versions.map(({ version, date }) => (
|
||||
<a
|
||||
@@ -70,9 +90,9 @@ export function VersionToc({ versions }: VersionTocProps) {
|
||||
e.preventDefault();
|
||||
const element = document.getElementById(`v${version}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
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>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
import { Logo, NavMenu } from "@/components/navbar";
|
||||
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 { DialogTitle } from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
@@ -22,7 +22,7 @@ export function Leftbar() {
|
||||
return (
|
||||
<aside
|
||||
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 */}
|
||||
<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)}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ArrowRightFromLine size={18} />
|
||||
<PanelLeftOpen size={18} />
|
||||
) : (
|
||||
<ArrowLeftFromLine size={18} />
|
||||
<PanelLeftClose size={18} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,22 @@ pre>code {
|
||||
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 {
|
||||
.animate-shine {
|
||||
--animate-shine: shine var(--duration) infinite linear;
|
||||
|
||||
@@ -9,6 +9,7 @@ const config = {
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
safelist: ["line-clamp-3","line-clam-2"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
|
||||
Reference in New Issue
Block a user