initial to gitea
This commit is contained in:
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal 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
14
.vscode/accordion.code-snippets
vendored
Normal 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
16
.vscode/button.code-snippets
vendored
Normal 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."
|
||||
}
|
||||
}
|
||||
26
.vscode/card.code-snippets
vendored
Normal file
26
.vscode/card.code-snippets
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"DocuCards": {
|
||||
"prefix": "card",
|
||||
"body": [
|
||||
"<div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">",
|
||||
"<Card>",
|
||||
" <Card.Title title=\"${1:Heading}\" icon=\"${2:Heading}\" />",
|
||||
" <Card.Description description=\"${3:Your description card here! change this text}\" />",
|
||||
"</Card>",
|
||||
"<Card>",
|
||||
" <Card.Title title=\"Link\" icon=\"Link\" />",
|
||||
" <Card.Description description=\"Your description card here! change this text\" />",
|
||||
"</Card>",
|
||||
"<Card>",
|
||||
" <Card.Title title=\"Images\" icon=\"Images\" />",
|
||||
" <Card.Description description=\"Your description card here! change this text\" />",
|
||||
"</Card>",
|
||||
"<Card>",
|
||||
" <Card.Title title=\"Code Block\" icon=\"Code\" />",
|
||||
" <Card.Description description=\"Your description card here! change this text\" />",
|
||||
"</Card>",
|
||||
"</div>"
|
||||
],
|
||||
"description": "Create a DocuCards component on markdown."
|
||||
}
|
||||
}
|
||||
16
.vscode/codeblock.code-snippets
vendored
Normal file
16
.vscode/codeblock.code-snippets
vendored
Normal 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
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": []
|
||||
}
|
||||
16
.vscode/image-link.code-snippets
vendored
Normal file
16
.vscode/image-link.code-snippets
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"DocuImage": {
|
||||
"prefix": "image",
|
||||
"body": [
|
||||
""
|
||||
],
|
||||
"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
13
.vscode/metadata.code-snippets
vendored
Normal 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
38
.vscode/note.code-snippets
vendored
Normal 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
24
.vscode/stepper.code-snippets
vendored
Normal 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
20
.vscode/table.code-snippets
vendored
Normal 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
33
.vscode/tabs.code-snippets
vendored
Normal 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."
|
||||
}
|
||||
}
|
||||
16
.vscode/tooltips.code-snippets
vendored
Normal file
16
.vscode/tooltips.code-snippets
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"DocuTooltips": {
|
||||
"prefix": "tooltips",
|
||||
"body": [
|
||||
"<div className=\"w-full\">",
|
||||
" <Tooltip tip=\"${1:this is tooltips! DocuBook on version 1.0.5}\">",
|
||||
" <span style={{ textDecoration: \"underline dotted\", cursor: \"pointer\" }}>",
|
||||
" ${2:Hover over me}",
|
||||
" </span>",
|
||||
" </Tooltip>",
|
||||
" <span> ${3:and this is some regular text.}</span>",
|
||||
"</div>"
|
||||
],
|
||||
"description": "Create a DocuTooltips component with version examples."
|
||||
}
|
||||
}
|
||||
33
.vscode/typography.code-snippets
vendored
Normal file
33
.vscode/typography.code-snippets
vendored
Normal 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
9
.vscode/youtube.code-snippets
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"DocuVideo": {
|
||||
"prefix": "youtube",
|
||||
"body": [
|
||||
"<Youtube videoId=\"${1:zQnBQ4tB3ZA}\" />"
|
||||
],
|
||||
"description": "Snippet untuk menampilkan komponen video Youtube."
|
||||
}
|
||||
}
|
||||
132
CHANGELOG.md
Normal file
132
CHANGELOG.md
Normal file
@@ -0,0 +1,132 @@
|
||||
## [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
|
||||
|
||||
> Major 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
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Mohd. Nisab
|
||||
|
||||
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.
|
||||
71
README.md
Normal file
71
README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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.
|
||||
|
||||
[](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
|
||||
```
|
||||
|
||||
## Update
|
||||
### How to Update DocuBook?
|
||||
- **Open a New Terminal**: Please open a new terminal on the desktop that has DocuBook installed.
|
||||
- **Move Directory**: for example, if the directory name is docubook, then write `cd docubook` and press enter.
|
||||
|
||||
```bash
|
||||
npx @docubook/update@latest
|
||||
```
|
||||
|
||||
#### command output
|
||||
|
||||
```bash
|
||||
📂 Updating Docubook project in /Users/wildan/Public/docubook...
|
||||
|
||||
ℹ ⚡ Skipped public
|
||||
ℹ ⚡ Skipped contents
|
||||
ℹ ⚡ Skipped app/page.tsx
|
||||
ℹ ⚡ Skipped docu.json
|
||||
ℹ ⚡ Skipped CHANGELOG.md
|
||||
✨ Replacing styles folder...
|
||||
✨ Replaced all CSS files in styles folder
|
||||
✔ ✅ Docubook v1.4.2 successfully updated in /Users/wildan/Public/docubook!
|
||||
|
||||
🎯 Next steps:
|
||||
1. Verify your changes in the current directory.
|
||||
2. Run the install script to check for package updates:
|
||||
npm install
|
||||
3. Run the development server:
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Access the app on => http://localhost:3000
|
||||
92
app/blog/[slug]/page.tsx
Normal file
92
app/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Typography } from "@/components/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/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
9
app/blog/layout.tsx
Normal 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
98
app/blog/page.tsx
Normal 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
11
app/changelog/layout.tsx
Normal 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
63
app/changelog/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
105
app/docs/[[...slug]]/page.tsx
Normal file
105
app/docs/[[...slug]]/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getDocsForSlug, getDocsTocs } from "@/lib/markdown";
|
||||
import DocsBreadcrumb from "@/components/docs-breadcrumb";
|
||||
import Pagination from "@/components/pagination";
|
||||
import Toc from "@/components/toc";
|
||||
import { Typography } from "@/components/typography";
|
||||
import EditThisPage from "@/components/edit-on-github";
|
||||
import { formatDate2 } from "@/lib/utils";
|
||||
import docuConfig from "@/docu.json";
|
||||
import MobToc from "@/components/mob-toc";
|
||||
import { ScrollToTop } from "@/components/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-end 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
14
app/docs/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Leftbar } from "@/components/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
44
app/error.tsx
Normal 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'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
18
app/hire-me/page.tsx
Normal 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
96
app/layout.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ThemeProvider } from "@/components/contexts/theme-provider";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { Footer } from "@/components/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
19
app/not-found.tsx
Normal 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're looking for doesn't exist.</p>
|
||||
</div>
|
||||
<Link href="/" className={buttonVariants({})}>
|
||||
Back to homepage
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
app/page.tsx
Normal file
95
app/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { page_routes } from "@/lib/routes-config";
|
||||
import { ArrowRightIcon, BookOpen, Headset, SquarePlay } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import AnimatedShinyText from "@/components/ui/animated-shiny-text";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getMetadata } from "@/app/layout";
|
||||
|
||||
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="https://addonsejoli.pro/product/"
|
||||
target="_blank"
|
||||
className="flex items-center mb-5 underline gap-2 sm:text-lg 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>✨ Beli Plugin Addon</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="mb-4 text-2xl font-bold sm:text-6xl sm:flex sm:flex-col">
|
||||
Panduan Penggunaan Plugin <span className="text-muted-foreground dark:text-accent">Addon Sejoli Pro</span>
|
||||
</h1>
|
||||
<p className="mb-8 sm:text-xl max-w-[800px] text-muted-foreground">
|
||||
Tutorial dengan konten video, gambar dan tulisan serta penjelasan tentang cara menggunakan plugin mulai dari cara instalasi, konfigurasi, dan penggunaannya.<span className="underline text-muted-foreground underline-offset-4 decoration-dotted"> "Dibahas secara lengkap dan mudah dipahami."</span>
|
||||
</p>
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<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",
|
||||
})}
|
||||
>
|
||||
Mulai Tutorial
|
||||
</Link>
|
||||
{/* <Link
|
||||
href="/blog"
|
||||
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",
|
||||
})}
|
||||
>
|
||||
Read Blog
|
||||
</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">
|
||||
<SquarePlay className="size-6 text-primary" />
|
||||
<CardTitle className="text-xl">Video Tutorial</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Video tutorial berdasarkan playlist di channel Youtube.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="px-2 py-6">
|
||||
<CardHeader className="flex flex-row justify-center items-center gap-3">
|
||||
<BookOpen className="size-6 text-primary" />
|
||||
<CardTitle className="text-xl">Dokumentasi</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Web dokumentasi akan membantu anda dalam penggunaan plugin.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="px-2 py-6">
|
||||
<CardHeader className="flex flex-row justify-center items-center gap-3">
|
||||
<Headset className="size-6 text-primary" />
|
||||
<CardTitle className="text-xl">Ticket Support</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Jika anda mengalami kendala team kami akan membantu dengan support ticket.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
app/playground/layout.tsx
Normal file
16
app/playground/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
518
app/playground/page.tsx
Normal file
518
app/playground/page.tsx
Normal file
@@ -0,0 +1,518 @@
|
||||
"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, '\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: `<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<Card.Title title="Heading" icon="Heading" />
|
||||
<Card.Description description="Your description card here! change this text" />
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title title="Link" icon="Link" />
|
||||
<Card.Description description="Your description card here! change this text" />
|
||||
</Card>
|
||||
</div>\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: `<Tooltip tip="Your tooltip text">
|
||||
<span>Hover over me</span>
|
||||
</Tooltip>\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
17
components.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
components/anchor.tsx
Normal file
38
components/anchor.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
type AnchorProps = ComponentProps<typeof Link> & {
|
||||
absolute?: boolean;
|
||||
activeClassName?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function Anchor({
|
||||
absolute,
|
||||
className = "",
|
||||
activeClassName = "",
|
||||
disabled,
|
||||
children,
|
||||
...props
|
||||
}: AnchorProps) {
|
||||
const path = usePathname();
|
||||
let isMatch = absolute
|
||||
? props.href.toString().split("/")[1] == path.split("/")[1]
|
||||
: path === props.href;
|
||||
|
||||
if (props.href.toString().includes("http")) isMatch = false;
|
||||
|
||||
if (disabled)
|
||||
return (
|
||||
<div className={cn(className, "cursor-not-allowed")}>{children}</div>
|
||||
);
|
||||
return (
|
||||
<Link className={cn(className, isMatch && activeClassName)} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
45
components/changelog/change-group.tsx
Normal file
45
components/changelog/change-group.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
86
components/changelog/floating-version.tsx
Normal file
86
components/changelog/floating-version.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
113
components/changelog/version-entry.tsx
Normal file
113
components/changelog/version-entry.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
components/changelog/version-tag.tsx
Normal file
14
components/changelog/version-tag.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
90
components/changelog/version-toc.tsx
Normal file
90
components/changelog/version-toc.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
components/contexts/theme-provider.tsx
Normal file
9
components/contexts/theme-provider.tsx
Normal 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>;
|
||||
}
|
||||
47
components/docs-breadcrumb.tsx
Normal file
47
components/docs-breadcrumb.tsx
Normal 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(" ");
|
||||
}
|
||||
24
components/docs-menu.tsx
Normal file
24
components/docs-menu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
components/edit-on-github.tsx
Normal file
35
components/edit-on-github.tsx
Normal 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 on Github</span>
|
||||
<SquarePenIcon className="w-4 h-4 text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditThisPage;
|
||||
57
components/footer.tsx
Normal file
57
components/footer.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import Link from "next/link";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
import docuConfig from "@/docu.json"; // Import JSON
|
||||
|
||||
export function Footer() {
|
||||
const { footer } = docuConfig; // Extract footer from JSON
|
||||
|
||||
return (
|
||||
<footer className="border-t w-full h-16">
|
||||
<div className="container flex items-center sm:justify-between justify-center sm:gap-0 gap-4 h-full text-muted-foreground text-sm flex-wrap sm:py-0 py-3 max-sm:px-4">
|
||||
{/* Footer Text */}
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-center">
|
||||
Copyright © {new Date().getFullYear()} {footer.copyright} - Crafted with love using{" "}
|
||||
<Link
|
||||
href="https://www.docubook.pro"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2"
|
||||
>
|
||||
DocuBook
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer Buttons */}
|
||||
<div className="gap-4 items-center hidden md:flex">
|
||||
<FooterButtons />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export function FooterButtons() {
|
||||
const { footer } = docuConfig; // Extract footer from JSON
|
||||
|
||||
return (
|
||||
<>
|
||||
{footer.buttons.map((button, index) => {
|
||||
const Icon = require("lucide-react")[button.iconName]; // Dynamically load icon
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={button.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={buttonVariants({ variant: "outline", size: "sm" })}
|
||||
>
|
||||
<Icon className="h-4 w-4 mr-2 dark:text-primary dark:hover:text-accent-foreground" />
|
||||
{button.text}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
55
components/leftbar.tsx
Normal file
55
components/leftbar.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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";
|
||||
|
||||
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="p-6 pb-4 flex gap-2.5">
|
||||
<FooterButtons />
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
47
components/markdown/accordion.tsx
Normal file
47
components/markdown/accordion.tsx
Normal 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;
|
||||
52
components/markdown/button.tsx
Normal file
52
components/markdown/button.tsx
Normal 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;
|
||||
57
components/markdown/card.tsx
Normal file
57
components/markdown/card.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import * as Icons from "lucide-react";
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
|
||||
// Props untuk Card utama
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// Props untuk CardTitle
|
||||
interface CardTitleProps {
|
||||
title: string;
|
||||
icon?: IconName; // Properti ikon berupa nama ikon yang valid
|
||||
}
|
||||
|
||||
// Props untuk CardDescription
|
||||
interface CardDescriptionProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Komponen Card Utama
|
||||
const Card: React.FC<CardProps> & {
|
||||
Title: React.FC<CardTitleProps>;
|
||||
Description: React.FC<CardDescriptionProps>;
|
||||
} = ({ children }) => {
|
||||
return (
|
||||
<div className="border rounded-lg shadow-md overflow-hidden py-4 px-8">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Komponen Card Title
|
||||
Card.Title = ({ title, icon }: CardTitleProps) => {
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null; // Tipe eksplisit sebagai React.FC
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-1">
|
||||
{Icon && <Icon className="text-xl text-primary" />} {/* Render ikon jika ada */}
|
||||
<h2 className="text-xl font-bold">{title}</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Menambahkan displayName untuk Card.Title
|
||||
Card.Title.displayName = "CardTitle";
|
||||
|
||||
// Komponen Card Description
|
||||
Card.Description = ({ description }: CardDescriptionProps) => (
|
||||
<p className="text-muted-foreground text-[16.5px] mt-2">{description}</p>
|
||||
);
|
||||
|
||||
// Menambahkan displayName untuk Card.Description
|
||||
Card.Description.displayName = "CardDescription";
|
||||
|
||||
export default Card;
|
||||
33
components/markdown/copy.tsx
Normal file
33
components/markdown/copy.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
components/markdown/image.tsx
Normal file
25
components/markdown/image.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
14
components/markdown/link.tsx
Normal file
14
components/markdown/link.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
52
components/markdown/note.tsx
Normal file
52
components/markdown/note.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
components/markdown/outlet.tsx
Normal file
29
components/markdown/outlet.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
components/markdown/pre.tsx
Normal file
19
components/markdown/pre.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
components/markdown/stepper.tsx
Normal file
41
components/markdown/stepper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
components/markdown/tooltips.tsx
Normal file
31
components/markdown/tooltips.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface TooltipProps {
|
||||
tip: string; // Teks yang akan ditampilkan dalam tooltip
|
||||
children: React.ReactNode; // Elemen yang akan memunculkan tooltip
|
||||
}
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({ tip, children }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative inline-block"
|
||||
onMouseEnter={() => setVisible(true)}
|
||||
onMouseLeave={() => setVisible(false)}
|
||||
>
|
||||
{children}
|
||||
{visible && (
|
||||
<div
|
||||
className="absolute bottom-12 bg-black border-solid-2 border border-white text-white text-sm px-2 py-1 rounded"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{tip}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
22
components/markdown/youtube.tsx
Normal file
22
components/markdown/youtube.tsx
Normal 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;
|
||||
38
components/mob-toc.tsx
Normal file
38
components/mob-toc.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
99
components/navbar.tsx
Normal file
99
components/navbar.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { ModeToggle } from "@/components/theme-toggle";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
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() {
|
||||
const { social } = docuConfig; // Extract navbar and social from JSON
|
||||
|
||||
return (
|
||||
<nav className="w-full border-b h-16 sticky top-0 z-50 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="sm:flex hidden">
|
||||
<Logo />
|
||||
</div>
|
||||
<div className="lg:flex hidden items-center gap-4 text-sm font-medium text-muted-foreground">
|
||||
<NavMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Search />
|
||||
<div className="flex ml-2.5 sm:ml-0 gap-2">
|
||||
{social.map((item) => {
|
||||
const Icon = require("lucide-react")[item.iconName]; // Dynamically load icon
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
className={buttonVariants({ variant: "ghost", size: "icon" })}
|
||||
>
|
||||
<Icon className="h-[1.1rem] w-[1.1rem]" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</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="text-md font-bold font-code">{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="h-4 w-4 text-muted-foreground" />}
|
||||
</Anchor>
|
||||
);
|
||||
return isSheet ? (
|
||||
<SheetClose key={item.title + item.href} asChild>
|
||||
{Comp}
|
||||
</SheetClose>
|
||||
) : (
|
||||
Comp
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
49
components/pagination.tsx
Normal file
49
components/pagination.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
components/scroll-to-top.tsx
Normal file
52
components/scroll-to-top.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
144
components/search.tsx
Normal file
144
components/search.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpIcon, ArrowDownIcon, CommandIcon, FileIcon, SearchIcon, CornerDownLeftIcon } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Anchor from "./anchor";
|
||||
import { advanceSearch, cn } from "@/lib/utils";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
export default function Search() {
|
||||
const [searchedInput, setSearchedInput] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSearchedInput("");
|
||||
setIsOpen(open);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<div className="relative flex-1 cursor-pointer sm:w-60">
|
||||
<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 documentation..."
|
||||
type="search"
|
||||
/>
|
||||
<div className="sm:flex hidden 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) => {
|
||||
const level = (item.href.split("/").slice(1).length -
|
||||
1) as keyof typeof paddingMap;
|
||||
const paddingClass = paddingMap[level];
|
||||
|
||||
return (
|
||||
<DialogClose key={item.href} asChild>
|
||||
<Anchor
|
||||
className={cn(
|
||||
"dark:hover:bg-stone-900 hover:bg-stone-100 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
|
||||
paddingClass
|
||||
)}
|
||||
href={`/docs${item.href}`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center w-fit h-full py-3 gap-1.5 px-2",
|
||||
level > 1 && "border-l pl-4"
|
||||
)}
|
||||
>
|
||||
<FileIcon 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",
|
||||
// Add more levels if needed
|
||||
} as const;
|
||||
85
components/sublink.tsx
Normal file
85
components/sublink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
components/terminal.tsx
Normal file
48
components/terminal.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
AnimatedSpan,
|
||||
Terminal,
|
||||
TypingAnimation,
|
||||
} from "@/components/ui/terminal";
|
||||
|
||||
export function NpxTerminal() {
|
||||
return (
|
||||
<Terminal>
|
||||
<TypingAnimation className="text-left pl-6 dark:text-blue-300 text-blue-600">> npx @docubook/create@latest</TypingAnimation>
|
||||
|
||||
<AnimatedSpan delay={1500} className="text-muted-foreground text-left pl-6">
|
||||
<span>Need to install the following packages:</span>
|
||||
</AnimatedSpan>
|
||||
|
||||
<AnimatedSpan delay={2000} className="text-muted-foreground text-left pl-6">
|
||||
<span>@docubook/create@1.4.0</span>
|
||||
</AnimatedSpan>
|
||||
|
||||
<AnimatedSpan delay={2500} className="text-muted-foreground text-left pl-6">
|
||||
<span>Ok to proceed? (y)</span>
|
||||
</AnimatedSpan>
|
||||
|
||||
<AnimatedSpan delay={3000} className="dark:text-blue-300 text-blue-600 text-left pl-6">
|
||||
<span>✔ ? Enter a name for your project directory: (docubook)</span>
|
||||
</AnimatedSpan>
|
||||
|
||||
<AnimatedSpan delay={3500} className="text-muted-foreground text-left pl-6">
|
||||
<span>Creating a new Docubook project in /path/your/docubook from the starter branch...</span>
|
||||
</AnimatedSpan>
|
||||
|
||||
<AnimatedSpan delay={4000} className="text-muted-foreground text-left pl-6">
|
||||
<span>✔ Docubook project successfully created in /path/your/docubook!</span>
|
||||
</AnimatedSpan>
|
||||
|
||||
<AnimatedSpan delay={6000} className="text-foreground text-left pl-6">
|
||||
<span>Next Step</span>
|
||||
<span className="pl-2 dark:text-blue-300 text-blue-600">1. Navigate to your project directory: cd docubook</span>
|
||||
<span className="pl-2 dark:text-blue-300 text-blue-600">2. Install dependencies: npm install</span>
|
||||
<span className="pl-2 dark:text-blue-300 text-blue-600">3. Start the development server: npm run dev</span>
|
||||
</AnimatedSpan>
|
||||
|
||||
<TypingAnimation delay={6500} className="text-muted-foreground text-left pl-6">
|
||||
Open the apps via browser http://localhost:3000.
|
||||
</TypingAnimation>
|
||||
</Terminal>
|
||||
);
|
||||
}
|
||||
40
components/theme-toggle.tsx
Normal file
40
components/theme-toggle.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Sun className="h-[1.1rem] w-[1.1rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.1rem] w-[1.1rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
69
components/toc-observer.tsx
Normal file
69
components/toc-observer.tsx
Normal 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/toc.tsx
Normal file
21
components/toc.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
components/typography.tsx
Normal file
9
components/typography.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
58
components/ui/accordion.tsx
Normal file
58
components/ui/accordion.tsx
Normal 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 }
|
||||
40
components/ui/animated-shiny-text.tsx
Normal file
40
components/ui/animated-shiny-text.tsx
Normal 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
50
components/ui/avatar.tsx
Normal 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
37
components/ui/badge.tsx
Normal 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 };
|
||||
115
components/ui/breadcrumb.tsx
Normal file
115
components/ui/breadcrumb.tsx
Normal 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
57
components/ui/button.tsx
Normal 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
76
components/ui/card.tsx
Normal 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 }
|
||||
11
components/ui/collapsible.tsx
Normal file
11
components/ui/collapsible.tsx
Normal 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
124
components/ui/dialog.tsx
Normal 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,
|
||||
};
|
||||
200
components/ui/dropdown-menu.tsx
Normal file
200
components/ui/dropdown-menu.tsx
Normal 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
25
components/ui/input.tsx
Normal 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
33
components/ui/popover.tsx
Normal 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 }
|
||||
48
components/ui/scroll-area.tsx
Normal file
48
components/ui/scroll-area.tsx
Normal 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 }
|
||||
30
components/ui/separator.tsx
Normal file
30
components/ui/separator.tsx
Normal 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
140
components/ui/sheet.tsx
Normal 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
31
components/ui/sonner.tsx
Normal 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
117
components/ui/table.tsx
Normal 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
55
components/ui/tabs.tsx
Normal 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 };
|
||||
119
components/ui/terminal.tsx
Normal file
119
components/ui/terminal.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, MotionProps } from "framer-motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface AnimatedSpanProps extends MotionProps {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AnimatedSpan = ({
|
||||
children,
|
||||
delay = 0,
|
||||
className,
|
||||
...props
|
||||
}: AnimatedSpanProps) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: delay / 1000 }}
|
||||
className={cn("grid text-sm font-normal tracking-tight", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
interface TypingAnimationProps extends MotionProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export const TypingAnimation = ({
|
||||
children,
|
||||
className,
|
||||
duration = 60,
|
||||
delay = 0,
|
||||
as: Component = "span",
|
||||
...props
|
||||
}: TypingAnimationProps) => {
|
||||
if (typeof children !== "string") {
|
||||
throw new Error("TypingAnimation: children must be a string. Received:");
|
||||
}
|
||||
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
});
|
||||
|
||||
const [displayedText, setDisplayedText] = useState<string>("");
|
||||
const [started, setStarted] = useState(false);
|
||||
const elementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const startTimeout = setTimeout(() => {
|
||||
setStarted(true);
|
||||
}, delay);
|
||||
return () => clearTimeout(startTimeout);
|
||||
}, [delay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!started) return;
|
||||
|
||||
let i = 0;
|
||||
const typingEffect = setInterval(() => {
|
||||
if (i < children.length) {
|
||||
setDisplayedText(children.substring(0, i + 1));
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(typingEffect);
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return () => {
|
||||
clearInterval(typingEffect);
|
||||
};
|
||||
}, [children, duration, started]);
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
ref={elementRef}
|
||||
className={cn("text-sm font-normal tracking-tight", className)}
|
||||
{...props}
|
||||
>
|
||||
{displayedText}
|
||||
</MotionComponent>
|
||||
);
|
||||
};
|
||||
|
||||
interface TerminalProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Terminal = ({ children, className }: TerminalProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-0 h-full max-h-[600px] w-full max-w-[640px] rounded-xl border border-border bg-backgroun",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-2 border-b border-border p-4">
|
||||
<div className="flex flex-row gap-x-2">
|
||||
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<pre className="p-8">
|
||||
<code className="grid gap-y-1 overflow-auto">{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
170
contents/blogs/file-rec.mdx
Normal file
170
contents/blogs/file-rec.mdx
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: "Building a Recursive File System with React: A Deep Dive"
|
||||
description: "Explore how to create a recursive file system in React. This blog post provides a comprehensive guide on building a file system where folders and files can be nested, added, renamed, and deleted."
|
||||
date: 02-09-2024
|
||||
authors:
|
||||
- avatar: "https://ui.shadcn.com/avatars/02.png"
|
||||
handle: nisabmohd
|
||||
username: Nisab Mohd
|
||||
handleUrl: "https://github.com/nisabmohd"
|
||||
cover: "https://img.freepik.com/premium-vector/many-monsters-various-colors-doodle-come-bless-birthday-happy_577083-84.jpg?w=826"
|
||||
---
|
||||
|
||||
## Introduction: Crafting a Recursive File System in React
|
||||
|
||||
In modern web development, creating interactive and dynamic file systems is a common requirement. Whether for managing documents, organizing projects, or building complex data structures, having a robust file system is crucial. In this blog post, we’ll explore how to build a recursive file system in React, focusing on nested folders and files that can be added, renamed, or deleted.
|
||||
|
||||
Check out the project on [GitHub](https://github.com/nisabmohd/recursive-file-system-react) for a complete implementation.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Recursive File System project is designed to simulate a file management system where users can interact with folders and files dynamically. It supports the following features:
|
||||
|
||||
- **Adding New Folders and Files**: Create new folders and files within any existing folder.
|
||||
- **Renaming Items**: Change the name of folders and files.
|
||||
- **Deleting Items**: Remove folders and files from the file system.
|
||||
- **Nested Structure**: Handle nested folders and files to create a hierarchical view.
|
||||
|
||||
## Key Features and Implementation
|
||||
|
||||
### 1. Recursive Data Structure
|
||||
|
||||
The core of the project is a recursive data structure that represents the file system. Each folder can contain other folders or files, and each file or folder has properties such as `id`, `name`, and `children` (for folders).
|
||||
|
||||
Here’s a basic structure for a folder:
|
||||
|
||||
```jsx
|
||||
const folder = {
|
||||
id: "1",
|
||||
name: "Documents",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "2", name: "Resume.pdf", type: "file" },
|
||||
{ id: "3", name: "CoverLetter.docx", type: "file" },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Components
|
||||
|
||||
The project includes several key components to handle different aspects of the file system:
|
||||
|
||||
- **FileExplorer**: Displays the entire file system and handles rendering folders and files.
|
||||
|
||||
```jsx
|
||||
// src/components/FileExplorer.js
|
||||
import React, { useState } from "react";
|
||||
import Folder from "./Folder";
|
||||
import File from "./File";
|
||||
|
||||
const FileExplorer = () => {
|
||||
const [files, setFiles] = useState(initialData); // initialData is your recursive data structure
|
||||
|
||||
const addItem = (parentId, type) => {
|
||||
// Logic to add a folder or file
|
||||
};
|
||||
|
||||
const renameItem = (id, newName) => {
|
||||
// Logic to rename a folder or file
|
||||
};
|
||||
|
||||
const deleteItem = (id) => {
|
||||
// Logic to delete a folder or file
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{files.map((file) =>
|
||||
file.type === "folder" ? (
|
||||
<Folder
|
||||
key={file.id}
|
||||
folder={file}
|
||||
onAdd={addItem}
|
||||
onRename={renameItem}
|
||||
onDelete={deleteItem}
|
||||
/>
|
||||
) : (
|
||||
<File
|
||||
key={file.id}
|
||||
file={file}
|
||||
onRename={renameItem}
|
||||
onDelete={deleteItem}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileExplorer;
|
||||
```
|
||||
|
||||
- **Folder**: Renders folders and handles nested items.
|
||||
|
||||
```jsx
|
||||
// src/components/Folder.js
|
||||
import React from "react";
|
||||
import FileExplorer from "./FileExplorer";
|
||||
|
||||
const Folder = ({ folder, onAdd, onRename, onDelete }) => {
|
||||
return (
|
||||
<div>
|
||||
<h3>{folder.name}</h3>
|
||||
<button onClick={() => onAdd(folder.id, "folder")}>Add Folder</button>
|
||||
<button onClick={() => onAdd(folder.id, "file")}>Add File</button>
|
||||
<button onClick={() => onRename(folder.id, "New Name")}>Rename</button>
|
||||
<button onClick={() => onDelete(folder.id)}>Delete</button>
|
||||
{folder.children && <FileExplorer files={folder.children} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Folder;
|
||||
```
|
||||
|
||||
- **File**: Renders individual files with options to rename and delete.
|
||||
|
||||
```jsx
|
||||
// src/components/File.js
|
||||
import React from "react";
|
||||
|
||||
const File = ({ file, onRename, onDelete }) => {
|
||||
return (
|
||||
<div>
|
||||
<p>{file.name}</p>
|
||||
<button onClick={() => onRename(file.id, "New Name")}>Rename</button>
|
||||
<button onClick={() => onDelete(file.id)}>Delete</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default File;
|
||||
```
|
||||
|
||||
### 3. Handling State and Actions
|
||||
|
||||
State management is handled using React hooks like `useState` to manage the file system data. Actions such as adding, renaming, and deleting items update the state accordingly.
|
||||
|
||||
```jsx
|
||||
const [files, setFiles] = useState(initialData);
|
||||
|
||||
const addItem = (parentId, type) => {
|
||||
// Logic to add a new item to the file system
|
||||
};
|
||||
|
||||
const renameItem = (id, newName) => {
|
||||
// Logic to rename an existing item
|
||||
};
|
||||
|
||||
const deleteItem = (id) => {
|
||||
// Logic to delete an item
|
||||
};
|
||||
```
|
||||
|
||||
## Conclusion: Building a Dynamic File System with React
|
||||
|
||||
Creating a recursive file system in React is a powerful way to manage hierarchical data and provide a dynamic user experience. By leveraging React's component-based architecture and state management, you can build interactive file systems that handle complex nested structures efficiently.
|
||||
|
||||
Check out the full implementation on [GitHub](https://github.com/nisabmohd/recursive-file-system-react) and explore how these concepts can be applied to your own projects. Happy coding!
|
||||
|
||||
🚀📁
|
||||
121
contents/blogs/next-react.mdx
Normal file
121
contents/blogs/next-react.mdx
Normal 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
|
||||
|
||||
Here’s 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
|
||||
|
||||
Here’s 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!
|
||||
|
||||
🚀✨
|
||||
24
contents/docs/getting-started/activate-license/index.mdx
Normal file
24
contents/docs/getting-started/activate-license/index.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Aktivasi Lisensi
|
||||
description: Aktivasi lisensi plugin Addon Sejoli Pro.
|
||||
date: 30-11-2024
|
||||
---
|
||||
|
||||
Untuk mengetahui lisensi yang anda miliki dan memanage lisensi aktif pada domain navigasi ke [member.dwindi.com](https://member.dwindi.com/member-area/license/)
|
||||
|
||||
<Stepper>
|
||||
<StepperItem title="Step 1: Navigasi ke menu `lisensi`">
|
||||
Halaman member-area/lisensi untuk melihat daftar lisensi.
|
||||
</StepperItem>
|
||||
<StepperItem title="Step 2: Klik `icon` copy">
|
||||
Untuk menyalin lisensi untuk kebutuhan aktivasi lisensi.
|
||||
</StepperItem>
|
||||
<StepperItem title="Step 3: Navigasi ke tombol `manage domain`">
|
||||
Untuk melihat daftar lisensi yang aktif di domain, anda bisa revoke jika
|
||||
ingin melepas lisensi pada domain.
|
||||
</StepperItem>
|
||||
</Stepper>
|
||||
|
||||
> Ikuti video tutorial dibawah ini untuk aktivasi lisensi plugin addon sejoli
|
||||
|
||||
<Youtube videoId="CMKWQ9b2q6c" />
|
||||
62
contents/docs/getting-started/installation/index.mdx
Normal file
62
contents/docs/getting-started/installation/index.mdx
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Installasi Plugin
|
||||
description: Install Plugin upload manual & melalui menu plugin wp-admin.
|
||||
date: 30-11-2024
|
||||
---
|
||||
|
||||
Persiapkan unduhan file `.zip` dari plugin yang akan anda instal di wordpress, untuk mendapatkan file plugin silahkan download melalui [https://member.dwindi.com](https://member.dwindi.com/member-area/assets/).
|
||||
|
||||
## Melalui wp-admin
|
||||
|
||||
langkah-langkah install plugin melalui wp-admin.
|
||||
|
||||
<Stepper>
|
||||
<StepperItem title="Step 1: Menu Plugin">
|
||||
Pada halaman wp-admin ke menu `plugin`
|
||||
</StepperItem>
|
||||
<StepperItem title="Step 2: Add New">
|
||||
Klik `add new` untuk menambahkan/menginstal plugin baru.
|
||||
</StepperItem>
|
||||
<StepperItem title="Step 3: Upload">
|
||||
Klik `upload` pilih dari media penyimpanan local dengan extension `.zip`
|
||||
</StepperItem>
|
||||
<StepperItem title="Step 4: Activate Plugin">
|
||||
Klik `activate` setelah proses instal selesai
|
||||
</StepperItem>
|
||||
</Stepper>
|
||||
|
||||
<Note type="note" title="Note">
|
||||
Kecepatan proses instalasi tergantung koneksi internet dan spesifikasi hosting
|
||||
yang anda gunakan.
|
||||
</Note>
|
||||
|
||||
## Melalui File Manager
|
||||
|
||||
Untuk instal plugin melalui metode upload ke `file manager` berikut struktur folder
|
||||
|
||||
```bash
|
||||
public_html // root folder
|
||||
├── subdomain // folder subdomain
|
||||
├── domain // folder domain utama
|
||||
│ └── wp-content // directory content untuk plugin dan theme
|
||||
│ └── plugin // upload file .zip plugin ke directory ini
|
||||
```
|
||||
|
||||
<Note type="warning" title="Perhatian">
|
||||
extract file `.zip` plugin yang telah anda upload kemudian ke `wp-admin` -->
|
||||
`plugin` --> `activate`
|
||||
</Note>
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
### Instal melalui Hpanel
|
||||
|
||||
> Jika anda pengguna hpanel bisa dengan cara ini;
|
||||
|
||||
<Youtube videoId="EMtsK4o5Dbo" />
|
||||
|
||||
### Instal melalui Cpanel
|
||||
|
||||
> Jika anda pengguna cpanel bisa dengan cara ini;
|
||||
|
||||
<Youtube videoId="ea-1RbPb0EU" />
|
||||
42
contents/docs/getting-started/introduction/index.mdx
Normal file
42
contents/docs/getting-started/introduction/index.mdx
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Introduction
|
||||
description: Latar Belakang dan Pengenalan Plugin.
|
||||
date: 30-11-2024
|
||||
---
|
||||
|
||||
Selamat datang di **AddonSejoliPro** penyedia produk Add-On plugin Sejoli yang siap membantu anda dalam memaksimalkan fitur Membership.
|
||||
|
||||
## Plugin Unggulan
|
||||
|
||||
> Beberapa plugin yang sering digunakan oleh pengguna untuk membuat tampilan custom member area, custom halaman checkout hanya dengan Elementor Page Builder.
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<Card.Title title="Sejoli Member Area UI" icon="LayoutPanelLeft" />
|
||||
<Card.Description description="Untuk mengubah desain tampilan member area hanya drag and drop." />
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title title="Sejoli Shortcodes" icon="Brackets" />
|
||||
<Card.Description description="Plugin ini merupakan depedensi dari Member Area UI." />
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title title="Sejoli Checkout UI" icon="ReceiptText" />
|
||||
<Card.Description description="Mengubah halaman checkout sepenuhnya dengan widget Elementor." />
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title title="Sejoli LMS Pro" icon="GraduationCap" />
|
||||
<Card.Description description="Plugin yang digunakan untuk membuat kursus online based on Sejoli." />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
## Depedensi
|
||||
|
||||
AddonSejoliPro memerlukan depedensi plugin untuk bisa menggunakannya :
|
||||
|
||||
- **Sejoli** - Core Plugin yang wajib anda install sebelum menggunakan plugin-plugin Addon Sejoli Pro.
|
||||
- **Elementor** - Untuk saat ini builder yang digunakan untuk membuat desain hanya dengan drag and drop (pro maupun free)
|
||||
|
||||
<Note title="Kesimpulan">
|
||||
Plugin **AddonSejoliPro** dibuat terkait banyak nya permintaan fungsi custom
|
||||
pada web membership Sejoli.
|
||||
</Note>
|
||||
21
contents/docs/getting-started/system-requirements/index.mdx
Normal file
21
contents/docs/getting-started/system-requirements/index.mdx
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: System Requirements
|
||||
description: Persyaratan sistem untuk menggunakan plugin addon sejoli pro
|
||||
date: 30-11-2024
|
||||
---
|
||||
|
||||
Untuk dapat menggunakan plugin **Addon Sejoli Pro** anda perlu menyewa hosting untuk menjalankan wordpress self hosted [wordpress.org](https://wordpress.org/).
|
||||
|
||||
## Minimum Requirements
|
||||
|
||||
- **wordpress.org** - versi 6.0.0 atau terbaru.
|
||||
- **elementor** - versi 3.0.0 atau terbaru.
|
||||
- **php** - versi 8.0.0 disarankan karena lebih stabil.
|
||||
- **sejoli** - versi 1.13.14 tested.
|
||||
- **RAM** - 1GB sudah cukup untuk menjalankan plugin.
|
||||
- **Storage** - 20GB tergantung kebutuhan konten.
|
||||
|
||||
<Note type="note" title="Note">
|
||||
Untuk bisa menggunakan seluruh addon plugin sejoli pastikan anda menginstal
|
||||
wordpress.org dan terinstal plugin **sejoli** sebagai core plugin
|
||||
</Note>
|
||||
7
contents/docs/plugins/affiliate-extra/as-admin/index.mdx
Normal file
7
contents/docs/plugins/affiliate-extra/as-admin/index.mdx
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: As Admin
|
||||
description: Pengaturan sebagai admin
|
||||
date: 13-12-2024
|
||||
---
|
||||
|
||||
<Youtube videoId="sAXGL-HWZPk" />
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: As Affiliate
|
||||
description: Pengaturan sebagai affiliasi
|
||||
date: 13-12-2024
|
||||
---
|
||||
|
||||
<Youtube videoId="-QpQ9Ipzsbg" />
|
||||
22
contents/docs/plugins/affiliate-extra/flyer-editor/index.mdx
Normal file
22
contents/docs/plugins/affiliate-extra/flyer-editor/index.mdx
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Flyer Editor
|
||||
description: Contoh penggunaan banner untuk affiliasi
|
||||
date: 13-12-2024
|
||||
---
|
||||
|
||||
<Youtube videoId="mvaLMS24YBo" />
|
||||
|
||||
## HTML
|
||||
|
||||
```html showLineNumbers {2}
|
||||
<a href="affiliate_link" target="_blank">
|
||||
<img src="url_banner_image" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## Penggunaan
|
||||
|
||||
<Note type="note" title="Note">
|
||||
`"affiliate_link"` ganti dengan URL affiliasi, `"url_banner_image"` ganti
|
||||
dengan URL gambar.
|
||||
</Note>
|
||||
16
contents/docs/plugins/affiliate-extra/index.mdx
Normal file
16
contents/docs/plugins/affiliate-extra/index.mdx
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Sejoli Affiliate Extra
|
||||
description: Panduan penggunaan Sejoli Affiliate Extra
|
||||
date: 30-11-2024
|
||||
---
|
||||
|
||||
<Button
|
||||
text="Lihat Sales Page"
|
||||
href="https://addonsejoli.pro/produk/sejoli-affiliate-extra/"
|
||||
icon="ArrowUpRight"
|
||||
size="md"
|
||||
target="_blank"
|
||||
variation="primary"
|
||||
/>
|
||||
|
||||
<Outlet path="plugins/affiliate-extra" />
|
||||
33
contents/docs/plugins/checkout-ui/activate-feature/index.mdx
Normal file
33
contents/docs/plugins/checkout-ui/activate-feature/index.mdx
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Aktifkan Fitur
|
||||
description: Aktifkan Checkout UI untuk halaman checkout produk
|
||||
date: 14-12-2024
|
||||
---
|
||||
|
||||
<Youtube videoId="f7zxTE2S8TY" />
|
||||
|
||||
## Langkah - Aktifkan Fitur
|
||||
|
||||
<Stepper>
|
||||
<StepperItem title="Step 1: Navigasi ke Product">
|
||||
Pada halaman wp-admin navigasi ke menu `product` akan muncul daftar produk
|
||||
kemudian sorot dan klik `edit`.
|
||||

|
||||
</StepperItem>
|
||||
<StepperItem title="Step 2: Tab Checkout UI">
|
||||
Pada halaman pengaturan produk akan anda temukan section `Sejoli Setup` ->
|
||||
tab `Checkout UI` centang opsi **Aktifkan Checkout UI**.
|
||||

|
||||
</StepperItem>
|
||||
<StepperItem title="Step 3: Edit With Elementor">
|
||||
Klik tombol **Edit With Elementor** untuk membuat desain halaman checkout
|
||||
dengan widget-widget dari plugin Checkout UI.
|
||||

|
||||
</StepperItem>
|
||||
</Stepper>
|
||||
|
||||
<Note type="note" title="Penting!">
|
||||
Jika tombol **Edit With Elementor** tidak muncul di halaman pengaturan produk,
|
||||
navigasi ke menu `Elementor` -> `Settings` lalu centang opsi `Products` pada
|
||||
**Post Types**.
|
||||
</Note>
|
||||
20
contents/docs/plugins/checkout-ui/checkout-button/index.mdx
Normal file
20
contents/docs/plugins/checkout-ui/checkout-button/index.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Checkout Button
|
||||
description: Widget Sejoli Checkout UI untuk kolom tombol pembayaran
|
||||
date: 14-12-2024
|
||||
---
|
||||
<Youtube videoId="T-Lpd1FRSQY" />
|
||||
|
||||

|
||||
|
||||
## Tab - Content
|
||||
|
||||
Opsi untuk menyesuaikan konten tombol pembayaran pada halaman checkout produk dengan widget **Checkout Button**.
|
||||
|
||||

|
||||
|
||||
## Tab - Style
|
||||
|
||||
Mengubah dan menyesuaikan warna background, text maupun tombol pada konten tombol pembayaran.
|
||||
|
||||

|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user