diff --git a/app/api/get-repo-blobs/route.ts b/app/api/get-repo-blobs/route.ts new file mode 100644 index 0000000..95660f1 --- /dev/null +++ b/app/api/get-repo-blobs/route.ts @@ -0,0 +1,56 @@ +import { octokit } from "@/components/github/octokit"; +import { getRepoBlobsSchema } from "@/components/github/schema"; + +import { z } from "zod"; + +const requestSchema = z.object({ + owner: z.string(), + repo: z.string(), + branch: z.string(), + paths: z.array(z.string()), +}); + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + const { owner, repo, branch, paths } = requestSchema.parse({ + owner: searchParams.get("owner"), + repo: searchParams.get("repo"), + branch: searchParams.get("branch"), + paths: searchParams.getAll("path"), + }); + + const { + data: { tree }, + } = await octokit.rest.git.getTree({ + owner, + repo, + tree_sha: branch, + recursive: "true", + }); + + let response: { path: string; contents: string }[] = []; + + const files = tree + .filter((i) => i.type === "blob") + .filter((i) => (i.path ? paths.includes(i.path) : false)); + + for (const file of files) { + const { + data: { content }, + } = await octokit.rest.git.getBlob({ + owner, + repo, + file_sha: file.sha!, + }); + + response.push({ + path: file.path!, + contents: Buffer.from(content, "base64").toString("ascii"), + }); + } + + const parsed = getRepoBlobsSchema.parse(response); + + return Response.json(parsed); +} diff --git a/app/api/get-repo-branches/route.ts b/app/api/get-repo-branches/route.ts new file mode 100644 index 0000000..a90bfaa --- /dev/null +++ b/app/api/get-repo-branches/route.ts @@ -0,0 +1,21 @@ +import { octokit } from "@/components/github/octokit"; +import { getRepoBranchesSchema } from "@/components/github/schema"; + +import { z } from "zod"; + +const requestSchema = z.object({ owner: z.string(), repo: z.string() }); + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + const { owner, repo } = requestSchema.parse({ + owner: searchParams.get("owner"), + repo: searchParams.get("repo"), + }); + + const { data } = await octokit.rest.repos.listBranches({ owner, repo }); + + const parsed = getRepoBranchesSchema.parse(data.map((i) => i.name)); + + return Response.json(parsed); +} diff --git a/app/api/get-repo-info/route.ts b/app/api/get-repo-info/route.ts new file mode 100644 index 0000000..511af1e --- /dev/null +++ b/app/api/get-repo-info/route.ts @@ -0,0 +1,21 @@ +import { octokit } from "@/components/github/octokit"; +import { getRepoInfoSchema } from "@/components/github/schema"; + +import { z } from "zod"; + +const requestSchema = z.object({ owner: z.string(), repo: z.string() }); + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + const { owner, repo } = requestSchema.parse({ + owner: searchParams.get("owner"), + repo: searchParams.get("repo"), + }); + + const { data } = await octokit.rest.repos.get({ owner, repo }); + + const parsed = getRepoInfoSchema.parse(data); + + return Response.json(parsed); +} diff --git a/app/api/get-repo-tree/route.ts b/app/api/get-repo-tree/route.ts new file mode 100644 index 0000000..092fb4b --- /dev/null +++ b/app/api/get-repo-tree/route.ts @@ -0,0 +1,40 @@ +import { octokit } from "@/components/github/octokit"; +import { getRepoTreeSchema } from "@/components/github/schema"; + +import { z } from "zod"; + +const requestSchema = z.object({ + owner: z.string(), + repo: z.string(), + branch: z.string(), +}); + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + const { owner, repo, branch } = requestSchema.parse({ + owner: searchParams.get("owner"), + repo: searchParams.get("repo"), + branch: searchParams.get("branch"), + }); + + const { + data: { default_branch }, + } = await octokit.rest.repos.get({ + owner, + repo, + }); + + const { + data: { tree }, + } = await octokit.rest.git.getTree({ + owner, + repo, + tree_sha: branch || default_branch, + recursive: "true", + }); + + const parsed = getRepoTreeSchema.parse(tree); + + return Response.json(parsed); +} diff --git a/app/import/page.tsx b/app/import/page.tsx new file mode 100644 index 0000000..5140ce2 --- /dev/null +++ b/app/import/page.tsx @@ -0,0 +1,10 @@ +import GitHub from "@/components/github"; + +export default function Page() { + return ( +
+

Import GitHub repository

+ +
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 3af092e..7c06c24 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -87,8 +87,12 @@ export default function Home() { ))} -

... enter a GitHub repo url:

- +

+ ... enter a{" "} + + GitHub repo url (click here) + {" "} +

... or generate from a prompt:

diff --git a/components/github/file-selection.css b/components/github/file-selection.css new file mode 100644 index 0000000..623875f --- /dev/null +++ b/components/github/file-selection.css @@ -0,0 +1,63 @@ +.checkbox { + font-size: 16px; + user-select: none; + padding: 20px; + box-sizing: content-box; +} + +.checkbox .tree, +.checkbox .tree-node, +.checkbox .tree-node-group { + list-style: none; + margin: 0; + padding: 0; +} + +.checkbox .tree-branch-wrapper, +.checkbox .tree-node__leaf { + outline: none; +} + +.checkbox .tree-node { + cursor: pointer; +} + +.checkbox .tree-node .name:hover { + background: rgba(0, 0, 0, 0.1); +} + +.checkbox .tree-node--focused .name { + background: rgba(0, 0, 0, 0.2); +} + +.checkbox .tree-node { + display: inline-block; +} + +.checkbox .tree-node > * { + display: inline; +} + +.checkbox .checkbox-icon { + margin: 0 5px; + vertical-align: middle; +} + +.checkbox button { + border: none; + background: transparent; + cursor: pointer; +} + +.checkbox .arrow { + margin-left: 5px; + vertical-align: middle; +} + +.checkbox .arrow--open { + transform: rotate(90deg); +} + +.checkbox .tree-leaf-list-item { + margin-left: 21px; +} \ No newline at end of file diff --git a/components/github/file-selection.tsx b/components/github/file-selection.tsx new file mode 100644 index 0000000..ddf037c --- /dev/null +++ b/components/github/file-selection.tsx @@ -0,0 +1,134 @@ +import React, { useMemo } from "react"; +import { FaSquare, FaCheckSquare, FaMinusSquare } from "react-icons/fa"; +import { IoMdArrowDropright } from "react-icons/io"; +import TreeView, { ITreeViewOnSelectProps } from "react-accessible-treeview"; +import clsx from "clsx"; +import { IconBaseProps } from "react-icons/lib"; +import "./file-selection.css"; +import { useRepoTree } from "./use-octokit"; + +function FileSelection({ + ownerrepo, + branch, + onSelect, +}: { + ownerrepo?: string; + branch?: string; + onSelect?: (props: ITreeViewOnSelectProps) => void; +}) { + const { data: repoData, isLoading } = useRepoTree(ownerrepo, branch); + + const data = useMemo(() => { + if (!!repoData) { + const tree = repoData; + + const root = { + id: 0, + children: tree + .filter((i) => !i.path?.includes("/")) + .map((i) => i.path!), + parent: null, + name: "", + }; + + const rest = tree.map((i) => ({ + id: i.path!, + name: i.path?.split("/").pop() || "", + children: tree + .filter((j) => i.path === j.path?.split("/").slice(0, -1).join("/")) + .map((k) => k.path!), + parent: + tree.find((j) => j.path === i.path?.split("/").slice(0, -2).join("/")) + ?.path || root.id, + })); + + return [root, ...rest]; + } + + return undefined; + }, [repoData]); + + if (isLoading || !data) return
Loading...
; + + return ( +
+
+ { + return ( +
+ {isBranch && } + { + handleSelect(e); + e.stopPropagation(); + }} + variant={ + isHalfSelected ? "some" : isSelected ? "all" : "none" + } + /> + {element.name} +
+ ); + }} + /> +
+
+ ); +} + +const ArrowIcon = ({ + isOpen, + className, +}: { + isOpen: boolean; + className?: string; +}) => { + const baseClass = "arrow"; + const classes = clsx( + baseClass, + { [`${baseClass}--closed`]: !isOpen }, + { [`${baseClass}--open`]: isOpen }, + className + ); + return ; +}; + +const CheckBoxIcon = ({ + variant, + ...rest +}: IconBaseProps & { variant: "all" | "none" | "some" | string }) => { + switch (variant) { + case "all": + return ; + case "none": + return ; + case "some": + return ; + default: + return null; + } +}; + +export default FileSelection; diff --git a/components/github/index.tsx b/components/github/index.tsx new file mode 100644 index 0000000..3f03619 --- /dev/null +++ b/components/github/index.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useState } from "react"; +import { RepoUrlForm } from "./repo-url-form"; +import { RepoSelectFiles } from "./repo-select-files"; +import { X } from "lucide-react"; +import { RepoBranchSelection } from "./repo-branch-selection"; +import { RepoWorkspaceName } from "./repo-workspace-name"; + +export default function GitHub() { + const [repo, setRepo] = useState(); + const [branch, setBranch] = useState(); + const [selected, setSelected] = useState([]); + + return ( + <> + {repo ? ( +
+

Currently selected repo:

+

+ {repo}{" "} + +

+
+ ) : ( + { + setRepo(repo); + setBranch(branch); + }} + /> + )} + {repo ? ( + <> + {branch ? ( +

+ You have selected {branch} branch.{" "} + +

+ ) : ( + setBranch(branch)} + /> + )} + {branch && selected.length === 0 ? ( + { + setSelected(paths); + }} + /> + ) : null} + + ) : null} + {selected.length > 0 && repo && branch ? ( +
+ {selected.join(", ")} selected.{" "} + + +
+ ) : null} + + ); +} diff --git a/components/github/octokit.ts b/components/github/octokit.ts new file mode 100644 index 0000000..62e4f90 --- /dev/null +++ b/components/github/octokit.ts @@ -0,0 +1,28 @@ +import { Octokit } from "octokit"; +import { throttling } from "@octokit/plugin-throttling"; +import { getGitHubToken } from "@/lib/env"; + +const MyOctokit = Octokit.plugin(throttling); + +export const octokit = new MyOctokit({ + auth: getGitHubToken(), + throttle: { + onRateLimit: (retryAfter, options, octokit, retryCount) => { + octokit.log.warn( + `Request quota exhausted for request ${options.method} ${options.url}` + ); + + if (retryCount < 1) { + // only retries once + octokit.log.info(`Retrying after ${retryAfter} seconds!`); + return true; + } + }, + onSecondaryRateLimit: (_retryAfter, options, octokit) => { + // does not retry, only logs a warning + octokit.log.warn( + `SecondaryRateLimit detected for request ${options.method} ${options.url}` + ); + }, + }, +}); diff --git a/components/github/repo-branch-selection.tsx b/components/github/repo-branch-selection.tsx new file mode 100644 index 0000000..0f328c8 --- /dev/null +++ b/components/github/repo-branch-selection.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { useRepoBranch, useRepoInfo } from "./use-octokit"; +import { useEffect, useMemo } from "react"; + +const FormSchema = z.object({ + branch: z.string({ + required_error: "Please select a branch.", + }), +}); + +export function RepoBranchSelection({ + repo, + onSubmit: _onSubmit, +}: { + repo: string; + onSubmit?: (val: { branch?: string }) => void; +}) { + const { data } = useRepoBranch(repo); + const { data: info } = useRepoInfo(repo); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + }); + + function onSubmit(data: z.infer) { + _onSubmit?.(data); + } + + const branches = useMemo(() => { + if (!data) return []; + + return data.map((i) => ({ label: i, value: i })); + }, [data]); + + useEffect(() => { + if (!!info) { + form.setValue("branch", info.default_branch); + } + }, [info]); + + return ( +
+ + ( + + Branches + + + + + + + + + + + No branch found. + + {branches.map((branch) => ( + { + form.setValue("branch", branch.value); + }} + > + + {branch.label} + + ))} + + + + + + Choose a branch. + + + )} + /> + + + + ); +} diff --git a/components/github/repo-select-files.tsx b/components/github/repo-select-files.tsx new file mode 100644 index 0000000..8d4460f --- /dev/null +++ b/components/github/repo-select-files.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Loader2 } from "lucide-react"; +import FileSelection from "./file-selection"; + +const FormSchema = z.object({ + paths: z.array(z.string()).min(1, "At least 1 file should be selected!"), +}); + +export function RepoSelectFiles({ + repo, + branch, + onSubmit: _onSubmit, +}: { + repo?: string; + branch?: string; + onSubmit?: (val: string[]) => Promise; +}) { + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + paths: [], + }, + }); + + async function onSubmit(data: z.infer) { + form.clearErrors(); + if (!!_onSubmit) { + await _onSubmit(data.paths); + } + } + + return ( +
+ + ( + + Select files + + + field.onChange(Array.from(treeState.selectedIds)) + } + /> + + Choose files to import + + + + )} + /> + + + ); +} diff --git a/components/github/repo-url-form.tsx b/components/github/repo-url-form.tsx new file mode 100644 index 0000000..2a92889 --- /dev/null +++ b/components/github/repo-url-form.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Loader2 } from "lucide-react"; +import gh from "parse-github-url"; + +const FormSchema = z.object({ + url: z.string().refine((arg) => { + const { repo } = gh(arg) || {}; + + return !!repo; + }, "Is not a valid github url"), +}); + +export function RepoUrlForm({ + onSubmit: _onSubmit, +}: { + onSubmit?: (val: { repo?: string; branch?: string }) => Promise; +}) { + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + url: "https://github.com/AElfProject/aelf-developer-tools", + }, + }); + + async function onSubmit(data: z.infer) { + form.clearErrors(); + if (!!_onSubmit) { + const { repo, branch } = gh(data.url) || {}; + await _onSubmit({ repo: repo || undefined, branch: branch || undefined }); + } + } + + return ( +
+ + ( + + + + + + + )} + /> + + + + ); +} diff --git a/components/github/repo-workspace-name.tsx b/components/github/repo-workspace-name.tsx new file mode 100644 index 0000000..b3d3f42 --- /dev/null +++ b/components/github/repo-workspace-name.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { useRouter } from "next/navigation"; +import { db } from "@/data/db"; +import { Loader2 } from "lucide-react"; +import { getRepoBlobsSchema } from "./schema"; + +const FormSchema = z.object({ + name: z.string().min(2, { + message: "Name must be at least 2 characters.", + }), + template: z.string(), +}); + +function formatError(err: unknown) { + const strErr = String(err); + + if (strErr.includes("Key already exists in the object store")) { + return "This workspace name already in use."; + } + + return strErr; +} + +export function RepoWorkspaceName({ + repo, + branch, + paths, +}: { + repo: string; + branch: string; + paths: string[]; +}) { + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: "", + template: `github.com/${repo}/tree/${branch}`, + }, + }); + + const router = useRouter(); + + async function onSubmit(data: z.infer) { + form.clearErrors(); + try { + await db.workspaces.add({ + name: data.name, + template: data.template, + dll: "", + }); + + const [owner, repoName] = repo.split("/"); + + const params = new URLSearchParams(); + + params.set("owner", owner); + params.set("repo", repoName); + params.set("branch", branch); + paths.forEach((path) => params.append("path", path)); + + const filesRes = await fetch(`/api/get-repo-blobs?${params.toString()}`); + const fileData = await filesRes.json(); + + const parsedData = getRepoBlobsSchema.parse(fileData); + + await db.files.bulkAdd( + parsedData.map(({ path, contents }) => ({ + path: `/workspace/${data.name}/${encodeURIComponent(path)}`, + contents, + })) + ); + await router.push(`/workspace/${data.name}`); + } catch (err) { + form.setError("name", { message: formatError(err) }); + } + } + + return ( +
+ + ( + + Name + + + + This is your workspace name. + + + )} + /> + + + + ); +} diff --git a/components/github/schema.ts b/components/github/schema.ts new file mode 100644 index 0000000..568933f --- /dev/null +++ b/components/github/schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +export const getRepoInfoSchema = z.object({ default_branch: z.string() }); + +export const getRepoBranchesSchema = z.array(z.string()); + +export const getRepoBlobSchema = z.object({ content: z.string() }); + +export const getRepoTreeSchema = z.array( + z.object({ + path: z.string().optional(), + mode: z.string().optional(), + type: z.string().optional(), + sha: z.string().optional(), + size: z.number().optional(), + url: z.string().optional(), + }) +); + +export const getRepoBlobsSchema = z.array( + z.object({ path: z.string(), contents: z.string() }) +); diff --git a/components/github/use-octokit.ts b/components/github/use-octokit.ts new file mode 100644 index 0000000..18285f6 --- /dev/null +++ b/components/github/use-octokit.ts @@ -0,0 +1,53 @@ +"use client"; +import useSWR from "swr"; +import { + getRepoBranchesSchema, + getRepoInfoSchema, + getRepoTreeSchema, +} from "./schema"; + +export function useRepoTree(ownerrepo?: string, branch?: string) { + return useSWR( + ownerrepo ? `repo-tree-${ownerrepo}-${branch}` : undefined, + async () => { + const [owner, repo] = ownerrepo!.split("/"); + + const res = await fetch( + `/api/get-repo-tree?owner=${owner}&repo=${repo}&branch=${branch}` + ); + + const data = await res.json(); + + return getRepoTreeSchema.parse(data); + } + ); +} + +export function useRepoBranch(ownerrepo?: string) { + return useSWR( + ownerrepo ? `repo-branch-${ownerrepo}` : undefined, + async () => { + const [owner, repo] = ownerrepo!.split("/"); + + const res = await fetch( + `/api/get-repo-branches?owner=${owner}&repo=${repo}` + ); + + const data = await res.json(); + + return getRepoBranchesSchema.parse(data); + } + ); +} + +export function useRepoInfo(ownerrepo?: string) { + return useSWR(ownerrepo ? `repo-info-${ownerrepo}` : undefined, async () => { + const [owner, repo] = ownerrepo!.split("/"); + + const res = await fetch(`/api/get-repo-info?owner=${owner}&repo=${repo}`); + + const data = await res.json(); + + return getRepoInfoSchema.parse(data); + }); +} diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000..1a37e67 --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx new file mode 100644 index 0000000..a0ec48b --- /dev/null +++ b/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"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 PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx new file mode 100644 index 0000000..521b94b --- /dev/null +++ b/components/ui/toast.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +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, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx new file mode 100644 index 0000000..e223385 --- /dev/null +++ b/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"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 ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/components/ui/use-toast.ts b/components/ui/use-toast.ts new file mode 100644 index 0000000..02e111d --- /dev/null +++ b/components/ui/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// 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 + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +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 + +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(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 { useToast, toast } diff --git a/package-lock.json b/package-lock.json index 2843c0a..d8f5cb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,11 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@replit/codemirror-lang-csharp": "^6.2.0", @@ -32,6 +34,7 @@ "aelf-sdk": "^3.4.14-beta.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "date-fns": "^3.6.0", "dexie": "^4.0.8", "dexie-react-hooks": "^1.1.7", @@ -43,9 +46,12 @@ "next-mdx-remote": "^5.0.0", "next-themes": "^0.3.0", "octokit": "^4.0.2", + "parse-github-url": "^1.0.3", "react": "^18", + "react-accessible-treeview": "^2.9.1", "react-dom": "^18", "react-hook-form": "^7.52.1", + "react-icons": "^5.2.1", "react-resizable-panels": "^2.0.20", "react-terminal": "^1.4.4", "reflect-metadata": "^0.2.2", @@ -63,6 +69,7 @@ }, "devDependencies": { "@types/node": "^20", + "@types/parse-github-url": "^1.0.3", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^10.0.0", @@ -1758,6 +1765,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", + "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", @@ -1984,6 +2028,40 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz", + "integrity": "sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", @@ -2399,6 +2477,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-github-url": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/parse-github-url/-/parse-github-url-1.0.3.tgz", + "integrity": "sha512-7sTbCVmSVzK/iAsHGIxoqiyAnqix9opZm68lOvaU6DBx9EQ9kHMSp0y7Criu2OCsZ9wDllEyCRU+LU4hPRxXUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -3480,6 +3568,13 @@ "node": ">=6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT", + "peer": true + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3495,6 +3590,384 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/codemirror": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", @@ -7593,6 +8066,18 @@ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", "license": "MIT" }, + "node_modules/parse-github-url": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz", + "integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==", + "license": "MIT", + "bin": { + "parse-github-url": "cli.js" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse-numeric-range": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", @@ -7973,6 +8458,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-accessible-treeview": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/react-accessible-treeview/-/react-accessible-treeview-2.9.1.tgz", + "integrity": "sha512-UlmeFJtlh1SryN8kHLf4ICLlF4HeIpiacWwHh+7AzhmOmOs9vObXAjJWTiLpxP342TQ1QbCdSGq086Y8oD7NSw==", + "license": "MIT", + "peerDependencies": { + "classnames": "^2.2.6", + "prop-types": "^15.7.2", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-device-detect": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.1.2.tgz", @@ -8015,6 +8512,15 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 749246d..c1827dc 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@replit/codemirror-lang-csharp": "^6.2.0", @@ -33,6 +35,7 @@ "aelf-sdk": "^3.4.14-beta.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "date-fns": "^3.6.0", "dexie": "^4.0.8", "dexie-react-hooks": "^1.1.7", @@ -44,9 +47,12 @@ "next-mdx-remote": "^5.0.0", "next-themes": "^0.3.0", "octokit": "^4.0.2", + "parse-github-url": "^1.0.3", "react": "^18", + "react-accessible-treeview": "^2.9.1", "react-dom": "^18", "react-hook-form": "^7.52.1", + "react-icons": "^5.2.1", "react-resizable-panels": "^2.0.20", "react-terminal": "^1.4.4", "reflect-metadata": "^0.2.2", @@ -64,6 +70,7 @@ }, "devDependencies": { "@types/node": "^20", + "@types/parse-github-url": "^1.0.3", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^10.0.0",