initial docs

This commit is contained in:
2025-04-12 14:34:53 +07:00
commit ea9a71e23c
138 changed files with 23045 additions and 0 deletions

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

14
.vscode/accordion.code-snippets vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"DocuAccordion": {
"prefix": "accordion",
"body": [
"<Accordion title=\"${1:Plain Text}\">",
" this is an example of plain text content from the accordion component and below is markdown ;",
" 1. number one",
" 2. number two",
" 3. number three",
"</Accordion>"
],
"description": "Create a DocuAccordion component with markdown list."
}
}

16
.vscode/button.code-snippets vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"DocuButton": {
"prefix": "button",
"body": [
"<Button",
" text=\"${1:Learn More}\"",
" href=\"${2:https://learn.example.com}\"",
" icon=\"${3:MoveUpRight}\"",
" size=\"${4:md}\"",
" target=\"${5:_blank}\"",
" variation=\"${6:primary}\"",
"/>"
],
"description": "Create a DocuButton component on markdown."
}
}

16
.vscode/card.code-snippets vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"DocuCards": {
"prefix": "card",
"body": [
"<CardGroup cols={2}>",
"<Card title=\"${1:Heading2}\" icon=\"${2:Heading2}\">",
" This is an example of card content with columns.",
"</Card>",
"<Card title=\"${3:Heading3}\" icon=\"${4:Heading3}\">",
" This is an example of card content with columns.",
"</Card>",
"</CardGroup>"
],
"description": "Create a DocuCards component on markdown."
}
}

16
.vscode/codeblock.code-snippets vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"DocuCodeBlock": {
"prefix": "codeblock",
"description": "Checks if the rocket is stable and prevents a crash if it's not.",
"body": [
"```${1:javascript:main.js} showLineNumbers {${2:3-4}}",
"function isRocketAboutToCrash() {",
" // Check if the rocket is stable",
" if (!isStable()) {",
" NoCrash(); // Prevent the crash",
" }",
"}",
"```",
],
}
}

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": []
}

16
.vscode/image-link.code-snippets vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"DocuImage": {
"prefix": "image",
"body": [
"![${1:Alt text for the image}](${2:https://via.placeholder.com/150})"
],
"description": "Snippet untuk menampilkan image komponen."
},
"DocuLink": {
"prefix": "link",
"body": [
"[${1:Text Link}](${2:https://www.openai.com})"
],
"description": "Snippet untuk menampilkan link komponen."
}
}

13
.vscode/metadata.code-snippets vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"DocuMetadata": {
"prefix": "metadata",
"body": [
"---",
"title : ${1:judul post}",
"description : ${2:deskripsi singkat dari post}",
"date : ${3:10-12-2024}",
"---"
],
"description": "Snippet untuk membuat metadata."
}
}

38
.vscode/note.code-snippets vendored Normal file
View File

@@ -0,0 +1,38 @@
{
"DocuNote - General Note": {
"prefix": "note",
"body": [
"<Note type=\"note\" title=\"Note\">",
" ${1:This is a general note to convey information to the user.}",
"</Note>"
],
"description": "Insert a general note"
},
"DocuNote - Danger Note": {
"prefix": "danger",
"body": [
"<Note type=\"danger\" title=\"Danger\">",
" ${1:This is a danger alert to notify the user of a critical issue.}",
"</Note>"
],
"description": "Insert a danger note"
},
"DocuNote - Warning Note": {
"prefix": "warning",
"body": [
"<Note type=\"warning\" title=\"Warning\">",
" ${1:This is a warning alert for issues that require attention.}",
"</Note>"
],
"description": "Insert a warning note"
},
"DocuNote - Success Note": {
"prefix": "success",
"body": [
"<Note type=\"success\" title=\"Success\">",
" ${1:This is a success message to inform the user of successful actions.}",
"</Note>"
],
"description": "Insert a success note"
}
}

24
.vscode/stepper.code-snippets vendored Normal file
View File

@@ -0,0 +1,24 @@
{
"DocuStepper": {
"prefix": "stepper",
"body": [
"<Stepper>",
" <StepperItem title=\"${1:Step 1: Clone the DocuBook Repository}\">",
" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec interdum,",
" felis sed efficitur tincidunt, justo nulla viverra enim, et maximus nunc",
" dolor in lorem.",
" </StepperItem>${2:}",
"</Stepper>"
],
"description": "Snippet untuk menampilkan stepper komponen."
},
"DocuStepperItem": {
"prefix": "item",
"body": [
"<StepperItem title=\"${1:Step X: Your Step Title}\">",
" ${2:Your step description here.}",
"</StepperItem>${3:}"
],
"description": "Snippet untuk menambahkan item baru ke dalam Stepper."
}
}

20
.vscode/table.code-snippets vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"DocuTable": {
"prefix": "table",
"body": [
"| **${1:Feature}** | **${2:Description}** |",
"| ------------------------------- | ----------------------------------------------------- |",
"| ${3:MDX Support} | ${4:Write interactive documentation with MDX.} |",
"| Nested Pages | Organize content in a nested, hierarchical structure. |",
"| Blog Section | Include a dedicated blog section. |",
"| Pagination | Split content across multiple pages. |",
"| Syntax Highlighting | Highlight code for better readability. |",
"| Code Line Highlighting & Titles | Highlight specific lines with descriptive titles. |",
"| Interactive Code Blocks | Language-specific and interactive code display. |",
"| Custom Markdown Components | Embed custom, reusable components in your docs. |",
"| Static Site Generation | Generate a static, high-performance site. |",
"| SEO-Optimized | Structured for optimal search engine indexing. |"
],
"description": "Create a DocuTable component on markdown."
}
}

33
.vscode/tabs.code-snippets vendored Normal file
View File

@@ -0,0 +1,33 @@
{
"DocuTabs": {
"prefix": "tabs",
"body": [
"<Tabs defaultValue=\"${1:java}\" className=\"pt-5 pb-1\">",
" <TabsList>",
" <TabsTrigger value=\"${2:java}\">Java</TabsTrigger>",
" <TabsTrigger value=\"${3:typescript}\">TypeScript</TabsTrigger>",
" </TabsList>",
" <TabsContent value=\"${4:java}\">",
" ```java",
" // HelloWorld.java",
" public class HelloWorld {",
" public static void main(String[] args) {",
" System.out.println(\"Hello, World!\");",
" }",
" }",
" ```",
" </TabsContent>",
" <TabsContent value=\"${5:typescript}\">",
" ```typescript",
" // helloWorld.ts",
" function helloWorld(): void {",
" console.log(\"Hello, World!\");",
" }",
" helloWorld();",
" ```",
" </TabsContent>",
"</Tabs>"
],
"description": "Create a DocuTabs component with Java and TypeScript examples."
}
}

9
.vscode/tooltips.code-snippets vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"DocuTooltips": {
"prefix": "tooltips",
"body": [
"${1:What do you know about }<Tooltip text=\"${2:DocuBook}\" tip=\"${3:npx @docubook/create@latest}\" /> ${4:? Create interactive nested documentations using MDX.}",
],
"description": "Create a DocuTooltips component with version examples."
}
}

33
.vscode/typography.code-snippets vendored Normal file
View File

@@ -0,0 +1,33 @@
{
"DocuH2": {
"prefix": "h2",
"body": [
"## Heading 2"
],
"description": "Tag Heading 2 for markdown."
},
"DocuH3": {
"prefix": "h3",
"body": [
"### Heading 3"
],
"description": "Tag Heading 3 for markdown."
},
"DocuText": {
"prefix": "text",
"body": [
"DocuBook is proudly **open-source**! 🎉 We believe in creating an accessible, collaborative platform that thrives on community contributions."
],
"description": "Tag Paragraph for markdown."
},
"Docu-UndorderList": {
"prefix": "unorderlist",
"body": [
"- ${1:**Next.js 14** - The powerful React framework optimized for production.}",
"- **Tailwind CSS** - Utility-first styling for quick, clean designs.",
"- **Shadcn-UI** - Elegant, accessible components for a polished look.",
"- **next-mdx-remote** - Enables MDX support for dynamic, interactive Markdown content."
],
"description": "Tag Undorderlist for markdown."
},
}

9
.vscode/youtube.code-snippets vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"DocuVideo": {
"prefix": "youtube",
"body": [
"<Youtube videoId=\"${1:zQnBQ4tB3ZA}\" />"
],
"description": "Snippet untuk menampilkan komponen video Youtube."
}
}

329
CHANGELOG.md Normal file
View File

@@ -0,0 +1,329 @@
## [1.8.0] - 2025-03-01
> Now looks more modern and clean which is a big change in layout and design
### Added
- Social footer
- Toggle group
- Site description {meta.description} in footer
- Site title {meta.title} in footer
### Improved
- Header design changes
- Footer design changes
- New functions in theme provider
- Object changes in docu.json
### Fixed
- Updates to path structure components
- Groups to organize components
## [1.7.0] - 2025-02-23
> Remove the old function in the search dialog and replace it with a new and more optimal feature
### Added
- Up and down navigation : search dialog.tsx
- Enter (return) to select : search dialog.tsx
- Escape to close the dialog : search dialog.tsx
### Improved
- Maintenance for anchor components
- Anchor.tsx adjustments for all elements that use it
### Removed
- Remove suboptimal search features
## [1.6.0] - 2025-02-21
> New Feature Card Groups with arrays for more Flexible Content
### Added
- Card Groups Components
- Props : href to url link
- Props : horizontal boolean
### Improved
- Card props styling
- Compability for Cards components
- {children} support for card content
### Removed
- remove unused props cards components
## [1.5.0] - 2025-02-18
> Minor Update - improved features and responsiveness on all devices
### Added
- New dialog footer on searchbox above @media 768px
- Icon X for close dialog on searcbox as esc on medium screen
### Improved
- Responsive Leftbar components on large screen
- Menu Trigger on medium screen
- Responsive Navbar components on medium screen
- Better UX for searchbox dialog
- tooltips components can be written together with regular paragraphs
### Fixed
- Responsive issue
- Compatibility for Bun
- Changes postcss.config.js to .cjs for Bun
- all CLI installer and updater not working
- adjustments for package managers npm, pnpm, bun, yarn
## [1.4.2] - 2025-02-16
> Complex Content for Accordion Component props {children}
### Added
- New Props with {children} in accordion
- Compatibility for markdown in accordion
- Nested components inside an accordion
- New icon on note components
- add CLI npx @docubook/create@latest
- add CLI npx @docubook/update@latest
### Improved
- Better UI design for accordion
- Styling Note components on markdown
- Change accordion output on playground
- Change accordion output on snippet
### Removed
- Remove depcreated props on accordion
- Remove CLI npx update_docu
- Remove CLI npx create_docu
## [1.4.0] - 2025-02-11
> Floating Button Version with Dynamic Tag {version} on Changelog page
### Added
- New components / changelog floating-version.tsx
- Button popover to open version-toc below @media 1024px
- Dynamic tag by section ID #version
- Dynamic url tag #version
- Dynamic version indicator on floating version when scrolling section by ID
### Improved
- change icon version history
- responsive version-toc
- improvement components to changelog page
## [1.3.8] - 2025-02-08
> Responsive Table of Content
### Added
- Components terminal MagicUI
- Components card Shadcn
- New mob-toc for a better experience on mobile devices
- New Components scroll to top button
- Scroll to top :blog-post
- Scroll to top :docs-post
### Improved
- lib/markdown for generated dynamic toc on markdown
- Responsive Table of Content below @media 1024px
- Improve docs page
## [1.3.6] - 2025-02-01
> Appears more modern editor for Docu Play
![version 1.3.6 - Playground](https://docubook.pro/images/new-editor.png)
### Added
- Line Number for editor
- editor.css
### Improved
- Better Design for Editor
- Similar to Github Editor
- Moved Handler Element (copy, download, reset and fullscreen) on Header
## [1.3.5] - 2025-01-30
> it's Easy to Write Markdown with Playground
![version 1.3.5 - Playground](https://docubook.pro/images/img-playground.png)
### Added
- New Playground Page
- New Playground Layout
- Toolbar for Markdown Components
- Fullscreen Mode to Focus Editing Your Content
- Copy to Clipboard Your Content
- Download Your Content as index.mdx
- Reset Your Content without refresh the Browser
- Only Large Screen for Better Experience
## [1.3.1] - 2025-01-20
> Snippet Feature to Easily Write Markdown and Call DocuBook Components
![version 1.3.1 - Snippet Features](https://docubook.pro/images/snippet.png)
### Added
- New Feature Snippet for Markdown Components
- Support Snippet for Visual Studio Code
### Removed
- remove props icon and props description for accordion components
## [1.3.0] - 2024-12-31
> Release Note Feature to Make it Easier to Write Changelogs
### Added
- New Release Note Feature
- New Layout for Changelog page
- New Changelog page
- Add Release Note Component
- Easily write release notes directly from the CHANGELOG.md file
- TOC for versioning
- Write with the markdown tag
- Add lib / changelog.ts
### Improved
- Improvement Responsive feature image for Version Entry
- Improvement Layout for changelog page
- Improvement Padding on mobile devices
- Only use containers of md size
- Improvement syntax.css for ul>li classes
### Fixed
- Fix og:image not showing on Page.tsx
- Fix text-indent on class li
### Removed
- Remove excessive padding
- Remove Logo on Footer
## [1.2.0] - 2024-12-22
> New Accordion Component : Support content plain text, html and all markdown component
### Added
- add New Accordion
### Improved
- Props Improvement
- Support Dynamic Content for Accordion
## [1.1.0] - 2024-12-15
> Minor Update : Easily manage set up with docu.json
### Added
- add docu.json file
- add openGraph (title, description, image)
- add Dynamic metadata
- Generate metadata as openGraph
- openGraph support for .mdx
### Improved
- routes-config from json
- Frontmatter improvement
- Edit the content of footer.tsx simply via the docu.json file
- Edit the content of navbar.tsx simply via the docu.json file
## [1.0.7] - 2024-12-14
> Easily updates your DocuBook Version with CLI npx update_docu
### Added
- CLI npx update_docu (update features into docubook existing directory)
- Playground (easily to written content)
- New Button component
- Navbar external link conditions
- CLI npx create_docu
### Improved
- Searchbar Improvement
- Navigation Improvement
- Edit on Github Improvement
### Removed
- Remove CLI npx create-docu (on this version not usage dash `-`)
## [1.0.6] - 2024-11-24
> New Components, Fix and Improvement
### Added
- New Card component
- New Tooltips component
### Fixed
- change root folder
### Improved
- logo on navbar & footer
- easily change logo
## [1.0.5] - 2024-11-16
> Add New Features and Improvement for this version
### Added
- New Youtube component
- edit this page - easily manage directory content via the github repo
- support installation via cli commant npx create-docu
### Improved
- keyboard shortcut command + k or ctrl + k to open search dialog
## [1.0.0] - 2024-11-10
> Initial release of DocuBook to create interactive nested docs with MDX
### Added
- Initial release of DocuBook
- Basic documentation structure
- Markdown support with MDX
- Responsive design
- Search functionality
- Dark mode support

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Wildan Nursahidan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# DocuBook
**DocuBook** is a documentation web project designed to provide a simple and user-friendly interface for accessing various types of documentation. This site is crafted for developers and teams who need quick access to references, guides, and essential documents.
> **Note**: This application is a fork of [AriaDocs](https://github.com/nisabmohd/Aria-Docs), created by [Nisab Mohd](https://github.com/nisabmohd). DocuBook provides an alternative to the documentation solution found on [Mintlify](https://mintlify.com/), utilizing `.mdx` (Markdown + JSX) for content creation and management.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/mywildancloud/docubook)
## Features
- **Easy Navigation**: Simple layout for quick navigation between pages.
- **Quick Search**: Easily find documentation using a search function.
- **Responsive Theme**: Responsive design optimized for devices ranging from desktops to mobile.
- **Markdown Content**: Support for markdown-based documents.
- **SEO Friendly**: Optimized structure for search visibility, enhancing accessibility on search engines.
## Installation
```bash
npx @docubook/create@latest
```
#### command output
```bash
? Enter a name for your project directory: (docubook)
Creating a new Docubook project in /path/your/docubook from the main branch...
✔ Docubook project successfully created in /path/your/docubook!
Next steps:
1. Navigate to your project directory:
cd docubook
2. Install dependencies:
npm install
3. Start the development server:
npm run dev
```

92
app/blog/[slug]/page.tsx Normal file
View File

@@ -0,0 +1,92 @@
import { Typography } from "@/components/docs/typography";
import { buttonVariants } from "@/components/ui/button";
import { Author, getAllBlogStaticPaths, getBlogForSlug } from "@/lib/markdown";
import { ArrowLeftIcon } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { formatDate } from "@/lib/utils";
import { ScrollToTop } from "@/components/docs/scroll-to-top";
type PageProps = {
params: { slug: string };
};
export async function generateMetadata({ params: { slug } }: PageProps) {
const res = await getBlogForSlug(slug);
if (!res) return null;
const { frontmatter } = res;
return {
title: frontmatter.title,
description: frontmatter.description,
};
}
export async function generateStaticParams() {
const val = await getAllBlogStaticPaths();
if (!val) return [];
return val.map((it) => ({ slug: it }));
}
export default async function BlogPage({ params: { slug } }: PageProps) {
const res = await getBlogForSlug(slug);
if (!res) notFound();
return (
<div className="lg:w-[60%] sm:[95%] md:[75%] mx-auto">
<Link
className={buttonVariants({
variant: "link",
className: "!mx-0 !px-0 mb-7 !-ml-1 ",
})}
href="/blog"
>
<ArrowLeftIcon className="w-4 h-4 mr-1.5" /> Back to blog
</Link>
<div className="flex flex-col gap-3 pb-7 w-full mb-2">
<p className="text-muted-foreground text-sm">
{formatDate(res.frontmatter.date)}
</p>
<h1 className="sm:text-4xl text-3xl font-extrabold">
{res.frontmatter.title}
</h1>
<div className="mt-6 flex flex-col gap-3">
<p className="text-sm text-muted-foreground">Posted by</p>
<Authors authors={res.frontmatter.authors} />
</div>
</div>
<div className="!w-full">
<Typography>{res.content}</Typography>
</div>
<ScrollToTop />
</div>
);
}
function Authors({ authors }: { authors: Author[] }) {
return (
<div className="flex items-center gap-8 flex-wrap">
{authors.map((author) => {
return (
<Link
href={author.handleUrl}
className="flex items-center gap-2"
key={author.username}
>
<Avatar className="w-10 h-10">
<AvatarImage src={author.avatar} />
<AvatarFallback>
{author.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="">
<p className="text-sm font-medium">{author.username}</p>
<p className="font-code text-[13px] text-muted-foreground">
@{author.handle}
</p>
</div>
</Link>
);
})}
</div>
);
}

9
app/blog/layout.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { PropsWithChildren } from "react";
export default function BlogLayout({ children }: PropsWithChildren) {
return (
<div className="flex flex-col items-start justify-center pt-8 pb-10 w-full mx-auto">
{children}
</div>
);
}

98
app/blog/page.tsx Normal file
View File

@@ -0,0 +1,98 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Author, BlogMdxFrontmatter, getAllBlogs } from "@/lib/markdown";
import { formatDate2, stringToDate } from "@/lib/utils";
import { getMetadata } from "@/app/layout";
import Image from "next/image";
import Link from "next/link";
import docuConfig from "@/docu.json";
export const metadata = getMetadata({
title: "Blog",
description: "Discover the latest updates, tutorials, and insights on DocuBook.",
});
const { meta } = docuConfig;
export default async function BlogIndexPage() {
const blogs = (await getAllBlogs()).sort(
(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] pt-2">
<div className="mb-7 flex flex-col gap-2">
<h1 className="text-2xl font-extrabold">
Blog Posts
</h1>
<p className="text-lg text-muted-foreground mt-2">
Discover the latest updates, tutorials, and insights on {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">
{blogs.map((blog) => (
<BlogCard {...blog} slug={blog.slug} key={blog.slug} />
))}
</div>
</div>
);
}
function BlogCard({
date,
title,
description,
slug,
cover,
authors,
}: BlogMdxFrontmatter & { slug: string }) {
return (
<Link
href={`/blog/${slug}`}
className="flex flex-col gap-2 items-start border rounded-md py-5 px-3 min-h-[400px]"
>
<h3 className="text-md font-semibold -mt-1 pr-7">{title}</h3>
<div className="w-full">
<Image
src={cover}
alt={title}
width={400}
height={150}
quality={80}
className="w-full rounded-md object-cover h-[180px] border"
/>
</div>
<p className="text-sm text-muted-foreground">{description}</p>
<div className="flex items-center justify-between w-full mt-auto">
<p className="text-[13px] text-muted-foreground">
Published on {formatDate2(date)}
</p>
<AvatarGroup users={authors} />
</div>
</Link>
);
}
function AvatarGroup({ users, max = 4 }: { users: Author[]; max?: number }) {
const displayUsers = users.slice(0, max);
const remainingUsers = Math.max(users.length - max, 0);
return (
<div className="flex items-center">
{displayUsers.map((user, index) => (
<Avatar
key={user.username}
className={`inline-block border-2 w-9 h-9 border-background ${
index !== 0 ? "-ml-3" : ""
} `}
>
<AvatarImage src={user.avatar} alt={user.username} />
<AvatarFallback>
{user.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
))}
{remainingUsers > 0 && (
<Avatar className="-ml-3 inline-block border-2 border-background hover:translate-y-1 transition-transform">
<AvatarFallback>+{remainingUsers}</AvatarFallback>
</Avatar>
)}
</div>
);
}

11
app/changelog/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
export default function ChangelogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex items-start gap-8">
{children}
</div>
);
}

63
app/changelog/page.tsx Normal file
View File

@@ -0,0 +1,63 @@
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]" />}>
<VersionToc
versions={entries.map(({ version, date }) => ({ version, date }))}
/>
</Suspense>
<main className="flex-1 lg:flex-[5.25] min-w-0">
<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">
{entries.map((entry, index) => (
<section
id={`version-${entry.version}`}
key={entry.version}
className="scroll-mt-20" // Tambahkan margin atas saat scroll
>
<VersionEntry {...entry} isLast={index === entries.length - 1} />
</section>
))}
</div>
</div>
</main>
</div>
</div>
{/* Floating TOC for smaller screens */}
{entries.length > 0 && (
<FloatingVersionToc
versions={entries.map(({ version, date }) => ({ version, date }))}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,105 @@
import { notFound } from "next/navigation";
import { getDocsForSlug, getDocsTocs } from "@/lib/markdown";
import DocsBreadcrumb from "@/components/docs/docs-breadcrumb";
import Pagination from "@/components/docs/pagination";
import Toc from "@/components/docs/toc";
import { Typography } from "@/components/docs/typography";
import EditThisPage from "@/components/docs/edit-on-github";
import { formatDate2 } from "@/lib/utils";
import docuConfig from "@/docu.json";
import MobToc from "@/components/docs/mob-toc";
import { ScrollToTop } from "@/components/docs/scroll-to-top";
const { meta } = docuConfig;
type PageProps = {
params: {
slug: string[];
};
};
// Function to generate metadata dynamically
export async function generateMetadata({ params: { slug = [] } }: PageProps) {
const pathName = slug.join("/");
const res = await getDocsForSlug(pathName);
if (!res) {
return {
title: "Page Not Found",
description: "The requested page was not found.",
};
}
const { title, description, image } = res.frontmatter;
// Absolute URL for og:image
const ogImage = image
? `${meta.baseURL}/images/${image}`
: `${meta.baseURL}/images/og-image.png`;
return {
title: `${title}`,
description,
openGraph: {
title,
description,
url: `${meta.baseURL}/docs/${pathName}`,
type: "article",
images: [
{
url: ogImage,
width: 1200,
height: 630,
alt: title,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [ogImage],
},
};
}
export default async function DocsPage({ params: { slug = [] } }: PageProps) {
const pathName = slug.join("/");
const res = await getDocsForSlug(pathName);
if (!res) notFound();
const { title, description, image, date } = res.frontmatter;
// File path for edit link
const filePath = `contents/docs/${slug.join("/") || ""}/index.mdx`;
const tocs = await getDocsTocs(pathName);
return (
<div className="flex items-start gap-10">
<div className="flex-[4.5] pt-10">
<DocsBreadcrumb paths={slug} />
<div className="mb-8">
<MobToc tocs={tocs} />
</div>
<Typography>
<h1 className="text-3xl !-mt-0.5">{title}</h1>
<p className="-mt-4 text-muted-foreground text-[16.5px]">{description}</p>
<div>{res.content}</div>
<div className="my-8 flex justify-between items-center border-b-2 border-x-muted-foreground">
{date && (
<p className="text-[13px] text-muted-foreground">
Published on {formatDate2(date)}
</p>
)}
<EditThisPage filePath={filePath} />
</div>
<Pagination pathname={pathName} />
</Typography>
<ScrollToTop />
</div>
<Toc path={pathName} />
</div>
);
}

14
app/docs/layout.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { Leftbar } from "@/components/docs/leftbar";
export default function DocsLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="flex items-start gap-8">
<Leftbar key="leftbar" />
<div className="flex-[5.25]">{children}</div>
</div>
);
}

44
app/error.tsx Normal file
View File

@@ -0,0 +1,44 @@
"use client"; // Error components must be Client Components
import { Button, buttonVariants } from "@/components/ui/button";
import Link from "next/link";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="min-h-[87vh] px-2 sm:py-28 py-36 flex flex-col gap-4 items-center">
<div className="text-center flex flex-col items-center justify-center w-fit gap-2">
<h2 className="text-7xl font-bold pr-1">Oops!</h2>
<p className="text-muted-foreground text-md font-medium">
Something went wrong {":`("}
</p>
<p>
We&apos;re sorry, but an error occurred while processing your request.
</p>
</div>
<div className="flex items-center gap-2">
<Button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Reload page
</Button>
<Link href="/" className={buttonVariants({})}>
Back to homepage
</Link>
</div>
</div>
);
}

18
app/hire-me/page.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { getMetadata } from "@/app/layout";
export const metadata = getMetadata({
title: "Hire Me",
description: "Hire me to start a documentation project with DocuBook",
});
export default function EmbeddedHTML() {
return (
<div className="w-full py-0 dark:bg-transparent mx-auto min-h-svh">
<iframe
src="/hire-me.html"
width="100%"
height="1000"
/>
</div>
);
}

96
app/layout.tsx Normal file
View File

@@ -0,0 +1,96 @@
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/contexts/theme-provider";
import { Navbar } from "@/components/docs/navbar";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { Footer } from "@/components/docs/footer";
import docuConfig from "@/docu.json";
import { Toaster } from "@/components/ui/sonner";
import "@/styles/globals.css";
const { meta } = docuConfig;
// Default Metadata
const defaultMetadata: Metadata = {
metadataBase: new URL(meta.baseURL),
description: meta.description,
title: meta.title,
icons: {
icon: meta.favicon,
},
openGraph: {
title: meta.title,
description: meta.description,
images: [
{
url: new URL("/images/og-image.png", meta.baseURL).toString(),
width: 1200,
height: 630,
alt: String(meta.title),
},
],
locale: "en_US",
type: "website",
},
};
// Dynamic Metadata Getter
export function getMetadata({
title,
description,
image,
}: {
title?: string;
description?: string;
image?: string;
}): Metadata {
const ogImage = image ? new URL(`/images/${image}`, meta.baseURL).toString() : undefined;
return {
...defaultMetadata,
title: title ? `${title}` : defaultMetadata.title,
description: description || defaultMetadata.description,
openGraph: {
...defaultMetadata.openGraph,
title: title || defaultMetadata.openGraph?.title,
description: description || defaultMetadata.openGraph?.description,
images: ogImage ? [
{
url: ogImage,
width: 1200,
height: 630,
alt: String(title || defaultMetadata.openGraph?.title),
},
] : defaultMetadata.openGraph?.images,
},
};
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${GeistSans.variable} ${GeistMono.variable} font-regular antialiased`}
suppressHydrationWarning
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Navbar />
<main className="sm:container mx-auto w-[90vw] h-auto scroll-smooth">
{children}
</main>
<Footer />
<Toaster position="top-center" />
</ThemeProvider>
</body>
</html>
);
}

19
app/not-found.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { buttonVariants } from "@/components/ui/button";
import Link from "next/link";
export default function NotFound() {
return (
<div className="min-h-[87vh] px-2 sm:py-28 py-36 flex flex-col gap-4 items-center">
<div className="text-center flex flex-col items-center justify-center w-fit gap-2">
<h2 className="text-7xl font-bold pr-1">404</h2>
<p className="text-muted-foreground text-md font-medium">
Page not found {":("}
</p>
<p>Oops! The page you&apos;re looking for doesn&apos;t exist.</p>
</div>
<Link href="/" className={buttonVariants({})}>
Back to homepage
</Link>
</div>
);
}

93
app/page.tsx Normal file
View File

@@ -0,0 +1,93 @@
import { buttonVariants } from "@/components/ui/button";
import { page_routes } from "@/lib/routes-config";
import { ArrowRightIcon, FileJson, GitCommitHorizontal, SquarePlay } from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
import AnimatedShinyText from "@/components/ui/animated-shiny-text";
import { getMetadata } from "@/app/layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export const metadata = getMetadata({
title: "Home",
});
export default function Home() {
return (
<div className="flex flex-col min-h:[100vh] items-center justify-center text-center px-4 sm:py-40 py-12">
<Link
href="/changelog"
className="mb-5 sm:text-lg flex items-center gap-2 underline underline-offset-4 sm:-mt-12"
>
<div className="z-10 flex min-h-10 items-center justify-center max-[800px]:mt-10">
<div
className={cn(
"group rounded-full border border-black/5 bg-black/5 text-base text-white transition-all ease-in hover:cursor-pointer hover:bg-accent dark:border-white/5 dark:bg-transparent dark:hover:bg-accent",
)}
>
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-100 hover:duration-300 hover:dark:text-neutral-200">
<span>🚀 New Version - Release v.1.8.0</span>
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</AnimatedShinyText>
</div>
</div>
</Link>
<h1 className="text-2xl font-bold mb-6 sm:text-6xl">DocuBook Starter Templates</h1>
<p className="mb-10 sm:text-xl max-w-[800px] text-muted-foreground">
Get started by editing app/page.tsx . Save and see your changes instantly.{' '}
<Link className="text-primary underline" href="https://www.docubook.pro/docs/getting-started/introduction" target="_blank">
Read Documentations
</Link>
</p>
<div className="flex flex-row items-center gap-6 mb-10">
<Link
href={`/docs${page_routes[0].href}`}
className={buttonVariants({
className: "px-6 bg-accent text-white hover:bg-primary dark:bg-accent dark:hover:bg-primary",
size: "lg",
})}
>
Get Started
</Link>
<Link
href="/playground"
className={buttonVariants({
variant: "secondary",
className: "px-6 bg-gray-200 text-gray-900 hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700",
size: "lg",
})}
>
Playground
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 py-12">
<Card className="px-2 py-6">
<CardHeader className="flex flex-row justify-center items-center gap-3">
<FileJson className="size-6 text-primary" />
<CardTitle className="text-xl">docu.json</CardTitle>
</CardHeader>
<CardContent>
<p>Edit the docu.json file to change the content in the header, footer and sidebar.</p>
</CardContent>
</Card>
<Card className="px-2 py-6">
<CardHeader className="flex flex-row justify-center items-center gap-3">
<GitCommitHorizontal className="size-6 text-primary" />
<CardTitle className="text-xl">CHANGELOG.md</CardTitle>
</CardHeader>
<CardContent>
<p>Manage changes to each version of your application in the CHANGELOG.md file.</p>
</CardContent>
</Card>
<Card className="px-2 py-6">
<CardHeader className="flex flex-row justify-center items-center gap-3">
<SquarePlay className="size-6 text-primary" />
<CardTitle className="text-xl">Docu Play</CardTitle>
</CardHeader>
<CardContent>
<p>Easy to write interactive markdown content with a playground.</p>
</CardContent>
</Card>
</div>
</div>
);
}

16
app/playground/layout.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { PropsWithChildren } from "react";
import { getMetadata } from "@/app/layout";
export const metadata = getMetadata({
title: "Playground",
description: "Test and experiment with DocuBook markdown components in real-time",
image: "img-playground.png",
});
export default function PlaygroundLayout({ children }: PropsWithChildren) {
return (
<div className="flex flex-col min-h-screen">
{children}
</div>
);
}

510
app/playground/page.tsx Normal file
View File

@@ -0,0 +1,510 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { toast } from "sonner";
import {
List,
ListOrdered,
Heading2,
Heading3,
Code,
Quote,
ImageIcon,
Link as LinkIcon,
Table,
Maximize2,
Minimize2,
Type,
ChevronDown,
Notebook,
Component,
Youtube as YoutubeIcon,
HelpCircle,
LayoutGrid,
MousePointer2,
Rows,
LayoutPanelTop,
Laptop2,
Copy,
Download,
RotateCcw
} from "lucide-react";
import { Button as UIButton } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import "@/styles/editor.css";
const ToolbarButton = ({ icon: Icon, label, onClick }: { icon: any, label: string, onClick?: () => void }) => (
<UIButton
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-muted"
title={label}
onClick={onClick}
>
<Icon className="h-4 w-4" />
</UIButton>
);
const ToolbarSeparator = () => (
<Separator orientation="vertical" className="mx-1 h-6" />
);
const MobileMessage = () => (
<div className="min-h-[80vh] flex flex-col items-center justify-center text-center px-4 animate-in fade-in-50 duration-500">
<Laptop2 className="w-16 h-16 mb-4 text-muted-foreground animate-bounce" />
<h2 className="text-2xl font-bold mb-2">Desktop View Recommended</h2>
<p className="text-muted-foreground max-w-md">
The Playground works best on larger screens. Please switch to a desktop device for the best experience.
</p>
</div>
);
export default function PlaygroundPage() {
const [markdown, setMarkdown] = useState("");
const [isFullscreen, setIsFullscreen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [lineCount, setLineCount] = useState(1);
const editorRef = useRef<HTMLTextAreaElement>(null);
const lineNumbersRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => {
window.removeEventListener('resize', checkMobile);
};
}, []);
useEffect(() => {
// Update line count when markdown content changes
const lines = markdown.split('\n').length;
setLineCount(Math.max(lines, 1));
}, [markdown]);
// Sync scroll position between editor and line numbers
useEffect(() => {
const textarea = editorRef.current;
const lineNumbers = lineNumbersRef.current;
if (!textarea || !lineNumbers) return;
const handleScroll = () => {
lineNumbers.scrollTop = textarea.scrollTop;
};
textarea.addEventListener('scroll', handleScroll);
return () => textarea.removeEventListener('scroll', handleScroll);
}, []);
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(markdown);
toast.success('Content copied to clipboard');
} catch (err) {
toast.error('Failed to copy content');
}
};
const handleDownload = () => {
try {
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'index.mdx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Content downloaded successfully');
} catch (err) {
toast.error('Failed to download content');
}
};
const handleReset = () => {
if (markdown.trim()) {
toast.custom((t) => (
<div className="flex flex-col gap-2 bg-background border rounded-lg p-4 shadow-lg">
<h3 className="font-semibold">Clear editor content?</h3>
<p className="text-sm text-muted-foreground">This action cannot be undone.</p>
<div className="flex gap-2 mt-2">
<UIButton
size="sm"
variant="destructive"
onClick={() => {
setMarkdown('');
toast.success('all content in the editor has been cleaned');
toast.dismiss(t);
}}
>
Clear
</UIButton>
<UIButton
size="sm"
variant="outline"
onClick={() => toast.dismiss(t)}
>
Cancel
</UIButton>
</div>
</div>
), {
duration: 10000,
});
}
};
const insertAtCursor = (textArea: HTMLTextAreaElement, text: string) => {
const start = textArea.selectionStart;
const end = textArea.selectionEnd;
const before = markdown.substring(0, start);
const after = markdown.substring(end);
const newText = before + text + after;
setMarkdown(newText);
requestAnimationFrame(() => {
textArea.focus();
const newPosition = start + text.length;
textArea.setSelectionRange(newPosition, newPosition);
});
};
const handleParagraphClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, 'this is regular text, **bold text**, *italic text*\n');
}
};
const handleHeading2Click = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '## Heading 2\n');
}
};
const handleHeading3Click = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '### Heading 3\n');
}
};
const handleBulletListClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '- List One\n- List Two\n- Other List\n');
}
};
const handleNumberedListClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '1. Number One\n2. Number Two\n3. Number Three\n');
}
};
const handleLinkClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '[Visit OpenAI](https://www.openai.com)\n');
}
};
const handleImageClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '![Alt text for the image](https://via.placeholder.com/150)\n');
}
};
const handleBlockquoteClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '> The overriding design goal for Markdown\'s formatting syntax is to make it as readable as possible.\n');
}
};
const handleCodeBlockClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '```javascript:main.js showLineNumbers {3-4}\nfunction isRocketAboutToCrash() {\n // Check if the rocket is stable\n if (!isStable()) {\n NoCrash(); // Prevent the crash\n }\n}\n```\n');
}
};
const handleTableClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, `| **Feature** | **Description** |
| ------------------------------- | ----------------------------------------------------- |
| MDX Support | Write interactive documentation with MDX. |
| Nested Pages | Organize content in a nested, hierarchical structure. |
| Blog Section | Include a dedicated blog section. |
| Pagination | Split content across multiple pages. |
`);
}
};
const handleNoteClick = (type: string) => {
const textArea = document.querySelector('textarea');
if (textArea) {
const noteTemplate = `<Note type="${type}" title="${type.charAt(0).toUpperCase() + type.slice(1)}">\n This is a ${type} message.\n</Note>\n`;
insertAtCursor(textArea, noteTemplate);
}
};
const handleComponentClick = (component: string) => {
const textArea = document.querySelector('textarea');
if (!textArea) return;
const templates: { [key: string]: string } = {
stepper: `<Stepper>
<StepperItem title="Step 1">
Content for step 1
</StepperItem>
<StepperItem title="Step 2">
Content for step 2
</StepperItem>
</Stepper>\n`,
card: `<Card title="Click on me" icon="Link" href="/docs/getting-started/components/button">
This is how you use a card with an icon and a link. Clicking on this card
brings you to the Card Group page.
</Card>\n`,
button: `<Button
text="Click Me"
href="#"
icon="ArrowRight"
size="md"
variation="primary"
/>\n`,
accordion: `<Accordion title="Markdown">
this is an example of plain text content from the accordion component and below is markdown ;
1. number one
2. number two
3. number three
</Accordion>\n`,
youtube: `<Youtube videoId="your-video-id" />\n`,
tooltip: `What do you know about <Tooltip text="DocuBook" tip="npx @docubook/create@latest" /> ? Create interactive nested documentations using MDX.\n`,
tabs: `<Tabs defaultValue="tab1" className="pt-5 pb-1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
Content for tab 1
</TabsContent>
<TabsContent value="tab2">
Content for tab 2
</TabsContent>
</Tabs>\n`
};
insertAtCursor(textArea, templates[component]);
};
if (isMobile) {
return <MobileMessage />;
}
return (
<div className={cn(
"flex flex-col transition-all duration-200",
isFullscreen ? "fixed inset-0 z-50 bg-background" : "min-h-[calc(100vh-4rem)]"
)}>
<div className="border-b bg-background">
<div className="py-8 px-2">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-extrabold">Docu<span className="text-primary text-lg ml-1">PLAY</span></h1>
<p className="text-lg text-muted-foreground mt-2">
Test and experiment with DocuBook markdown components in real-time
</p>
</div>
</div>
</div>
<div className="flex-1 py-8 px-2">
<div className="flex flex-col h-full pb-12">
<ScrollArea className="flex-1 border rounded-lg">
<div className="sticky top-0 z-20 bg-background border-b">
<div className="flex items-center justify-between p-2 bg-muted/40">
<div className="flex items-center gap-2">
{markdown.trim() && (
<>
<UIButton
variant="ghost"
size="sm"
onClick={handleCopy}
className="gap-2 text-xs"
>
<Copy className="h-3.5 w-3.5" />
Copy
</UIButton>
<UIButton
variant="ghost"
size="sm"
onClick={handleDownload}
className="gap-2 text-xs"
>
<Download className="h-3.5 w-3.5" />
Download
</UIButton>
<UIButton
variant="ghost"
size="sm"
onClick={handleReset}
className="gap-2 text-xs"
>
<RotateCcw className="h-3.5 w-3.5" />
Reset
</UIButton>
<Separator orientation="vertical" className="h-4" />
</>
)}
</div>
<UIButton
variant="ghost"
size="sm"
onClick={toggleFullscreen}
className="gap-2 text-xs"
>
{isFullscreen ? (
<>
<Minimize2 className="h-3.5 w-3.5" />
Exit Fullscreen
</>
) : (
<>
<Maximize2 className="h-3.5 w-3.5" />
Fullscreen
</>
)}
</UIButton>
</div>
<div className="flex items-center border-b p-1 bg-background">
<ToolbarButton icon={Type} label="Paragraph" onClick={handleParagraphClick} />
<ToolbarButton icon={Heading2} label="Heading 2" onClick={handleHeading2Click} />
<ToolbarButton icon={Heading3} label="Heading 3" onClick={handleHeading3Click} />
<ToolbarButton icon={List} label="Bullet List" onClick={handleBulletListClick} />
<ToolbarButton icon={ListOrdered} label="Numbered List" onClick={handleNumberedListClick} />
<ToolbarSeparator />
<ToolbarButton icon={Code} label="Code Block" onClick={handleCodeBlockClick} />
<ToolbarButton icon={Quote} label="Blockquote" onClick={handleBlockquoteClick} />
<ToolbarButton icon={ImageIcon} label="Image" onClick={handleImageClick} />
<ToolbarButton icon={LinkIcon} label="Link" onClick={handleLinkClick} />
<ToolbarButton icon={Table} label="Table" onClick={handleTableClick} />
<ToolbarSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<UIButton
variant="ghost"
size="sm"
className="h-8 px-2 flex items-center gap-1 font-normal"
>
<Notebook className="h-4 w-4 text-muted-foreground" />
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</UIButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleNoteClick('note')}>
Note
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleNoteClick('danger')}>
Danger
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleNoteClick('warning')}>
Warning
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleNoteClick('success')}>
Success
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ToolbarSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<UIButton
variant="ghost"
size="sm"
className="h-8 px-2 flex items-center gap-1 font-normal"
>
<Component className="h-4 w-4 text-muted-foreground" />
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</UIButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleComponentClick('stepper')}>
<Rows className="h-4 w-4 mr-2" />
Stepper
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('card')}>
<LayoutGrid className="h-4 w-4 mr-2" />
Card
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('button')}>
<MousePointer2 className="h-4 w-4 mr-2" />
Button
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('accordion')}>
<ChevronDown className="h-4 w-4 mr-2" />
Accordion
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('tabs')}>
<LayoutPanelTop className="h-4 w-4 mr-2" />
Tabs
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('youtube')}>
<YoutubeIcon className="h-4 w-4 mr-2" />
Youtube
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('tooltip')}>
<HelpCircle className="h-4 w-4 mr-2" />
Tooltip
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="editor-container">
<div className="editor-line-numbers" ref={lineNumbersRef}>
<div className="editor-line-numbers-content">
{Array.from({ length: lineCount }).map((_, i) => (
<div key={i} data-line-number={i + 1} />
))}
</div>
</div>
<textarea
ref={editorRef}
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
className="editor-textarea"
spellCheck={false}
placeholder="Start writing markdown..."
/>
</div>
</ScrollArea>
</div>
</div>
</div>
);
}

17
components.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -0,0 +1,45 @@
"use client";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
type ChangeType = "Added" | "Improved" | "Fixed" | "Deprecated" | "Removed";
interface ChangeGroupProps {
type: ChangeType;
changes: string[];
expanded: boolean;
}
const typeColors: Record<ChangeType, string> = {
Added: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
Improved: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
Fixed: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
Deprecated: "bg-red-500/10 text-red-600 dark:text-red-400",
Removed: "bg-slate-500/10 text-slate-600 dark:text-slate-400"
};
export function ChangeGroup({ type, changes, expanded }: ChangeGroupProps) {
const visibleChanges = expanded ? changes : changes.slice(0, 5);
const hasMore = changes.length > 5;
return (
<div className="space-y-3">
<Badge variant="outline" className={cn("font-medium", typeColors[type])}>
{type}
</Badge>
<ul className="list-disc list-inside space-y-2 text-muted-foreground pl-2">
{visibleChanges.map((change, i) => (
<li key={i} id="changelog" className="text-sm leading-relaxed marker:text-muted-foreground/60">
{change}
</li>
))}
{!expanded && hasMore && (
<li id="changelog-more" className="text-sm text-muted-foreground/60">
+{changes.length - 5} more improvements
</li>
)}
</ul>
</div>
);
}

View File

@@ -0,0 +1,86 @@
"use client";
import { useState, useEffect } from "react";
import { History } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
interface FloatingVersionTocProps {
versions: { version: string; date: string }[];
}
export function FloatingVersionToc({ versions }: FloatingVersionTocProps) {
const [open, setOpen] = useState(false);
const [activeVersion, setActiveVersion] = useState(versions[0]?.version || "");
useEffect(() => {
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveVersion(entry.target.id.replace("version-", ""));
}
});
};
const observer = new IntersectionObserver(handleIntersection, {
root: null,
rootMargin: "-64px 0px -50% 0px",
threshold: 0.25,
});
versions.forEach(({ version }) => {
const section = document.getElementById(`version-${version}`);
if (section) observer.observe(section);
});
return () => observer.disconnect();
}, [versions]);
const handleScrollToVersion = (version: string) => {
const element = document.getElementById(`version-${version}`);
if (element) {
setTimeout(() => {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}, 100);
setActiveVersion(version);
setOpen(false);
}
};
return (
<div className="fixed bottom-4 right-4 lg: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">
<History className="w-5 h-5" />
Version - {activeVersion}
</Button>
</PopoverTrigger>
<PopoverContent className="w-56 p-2 bg-background shadow-md rounded-lg">
<ScrollArea className="h-72">
<h2 className="px-4 py-2 font-semibold">Version History</h2>
<ul className="space-y-1">
{versions.map(({ version }) => (
<li key={version}>
<Separator />
<Button
variant="ghost"
className={cn("w-full justify-start text-sm", {
"text-primary font-bold": activeVersion === version,
})}
onClick={() => handleScrollToVersion(version)}
>
v.{version}
</Button>
</li>
))}
</ul>
</ScrollArea>
</PopoverContent>
</Popover>
</div>
);
}

View File

@@ -0,0 +1,113 @@
"use client";
import { useState } from "react";
import { VersionTag } from "./version-tag";
import { ChangeGroup } from "./change-group";
import { formatDate2 } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { Separator } from "@/components/ui/separator";
interface VersionEntryProps {
version: string;
date: string;
description?: string;
image?: string;
changes: {
Added?: string[];
Improved?: string[];
Fixed?: string[];
Deprecated?: string[];
Removed?: string[];
};
isLast?: boolean;
}
export function VersionEntry({
version,
date,
description,
image,
changes,
isLast
}: VersionEntryProps) {
const [expanded, setExpanded] = useState(false);
return (
<div id={`v${version}`} className="relative scroll-mt-24">
<div className="relative pb-12">
{/* Version header */}
<div className="flex flex-col gap-4 mb-6">
<div className="flex items-center gap-3">
<VersionTag version={version} />
<time className="text-sm text-muted-foreground">
{formatDate2(date)}
</time>
</div>
{description && (
<p className="text-dark text-xl">{description}</p>
)}
{image && (
<div className="relative w-full h-0 pb-[56.25%] rounded-lg overflow-hidden border">
<Image
src={image}
alt={`Version ${version} preview`}
fill
className="object-cover"
priority
quality={90}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
)}
</div>
{/* Changes */}
<div className="space-y-6">
{Object.entries(changes).map(([type, items]) => (
items && items.length > 0 && (
<ChangeGroup
key={type}
type={type as keyof typeof changes}
changes={items}
expanded={expanded}
/>
)
))}
</div>
{/* Show more/less button */}
{Object.values(changes).some(items => items && items.length > 5) && (
<Button
variant="ghost"
size="sm"
onClick={() => setExpanded(!expanded)}
className="mt-4 text-muted-foreground hover:text-foreground"
>
{expanded ? (
<>
Show less
<ChevronUpIcon className="ml-2 h-4 w-4" />
</>
) : (
<>
Show more
<ChevronDownIcon className="ml-2 h-4 w-4" />
</>
)}
</Button>
)}
</div>
{/* Version divider */}
{!isLast && (
<div className="absolute left-0 bottom-0 w-full">
<Separator className="my-8" />
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,14 @@
"use client";
import { cn } from "@/lib/utils";
export function VersionTag({ version }: { version: string }) {
return (
<span className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-medium",
"bg-primary/10 text-primary"
)}>
v{version}
</span>
);
}

View File

@@ -0,0 +1,90 @@
"use client";
import { useEffect, useState } from "react";
import { cn, formatDate2 } from "@/lib/utils";
import { History } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
interface VersionTocProps {
versions: Array<{
version: string;
date: string;
}>;
}
export function VersionToc({ versions }: VersionTocProps) {
const [activeId, setActiveId] = useState<string | null>(null);
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}`);
}
});
},
{
threshold: 0.2,
rootMargin: '-20% 0px -60% 0px'
}
);
// Observe version elements
versions.forEach(({ version }) => {
const element = document.getElementById(`v${version}`);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, [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">
<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">
<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>
);
}

View File

@@ -0,0 +1,9 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1,43 @@
"use client";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ComponentProps, forwardRef } from "react";
type AnchorProps = ComponentProps<typeof Link> & {
absolute?: boolean;
activeClassName?: string;
disabled?: boolean;
};
const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(
({ absolute, className = "", activeClassName = "", disabled, children, ...props }, ref) => {
const path = usePathname();
const href = props.href.toString();
// Deteksi URL eksternal menggunakan regex
const isExternal = /^(https?:\/\/|\/\/)/.test(href);
let isMatch = absolute
? href.split("/")[1] === path.split("/")[1]
: path === href;
if (isExternal) isMatch = false; // Hindari mencocokkan URL eksternal
if (disabled)
return (
<div className={cn(className, "cursor-not-allowed")}>{children}</div>
);
return (
<Link ref={ref} className={cn(className, isMatch && activeClassName)} {...props}>
{children}
</Link>
);
}
);
Anchor.displayName = "Anchor";
export default Anchor;

View File

@@ -0,0 +1,47 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Fragment } from "react";
export default function DocsBreadcrumb({ paths }: { paths: string[] }) {
return (
<div className="pb-5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink>Docs</BreadcrumbLink>
</BreadcrumbItem>
{paths.map((path, index) => (
<Fragment key={path}>
<BreadcrumbSeparator />
<BreadcrumbItem>
{index < paths.length - 1 ? (
<BreadcrumbLink className="a">
{toTitleCase(path)}
</BreadcrumbLink>
) : (
<BreadcrumbPage className="b">
{toTitleCase(path)}
</BreadcrumbPage>
)}
</BreadcrumbItem>
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
</div>
);
}
function toTitleCase(input: string): string {
const words = input.split("-");
const capitalizedWords = words.map(
(word) => word.charAt(0).toUpperCase() + word.slice(1)
);
return capitalizedWords.join(" ");
}

View File

@@ -0,0 +1,24 @@
"use client";
import { ROUTES } from "@/lib/routes-config";
import SubLink from "./sublink";
import { usePathname } from "next/navigation";
export default function DocsMenu({ isSheet = false }) {
const pathname = usePathname();
if (!pathname.startsWith("/docs")) return null;
return (
<div className="flex flex-col gap-3.5 mt-5 pr-2 pb-6">
{ROUTES.map((item, index) => {
const modifiedItems = {
...item,
href: `/docs${item.href}`,
level: 0,
isSheet,
};
return <SubLink key={item.title + index} {...modifiedItems} />;
})}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import docuConfig from '@/docu.json'; // Import JSON
import { SquarePenIcon } from 'lucide-react';
import Link from 'next/link';
interface EditThisPageProps {
filePath: string;
}
const EditThisPage: React.FC<EditThisPageProps> = ({ filePath }) => {
const { repository } = docuConfig;
const editUrl = `${repository.url}${repository.editPathTemplate.replace("{filePath}", filePath)}`;
return (
<div style={{ textAlign: 'right' }}>
<Link
href={editUrl}
target='_blank'
rel="noopener noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
textDecoration: 'none',
fontWeight: 'bold',
}}
>
<span className='text-primary text-sm max-[480px]:hidden'>Edit this page</span>
<SquarePenIcon className="w-4 h-4 text-primary" />
</Link>
</div>
);
};
export default EditThisPage;

View File

@@ -0,0 +1,56 @@
import Link from "next/link";
import { ModeToggle } from "@/components/docs/theme-toggle";
import docuConfig from "@/docu.json";
import * as LucideIcons from "lucide-react"; // Import all icons
export function Footer() {
const { footer } = docuConfig;
const { meta } = docuConfig;
return (
<footer className="w-full py-4 px-2 border-t lg:py-8 bg-background">
<div className="container flex flex-wrap items-center justify-between text-sm">
<div className="items-start justify-center hidden gap-4 lg:flex-col lg:flex lg:w-3/5">
<h3 className="text-lg font-bold font-code">{meta.title}</h3>
<span className="w-3/4 text-base text-wrap text-muted-foreground">{meta.description}</span>
<div className="flex items-center gap-6 mt-2">
<FooterButtons />
</div>
</div>
<div className="flex flex-col items-start justify-center w-full gap-4 mt-4 xl:items-end lg:w-2/5">
<p className="text-center text-muted-foreground">
Copyright © {new Date().getFullYear()} {footer.copyright} - Made with{" "}
<Link href="https://www.docubook.pro" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">
DocuBook
</Link>
</p>
<div className="hidden lg:flex">
<ModeToggle />
</div>
</div>
</div>
</footer>
);
}
export function FooterButtons() {
const { footer } = docuConfig;
return (
<>
{footer.social?.map((item) => {
const IconComponent = (LucideIcons[item.iconName as keyof typeof LucideIcons] ?? LucideIcons["Globe"]) as unknown as React.FC<{ className?: string }>;
return (
<Link
key={item.name}
href={item.url}
target="_blank"
rel="noopener noreferrer"
aria-label={item.name}
>
<IconComponent className="w-4 h-4 text-gray-800 transition-colors dark:text-gray-400 hover:text-primary" />
</Link>
);
})}
</>
);
}

View File

@@ -0,0 +1,59 @@
import {
Sheet,
SheetClose,
SheetContent,
SheetHeader,
SheetTrigger,
} from "@/components/ui/sheet";
import { Logo, NavMenu } from "./navbar";
import { Button } from "../ui/button";
import { AlignLeftIcon } from "lucide-react";
import { FooterButtons } from "./footer";
import { DialogTitle } from "../ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import DocsMenu from "./docs-menu";
import { ModeToggle } from "./theme-toggle";
export function Leftbar() {
return (
<aside className="lg:flex hidden flex-[1.5] min-w-[238px] sticky top-16 flex-col h-[93.75vh] overflow-y-auto">
<ScrollArea className="py-4">
<DocsMenu />
</ScrollArea>
</aside>
);
}
export function SheetLeftbar() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="max-lg:flex hidden">
<AlignLeftIcon />
</Button>
</SheetTrigger>
<SheetContent className="flex flex-col gap-4 px-0" side="left">
<DialogTitle className="sr-only">Menu</DialogTitle>
<SheetHeader>
<SheetClose className="px-5" asChild>
<Logo />
</SheetClose>
</SheetHeader>
<div className="flex flex-col gap-4 overflow-y-auto">
<div className="flex flex-col gap-2.5 mt-3 mx-2 px-5">
<NavMenu isSheet />
</div>
<div className="mx-2 px-5">
<DocsMenu isSheet />
</div>
<div className="px-6 py-2 flex justify-start items-center gap-6">
<FooterButtons />
</div>
<div className="flex w-2/4 px-5">
<ModeToggle />
</div>
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
import { ListIcon } from "lucide-react";
import TocObserver from "./toc-observer";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
interface MobTocProps {
tocs: {
level: number;
text: string;
href: string;
}[];
}
export default function MobToc({ tocs }: MobTocProps) {
return (
<div className="lg:hidden block w-full">
<Accordion type="single" collapsible>
<AccordionItem value="toc">
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center gap-2">
<ListIcon className="w-4 h-4" />
<span className="font-medium text-sm">On this page</span>
</div>
</AccordionTrigger>
<AccordionContent className="h-auto py-2">
<TocObserver data={tocs} />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}

View File

@@ -0,0 +1,78 @@
import { ArrowUpRight } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import Search from "./search";
import Anchor from "./anchor";
import { SheetLeftbar } from "./leftbar";
import { SheetClose } from "@/components/ui/sheet";
import docuConfig from "@/docu.json"; // Import JSON
export function Navbar() {
return (
<nav className="sticky top-0 z-50 w-full h-16 border-b bg-background">
<div className="sm:container mx-auto w-[95vw] h-full flex items-center justify-between md:gap-2">
<div className="flex items-center gap-5">
<SheetLeftbar />
<div className="flex items-center gap-6">
<div className="hidden sm:flex">
<Logo />
</div>
<div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
<NavMenu />
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Search />
</div>
</div>
</nav>
);
}
export function Logo() {
const { navbar } = docuConfig; // Extract navbar from JSON
return (
<Link href="/" className="flex items-center gap-2.5">
<Image src={navbar.logo.src} alt={navbar.logo.alt} width="24" height="24" />
<h2 className="font-bold font-code text-md">{navbar.logoText}</h2>
</Link>
);
}
export function NavMenu({ isSheet = false }) {
const { navbar } = docuConfig; // Extract navbar from JSON
return (
<>
{navbar?.menu?.map((item) => {
const isExternal = item.href.startsWith("http");
const Comp = (
<Anchor
key={`${item.title}-${item.href}`}
activeClassName="!text-primary md:font-semibold font-medium"
absolute
className="flex items-center gap-1 dark:text-stone-300/85 text-stone-800"
href={item.href}
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noopener noreferrer" : undefined}
>
{item.title}
{isExternal && <ArrowUpRight className="w-4 h-4 text-muted-foreground" />}
</Anchor>
);
return isSheet ? (
<SheetClose key={item.title + item.href} asChild>
{Comp}
</SheetClose>
) : (
Comp
);
})}
</>
);
}

View File

@@ -0,0 +1,49 @@
import { getPreviousNext } from "@/lib/markdown";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import Link from "next/link";
import { buttonVariants } from "../ui/button";
export default function Pagination({ pathname }: { pathname: string }) {
const res = getPreviousNext(pathname);
return (
<div className="grid grid-cols-1 sm:grid-cols-2 flex-grow sm:py-10 py-7 gap-3">
<div>
{res.prev && (
<Link
className={buttonVariants({
variant: "outline",
className:
"no-underline w-full flex flex-col pl-3 !py-8 !items-start",
})}
href={`/docs${res.prev.href}`}
>
<span className="flex items-center text-xs">
<ChevronLeftIcon className="w-[1rem] h-[1rem] mr-1" />
Previous
</span>
<span className="mt-1 ml-1">{res.prev.title}</span>
</Link>
)}
</div>
<div>
{res.next && (
<Link
className={buttonVariants({
variant: "outline",
className:
"no-underline w-full flex flex-col pr-3 !py-8 !items-end",
})}
href={`/docs${res.next.href}`}
>
<span className="flex items-center text-xs">
Next
<ChevronRightIcon className="w-[1rem] h-[1rem] ml-1" />
</span>
<span className="mt-1 mr-1">{res.next.title}</span>
</Link>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
"use client";
import { ArrowUpIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "../ui/button";
import { cn } from "@/lib/utils";
export function ScrollToTop() {
const [show, setShow] = useState(false);
useEffect(() => {
const handleScroll = () => {
// Check if user has scrolled to bottom
const scrolledToBottom =
window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 100;
if (scrolledToBottom) {
setShow(true);
} else {
setShow(false);
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
<div
className={cn(
"lg:hidden fixed top-16 items-center z-50 w-full transition-all duration-300",
show ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-full pointer-events-none"
)}
>
<div className="flex justify-center items-center pt-3 mx-auto">
<Button
variant="outline"
size="sm"
className="gap-2 rounded-full shadow-md bg-background/80 backdrop-blur-sm border-primary/20 hover:bg-background hover:text-primary"
onClick={scrollToTop}
>
<ArrowUpIcon className="h-4 w-4" />
<span className="font-medium">Scroll to Top</span>
</Button>
</div>
</div>
);
}

193
components/docs/search.tsx Normal file
View File

@@ -0,0 +1,193 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState, useRef } from "react";
import { ArrowUpIcon, ArrowDownIcon, CommandIcon, FileTextIcon, SearchIcon, CornerDownLeftIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogTrigger,
DialogClose,
DialogTitle,
} from "@/components/ui/dialog";
import Anchor from "./anchor";
import { advanceSearch, cn } from "@/lib/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
export default function Search() {
const router = useRouter();
const [searchedInput, setSearchedInput] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
event.preventDefault();
setIsOpen(true);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
const filteredResults = useMemo(
() => advanceSearch(searchedInput.trim()),
[searchedInput]
);
useEffect(() => {
setSelectedIndex(0);
}, [filteredResults]);
useEffect(() => {
const handleNavigation = (event: KeyboardEvent) => {
if (!isOpen || filteredResults.length === 0) return;
if (event.key === "ArrowDown") {
event.preventDefault();
setSelectedIndex((prev) => (prev + 1) % filteredResults.length);
}
if (event.key === "ArrowUp") {
event.preventDefault();
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length);
}
if (event.key === "Enter") {
event.preventDefault();
const selectedItem = filteredResults[selectedIndex];
if (selectedItem) {
router.push(`/docs${selectedItem.href}`);
setIsOpen(false);
}
}
};
window.addEventListener("keydown", handleNavigation);
return () => {
window.removeEventListener("keydown", handleNavigation);
};
}, [isOpen, filteredResults, selectedIndex, router]);
useEffect(() => {
if (itemRefs.current[selectedIndex]) {
itemRefs.current[selectedIndex]?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}
}, [selectedIndex]);
return (
<div>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) setSearchedInput("");
setIsOpen(open);
}}
>
<DialogTrigger asChild>
<div className="relative flex-1 cursor-pointer max-w-[160px]">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-500 dark:text-stone-400" />
<Input
className="md:w-full rounded-md dark:bg-background/95 bg-background border h-9 pl-10 pr-0 sm:pr-4 text-sm shadow-sm overflow-ellipsis"
placeholder="Search"
type="search"
/>
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-black dark:border dark:border-white/20 bg-stone-200/50 border border-black/40 p-1 rounded-sm">
<CommandIcon className="w-3 h-3" />
<span>K</span>
</div>
</div>
</DialogTrigger>
<DialogContent className="p-0 max-w-[650px] sm:top-[38%] top-[45%] !rounded-md">
<DialogTitle className="sr-only">Search</DialogTitle>
<DialogHeader>
<input
value={searchedInput}
onChange={(e) => setSearchedInput(e.target.value)}
placeholder="Type something to search..."
autoFocus
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none"
/>
</DialogHeader>
{filteredResults.length == 0 && searchedInput && (
<p className="text-muted-foreground mx-auto mt-2 text-sm">
No results found for{" "}
<span className="text-primary">{`"${searchedInput}"`}</span>
</p>
)}
<ScrollArea className="max-h-[400px] overflow-y-auto">
<div className="flex flex-col items-start overflow-y-auto sm:px-2 px-1 pb-4">
{filteredResults.map((item, index) => {
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap;
const paddingClass = paddingMap[level];
const isActive = index === selectedIndex;
return (
<DialogClose key={item.href} asChild>
<Anchor
ref={(el) => {
itemRefs.current[index] = el as HTMLDivElement | null;
}}
className={cn(
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
isActive && "bg-primary/20 dark:bg-primary/30",
paddingClass
)}
href={`/docs${item.href}`}
tabIndex={0}
>
<div
className={cn(
"flex items-center w-fit h-full py-3 gap-1.5 px-2",
level > 1 && "border-l pl-4"
)}
>
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" /> {item.title}
</div>
</Anchor>
</DialogClose>
);
})}
</div>
</ScrollArea>
<DialogFooter className="md:flex md:justify-start hidden h-14 px-6 bg-transparent border-t text-[14px] outline-none">
<div className="flex items-center gap-2">
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
<ArrowUpIcon className="w-3 h-3"/>
</span>
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
<ArrowDownIcon className="w-3 h-3"/>
</span>
<p className="text-muted-foreground">to navigate</p>
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
<CornerDownLeftIcon className="w-3 h-3"/>
</span>
<p className="text-muted-foreground">to select</p>
<span className="dark:bg-accent/15 bg-slate-200 border rounded px-2 py-1">
esc
</span>
<p className="text-muted-foreground">to close</p>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
const paddingMap = {
1: "pl-2",
2: "pl-4",
3: "pl-10",
} as const;

View File

@@ -0,0 +1,85 @@
import { EachRoute } from "@/lib/routes-config";
import Anchor from "./anchor";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { SheetClose } from "@/components/ui/sheet";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
export default function SubLink({
title,
href,
items,
noLink,
level,
isSheet,
}: EachRoute & { level: number; isSheet: boolean }) {
const path = usePathname();
const [isOpen, setIsOpen] = useState(level == 0);
useEffect(() => {
if (path == href || path.includes(href)) setIsOpen(true);
}, [href, path]);
const Comp = (
<Anchor activeClassName="text-primary font-medium" href={href}>
{title}
</Anchor>
);
const titleOrLink = !noLink ? (
isSheet ? (
<SheetClose asChild>{Comp}</SheetClose>
) : (
Comp
)
) : (
<h4 className="font-medium sm:text-sm text-primary">{title}</h4>
);
if (!items) {
return <div className="flex flex-col">{titleOrLink}</div>;
}
return (
<div className="flex flex-col gap-1 w-full">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="w-full pr-5">
<div className="flex items-center justify-between cursor-pointer w-full">
{titleOrLink}
<span>
{!isOpen ? (
<ChevronRight className="h-[0.9rem] w-[0.9rem]" />
) : (
<ChevronDown className="h-[0.9rem] w-[0.9rem]" />
)}
</span>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div
className={cn(
"flex flex-col items-start sm:text-sm dark:text-stone-300/85 text-stone-800 ml-0.5 mt-2.5 gap-3",
level > 0 && "pl-4 border-l ml-1.5"
)}
>
{items?.map((innerLink) => {
const modifiedItems = {
...innerLink,
href: `${href + innerLink.href}`,
level: level + 1,
isSheet,
};
return <SubLink key={modifiedItems.href} {...modifiedItems} />;
})}
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
}

View File

@@ -0,0 +1,71 @@
"use client";
import * as React from "react";
import { Moon, Sun, Monitor } from "lucide-react";
import { useTheme } from "next-themes";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export function ModeToggle() {
const { theme, setTheme } = useTheme();
const [selectedTheme, setSelectedTheme] = React.useState<string>("system");
// Pastikan toggle tetap di posisi yang benar setelah reload
React.useEffect(() => {
if (theme) {
setSelectedTheme(theme);
} else {
setSelectedTheme("system"); // Default ke system jika undefined
}
}, [theme]);
return (
<ToggleGroup
type="single"
value={selectedTheme}
onValueChange={(value) => {
if (value) {
setTheme(value);
setSelectedTheme(value);
}
}}
className="flex items-center gap-1 rounded-full border border-gray-300 dark:border-gray-700 p-1 transition-all"
>
<ToggleGroupItem
value="light"
size="sm"
aria-label="Light Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "light"
? "bg-blue-500 text-white"
: "bg-transparent"
}`}
>
<Sun className={`h-4 w-4 ${selectedTheme === "light" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
</ToggleGroupItem>
<ToggleGroupItem
value="system"
size="sm"
aria-label="System Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "system"
? "bg-blue-500 text-white"
: "bg-transparent"
}`}
>
<Monitor className={`h-4 w-4 ${selectedTheme === "system" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
</ToggleGroupItem>
<ToggleGroupItem
value="dark"
size="sm"
aria-label="Dark Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "dark"
? "bg-blue-500 text-white"
: "bg-transparent"
}`}
>
<Moon className={`h-4 w-4 ${selectedTheme === "dark" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
</ToggleGroupItem>
</ToggleGroup>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import { getDocsTocs } from "@/lib/markdown";
import clsx from "clsx";
import Link from "next/link";
import { useState, useRef, useEffect } from "react";
type Props = { data: Awaited<ReturnType<typeof getDocsTocs>> };
export default function TocObserver({ data }: Props) {
const [activeId, setActiveId] = useState<string | null>(null);
const observer = useRef<IntersectionObserver | null>(null);
useEffect(() => {
const handleIntersect = (entries: IntersectionObserverEntry[]) => {
const visibleEntry = entries.find((entry) => entry.isIntersecting);
if (visibleEntry) {
setActiveId(visibleEntry.target.id);
}
};
observer.current = new IntersectionObserver(handleIntersect, {
root: null,
rootMargin: "-20px 0px 0px 0px",
threshold: 0.1,
});
const elements = data.map((item) =>
document.getElementById(item.href.slice(1))
);
elements.forEach((el) => {
if (el && observer.current) {
observer.current.observe(el);
}
});
return () => {
if (observer.current) {
elements.forEach((el) => {
if (el) {
observer.current!.unobserve(el);
}
});
}
};
}, [data]);
return (
<div className="flex flex-col gap-2.5 text-sm dark:text-stone-300/85 text-stone-800 ml-0.5">
{data.map(({ href, level, text }) => {
return (
<Link
key={href}
href={href}
className={clsx({
"pl-0": level == 2,
"pl-4": level == 3,
"pl-8 ": level == 4,
"font-medium text-primary": activeId == href.slice(1),
})}
>
{text}
</Link>
);
})}
</div>
);
}

21
components/docs/toc.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { getDocsTocs } from "@/lib/markdown";
import TocObserver from "./toc-observer";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ListIcon } from "lucide-react";
export default async function Toc({ path }: { path: string }) {
const tocs = await getDocsTocs(path);
return (
<div className="lg:flex hidden toc flex-[1.5] min-w-[238px] py-9 sticky top-16 h-[96.95vh]">
<div className="flex flex-col gap-3 w-full pl-2">
<div className="flex items-center gap-2">
<ListIcon className="w-5 h-5" /><h3 className="font-medium text-sm">On this page</h3>
</div>
<ScrollArea className="pb-2 pt-0.5 overflow-y-auto">
<TocObserver data={tocs} />
</ScrollArea>
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { PropsWithChildren } from "react";
export function Typography({ children }: PropsWithChildren) {
return (
<div className="prose prose-zinc dark:prose-invert prose-code:font-code dark:prose-code:bg-stone-900/25 prose-code:bg-stone-50 prose-pre:bg-background prose-headings:scroll-m-20 w-[85vw] sm:w-full sm:mx-auto prose-code:text-sm prose-code:leading-6 dark:prose-code:text-white prose-code:text-stone-800 prose-code:p-1 prose-code:rounded-md prose-code:border pt-2 !min-w-full prose-img:rounded-md prose-img:border prose-code:before:content-none prose-code:after:content-none prose-code:px-1.5 prose-code:overflow-x-auto !max-w-[500px] prose-img:my-3 prose-h2:my-4 prose-h2:mt-8">
{children}
</div>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
import { useState } from "react";
import { TerminalSquareIcon, ClipboardIcon, CheckIcon } from "lucide-react";
export function CopyCommand() {
const [copied, setCopied] = useState(false);
const command = "npx @docubook/create@latest";
const copyToClipboard = () => {
navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 5000);
};
return (
<div className="relative flex flex-row items-center justify-center sm:gap-2 gap-0.5 text-muted-foreground text-md mt-10 mb-12 font-code text-base font-medium group">
<TerminalSquareIcon className="w-5 h-5 mr-1 mt-0.5" />
<span className="select-all">{command}</span>
<button
onClick={copyToClipboard}
className="p-1 ml-1 transition-opacity rounded-md opacity-0 group-hover:opacity-100 hover:bg-gray-200 dark:hover:bg-gray-700"
>
{copied ? (
<CheckIcon className="w-4 h-4 text-green-500" />
) : (
<ClipboardIcon className="w-4 h-4" />
)}
</button>
</div>
);
}
export default CopyCommand;

View File

@@ -0,0 +1,91 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Play } from "lucide-react";
const scriptOutput = [
"DocuBook CLI Installer",
"✔ Enter your project directory name: docubook",
"? Choose a package manager:",
"> npm",
" pnpm",
" yarn",
" bun",
"",
":: Cloning starter from GitLab...",
"✔ Docubook project successfully created!",
"Skipping rename postcss.config.js because Bun is not installed.",
"",
"[ DocuBook Version 1.8.0 ]",
"",
"Starting the installation process...",
"Installation | ████████████████████████████████ | 100% 100/100",
"",
"Dependencies installed successfully using npm!",
"",
"┌────────────────────────────────────┐",
"│ Next Steps: │",
"│ │",
"│ 1. Navigate to project directory: │",
"│ cd docubook │",
"│ │",
"│ 2. Install dependencies: │",
"│ npm install │",
"│ │",
"│ 3. Start the development server: │",
"│ npm run dev │",
"└────────────────────────────────────┘"
];
export function RuntimeSimulator() {
const [logs, setLogs] = useState<string[]>([]);
const [running, setRunning] = useState(false);
const terminalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (running) {
setLogs([]);
scriptOutput.forEach((line, index) => {
setTimeout(() => {
setLogs((prev) => [...prev, line]);
}, index * 500);
});
}
}, [running]);
useEffect(() => {
terminalRef.current?.scrollTo({ top: terminalRef.current.scrollHeight, behavior: "smooth" });
}, [logs]);
return (
<div className="bg-gray-900 text-green-400 font-mono rounded-lg overflow-hidden w-full h-auto max-w-2xl shadow-lg">
<div className="bg-gray-800 text-gray-300 px-4 py-2 flex items-center space-x-2">
<div className="flex space-x-1">
<span className="w-3 h-3 bg-red-500 rounded-full"></span>
<span className="w-3 h-3 bg-yellow-500 rounded-full"></span>
<span className="w-3 h-3 bg-green-500 rounded-full"></span>
</div>
<span className="ml-3">docubook@localhost</span>
</div>
<div ref={terminalRef} className="p-4 md:h-[400px] h-72 overflow-y-auto text-left">
{logs.map((log, index) => (
<pre key={index} className="whitespace-pre-wrap">{log}</pre>
))}
</div>
<div className="bg-gray-800 p-2 flex items-center space-x-2">
<input
type="text"
value="npx @docubook/create@latest"
readOnly
className="bg-gray-700 text-gray-300 px-2 py-1 rounded w-full text-left"
/>
<Button onClick={() => setRunning(true)} className="bg-green-600 hover:bg-green-500 px-4 py-1">
<Play className="w-5 h-5" />
</Button>
</div>
</div>
);
}
export default RuntimeSimulator;

View File

@@ -0,0 +1,47 @@
"use client";
import { ReactNode, useState } from 'react';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
type AccordionProps = {
title: string;
children?: ReactNode;
defaultOpen?: boolean;
className?: string;
};
const Accordion = ({
title,
children,
defaultOpen = false,
className,
}: AccordionProps) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className={cn("border rounded-lg overflow-hidden", className)}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex items-center my-auto space-x-2 space-y-2 w-full px-4 h-12 transition-colors bg-background dark:hover:bg-muted/50 hover:bg-muted/15"
>
<ChevronRight
className={cn(
"w-4 h-4 text-muted-foreground transition-transform duration-200",
isOpen && "rotate-90"
)}
/>
<h3 className="font-medium text-base text-foreground pb-2">{title}</h3>
</button>
{isOpen && (
<div className="px-4 py-3 border-t dark:bg-muted/50 bg-muted/15">
{children}
</div>
)}
</div>
);
};
export default Accordion;

View File

@@ -0,0 +1,52 @@
import React from "react";
import * as Icons from "lucide-react";
import Link from "next/link";
type IconName = keyof typeof Icons;
type ButtonProps = {
icon?: keyof typeof Icons;
text?: string;
href: string;
target?: "_blank" | "_self" | "_parent" | "_top";
size?: "sm" | "md" | "lg";
variation?: "primary" | "accent" | "outline";
};
const Button: React.FC<ButtonProps> = ({
icon,
text,
href,
target,
size = "md",
variation = "primary",
}) => {
const baseStyles = "inline-flex items-center justify-center rounded font-medium focus:outline-none transition no-underline";
const sizeStyles = {
sm: "px-3 py-1 my-6 text-sm",
md: "px-4 py-2 my-6 text-base",
lg: "px-5 py-3 my-6 text-lg",
};
const variationStyles = {
primary: "bg-primary text-white hover:bg-primary/90",
accent: "bg-accent text-white hover:bg-accent/90",
outline: "border border-accent text-accent hover:bg-accent/10",
};
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null; // Tipe eksplisit sebagai React.FC
return (
<Link
href={href}
target={target}
rel={target === "_blank" ? "noopener noreferrer" : undefined}
className={`${baseStyles} ${sizeStyles[size]} ${variationStyles[variation]}`}
>
{text && <span>{text}</span>}
{Icon && <Icon className="mr-2 h-5 w-5" />}
</Link>
);
};
export default Button;

View File

@@ -0,0 +1,41 @@
import React, { ReactNode } from "react";
import * as Icons from "lucide-react";
import Link from "next/link";
import clsx from "clsx";
type IconName = keyof typeof Icons;
interface CardProps {
title: string;
icon?: IconName;
href?: string;
horizontal?: boolean;
children: ReactNode;
className?: string;
}
const Card: React.FC<CardProps> = ({ title, icon, href, horizontal, children, className }) => {
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null;
const content = (
<div
className={clsx(
"border rounded-lg shadow-sm p-4 transition-all duration-200 bg-white dark:bg-gray-900",
"hover:bg-gray-50 dark:hover:bg-gray-800",
"flex gap-2",
horizontal ? "flex-row items-center gap-1" : "flex-col space-y-1",
className
)}
>
{Icon && <Icon className="w-5 h-5 text-primary flex-shrink-0" />}
<div className="flex-1 min-w-0 my-auto h-full">
<span className="text-base font-semibold">{title}</span>
<div className="text-sm text-gray-600 dark:text-gray-400 -mt-3">{children}</div>
</div>
</div>
);
return href ? <Link className="no-underline block" href={href}>{content}</Link> : content;
};
export default Card;

View File

@@ -0,0 +1,28 @@
import React, { ReactNode } from "react";
import clsx from "clsx";
interface CardGroupProps {
children: ReactNode;
cols?: number;
className?: string;
}
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => {
const cardsArray = React.Children.toArray(children); // Pastikan children berupa array
return (
<div
className={clsx(
"grid gap-4",
`grid-cols-1 sm:grid-cols-${cols}`,
className
)}
>
{cardsArray.map((card, index) => (
<div key={index}>{card}</div>
))}
</div>
);
};
export default CardGroup;

View File

@@ -0,0 +1,33 @@
"use client";
import { CheckIcon, CopyIcon } from "lucide-react";
import { Button } from "../ui/button";
import { useState } from "react";
export default function Copy({ content }: { content: string }) {
const [isCopied, setIsCopied] = useState(false);
async function handleCopy() {
await navigator.clipboard.writeText(content);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
}
return (
<Button
variant="secondary"
className="border"
size="xs"
onClick={handleCopy}
>
{isCopied ? (
<CheckIcon className="w-3 h-3" />
) : (
<CopyIcon className="w-3 h-3" />
)}
</Button>
);
}

View File

@@ -0,0 +1,25 @@
import { ComponentProps } from "react";
import NextImage from "next/image";
type Height = ComponentProps<typeof NextImage>["height"];
type Width = ComponentProps<typeof NextImage>["width"];
export default function Image({
src,
alt = "alt",
width = 800,
height = 350,
...props
}: ComponentProps<"img">) {
if (!src) return null;
return (
<NextImage
src={src}
alt={alt}
width={width as Width}
height={height as Height}
quality={40}
{...props}
/>
);
}

View File

@@ -0,0 +1,14 @@
import NextLink from "next/link";
import { ComponentProps } from "react";
export default function Link({ href, ...props }: ComponentProps<"a">) {
if (!href) return null;
return (
<NextLink
href={href}
{...props}
target="_blank"
rel="noopener noreferrer"
/>
);
}

View File

@@ -0,0 +1,52 @@
import { cn } from "@/lib/utils";
import clsx from "clsx";
import { PropsWithChildren } from "react";
import {
Info,
AlertTriangle,
ShieldAlert,
CheckCircle,
} from "lucide-react";
type NoteProps = PropsWithChildren & {
title?: string;
type?: "note" | "danger" | "warning" | "success";
};
const iconMap = {
note: <Info size={16} className="text-blue-500" />,
danger: <ShieldAlert size={16} className="text-red-500" />,
warning: <AlertTriangle size={16} className="text-orange-500" />,
success: <CheckCircle size={16} className="text-green-500" />,
};
export default function Note({
children,
title = "Note",
type = "note",
}: NoteProps) {
const noteClassNames = clsx({
"dark:bg-stone-950/25 bg-stone-50": type === "note",
"dark:bg-red-950 bg-red-100 border-red-200 dark:border-red-900":
type === "danger",
"dark:bg-orange-950 bg-orange-100 border-orange-200 dark:border-orange-900":
type === "warning",
"dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900":
type === "success",
});
return (
<div
className={cn(
"border rounded-md px-5 pb-0.5 mt-5 mb-7 text-sm tracking-wide",
noteClassNames
)}
>
<div className="flex items-center gap-2 font-bold -mb-2.5 pt-6">
{iconMap[type]}
<span className="text-base">{title}:</span>
</div>
{children}
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { BaseMdxFrontmatter, getAllChilds } from "@/lib/markdown";
import Link from "next/link";
export default async function Outlet({ path }: { path: string }) {
if (!path) throw new Error("path not provided");
const output = await getAllChilds(path);
return (
<div className="grid md:grid-cols-2 gap-5">
{output.map((child) => (
<ChildCard {...child} key={child.title} />
))}
</div>
);
}
type ChildCardProps = BaseMdxFrontmatter & { href: string };
function ChildCard({ description, href, title }: ChildCardProps) {
return (
<Link
href={href}
className="border rounded-md p-4 no-underline flex flex-col gap-0.5"
>
<h4 className="!my-0">{title}</h4>
<p className="text-sm text-muted-foreground !my-0">{description}</p>
</Link>
);
}

View File

@@ -0,0 +1,19 @@
import { ComponentProps } from "react";
import Copy from "./copy";
export default function Pre({
children,
raw,
...rest
}: ComponentProps<"pre"> & { raw?: string }) {
return (
<div className="my-5 relative">
<div className="absolute top-3 right-2.5 z-10 sm:block hidden">
<Copy content={raw!} />
</div>
<div className="relative">
<pre {...rest}>{children}</pre>
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { cn } from "@/lib/utils";
import clsx from "clsx";
import { Children, PropsWithChildren } from "react";
export function Stepper({ children }: PropsWithChildren) {
const length = Children.count(children);
return (
<div className="flex flex-col">
{Children.map(children, (child, index) => {
return (
<div
className={cn(
"border-l pl-9 ml-3 relative",
clsx({
"pb-5 ": index < length - 1,
})
)}
>
<div className="bg-muted w-8 h-8 text-xs font-medium rounded-md border flex items-center justify-center absolute -left-4 font-code">
{index + 1}
</div>
{child}
</div>
);
})}
</div>
);
}
export function StepperItem({
children,
title,
}: PropsWithChildren & { title?: string }) {
return (
<div className="pt-0.5">
<h4 className="mt-0">{title}</h4>
<div>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import React, { useState } from "react";
interface TooltipProps {
text: string;
tip: string;
}
const Tooltip: React.FC<TooltipProps> = ({ text, tip }) => {
const [visible, setVisible] = useState(false);
return (
<span
className="relative inline-block cursor-pointer underline decoration-dotted text-blue-500"
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
>
{text}
{visible && (
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-max max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl bg-background text-foreground text-sm p-2 rounded shadow-md break-words text-center outline outline-1 outline-offset-2">
{tip}
</span>
)}
</span>
);
};
export default Tooltip;

View File

@@ -0,0 +1,22 @@
import React from "react";
interface YoutubeProps {
videoId: string;
className?: string;
}
const Youtube: React.FC<YoutubeProps> = ({ videoId, className }) => {
return (
<div className={`youtube ${className || ""}`}>
<iframe
src={`https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1&showinfo=0&autohide=1&controls=1`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
);
};
export default Youtube;

View File

@@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -0,0 +1,40 @@
import { CSSProperties, FC, ReactNode } from "react";
import { cn } from "@/lib/utils";
interface AnimatedShinyTextProps {
children: ReactNode;
className?: string;
shimmerWidth?: number;
}
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
children,
className,
shimmerWidth = 100,
}) => {
return (
<p
style={
{
"--shiny-width": `${shimmerWidth}px`,
} as CSSProperties
}
className={cn(
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
// Shine effect
"animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
// Shine gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
className,
)}
>
{children}
</p>
);
};
export default AnimatedShinyText;

50
components/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

37
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,37 @@
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

57
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,57 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-9 w-9",
xs: "h-7 rounded-md px-2",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

76
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

124
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,124 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-3 top-3.5 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<div className="hidden md:flex rounded-sm text-xs border py-1 px-2 hover:bg-muted">
Esc
</div>
<X className="h-5 w-5 hidden max-md:flex" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

25
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

33
components/ui/popover.tsx Normal file
View File

@@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

140
components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-7 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

31
components/ui/sonner.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

117
components/ui/table.tsx Normal file
View File

@@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

55
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,55 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center gap-2 text-muted-foreground font-mono -mb-28 w-full border-b",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap px-1.5 py-[0.58rem] text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:border-primary border-b-2 border-transparent data-[state=active]:text-foreground font-code",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

46
components/ui/toggle.tsx Normal file
View File

@@ -0,0 +1,46 @@
"use client"
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-1 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
xs: "h-7 px-1 min-w-7",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

View File

@@ -0,0 +1,133 @@
---
title: "Getting Started with DocuBook Components"
description: "Learn how to leverage the power of DocuBook components to create dynamic and interactive documentation. This guide explores the key components available and how to integrate them into your documentation workflow."
date: 31-12-2024
authors:
- avatar: "https://ui.shadcn.com/avatars/02.png"
handle: mywildancloud
username: Wildan nrs
handleUrl: "https://github.com/mywildancloud"
cover: "https://img.freepik.com/free-vector/spring-landscape-scene_23-2148860692.jpg?t=st=1735654206~exp=1735657806~hmac=b65033387b5519b48c72a87333cf1a5d2462de255865104c612500161b248a8a&w=2000"
---
## Introduction to DocuBook Components
DocuBook provides a robust set of components that enable developers to build dynamic, user-friendly, and visually appealing documentation. These components are designed to enhance the user experience, making it easier for readers to navigate, understand, and interact with the content.
In this guide, well explore the core components available in DocuBook and how you can integrate them into your documentation projects. For a complete list of components, visit the [official documentation](https://www.docubook.pro/docs/getting-started/components).
## Key Components and Their Usage
### 1. **Stepper**
#### Preview
<Stepper>
<StepperItem title="Step 1: Clone the DocuBook Repository">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec interdum,
felis sed efficitur tincidunt, justo nulla viverra enim, et maximus nunc
dolor in lorem.
</StepperItem>
<StepperItem title="Step 2: Access the Project Directory">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin non neque ut
eros auctor accumsan. Mauris a nisl vitae magna ultricies aliquam.
</StepperItem>
<StepperItem title="Step 3: Install Required Dependencies">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ut
ipsum nec nulla ultricies porttitor et non justo.
</StepperItem>
</Stepper>
#### Code
```
<Stepper>
<StepperItem title="Step 1: Clone the DocuBook Repository">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec interdum,
felis sed efficitur tincidunt, justo nulla viverra enim, et maximus nunc
dolor in lorem.
</StepperItem>
<StepperItem title="Step 2: Access the Project Directory">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin non neque ut
eros auctor accumsan. Mauris a nisl vitae magna ultricies aliquam.
</StepperItem>
<StepperItem title="Step 3: Install Required Dependencies">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ut
ipsum nec nulla ultricies porttitor et non justo.
</StepperItem>
</Stepper>
```
### 2. **Note**
#### Preview
<Note type="note" title="Note">
This is a general note to convey information to the user.
</Note>
<Note type="danger" title="Danger">
This is a danger alert to notify the user of a critical issue.
</Note>
<Note type="warning" title="Warning">
This is a warning alert for issues that require attention.
</Note>
<Note type="success" title="Success">
This is a success message to inform the user of successful actions.
</Note>
#### Code
```
<Note type="note" title="Note">
This is a general note to convey information to the user.
</Note>
<Note type="danger" title="Danger">
This is a danger alert to notify the user of a critical issue.
</Note>
<Note type="warning" title="Warning">
This is a warning alert for issues that require attention.
</Note>
<Note type="success" title="Success">
This is a success message to inform the user of successful actions.
</Note>
```
### 3. **Code Block**
#### Preview
```javascript:main.js showLineNumbers {3-4}
function isRocketAboutToCrash() {
// Check if the rocket is stable
if (!isStable()) {
NoCrash(); // Prevent the crash
}
}
```
#### Code
````plaintext
```javascript:main.js showLineNumbers {3-4}
function isRocketAboutToCrash() {
// Check if the rocket is stable
if (!isStable()) {
NoCrash(); // Prevent the crash
}
}```
````
## How to Integrate Components into Your Workflow
1. **Install DocuBook**: Ensure you have DocuBook set up in your project. Refer to the [installation guide](https://www.docubook.pro/docs/getting-started).
2. **Import Components**: Import the required components into your MDX files.
3. **Customize**: Tailor the components to fit your documentation needs using props and styles.
4. **Test and Deploy**: Preview your documentation locally and deploy it to your preferred hosting platform.
## Conclusion
DocuBook components are powerful tools for creating engaging and functional documentation. By incorporating these components, you can provide a seamless and intuitive experience for your users.
Ready to get started? Explore the full range of components in the [DocuBook documentation](https://www.docubook.pro/docs/getting-started/components) and elevate your documentation today!
🚀📚

View File

@@ -0,0 +1,98 @@
---
title: "Building a Dynamic Documentation Platform with DocuBook: A Deep Dive"
description: "Explore how DocuBook simplifies the creation of interactive and dynamic documentation platforms. This blog post provides insights into its features, changelog updates, and how it enhances the documentation experience."
date: 31-12-2024
authors:
- avatar: "https://ui.shadcn.com/avatars/02.png"
handle: mywildancloud
username: Wildan nrs
handleUrl: "https://github.com/mywildancloud"
cover: "https://img.freepik.com/free-photo/high-angle-designer-working-floor_23-2149930985.jpg?t=st=1735654027~exp=1735657627~hmac=2cd2d48f845d1691a28992aec65a3bc11b8e28680d06eeff218e8a773fffc36e&w=2000"
---
## Introduction: Why DocuBook Stands Out in Documentation Platforms
In the fast-evolving world of web development, creating and maintaining well-structured documentation is crucial. Whether you're working on open-source projects, internal tools, or client deliverables, a robust documentation platform can make all the difference. Enter **DocuBook**—a modern, interactive, and dynamic platform designed to streamline the documentation process.
In this blog post, well explore the key features of DocuBook, highlight recent updates from its [changelog](https://www.docubook.pro/changelog), and discuss why its an essential tool for developers and teams.
## Key Features of DocuBook
### 1. Interactive Documentation
DocuBook offers a seamless user experience with interactive elements such as expandable sections, live code previews, and integrated search functionality. This ensures that users can quickly find and engage with the content they need.
### 2. Dynamic Changelog Management
The changelog feature in DocuBook allows teams to document updates, bug fixes, and feature rollouts in a structured format. This helps maintain transparency and keeps users informed about the latest developments.
### 3. Customizable Themes
DocuBook supports theme customization, enabling you to align the documentation's appearance with your brand identity. From color schemes to typography, you have full control over the design.
### 4. Built-in Versioning
With built-in versioning, DocuBook makes it easy to manage multiple versions of your documentation. This is particularly useful for projects that evolve over time or have distinct releases.
### 5. Markdown and MDX Support
Leverage the simplicity of Markdown and the flexibility of MDX to create rich, component-based documentation. DocuBook seamlessly integrates with modern frameworks like React for a smooth development experience.
## Recent Updates: Highlights from the Changelog
## [v1.3.0] - 2024-12-31
> Release Note Feature to Make it Easier to Write Changelogs
![version 1.3.0 - release note image](https://docubook.pro/images/release-note.png)
### Added
- New Release Note Feature
- New Layout for Changelog page
- New Changelog page
- Add Release Note Component
- Easily write release notes directly from the CHANGELOG.md file
- TOC for versioning
- Write with the markdown tag
- Add lib / changelog.ts
### Improved
- Improvement Responsive feature image for Version Entry
- Improvement Layout for changelog page
- Improvement Padding on mobile devices
### Fixed
- Fix og:image not showing on Page.tsx
### Removed
- Remove excessive padding
- Only use containers of md size
- Remove Logo on Footer
For a full list of updates, visit the [changelog](https://www.docubook.pro/changelog).
## Why Choose DocuBook?
### Streamlined Collaboration
DocuBook enables teams to collaborate effectively by providing tools for inline comments, suggestions, and version control. This ensures that everyone stays on the same page.
### Developer-Friendly
With support for modern frameworks, DocuBook simplifies the integration process. Whether youre using React, Next.js, or plain HTML, DocuBook adapts to your workflow.
### Scalability
Designed to handle projects of any size, DocuBook scales effortlessly, making it ideal for both small teams and enterprise-level organizations.
## Conclusion: Elevate Your Documentation Game with DocuBook
DocuBook is more than just a documentation tool; its a platform that empowers developers and teams to create, manage, and maintain high-quality documentation with ease. Its rich feature set, combined with regular updates, makes it a must-have for any project.
Ready to transform your documentation experience? Explore more at [DocuBook](https://www.docubook.pro) and start building documentation that stands out.
🚀📚

View File

@@ -0,0 +1,121 @@
---
title: "Using React Server Components and Server Actions in Next.js"
description: "Explore how to leverage React Server Components and Server Actions in Next.js to build modern, efficient web applications. Learn how these features enhance performance and simplify server-side logic."
date: 05-09-2024
authors:
- avatar: "https://ui.shadcn.com/avatars/02.png"
handle: reactdev
username: React Dev
handleUrl: "https://github.com/reactdev"
- avatar: "https://ui.shadcn.com/avatars/01.png"
handle: nextjsguru
username: Next.js Guru
handleUrl: "https://github.com/nextjsguru"
cover: "https://img.freepik.com/premium-vector/many-monsters-various-colors-doodle-come-bless-birthday-happy_577083-85.jpg?w=826"
---
## Introduction: Enhancing Next.js with React Server Components
Next.js has evolved to include powerful features like React Server Components and Server Actions, which offer a new way to handle server-side rendering and logic. These features provide a more efficient and streamlined approach to building web applications, allowing you to fetch data and render components on the server without compromising performance.
In this blog post, we'll explore how to use React Server Components and Server Actions in Next.js with practical examples and code snippets.
## What Are React Server Components?
React Server Components (RSC) are a new type of component introduced by React that allows you to render components on the server. This approach helps reduce the amount of JavaScript sent to the client and enhances performance by offloading rendering work to the server.
### Benefits of React Server Components
- **Improved Performance**: By rendering on the server, you reduce the amount of client-side JavaScript and improve load times.
- **Enhanced User Experience**: Faster initial page loads and smoother interactions.
- **Simplified Data Fetching**: Fetch data on the server and pass it directly to components.
### Example: Creating a Server Component
Heres a basic example of a React Server Component in a Next.js application:
```jsx
// app/components/UserProfile.server.js
import { getUserData } from "../lib/api";
export default async function UserProfile() {
const user = await getUserData();
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
```
In this example, `UserProfile` is a server component that fetches user data on the server and renders it.
## What Are Server Actions?
Server Actions are functions that run on the server in response to user interactions or other events. They allow you to handle server-side logic, such as form submissions or API requests, directly from your React components.
### Benefits of Server Actions
- **Simplified Server Logic**: Write server-side code directly in your components.
- **Enhanced Security**: Handle sensitive operations on the server rather than the client.
- **Improved Performance**: Reduce client-side JavaScript and offload tasks to the server.
### Example: Using Server Actions
Heres how you can use Server Actions in a Next.js application to handle form submissions:
```jsx
// app/actions/submitForm.js
import { saveFormData } from "../lib/api";
export async function submitForm(data) {
await saveFormData(data);
return { success: true };
}
```
```jsx
// app/components/ContactForm.js
"use client";
import { submitForm } from "../actions/submitForm";
export default function ContactForm() {
const handleSubmit = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const result = await submitForm(Object.fromEntries(formData));
if (result.success) {
alert("Form submitted successfully!");
}
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" name="name" required />
</label>
<label>
Email:
<input type="email" name="email" required />
</label>
<button type="submit">Submit</button>
</form>
);
}
```
In this example, `submitForm` is a server action that processes form data on the server, and `ContactForm` is a client component that uses this action to handle form submissions.
## Conclusion: Leveraging Modern Features for Better Web Apps
React Server Components and Server Actions in Next.js provide powerful tools for building efficient, modern web applications. By leveraging these features, you can improve performance, simplify server-side logic, and create a more responsive user experience.
As you build your Next.js applications, consider incorporating React Server Components and Server Actions to take full advantage of the latest advancements in web development.
Happy coding!
🚀✨

View File

@@ -0,0 +1,72 @@
---
title: Accordion
description: This section previews the Accordion component.
date: 22-12-2024
---
I have implemented the `accordion` component into markdown which is ready to use.
## Preview
### Code Block
<Accordion title="Code Block" defaultOpen={true}>
```javascript:main.js showLineNumbers {3-4}
function isRocketAboutToCrash() {
// Check if the rocket is stable
if (!isStable()) {
NoCrash(); // Prevent the crash
}
}
```
</Accordion>
### Text with Markdown
<Accordion title="Markdown">
this is an example of plain text content from the accordion component and below is markdown ;
1. number one
2. number two
3. number three
</Accordion>
## Props
| Prop | Type | Default | Description |
| ------------- | --------- | -------------- | -------------------------------------------------------------------------------------- |
| `title` | `string` | `null` | The value of Accordion title. |
| `children` | `ReactNode` | `null` | Flexible content, both HTML elements, markdown componenst and plain text. |
| `defaultOpen` | `boolean` | `false` | You can change the value to `true` if you want the content to open when the page loads |
## Code
<Tabs defaultValue="markdown" className="pt-5 pb-1">
<TabsList>
<TabsTrigger value="markdown">Markdown</TabsTrigger>
<TabsTrigger value="codeblock">Code Block</TabsTrigger>
</TabsList>
<TabsContent value="markdown">
```plaintext
<Accordion title="Markdown">
this is an example of plain text content from the accordion component and below is markdown ;
1. number one
2. number two
3. number three
</Accordion>
```
</TabsContent>
<TabsContent value="codeblock">
````plaintext
<Accordion title="Code Block" defaultOpen={true}>
```javascript:main.js showLineNumbers {3-4}
function isRocketAboutToCrash() {
// Check if the rocket is stable
if (!isStable()) {
NoCrash(); // Prevent the crash
}
}
```
</Accordion>
````
</TabsContent>
</Tabs>

View File

@@ -0,0 +1,42 @@
---
title: Button
description: This section previews the Button component.
date: 14-12-2024
---
I have implemented the `button` component into markdown which is ready to use.
## Preview
<Button
text="Learn More"
href="https://learn.example.com"
icon="ArrowUpRight"
size="md"
target="_blank"
variation="primary"
/>
## Props
| Prop | Type | Default | Description |
| ----------- | -------- | -------------- | -------------------------------------------- |
| `text` | `string` | `""` | The value of text button. |
| `href` | `string` | `""` | The value of url button. |
| `icon` | `string` | `null` | The value of button icon render from lucide. |
| `size` | `string` | `"sm, md, lg"` | The value of size button. |
| `target` | `string` | `"_blank"` | By default target `_blank` |
| `variation` | `string` | `"primary"` | By default variation is **Primary** |
## Code
```bash
<Button
text="Learn More"
href="https://learn.example.com"
icon="MoveUpRight"
size="md"
target="_blank"
variation="primary"
/>
```

View File

@@ -0,0 +1,49 @@
---
title: Card Group
description: This section previews the Card Group component.
date: 20-02-2025
---
I have implemented the `card group` component into markdown which is ready to use.
## Preview
<CardGroup cols={2}>
<Card title="Heading 1" icon="Heading1">
This is an example of card content with columns.
</Card>
<Card title="Heading 2" icon="Heading2">
This is an example of card content with columns.
</Card>
<Card title="Grid Card" icon="Grid" horizontal>
This is a horizontal card layout.
</Card>
<Card title="Horizontal Card" icon="Layout" horizontal>
This is a horizontal card layout.
</Card>
</CardGroup>
## Props
| Prop | Type | Default | Description |
| ------------- | -------- | ------- | ------------------------------------------------------- |
| `cols` | `number` | `{2}` | By default 2 The number of columns per row |
## Code
```markdown
<CardGroup cols={2}>
<Card title="Heading 1" icon="Heading1">
This is an example of card content with columns.
</Card>
<Card title="Heading 2" icon="Heading2">
This is an example of card content with columns.
</Card>
<Card title="Grid Card" icon="Grid" horizontal>
This is a horizontal card layout.
</Card>
<Card title="Horizontal Card" icon="Layout" horizontal>
This is a horizontal card layout.
</Card>
</CardGroup>
```

View File

@@ -0,0 +1,70 @@
---
title: Cards
description: This section previews the Cards component.
date: 20-02-2025
---
I have implemented the `cards` component into markdown which is ready to use.
## Example
### Card with Link and icon
<Card title="Click on me" icon="Link" href="/docs/getting-started/components/card-group">
This is how you use a card with an icon and a link. Clicking on this card
brings you to the Card Group page.
</Card>
### Card Horizontal
<Card title="Horizontal Card" icon="Layout" horizontal>
This is a horizontal card layout.
</Card>
### Card Simple
<Card title="Simple Card">
This is a simple card without an icon or link.
</Card>
## Props
| Prop | Type | Default | Description |
| ------------- | -------- | ------- | ------------------------------------------------------- |
| `title` | `string` | `""` | The value of card title. |
| `icon` | `string` | `null` | The value of card icon render from lucide. |
| `href` | `string` | `null` | The value of card link url. |
| `horizontal` | `boolean` | `""` | horizontal layout for card. |
## Code
<Tabs defaultValue="link" className="pt-5 pb-1">
<TabsList>
<TabsTrigger value="link">Card with Link & Icon</TabsTrigger>
<TabsTrigger value="horizontal">Card Horizontal</TabsTrigger>
<TabsTrigger value="simple">Card Simple</TabsTrigger>
</TabsList>
<TabsContent value="link">
```markdown
<Card title="Click on me" icon="Link" href="/docs/getting-started/components/button">
This is how you use a card with an icon and a link. Clicking on this card
brings you to the Card Group page.
</Card>
```
</TabsContent>
<TabsContent value="horizontal">
```markdown
<Card title="Horizontal Card" icon="Layout" horizontal>
This is a horizontal card layout.
</Card>
```
</TabsContent>
<TabsContent value="simple">
```markdown
<Card title="Simple Card">
This is a simple card without an icon or link.
</Card>
```
</TabsContent>
</Tabs>

View File

@@ -0,0 +1,41 @@
---
title: Code Block
description: This section previews the Code Block features in markdown.
date: 14-12-2024
---
The Code Block in this documentation allows you to display code snippets with optional line numbering and line highlighting.
## Preview
```javascript:main.js showLineNumbers {3-4}
function isRocketAboutToCrash() {
// Check if the rocket is stable
if (!isStable()) {
NoCrash(); // Prevent the crash
}
}
```
In this example, line numbers are displayed for lines 1 to 4. You can specify which lines to highlight using the format `{2,3-5}`.
## Usage
You can directly use the following syntax to create a code block with line numbers and highlight specific lines:
````plaintext
```javascript:main.js showLineNumbers {3-4}
function isRocketAboutToCrash() {
// Check if the rocket is stable
if (!isStable()) {
NoCrash(); // Prevent the crash
}
}
```
````
### Features
- **Line Numbers**: Enable line numbers by adding `showLineNumbers` after the opening backticks.
- **Highlight Lines**: Specify lines to highlight using curly braces (e.g., `{2,3-5}`).
- **Syntax Highlighting**: Use the appropriate language for syntax highlighting.

View File

@@ -0,0 +1,38 @@
---
title: Custom Components
description: How to create custom components for Markdown.
date: 14-12-2024
---
To add custom components in DocuBook, follow these steps:
1. **Create Your Component**: First, create your custom component in the `@components/markdown` folder. For example, you might create a file named `Outlet.tsx`.
2. **Import Your Component**: Next, open the `@lib/markdown.ts` file. This is where you'll register your custom component for use in Markdown.
3. **Add Your Component to the Components Object**: In the `@lib/markdown.ts` file, import your custom component and add it to the `components` object. Heres how to do it:
```ts
import Outlet from "@/components/markdown/outlet";
// Add custom components
const components = {
Outlet,
};
```
4. **Using Your Custom Component in Markdown**: After registering your component, you can now use it anywhere in your Markdown content. For instance, if your `Outlet` component is designed to display additional information, you can use it as follows:
### Markdown Example
```markdown
<Outlet>
This is some custom content rendered by the Outlet component!
</Outlet>
```
### Rendered Output
This will render the content inside the `Outlet` component, allowing you to create reusable and dynamic Markdown content.
By following these steps, you can extend the capabilities of your Markdown documentation and create a more engaging user experience.

Some files were not shown because too many files have changed in this diff Show More