initial docs
This commit is contained in:
47
components/markdown/accordion.tsx
Normal file
47
components/markdown/accordion.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type AccordionProps = {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Accordion = ({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
className,
|
||||
}: AccordionProps) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className={cn("border rounded-lg overflow-hidden", className)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center my-auto space-x-2 space-y-2 w-full px-4 h-12 transition-colors bg-background dark:hover:bg-muted/50 hover:bg-muted/15"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
||||
isOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
<h3 className="font-medium text-base text-foreground pb-2">{title}</h3>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="px-4 py-3 border-t dark:bg-muted/50 bg-muted/15">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accordion;
|
||||
52
components/markdown/button.tsx
Normal file
52
components/markdown/button.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import * as Icons from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
type ButtonProps = {
|
||||
icon?: keyof typeof Icons;
|
||||
text?: string;
|
||||
href: string;
|
||||
target?: "_blank" | "_self" | "_parent" | "_top";
|
||||
size?: "sm" | "md" | "lg";
|
||||
variation?: "primary" | "accent" | "outline";
|
||||
};
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
icon,
|
||||
text,
|
||||
href,
|
||||
target,
|
||||
size = "md",
|
||||
variation = "primary",
|
||||
}) => {
|
||||
const baseStyles = "inline-flex items-center justify-center rounded font-medium focus:outline-none transition no-underline";
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "px-3 py-1 my-6 text-sm",
|
||||
md: "px-4 py-2 my-6 text-base",
|
||||
lg: "px-5 py-3 my-6 text-lg",
|
||||
};
|
||||
|
||||
const variationStyles = {
|
||||
primary: "bg-primary text-white hover:bg-primary/90",
|
||||
accent: "bg-accent text-white hover:bg-accent/90",
|
||||
outline: "border border-accent text-accent hover:bg-accent/10",
|
||||
};
|
||||
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null; // Tipe eksplisit sebagai React.FC
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
target={target}
|
||||
rel={target === "_blank" ? "noopener noreferrer" : undefined}
|
||||
className={`${baseStyles} ${sizeStyles[size]} ${variationStyles[variation]}`}
|
||||
>
|
||||
{text && <span>{text}</span>}
|
||||
{Icon && <Icon className="mr-2 h-5 w-5" />}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
41
components/markdown/card.tsx
Normal file
41
components/markdown/card.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import * as Icons from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
|
||||
interface CardProps {
|
||||
title: string;
|
||||
icon?: IconName;
|
||||
href?: string;
|
||||
horizontal?: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Card: React.FC<CardProps> = ({ title, icon, href, horizontal, children, className }) => {
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={clsx(
|
||||
"border rounded-lg shadow-sm p-4 transition-all duration-200 bg-white dark:bg-gray-900",
|
||||
"hover:bg-gray-50 dark:hover:bg-gray-800",
|
||||
"flex gap-2",
|
||||
horizontal ? "flex-row items-center gap-1" : "flex-col space-y-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className="w-5 h-5 text-primary flex-shrink-0" />}
|
||||
<div className="flex-1 min-w-0 my-auto h-full">
|
||||
<span className="text-base font-semibold">{title}</span>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 -mt-3">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return href ? <Link className="no-underline block" href={href}>{content}</Link> : content;
|
||||
};
|
||||
|
||||
export default Card;
|
||||
28
components/markdown/cardgroup.tsx
Normal file
28
components/markdown/cardgroup.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface CardGroupProps {
|
||||
children: ReactNode;
|
||||
cols?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => {
|
||||
const cardsArray = React.Children.toArray(children); // Pastikan children berupa array
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"grid gap-4",
|
||||
`grid-cols-1 sm:grid-cols-${cols}`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{cardsArray.map((card, index) => (
|
||||
<div key={index}>{card}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardGroup;
|
||||
33
components/markdown/copy.tsx
Normal file
33
components/markdown/copy.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Copy({ content }: { content: string }) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
async function handleCopy() {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setIsCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="border"
|
||||
size="xs"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckIcon className="w-3 h-3" />
|
||||
) : (
|
||||
<CopyIcon className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
25
components/markdown/image.tsx
Normal file
25
components/markdown/image.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ComponentProps } from "react";
|
||||
import NextImage from "next/image";
|
||||
|
||||
type Height = ComponentProps<typeof NextImage>["height"];
|
||||
type Width = ComponentProps<typeof NextImage>["width"];
|
||||
|
||||
export default function Image({
|
||||
src,
|
||||
alt = "alt",
|
||||
width = 800,
|
||||
height = 350,
|
||||
...props
|
||||
}: ComponentProps<"img">) {
|
||||
if (!src) return null;
|
||||
return (
|
||||
<NextImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width as Width}
|
||||
height={height as Height}
|
||||
quality={40}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
14
components/markdown/link.tsx
Normal file
14
components/markdown/link.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import NextLink from "next/link";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
export default function Link({ href, ...props }: ComponentProps<"a">) {
|
||||
if (!href) return null;
|
||||
return (
|
||||
<NextLink
|
||||
href={href}
|
||||
{...props}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
);
|
||||
}
|
||||
52
components/markdown/note.tsx
Normal file
52
components/markdown/note.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { PropsWithChildren } from "react";
|
||||
import {
|
||||
Info,
|
||||
AlertTriangle,
|
||||
ShieldAlert,
|
||||
CheckCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
type NoteProps = PropsWithChildren & {
|
||||
title?: string;
|
||||
type?: "note" | "danger" | "warning" | "success";
|
||||
};
|
||||
|
||||
const iconMap = {
|
||||
note: <Info size={16} className="text-blue-500" />,
|
||||
danger: <ShieldAlert size={16} className="text-red-500" />,
|
||||
warning: <AlertTriangle size={16} className="text-orange-500" />,
|
||||
success: <CheckCircle size={16} className="text-green-500" />,
|
||||
};
|
||||
|
||||
export default function Note({
|
||||
children,
|
||||
title = "Note",
|
||||
type = "note",
|
||||
}: NoteProps) {
|
||||
const noteClassNames = clsx({
|
||||
"dark:bg-stone-950/25 bg-stone-50": type === "note",
|
||||
"dark:bg-red-950 bg-red-100 border-red-200 dark:border-red-900":
|
||||
type === "danger",
|
||||
"dark:bg-orange-950 bg-orange-100 border-orange-200 dark:border-orange-900":
|
||||
type === "warning",
|
||||
"dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900":
|
||||
type === "success",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border rounded-md px-5 pb-0.5 mt-5 mb-7 text-sm tracking-wide",
|
||||
noteClassNames
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 font-bold -mb-2.5 pt-6">
|
||||
{iconMap[type]}
|
||||
<span className="text-base">{title}:</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
components/markdown/outlet.tsx
Normal file
29
components/markdown/outlet.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { BaseMdxFrontmatter, getAllChilds } from "@/lib/markdown";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Outlet({ path }: { path: string }) {
|
||||
if (!path) throw new Error("path not provided");
|
||||
const output = await getAllChilds(path);
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-2 gap-5">
|
||||
{output.map((child) => (
|
||||
<ChildCard {...child} key={child.title} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ChildCardProps = BaseMdxFrontmatter & { href: string };
|
||||
|
||||
function ChildCard({ description, href, title }: ChildCardProps) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="border rounded-md p-4 no-underline flex flex-col gap-0.5"
|
||||
>
|
||||
<h4 className="!my-0">{title}</h4>
|
||||
<p className="text-sm text-muted-foreground !my-0">{description}</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
19
components/markdown/pre.tsx
Normal file
19
components/markdown/pre.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ComponentProps } from "react";
|
||||
import Copy from "./copy";
|
||||
|
||||
export default function Pre({
|
||||
children,
|
||||
raw,
|
||||
...rest
|
||||
}: ComponentProps<"pre"> & { raw?: string }) {
|
||||
return (
|
||||
<div className="my-5 relative">
|
||||
<div className="absolute top-3 right-2.5 z-10 sm:block hidden">
|
||||
<Copy content={raw!} />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<pre {...rest}>{children}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
components/markdown/stepper.tsx
Normal file
41
components/markdown/stepper.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { Children, PropsWithChildren } from "react";
|
||||
|
||||
export function Stepper({ children }: PropsWithChildren) {
|
||||
const length = Children.count(children);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{Children.map(children, (child, index) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-l pl-9 ml-3 relative",
|
||||
clsx({
|
||||
"pb-5 ": index < length - 1,
|
||||
})
|
||||
)}
|
||||
>
|
||||
<div className="bg-muted w-8 h-8 text-xs font-medium rounded-md border flex items-center justify-center absolute -left-4 font-code">
|
||||
{index + 1}
|
||||
</div>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StepperItem({
|
||||
children,
|
||||
title,
|
||||
}: PropsWithChildren & { title?: string }) {
|
||||
return (
|
||||
<div className="pt-0.5">
|
||||
<h4 className="mt-0">{title}</h4>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
components/markdown/tooltips.tsx
Normal file
28
components/markdown/tooltips.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface TooltipProps {
|
||||
text: string;
|
||||
tip: string;
|
||||
}
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({ text, tip }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="relative inline-block cursor-pointer underline decoration-dotted text-blue-500"
|
||||
onMouseEnter={() => setVisible(true)}
|
||||
onMouseLeave={() => setVisible(false)}
|
||||
>
|
||||
{text}
|
||||
{visible && (
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-max max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl bg-background text-foreground text-sm p-2 rounded shadow-md break-words text-center outline outline-1 outline-offset-2">
|
||||
{tip}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
22
components/markdown/youtube.tsx
Normal file
22
components/markdown/youtube.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
interface YoutubeProps {
|
||||
videoId: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Youtube: React.FC<YoutubeProps> = ({ videoId, className }) => {
|
||||
return (
|
||||
<div className={`youtube ${className || ""}`}>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1&showinfo=0&autohide=1&controls=1`}
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Youtube;
|
||||
Reference in New Issue
Block a user