initial to gitea

This commit is contained in:
2025-02-23 10:43:08 +07:00
commit d6e3946296
183 changed files with 22627 additions and 0 deletions

38
components/anchor.tsx Normal file
View 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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
"use client";
import { useEffect, useState } from "react";
import { cn, formatDate2 } from "@/lib/utils";
import { History } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
interface VersionTocProps {
versions: Array<{
version: string;
date: string;
}>;
}
export function VersionToc({ versions }: VersionTocProps) {
const [activeId, setActiveId] = useState<string | null>(null);
useEffect(() => {
// Handle initial hash
const hash = window.location.hash.slice(1);
if (hash) {
setActiveId(hash);
}
// Set up intersection observer
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.id;
setActiveId(id);
// Use pushState instead of replaceState to maintain history
window.history.pushState(null, '', `#${id}`);
}
});
},
{
threshold: 0.2,
rootMargin: '-20% 0px -60% 0px'
}
);
// Observe version elements
versions.forEach(({ version }) => {
const element = document.getElementById(`v${version}`);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, [versions]);
return (
<aside className="lg:flex hidden toc flex-[1.5] min-w-[238px] pt-8 sticky top-16 h-[calc(100vh-4rem)]">
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center gap-2 mb-2">
<History className="w-4 h-4" />
<h3 className="font-medium text-sm">Version History</h3>
</div>
<ScrollArea className="h-full">
<div className="flex flex-col gap-1.5 text-sm dark:text-stone-300/85 text-stone-800 pr-4">
{versions.map(({ version, date }) => (
<a
key={version}
href={`#v${version}`}
className={cn(
"hover:text-foreground transition-colors py-1",
activeId === `v${version}` && "font-medium text-primary"
)}
onClick={(e) => {
e.preventDefault();
const element = document.getElementById(`v${version}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
setActiveId(`v${version}`);
window.history.pushState(null, '', `#v${version}`);
}
}}
>
v{version}
<span className="text-xs text-muted-foreground ml-2">
{formatDate2(date)}
</span>
</a>
))}
</div>
</ScrollArea>
</div>
</aside>
);
}

View File

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

View File

@@ -0,0 +1,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
View File

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

View File

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

57
components/footer.tsx Normal file
View 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
View 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>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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;

View File

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

38
components/mob-toc.tsx Normal file
View File

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

99
components/navbar.tsx Normal file
View 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
View File

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

View File

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

144
components/search.tsx Normal file
View 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
View File

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

48
components/terminal.tsx Normal file
View 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">&gt; 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>
);
}

View 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>
);
}

View File

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

21
components/toc.tsx Normal file
View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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

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

119
components/ui/terminal.tsx Normal file
View 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>
);
};