update versi docubook
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
|
||||
> **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)
|
||||
[](https://vercel.com/import/project?template=https://github.com/gitfromwildan/docubook)
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export default async function BlogIndexPage() {
|
||||
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
|
||||
);
|
||||
return (
|
||||
<div className="w-full mx-auto flex flex-col gap-1 sm:min-h-[91vh] min-h-[88vh] pt-2">
|
||||
<div className="w-full mx-auto flex flex-col gap-1 sm:min-h-[91vh] min-h-[88vh] py-2">
|
||||
<div className="mb-7 flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-extrabold">
|
||||
Blog Posts
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
Laptop2,
|
||||
Copy,
|
||||
Download,
|
||||
RotateCcw
|
||||
RotateCcw,
|
||||
Calendar
|
||||
} from "lucide-react";
|
||||
import { Button as UIButton } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -38,8 +39,23 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
handleParagraphClick,
|
||||
handleHeading2Click,
|
||||
handleHeading3Click,
|
||||
handleBulletListClick,
|
||||
handleNumberedListClick,
|
||||
handleLinkClick,
|
||||
handleImageClick,
|
||||
handleBlockquoteClick,
|
||||
handleCodeBlockClick,
|
||||
handleTableClick,
|
||||
handleNoteClick,
|
||||
handleComponentClick,
|
||||
handleMetadataClick,
|
||||
} from "@/components/playground/MarkComponent";
|
||||
|
||||
import "@/styles/editor.css";
|
||||
|
||||
const ToolbarButton = ({ icon: Icon, label, onClick }: { icon: any, label: string, onClick?: () => void }) => (
|
||||
@@ -167,9 +183,7 @@ export default function PlaygroundPage() {
|
||||
</UIButton>
|
||||
</div>
|
||||
</div>
|
||||
), {
|
||||
duration: 10000,
|
||||
});
|
||||
), { duration: 10000 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,149 +193,17 @@ export default function PlaygroundPage() {
|
||||
const before = markdown.substring(0, start);
|
||||
const after = markdown.substring(end);
|
||||
|
||||
const newText = before + text + after;
|
||||
// Menambahkan satu baris kosong sebelum dan sesudah komponen
|
||||
const newText = `${before}${text}\n${after}`;
|
||||
setMarkdown(newText);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
textArea.focus();
|
||||
const newPosition = start + text.length;
|
||||
const newPosition = start + text.length + 1;
|
||||
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: `<Card title="Click on me" icon="Link" href="/docs/getting-started/components/button">
|
||||
This is how you use a card with an icon and a link. Clicking on this card
|
||||
brings you to the Card Group page.
|
||||
</Card>\n`,
|
||||
button: `<Button
|
||||
text="Click Me"
|
||||
href="#"
|
||||
icon="ArrowRight"
|
||||
size="md"
|
||||
variation="primary"
|
||||
/>\n`,
|
||||
accordion: `<Accordion title="Markdown">
|
||||
this is an example of plain text content from the accordion component and below is markdown ;
|
||||
1. number one
|
||||
2. number two
|
||||
3. number three
|
||||
</Accordion>\n`,
|
||||
youtube: `<Youtube videoId="your-video-id" />\n`,
|
||||
tooltip: `What do you know about <Tooltip text="DocuBook" tip="npx @docubook/create@latest" /> ? Create interactive nested documentations using MDX.\n`,
|
||||
tabs: `<Tabs defaultValue="tab1" className="pt-5 pb-1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">
|
||||
Content for tab 1
|
||||
</TabsContent>
|
||||
<TabsContent value="tab2">
|
||||
Content for tab 2
|
||||
</TabsContent>
|
||||
</Tabs>\n`
|
||||
};
|
||||
|
||||
insertAtCursor(textArea, templates[component]);
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
return <MobileMessage />;
|
||||
@@ -402,86 +284,88 @@ export default function PlaygroundPage() {
|
||||
</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} />
|
||||
<ToolbarButton icon={Calendar} label="Metadata" onClick={() => handleMetadataClick(insertAtCursor)} />
|
||||
<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} />
|
||||
<ToolbarButton icon={Type} label="Paragraph" onClick={() => handleParagraphClick(insertAtCursor)} />
|
||||
<ToolbarButton icon={Heading2} label="Heading 2" onClick={() => handleHeading2Click(insertAtCursor)} />
|
||||
<ToolbarButton icon={Heading3} label="Heading 3" onClick={() => handleHeading3Click(insertAtCursor)} />
|
||||
<ToolbarButton icon={List} label="Bullet List" onClick={() => handleBulletListClick(insertAtCursor)} />
|
||||
<ToolbarButton icon={ListOrdered} label="Numbered List" onClick={() => handleNumberedListClick(insertAtCursor)} />
|
||||
<ToolbarSeparator />
|
||||
<ToolbarButton icon={LinkIcon} label="Link" onClick={() => handleLinkClick(insertAtCursor)} />
|
||||
<ToolbarButton icon={ImageIcon} label="Image" onClick={() => handleImageClick(insertAtCursor)} />
|
||||
<ToolbarButton icon={Quote} label="Blockquote" onClick={() => handleBlockquoteClick(insertAtCursor)} />
|
||||
<ToolbarButton icon={Code} label="Code Block" onClick={() => handleCodeBlockClick(insertAtCursor)} />
|
||||
<ToolbarButton icon={Table} label="Table" onClick={() => handleTableClick(insertAtCursor)} />
|
||||
<ToolbarSeparator />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<UIButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 flex items-center gap-1 font-normal"
|
||||
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" />
|
||||
<Notebook className="h-4 w-4" />
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</UIButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => handleNoteClick('note')}>
|
||||
Note
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'note')}>
|
||||
Note
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleNoteClick('danger')}>
|
||||
Danger
|
||||
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'danger')}>
|
||||
Danger
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleNoteClick('warning')}>
|
||||
Warning
|
||||
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'warning')}>
|
||||
Warning
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleNoteClick('success')}>
|
||||
Success
|
||||
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'success')}>
|
||||
Success
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ToolbarSeparator />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<UIButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 flex items-center gap-1 font-normal"
|
||||
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" />
|
||||
<Component className="h-4 w-4" />
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</UIButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => handleComponentClick('stepper')}>
|
||||
<Rows className="h-4 w-4 mr-2" />
|
||||
Stepper
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, '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 onClick={handleComponentClick(insertAtCursor, '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 onClick={handleComponentClick(insertAtCursor, '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 onClick={handleComponentClick(insertAtCursor, '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 onClick={handleComponentClick(insertAtCursor, '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 onClick={handleComponentClick(insertAtCursor, '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 onClick={handleComponentClick(insertAtCursor, 'tooltip')}>
|
||||
<HelpCircle className="h-4 w-4 mr-2" />
|
||||
Tooltip
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,13 +378,13 @@ export default function PlaygroundPage() {
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
ref={editorRef}
|
||||
value={markdown}
|
||||
onChange={(e) => setMarkdown(e.target.value)}
|
||||
className="editor-textarea"
|
||||
spellCheck={false}
|
||||
placeholder="Start writing markdown..."
|
||||
/>
|
||||
ref={editorRef}
|
||||
value={markdown}
|
||||
onChange={(e) => setMarkdown(e.target.value)}
|
||||
className="editor-textarea"
|
||||
spellCheck={false}
|
||||
placeholder="Type '/' for commands..."
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Logo, NavMenu } from "./navbar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button } from "./ui/button";
|
||||
import { AlignLeftIcon } from "lucide-react";
|
||||
import { FooterButtons } from "@/components/footer";
|
||||
import { DialogTitle } from "@/components/ui/dialog";
|
||||
import { FooterButtons } from "./footer";
|
||||
import { DialogTitle } from "./ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import DocsMenu from "@/components/docs-menu";
|
||||
import { ModeToggle } from "@/components/theme-toggle";
|
||||
import DocsMenu from "./docs-menu";
|
||||
import { ModeToggle } from "./theme-toggle";
|
||||
|
||||
export function Leftbar() {
|
||||
return (
|
||||
@@ -46,9 +46,9 @@ export function SheetLeftbar() {
|
||||
<div className="mx-2 px-5">
|
||||
<DocsMenu isSheet />
|
||||
</div>
|
||||
{/* <div className="px-6 py-2 flex justify-start items-center gap-6">
|
||||
<div className="px-6 py-2 flex justify-start items-center gap-6">
|
||||
<FooterButtons />
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex w-2/4 px-5">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Search from "@/components/search";
|
||||
import Anchor from "@/components/anchor";
|
||||
import { SheetLeftbar } from "@/components/leftbar";
|
||||
import { SheetClose } from "@/components/ui/sheet";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import docuConfig from "@/docu.json"; // Import JSON
|
||||
|
||||
export function Navbar() {
|
||||
@@ -18,13 +19,13 @@ export function Navbar() {
|
||||
<div className="hidden sm:flex">
|
||||
<Logo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
|
||||
<NavMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Separator className="hidden sm:flex my-4 h-9" orientation="vertical" />
|
||||
<Search />
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,7 +37,7 @@ export function Logo() {
|
||||
const { navbar } = docuConfig; // Extract navbar from JSON
|
||||
|
||||
return (
|
||||
<Link href="/" className="flex items-center gap-2.5">
|
||||
<Link href="/" className="flex items-center gap-1.5">
|
||||
<Image src={navbar.logo.src} alt={navbar.logo.alt} width="24" height="24" />
|
||||
<h2 className="font-bold font-code text-md">{navbar.logoText}</h2>
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getPreviousNext } from "@/lib/markdown";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
|
||||
export default function Pagination({ pathname }: { pathname: string }) {
|
||||
const res = getPreviousNext(pathname);
|
||||
|
||||
252
components/playground/MarkComponent.tsx
Normal file
252
components/playground/MarkComponent.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
type InsertAtCursor = (textArea: HTMLTextAreaElement, text: string) => void;
|
||||
|
||||
// toolbar handler
|
||||
export const handleMetadataClick = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
const metadata = `---
|
||||
title: Post Title
|
||||
description: Your Post Description
|
||||
date: ${new Date().toISOString().split("T")[0]}
|
||||
image: example-img.png
|
||||
---\n\n`;
|
||||
|
||||
insertAtCursor(textArea, metadata);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleParagraphClick = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
insertAtCursor(textArea, "this is regular text, **bold text**, *italic text*\n");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleHeading2Click = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
insertAtCursor(textArea, "## Heading 2\n");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleHeading3Click = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
insertAtCursor(textArea, "### Heading 3\n");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleBulletListClick = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
insertAtCursor(textArea, "- List One\n- List Two\n- Other List\n");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleNumberedListClick = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
insertAtCursor(textArea, "1. Number One\n2. Number Two\n3. Number Three\n");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleLinkClick = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
insertAtCursor(textArea, "[Visit OpenAI](https://www.openai.com)\n");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleImageClick = (insertAtCursor: InsertAtCursor) => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (textArea) {
|
||||
insertAtCursor(textArea, "\n");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleBlockquoteClick = (insertAtCursor: InsertAtCursor) => {
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
export const handleCodeBlockClick = (insertAtCursor: InsertAtCursor) => {
|
||||
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"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleTableClick = (insertAtCursor: InsertAtCursor) => {
|
||||
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. |
|
||||
|
||||
`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleNoteClick = (insertAtCursor: InsertAtCursor, type: string) => {
|
||||
return () => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const handleComponentClick = (insertAtCursor: InsertAtCursor, component: string) => {
|
||||
return () => {
|
||||
const textArea = document.querySelector("textarea");
|
||||
if (!textArea) return;
|
||||
|
||||
const templates: { [key: string]: string } = {
|
||||
stepper: `<Stepper>
|
||||
<StepperItem title="Step 1">
|
||||
Content for step 1
|
||||
</StepperItem>
|
||||
<StepperItem title="Step 2">
|
||||
Content for step 2
|
||||
</StepperItem>
|
||||
</Stepper>\n`,
|
||||
card: `<Card title="Click on me" icon="Link" href="/docs/getting-started/components/button">
|
||||
This is how you use a card with an icon and a link. Clicking on this card brings you to the Card Group page.
|
||||
</Card>\n`,
|
||||
button: `<Button
|
||||
text="Click Me"
|
||||
href="#"
|
||||
icon="ArrowRight"
|
||||
size="md"
|
||||
variation="primary"
|
||||
/>\n`,
|
||||
accordion: `<Accordion title="Markdown">
|
||||
this is an example of plain text content from the accordion component and below is markdown ;
|
||||
1. number one
|
||||
2. number two
|
||||
3. number three
|
||||
</Accordion>\n`,
|
||||
youtube: `<Youtube videoId="your-video-id" />\n`,
|
||||
tooltip: `What do you know about <Tooltip text="DocuBook" tip="npx @docubook/create@latest" /> ? Create interactive nested documentations using MDX.\n`,
|
||||
tabs: `<Tabs defaultValue="tab1" className="pt-5 pb-1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">
|
||||
Content for tab 1
|
||||
</TabsContent>
|
||||
<TabsContent value="tab2">
|
||||
Content for tab 2
|
||||
</TabsContent>
|
||||
</Tabs>\n`
|
||||
};
|
||||
|
||||
insertAtCursor(textArea, templates[component]);
|
||||
};
|
||||
};
|
||||
|
||||
// slash command handler
|
||||
export const MARK_COMPONENTS = [
|
||||
{ label: "Metadata", value: `---
|
||||
title: Post Title
|
||||
description: Your Post Description
|
||||
date: ${new Date().toISOString().split("T")[0]}
|
||||
image: example-img.png
|
||||
---\n\n` },
|
||||
|
||||
{ label: "Heading 2", value: "## Heading 2\n" },
|
||||
{ label: "Heading 3", value: "### Heading 3\n" },
|
||||
|
||||
{ label: "Paragraph", value: "this is regular text, **bold text**, *italic text*\n" },
|
||||
|
||||
{ label: "Bullet List", value: "- List One\n- List Two\n- Other List\n" },
|
||||
{ label: "Numbered List", value: "1. Number One\n2. Number Two\n3. Number Three\n" },
|
||||
|
||||
{ label: "Blockquote", value: "> The overriding design goal for Markdown's formatting syntax is to make it as readable as possible.\n" },
|
||||
|
||||
{ label: "Code Block", value: "```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" },
|
||||
|
||||
{ label: "Table", value: `| **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. |
|
||||
|
||||
` },
|
||||
|
||||
{ label: "Link", value: "[Visit OpenAI](https://www.openai.com)\n" },
|
||||
{ label: "Image", value: "\n" },
|
||||
|
||||
// ⭐ Komponen Interaktif DocuBook ⭐
|
||||
{ label: "Stepper", value: `<Stepper>
|
||||
<StepperItem title="Step 1">
|
||||
Content for step 1
|
||||
</StepperItem>
|
||||
<StepperItem title="Step 2">
|
||||
Content for step 2
|
||||
</StepperItem>
|
||||
</Stepper>\n` },
|
||||
|
||||
{ label: "Button", value: `<Button
|
||||
text="Click Me"
|
||||
href="#"
|
||||
icon="ArrowRight"
|
||||
size="md"
|
||||
variation="primary"
|
||||
/>\n` },
|
||||
|
||||
{ label: "Accordion", value: `<Accordion title="Markdown">
|
||||
This is an example of plain text content inside the accordion component.
|
||||
1. Number One
|
||||
2. Number Two
|
||||
3. Number Three
|
||||
</Accordion>\n` },
|
||||
|
||||
{ label: "Youtube", value: `<Youtube videoId="your-video-id" />\n` },
|
||||
|
||||
{ label: "Tooltip", value: `What do you know about <Tooltip text="DocuBook" tip="npx @docubook/create@latest" /> ?\n` },
|
||||
|
||||
{ label: "Tabs", value: `<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` },
|
||||
|
||||
{ label: "Note", value: `<Note type="note" title="Note">
|
||||
This is a note message.
|
||||
</Note>\n` },
|
||||
|
||||
{ label: "Danger", value: `<Note type="danger" title="Danger">
|
||||
This is a danger message.
|
||||
</Note>\n` },
|
||||
|
||||
{ label: "Warning", value: `<Note type="warning" title="Warning">
|
||||
This is a warning message.
|
||||
</Note>\n` },
|
||||
|
||||
{ label: "Success", value: `<Note type="success" title="Success">
|
||||
This is a success message.
|
||||
</Note>\n` }
|
||||
|
||||
];
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ArrowUpIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ScrollToTop() {
|
||||
|
||||
@@ -96,14 +96,14 @@ export default function Search() {
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<div className="relative flex-1 cursor-pointer max-w-[160px]">
|
||||
<div className="relative flex-1 cursor-pointer max-w-[140px]">
|
||||
<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"
|
||||
className="md:w-full rounded-full dark:bg-background/95 bg-background border h-9 pl-10 pr-0 sm:pr-4 text-sm shadow-sm overflow-ellipsis"
|
||||
placeholder="Search"
|
||||
type="search"
|
||||
/>
|
||||
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-black dark:border dark:border-white/20 bg-stone-200/50 border border-black/40 p-1 rounded-sm">
|
||||
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-accent bg-accent text-white px-2 py-0.5 rounded-full">
|
||||
<CommandIcon className="w-3 h-3" />
|
||||
<span>K</span>
|
||||
</div>
|
||||
|
||||
45
components/ui/aurora.tsx
Normal file
45
components/ui/aurora.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react";
|
||||
|
||||
interface AuroraTextProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
colors?: string[];
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export const AuroraText = memo(
|
||||
({
|
||||
children,
|
||||
className = "",
|
||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||
speed = 1,
|
||||
}: AuroraTextProps) => {
|
||||
const gradientStyle = {
|
||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||
colors[0]
|
||||
})`,
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
animationDuration: `${10 / speed}s`,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${className}`}>
|
||||
<span className="sr-only">{children}</span>
|
||||
<span
|
||||
className="relative animate-aurora bg-[length:200%_auto] bg-clip-text text-transparent"
|
||||
style={gradientStyle}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AuroraText.displayName = "AuroraText";
|
||||
|
||||
export default AuroraText;
|
||||
153
components/ui/command.tsx
Normal file
153
components/ui/command.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
324
components/ui/icon-cloud.tsx
Normal file
324
components/ui/icon-cloud.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
|
||||
interface Icon {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
scale: number;
|
||||
opacity: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface IconCloudProps {
|
||||
icons?: React.ReactNode[];
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
function easeOutCubic(t: number): number {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
export function IconCloud({ icons, images }: IconCloudProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [iconPositions, setIconPositions] = useState<Icon[]>([]);
|
||||
const [rotation, setRotation] = useState({ x: 0, y: 0 });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 });
|
||||
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
||||
const [targetRotation, setTargetRotation] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
distance: number;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
} | null>(null);
|
||||
const animationFrameRef = useRef<number>();
|
||||
const rotationRef = useRef(rotation);
|
||||
const iconCanvasesRef = useRef<HTMLCanvasElement[]>([]);
|
||||
const imagesLoadedRef = useRef<boolean[]>([]);
|
||||
|
||||
// Create icon canvases once when icons/images change
|
||||
useEffect(() => {
|
||||
if (!icons && !images) return;
|
||||
|
||||
const items = icons || images || [];
|
||||
imagesLoadedRef.current = new Array(items.length).fill(false);
|
||||
|
||||
const newIconCanvases = items.map((item, index) => {
|
||||
const offscreen = document.createElement("canvas");
|
||||
offscreen.width = 40;
|
||||
offscreen.height = 40;
|
||||
const offCtx = offscreen.getContext("2d");
|
||||
|
||||
if (offCtx) {
|
||||
if (images) {
|
||||
// Handle image URLs directly
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.src = items[index] as string;
|
||||
img.onload = () => {
|
||||
offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
|
||||
|
||||
// Create circular clipping path
|
||||
offCtx.beginPath();
|
||||
offCtx.arc(20, 20, 20, 0, Math.PI * 2);
|
||||
offCtx.closePath();
|
||||
offCtx.clip();
|
||||
|
||||
// Draw the image
|
||||
offCtx.drawImage(img, 0, 0, 40, 40);
|
||||
|
||||
imagesLoadedRef.current[index] = true;
|
||||
};
|
||||
} else {
|
||||
// Handle SVG icons
|
||||
offCtx.scale(0.4, 0.4);
|
||||
const svgString = renderToString(item as React.ReactElement);
|
||||
const img = new Image();
|
||||
img.src = "data:image/svg+xml;base64," + btoa(svgString);
|
||||
img.onload = () => {
|
||||
offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
|
||||
offCtx.drawImage(img, 0, 0);
|
||||
imagesLoadedRef.current[index] = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
return offscreen;
|
||||
});
|
||||
|
||||
iconCanvasesRef.current = newIconCanvases;
|
||||
}, [icons, images]);
|
||||
|
||||
// Generate initial icon positions on a sphere
|
||||
useEffect(() => {
|
||||
const items = icons || images || [];
|
||||
const newIcons: Icon[] = [];
|
||||
const numIcons = items.length || 20;
|
||||
|
||||
// Fibonacci sphere parameters
|
||||
const offset = 2 / numIcons;
|
||||
const increment = Math.PI * (3 - Math.sqrt(5));
|
||||
|
||||
for (let i = 0; i < numIcons; i++) {
|
||||
const y = i * offset - 1 + offset / 2;
|
||||
const r = Math.sqrt(1 - y * y);
|
||||
const phi = i * increment;
|
||||
|
||||
const x = Math.cos(phi) * r;
|
||||
const z = Math.sin(phi) * r;
|
||||
|
||||
newIcons.push({
|
||||
x: x * 100,
|
||||
y: y * 100,
|
||||
z: z * 100,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
id: i,
|
||||
});
|
||||
}
|
||||
setIconPositions(newIcons);
|
||||
}, [icons, images]);
|
||||
|
||||
// Handle mouse events
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect || !canvasRef.current) return;
|
||||
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const ctx = canvasRef.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
iconPositions.forEach((icon) => {
|
||||
const cosX = Math.cos(rotationRef.current.x);
|
||||
const sinX = Math.sin(rotationRef.current.x);
|
||||
const cosY = Math.cos(rotationRef.current.y);
|
||||
const sinY = Math.sin(rotationRef.current.y);
|
||||
|
||||
const rotatedX = icon.x * cosY - icon.z * sinY;
|
||||
const rotatedZ = icon.x * sinY + icon.z * cosY;
|
||||
const rotatedY = icon.y * cosX + rotatedZ * sinX;
|
||||
|
||||
const screenX = canvasRef.current!.width / 2 + rotatedX;
|
||||
const screenY = canvasRef.current!.height / 2 + rotatedY;
|
||||
|
||||
const scale = (rotatedZ + 200) / 300;
|
||||
const radius = 20 * scale;
|
||||
const dx = x - screenX;
|
||||
const dy = y - screenY;
|
||||
|
||||
if (dx * dx + dy * dy < radius * radius) {
|
||||
const targetX = -Math.atan2(
|
||||
icon.y,
|
||||
Math.sqrt(icon.x * icon.x + icon.z * icon.z),
|
||||
);
|
||||
const targetY = Math.atan2(icon.x, icon.z);
|
||||
|
||||
const currentX = rotationRef.current.x;
|
||||
const currentY = rotationRef.current.y;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2),
|
||||
);
|
||||
|
||||
const duration = Math.min(2000, Math.max(800, distance * 1000));
|
||||
|
||||
setTargetRotation({
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
startX: currentX,
|
||||
startY: currentY,
|
||||
distance,
|
||||
startTime: performance.now(),
|
||||
duration,
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
setIsDragging(true);
|
||||
setLastMousePos({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
setMousePos({ x, y });
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
const deltaX = e.clientX - lastMousePos.x;
|
||||
const deltaY = e.clientY - lastMousePos.y;
|
||||
|
||||
rotationRef.current = {
|
||||
x: rotationRef.current.x + deltaY * 0.002,
|
||||
y: rotationRef.current.y + deltaX * 0.002,
|
||||
};
|
||||
|
||||
setLastMousePos({ x: e.clientX, y: e.clientY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
// Animation and rendering
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2;
|
||||
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
|
||||
const dx = mousePos.x - centerX;
|
||||
const dy = mousePos.y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const speed = 0.003 + (distance / maxDistance) * 0.01;
|
||||
|
||||
if (targetRotation) {
|
||||
const elapsed = performance.now() - targetRotation.startTime;
|
||||
const progress = Math.min(1, elapsed / targetRotation.duration);
|
||||
const easedProgress = easeOutCubic(progress);
|
||||
|
||||
rotationRef.current = {
|
||||
x:
|
||||
targetRotation.startX +
|
||||
(targetRotation.x - targetRotation.startX) * easedProgress,
|
||||
y:
|
||||
targetRotation.startY +
|
||||
(targetRotation.y - targetRotation.startY) * easedProgress,
|
||||
};
|
||||
|
||||
if (progress >= 1) {
|
||||
setTargetRotation(null);
|
||||
}
|
||||
} else if (!isDragging) {
|
||||
rotationRef.current = {
|
||||
x: rotationRef.current.x + (dy / canvas.height) * speed,
|
||||
y: rotationRef.current.y + (dx / canvas.width) * speed,
|
||||
};
|
||||
}
|
||||
|
||||
iconPositions.forEach((icon, index) => {
|
||||
const cosX = Math.cos(rotationRef.current.x);
|
||||
const sinX = Math.sin(rotationRef.current.x);
|
||||
const cosY = Math.cos(rotationRef.current.y);
|
||||
const sinY = Math.sin(rotationRef.current.y);
|
||||
|
||||
const rotatedX = icon.x * cosY - icon.z * sinY;
|
||||
const rotatedZ = icon.x * sinY + icon.z * cosY;
|
||||
const rotatedY = icon.y * cosX + rotatedZ * sinX;
|
||||
|
||||
const scale = (rotatedZ + 200) / 300;
|
||||
const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200));
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(
|
||||
canvas.width / 2 + rotatedX,
|
||||
canvas.height / 2 + rotatedY,
|
||||
);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.globalAlpha = opacity;
|
||||
|
||||
if (icons || images) {
|
||||
// Only try to render icons/images if they exist
|
||||
if (
|
||||
iconCanvasesRef.current[index] &&
|
||||
imagesLoadedRef.current[index]
|
||||
) {
|
||||
ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40);
|
||||
}
|
||||
} else {
|
||||
// Show numbered circles if no icons/images are provided
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 20, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#4444ff";
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "white";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = "16px Arial";
|
||||
ctx.fillText(`${icon.id + 1}`, 0, 0);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [icons, images, iconPositions, isDragging, mousePos, targetRotation]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={400}
|
||||
height={400}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
className="rounded-full"
|
||||
aria-label="Interactive 3D Icon Cloud"
|
||||
role="img"
|
||||
/>
|
||||
);
|
||||
}
|
||||
35
components/ui/interactive-hover-button.tsx
Normal file
35
components/ui/interactive-hover-button.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface InteractiveHoverButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
|
||||
|
||||
export const InteractiveHoverButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
InteractiveHoverButtonProps
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-primary transition-all duration-300 group-hover:scale-[100.8]"></div>
|
||||
<span className="inline-block transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute top-0 z-10 flex h-full w-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100">
|
||||
<span>{children}</span>
|
||||
<ArrowRight />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
InteractiveHoverButton.displayName = "InteractiveHoverButton";
|
||||
64
components/ui/shine-border.tsx
Normal file
64
components/ui/shine-border.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Width of the border in pixels
|
||||
* @default 1
|
||||
*/
|
||||
borderWidth?: number;
|
||||
/**
|
||||
* Duration of the animation in seconds
|
||||
* @default 14
|
||||
*/
|
||||
duration?: number;
|
||||
/**
|
||||
* Color of the border, can be a single color or an array of colors
|
||||
* @default "#000000"
|
||||
*/
|
||||
shineColor?: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shine Border
|
||||
*
|
||||
* An animated background border effect component with configurable properties.
|
||||
*/
|
||||
export function ShineBorder({
|
||||
borderWidth = 1,
|
||||
duration = 14,
|
||||
shineColor = "#000000",
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: ShineBorderProps) {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--border-width": `${borderWidth}px`,
|
||||
"--duration": `${duration}s`,
|
||||
backgroundImage: `radial-gradient(transparent,transparent, ${
|
||||
Array.isArray(shineColor) ? shineColor.join(",") : shineColor
|
||||
},transparent,transparent)`,
|
||||
backgroundSize: "300% 300%",
|
||||
mask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||
WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||
WebkitMaskComposite: "xor",
|
||||
maskComposite: "exclude",
|
||||
padding: "var(--border-width)",
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position] motion-safe:animate-shine",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default ShineBorder;
|
||||
15
components/ui/skeleton.tsx
Normal file
15
components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -45,7 +45,7 @@ date : 15-04-2025
|
||||
</StepperItem>
|
||||
</Stepper>
|
||||
|
||||
## Tutorial Video
|
||||
## Video Tutorial
|
||||
|
||||
<Accordion title="API Sandbox" defaultOpen={true}>
|
||||
<Youtube videoId="KzD4FxFDKE0" />
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"framer-motion": "^12.4.1",
|
||||
"geist": "^1.3.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -50,6 +50,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
cmdk:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
framer-motion:
|
||||
specifier: ^12.4.1
|
||||
version: 12.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -1017,6 +1020,12 @@ packages:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cmdk@1.1.1:
|
||||
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
collapse-white-space@2.1.0:
|
||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||
|
||||
@@ -3723,6 +3732,18 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cmdk@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-dialog': 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
collapse-white-space@2.1.0: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
|
||||
Reference in New Issue
Block a user