initial commit
This commit is contained in:
31
components/homepage/HomeCover.tsx
Normal file
31
components/homepage/HomeCover.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { SocialIcons } from "@/components/utils/SocialIcons";
|
||||
import { Config } from "@/data/config";
|
||||
import { fontFzxbs, fontSypxzs } from "@/styles/font";
|
||||
|
||||
export const HomeCover = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="mb-24 mt-3 flex w-full justify-center rounded-xl"
|
||||
style={{
|
||||
aspectRatio: "4/1",
|
||||
background: `url(${Config.PageCovers.websiteCoverURL})`,
|
||||
backgroundSize: "cover",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className="relative top-1/2 my-auto h-24 w-24 rounded-full shadow-2xl md:h-32 md:w-32"
|
||||
alt={Config.Nickname}
|
||||
src={Config.AvatarURL}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${fontFzxbs.className} my-8 text-center text-4xl font-bold`}>{Config.Nickname}</div>
|
||||
<SocialIcons />
|
||||
{Config.Sentence && (
|
||||
<div className="my-8 flex justify-center">
|
||||
<p className={`${fontSypxzs.className} text-lg`}>{Config.Sentence}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
7
components/layouts/layouts.tsx
Normal file
7
components/layouts/layouts.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="flex min-h-screen flex-col justify-between">{children}</div>;
|
||||
};
|
||||
|
||||
export const ContentContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
return <main className="responsive-width">{children}</main>;
|
||||
};
|
||||
9
components/mdx/H2.tsx
Normal file
9
components/mdx/H2.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { fontFzxbs } from "@/styles/font";
|
||||
|
||||
export const H2 = (props: JSX.IntrinsicElements["h2"]) => {
|
||||
return (
|
||||
<h2 className={`${fontFzxbs.className} mt-3 mb-1 scroll-mt-20`} id={props.id}>
|
||||
{props.children}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
12
components/mdx/ImageWrapper.tsx
Normal file
12
components/mdx/ImageWrapper.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
// Unlike other mdx elements, it does not receive the converted img tag,
|
||||
// but all the attributes of the img tag.
|
||||
const ImageWrapper = (props: JSX.IntrinsicElements["img"]) => {
|
||||
return (
|
||||
<div className="flex flex-col my-5">
|
||||
<img alt={props.alt} className="mx-auto my-0" src={props.src} />
|
||||
<div className="mx-auto my-1 text-sm text-gray-500 dark:text-gray-300">{props.alt}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageWrapper;
|
||||
68
components/mdx/PreWrapper.tsx
Normal file
68
components/mdx/PreWrapper.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
const PreWrapper = ({ children }: { children: JSX.Element }) => {
|
||||
const textInput = useRef(null);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const onEnter = () => {
|
||||
setHovered(true);
|
||||
};
|
||||
const onExit = () => {
|
||||
setHovered(false);
|
||||
setCopied(false);
|
||||
};
|
||||
const onCopy = () => {
|
||||
setCopied(true);
|
||||
//@ts-ignore
|
||||
textInput.current && navigator.clipboard.writeText(textInput.current.textContent);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={textInput} onMouseEnter={onEnter} onMouseLeave={onExit} className="relative">
|
||||
{hovered && (
|
||||
<button
|
||||
aria-label="Copy code"
|
||||
className={`absolute right-2 top-2 h-8 w-8 rounded border-2 bg-gray-700 p-1 dark:bg-gray-800 ${
|
||||
copied ? "border-green-400 focus:border-green-400 focus:outline-none" : "border-gray-300"
|
||||
}`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
className={copied ? "text-green-400" : "text-gray-300"}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<pre>{children}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreWrapper;
|
||||
9
components/mdx/TableWrapper.tsx
Normal file
9
components/mdx/TableWrapper.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
const TableWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="w-full overflow-x-auto">
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableWrapper;
|
||||
11
components/mdx/index.ts
Normal file
11
components/mdx/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { H2 } from "./H2";
|
||||
import ImageWrapper from "./ImageWrapper";
|
||||
import PreWrapper from "./PreWrapper";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
export const MDXComponentsSet = {
|
||||
pre: PreWrapper,
|
||||
table: TableWrapper,
|
||||
img: ImageWrapper,
|
||||
h2: H2,
|
||||
};
|
||||
28
components/readerpage/PostComments.tsx
Normal file
28
components/readerpage/PostComments.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Config } from "@/data/config";
|
||||
import Giscus from "@giscus/react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export const PostComments = (props: { postId: string }) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
Config.Giscus && (
|
||||
<div className="my-5">
|
||||
<Giscus
|
||||
id={props.postId}
|
||||
repo={Config.Giscus.repo as `${string}/${string}`}
|
||||
repoId={Config.Giscus.repoId}
|
||||
category={Config.Giscus.category}
|
||||
categoryId={Config.Giscus.categoryId}
|
||||
mapping="pathname"
|
||||
term={props.postId}
|
||||
reactionsEnabled="1"
|
||||
emitMetadata="0"
|
||||
theme={theme === "light" ? "light_tritanopia" : "dark_tritanopia"}
|
||||
inputPosition="top"
|
||||
loading="eager"
|
||||
lang="en"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
14
components/readerpage/PostCover.tsx
Normal file
14
components/readerpage/PostCover.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export const PostCover = (props: { coverURL: string }) => {
|
||||
return (
|
||||
<div
|
||||
className="mb-8 mt-0 flex w-full justify-center rounded-xl"
|
||||
style={{
|
||||
aspectRatio: "5/2",
|
||||
background: `url(${props.coverURL})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
53
components/readerpage/ShareButtons.tsx
Normal file
53
components/readerpage/ShareButtons.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { Config } from "@/data/config";
|
||||
import { FacebookShareButton, LinkedinShareButton, RedditShareButton, TwitterShareButton } from "next-share";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { FaFacebook, FaLink, FaLinkedin, FaReddit, FaTwitter } from "react-icons/fa6";
|
||||
|
||||
export const ShareButtons = (props: {
|
||||
postId: string;
|
||||
allowShare?: boolean | null;
|
||||
subtitle?: string | null;
|
||||
title: string;
|
||||
quote?: string | null;
|
||||
}) => {
|
||||
const url = `https://${Config.SiteDomain}/blog/${props.postId}`;
|
||||
const copyShareText = `${props.title} ${props.subtitle ? `- ${props.subtitle}` : ""} - ${
|
||||
Config.Nickname
|
||||
}'s Blog ${url}`;
|
||||
const { toast } = useToast();
|
||||
return (
|
||||
<div className="my-3 flex space-x-4 text-2xl">
|
||||
{props.allowShare != false ? (
|
||||
<>
|
||||
<div className="my-auto text-sm font-bold">{"SHARE :"}</div>
|
||||
<FacebookShareButton className="mx-2" url={url} quote={props.quote ?? props.title}>
|
||||
<FaFacebook title="Share to Facebook" className="hover:text-blue-500" />
|
||||
</FacebookShareButton>
|
||||
<TwitterShareButton className="mx-2" url={url} title={props.title}>
|
||||
<FaTwitter title="Share to Twitter" className="hover:text-sky-500" />
|
||||
</TwitterShareButton>
|
||||
<LinkedinShareButton className="mx-2" url={url} title={props.title}>
|
||||
<FaLinkedin title="Share to Linkedin" className="hover:text-blue-500" />
|
||||
</LinkedinShareButton>
|
||||
<RedditShareButton className="mx-2" url={url} title={props.title}>
|
||||
<FaReddit title="Share to Reddit" className="hover:text-orange-500" />
|
||||
</RedditShareButton>
|
||||
<CopyToClipboard
|
||||
onCopy={() => {
|
||||
toast({ description: "Link is copied successfully" });
|
||||
}}
|
||||
text={copyShareText}
|
||||
>
|
||||
<FaLink
|
||||
title="Share with the post url and description"
|
||||
className="hover:text-gray-500 mx-2 cursor-pointer"
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</>
|
||||
) : (
|
||||
<div className="my-auto text-sm font-bold">{"SHARING IS NOT ALLOWED"}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
41
components/readerpage/SideTOC.tsx
Normal file
41
components/readerpage/SideTOC.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { TTOCItem } from "@/types/toc.type";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { FaHeading } from "react-icons/fa";
|
||||
|
||||
export const SideTOC = (props: { data: TTOCItem[] }) => {
|
||||
const [isTOCOpen, setIsTOCOpen] = useState(false);
|
||||
return (
|
||||
<Sheet open={isTOCOpen} onOpenChange={setIsTOCOpen}>
|
||||
<SheetTrigger
|
||||
title="Open the table of contents"
|
||||
className="bottom-7 right-4 fixed bg-white dark:bg-black border dark:border-gray-500 shadow-xl"
|
||||
>
|
||||
<FaHeading onClick={() => setIsTOCOpen(!isTOCOpen)} className="p-3 w-14 h-14" />
|
||||
</SheetTrigger>
|
||||
<SheetContent side={"left"}>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="mt-8 font-bold">{"TABLE OF CONTENTS"}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ul className="my-3 flat-scrollbar h-[70vh] flex flex-col overflow-y-auto">
|
||||
{props.data?.map((item) => (
|
||||
<Link
|
||||
className="hover:text-sky-500 border-t border-b py-2 border-dashed"
|
||||
onClick={() => {
|
||||
setIsTOCOpen(false);
|
||||
}}
|
||||
key={`flat-toc-${item.anchorId}`}
|
||||
href={`#${item.anchorId}`}
|
||||
>
|
||||
<li
|
||||
className="my-2 target:text-blue-500"
|
||||
style={{ paddingLeft: `${item.level - 2}em` }}
|
||||
>{`${item.title}`}</li>
|
||||
</Link>
|
||||
))}
|
||||
</ul>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
21
components/readerpage/TOC.tsx
Normal file
21
components/readerpage/TOC.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { TTOCItem } from "@/types/toc.type";
|
||||
import Link from "next/link";
|
||||
|
||||
export const TOC = (props: { data: TTOCItem[] }) => {
|
||||
return (
|
||||
<div className="sticky top-[5em] m-5 p-2 rounded-md border-2">
|
||||
<div className="p-2 text-center font-bold">{"TABLE OF CONTENTS"}</div>
|
||||
<hr />
|
||||
<ul className="flat-scrollbar my-1 px-1 h-[60vh] overflow-y-auto">
|
||||
{props.data?.map((item) => (
|
||||
<Link className="hover:text-sky-500" href={`#${item.anchorId}`} key={`toc-${item.anchorId}`}>
|
||||
<li
|
||||
className="my-2 text-sm target:text-blue-500"
|
||||
style={{ paddingLeft: `${item.level - 1}em` }}
|
||||
>{`${item.title}`}</li>
|
||||
</Link>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
components/ui/button.tsx
Normal file
46
components/ui/button.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center 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-10 w-10",
|
||||
},
|
||||
},
|
||||
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 };
|
||||
134
components/ui/command.tsx
Normal file
134
components/ui/command.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
97
components/ui/dialog.tsx
Normal file
97
components/ui/dialog.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "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-background/80 backdrop-blur-sm 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-4 top-4 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">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</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,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
22
components/ui/input.tsx
Normal file
22
components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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 };
|
||||
22
components/ui/separator.tsx
Normal file
22
components/ui/separator.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import * as React from "react";
|
||||
|
||||
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 };
|
||||
113
components/ui/sheet.tsx
Normal file
113
components/ui/sheet.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
//@ts-ignore
|
||||
const SheetPortal = ({ className, ...props }: SheetPrimitive.DialogPortalProps) => (
|
||||
//@ts-ignore
|
||||
<SheetPrimitive.Portal className={cn(className)} {...props} />
|
||||
);
|
||||
SheetPortal.displayName = SheetPrimitive.Portal.displayName;
|
||||
|
||||
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-background/80 backdrop-blur-sm 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-4 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,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
111
components/ui/toast.tsx
Normal file
111
components/ui/toast.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
Toast,
|
||||
ToastAction,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
type ToastActionElement,
|
||||
type ToastProps,
|
||||
};
|
||||
26
components/ui/toaster.tsx
Normal file
26
components/ui/toaster.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && <ToastDescription>{description}</ToastDescription>}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
187
components/ui/use-toast.ts
Normal file
187
components/ui/use-toast.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react";
|
||||
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { toast, useToast };
|
||||
75
components/utils/Footer.tsx
Normal file
75
components/utils/Footer.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { RSSFeedLink } from "@/consts/consts";
|
||||
import { Config } from "@/data/config";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
import { FaCheck, FaCopy } from "react-icons/fa";
|
||||
import { IoLogoRss } from "react-icons/io5";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export const Footer = () => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={() => {
|
||||
setIsCopied(false);
|
||||
}}
|
||||
>
|
||||
<footer className="my-5 flex flex-col justify-center py-2 text-sm">
|
||||
<div className="mx-auto px-3 text-center font-bold">{`COPYRIGHT © ${
|
||||
Config.YearStart
|
||||
}-${new Date().getFullYear()} ${Config.AuthorName} ALL RIGHTS RESERVED`}</div>
|
||||
<div className="my-3 flex flex-wrap justify-center space-x-3 text-center text-gray-500 underline dark:text-gray-400">
|
||||
<Link href="/sponsor" title="Sponsor me for my works.">
|
||||
{"Sponsor"}
|
||||
</Link>
|
||||
<Link href="/friends" title="My friend links.">
|
||||
{"Friends"}
|
||||
</Link>
|
||||
<DialogTrigger asChild>
|
||||
<button title="Subscribe the RSS Feed.">{"Feed"}</button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex">
|
||||
<IoLogoRss className="mr-2 my-auto" />
|
||||
{"RSS Feed"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<div className="w-full text-sm my-2">
|
||||
<div>
|
||||
<b>NOTE: </b>Some RSS Feed Reader may has deficient in rendering SVG formulations, graphs. Such as the
|
||||
Inoreader, Feedly. If it happens, please read the origin web page for better experience.
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="w-full flex my-3">
|
||||
<Input defaultValue={RSSFeedLink} readOnly />
|
||||
<CopyToClipboard
|
||||
onCopy={() => {
|
||||
setIsCopied(true);
|
||||
}}
|
||||
text={RSSFeedLink}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className={`ml-3 my-auto ${isCopied && "bg-green-500 hover:bg-green-500"}`}
|
||||
>
|
||||
<span className="sr-only">{"Copy"}</span>
|
||||
{isCopied ? <FaCheck className="h-4 w-4" /> : <FaCopy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</footer>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
103
components/utils/NavBar.tsx
Normal file
103
components/utils/NavBar.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Config } from "@/data/config";
|
||||
import { fontFzxbs } from "@/styles/font";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { MdMenu, MdOutlineDarkMode, MdOutlineLightMode } from "react-icons/md";
|
||||
|
||||
const MenuItems = [
|
||||
{
|
||||
title: "HOME",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
title: "TAGS",
|
||||
href: "/tags",
|
||||
},
|
||||
{
|
||||
title: "POSTS",
|
||||
href: "/posts",
|
||||
},
|
||||
{
|
||||
title: "ABOUT",
|
||||
href: "/about",
|
||||
},
|
||||
];
|
||||
|
||||
export const NavBar = () => {
|
||||
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const handleSwitchTheme = () => {
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
setIsSideNavOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isSideNavOpen} onOpenChange={(open) => setIsSideNavOpen(open)}>
|
||||
<nav className="responsive-width sticky top-0 z-50 flex justify-between bg-inherit py-3 backdrop-blur">
|
||||
<Link href="/" className="cursor-pointer">
|
||||
<h1
|
||||
className={`${fontFzxbs.className} my-auto border-b-4 border-b-black text-2xl font-bold dark:border-b-white`}
|
||||
>
|
||||
{Config.SiteTitle}
|
||||
</h1>
|
||||
</Link>
|
||||
<div className="my-auto hidden sm:flex">
|
||||
{MenuItems.map((menuItem) => (
|
||||
<Link href={menuItem.href} key={nanoid()} className="nav-link mx-2 my-auto px-2">
|
||||
{menuItem.title}
|
||||
</Link>
|
||||
))}
|
||||
<div
|
||||
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
||||
className="cursor-pointer mx-2 rounded-full p-1 text-3xl text-black hover:bg-gray-200 dark:text-gray-50 dark:hover:bg-gray-800"
|
||||
onClick={handleSwitchTheme}
|
||||
>
|
||||
{theme === "light" ? <MdOutlineDarkMode /> : <MdOutlineLightMode />}
|
||||
</div>
|
||||
</div>
|
||||
<SheetTrigger title="Spread the navigation menu" className="sm:hidden">
|
||||
<MdMenu
|
||||
className="my-auto text-3xl hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsSideNavOpen(!isSideNavOpen);
|
||||
}}
|
||||
/>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="bg:white border-none shadow-md dark:bg-black">
|
||||
<div className="my-5 flex flex-col">
|
||||
{MenuItems.map((menuItem) => (
|
||||
<Link
|
||||
href={menuItem.href}
|
||||
key={nanoid()}
|
||||
className="border-b border-dashed p-3 text-xl hover:text-sky-500"
|
||||
>
|
||||
{menuItem.title}
|
||||
</Link>
|
||||
))}
|
||||
<div
|
||||
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
||||
className="cursor-pointer m-2 rounded-full p-1 text-xl text-black dark:text-gray-50"
|
||||
onClick={handleSwitchTheme}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<div className="flex">
|
||||
{"DARK MODE"}
|
||||
<MdOutlineDarkMode className="mx-2 my-auto" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex">
|
||||
{"LIGHT MODE"}
|
||||
<MdOutlineLightMode className="mx-2 my-auto" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</nav>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
12
components/utils/PageCover.tsx
Normal file
12
components/utils/PageCover.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export const PageCover = (props: { coverURL: string }) => {
|
||||
return (
|
||||
<div
|
||||
className="my-5 mt-0 flex w-full justify-center rounded-xl"
|
||||
style={{
|
||||
aspectRatio: "4/1",
|
||||
background: `url(${props.coverURL})`,
|
||||
backgroundSize: "cover",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
44
components/utils/PostList.tsx
Normal file
44
components/utils/PostList.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { normalizeDate } from "@/lib/date";
|
||||
import { fontSypxzs } from "@/styles/font";
|
||||
import { TPostListItem } from "@/types/post-list";
|
||||
import { nanoid } from "nanoid";
|
||||
import Link from "next/link";
|
||||
|
||||
export const PostList = (props: { data: TPostListItem[] }) => {
|
||||
return (
|
||||
<div>
|
||||
{props.data.map((postListItem, index) => (
|
||||
<div
|
||||
key={`post-list-${nanoid()}`}
|
||||
className={`${fontSypxzs.className} flex flex-col justify-center ${
|
||||
index !== props.data.length - 1 && "border-b"
|
||||
} border-dashed border-gray-400 py-3`}
|
||||
>
|
||||
<Link className="hover:text-gray-600 dark:hover:text-gray-400" href={`/blog/${postListItem.id}`}>
|
||||
<div className="flex-center flex flex-col py-2 ">
|
||||
<h3 className="mx-auto text-xl font-extrabold capitalize">{postListItem.frontMatter.title}</h3>
|
||||
{postListItem.frontMatter.subtitle && (
|
||||
<div className="mx-auto text-base font-semibold capitalize">{postListItem.frontMatter.subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center">{normalizeDate(postListItem.frontMatter.time)}</div>
|
||||
{postListItem.frontMatter.summary && (
|
||||
<div className="flex my-1 justify-center">
|
||||
<p>{postListItem.frontMatter.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
{postListItem.frontMatter.tags && (
|
||||
<div className="my-2 flex justify-center">
|
||||
{postListItem.frontMatter.tags.map((tagName) => (
|
||||
<Link href={`/tags/${tagName}`} className="tag-link mx-1 text-sm" key={`tags-${nanoid()}`}>
|
||||
{tagName}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
35
components/utils/SEO.tsx
Normal file
35
components/utils/SEO.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { RSSFeedLink } from "@/consts/consts";
|
||||
import { Config } from "@/data/config";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
export const SEO = (props: { title: string; description?: string | null; coverURL?: string | null }) => {
|
||||
return (
|
||||
<>
|
||||
<title>{props.title}</title>
|
||||
<link rel="alternate" type="application/rss+xml" href={RSSFeedLink} />
|
||||
<NextSeo
|
||||
title={props.title}
|
||||
description={props.description ?? undefined}
|
||||
openGraph={{
|
||||
title: props.title,
|
||||
description: props.description ?? undefined,
|
||||
images: props.coverURL
|
||||
? [
|
||||
{
|
||||
url: props.coverURL,
|
||||
width: 850,
|
||||
height: 650,
|
||||
alt: props.title,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}}
|
||||
twitter={{
|
||||
handle: `@${Config.SocialLinks.twitter}`,
|
||||
site: Config.SiteDomain,
|
||||
cardType: "summary_large_image",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
46
components/utils/SocialIcons.tsx
Normal file
46
components/utils/SocialIcons.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Config } from "@/data/config";
|
||||
import Link from "next/link";
|
||||
import { FiGithub, FiInstagram, FiMail, FiTwitter } from "react-icons/fi";
|
||||
import { TbBrandFacebook, TbBrandLinkedin, TbBrandMastodon } from "react-icons/tb";
|
||||
|
||||
export const SocialIcons = () => {
|
||||
return (
|
||||
<div className="my-5 flex justify-center space-x-4 text-2xl font-bold">
|
||||
{Config.SocialLinks.twitter && (
|
||||
<Link target="_blank" href={`https://x.com/${Config.SocialLinks.twitter}`} title="Twitter">
|
||||
<FiTwitter className="hover:text-sky-500" />
|
||||
</Link>
|
||||
)}
|
||||
{Config.SocialLinks.mastodon && (
|
||||
<Link target="_blank" href={Config.SocialLinks.mastodon} title="Mastodon">
|
||||
<TbBrandMastodon className="hover:text-purple-500" />
|
||||
</Link>
|
||||
)}
|
||||
{Config.SocialLinks.instagram && (
|
||||
<Link target="_blank" href={`https://instagram.com/${Config.SocialLinks.instagram}`} title="Instagram">
|
||||
<FiInstagram className="hover:text-orange-500" />
|
||||
</Link>
|
||||
)}
|
||||
{Config.SocialLinks.facebook && (
|
||||
<Link target="_blank" href={`https://instagram.com/${Config.SocialLinks.facebook}`} title="Instagram">
|
||||
<TbBrandFacebook className="hover:text-blue-500" />
|
||||
</Link>
|
||||
)}
|
||||
{Config.SocialLinks.linkedin && (
|
||||
<Link target="_blank" href={`https://linkedin.com/in/${Config.SocialLinks.linkedin}`} title="Instagram">
|
||||
<TbBrandLinkedin className="hover:text-blue-500" />
|
||||
</Link>
|
||||
)}
|
||||
{Config.SocialLinks.github && (
|
||||
<Link target="_blank" href={`https://github.com/${Config.SocialLinks.github}`} title="Github">
|
||||
<FiGithub className="hover:text-gray-500" />
|
||||
</Link>
|
||||
)}
|
||||
{Config.SocialLinks.email && (
|
||||
<Link target="_blank" href={`mailto:${Config.SocialLinks.email}`} title="EMail Address">
|
||||
<FiMail className="hover:text-gray-500" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user