diff --git a/components/workspace/file-explorer-top-menu.tsx b/components/workspace/file-explorer-top-menu.tsx
new file mode 100644
index 0000000..00c832e
--- /dev/null
+++ b/components/workspace/file-explorer-top-menu.tsx
@@ -0,0 +1,9 @@
+import NewFile from "./new-file";
+
+export function FileExplorerTopMenu() {
+ return (
+
+
+
+ );
+}
diff --git a/components/workspace/file-explorer.tsx b/components/workspace/file-explorer.tsx
index 89377bc..9ce5ef0 100644
--- a/components/workspace/file-explorer.tsx
+++ b/components/workspace/file-explorer.tsx
@@ -12,6 +12,7 @@ import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import useSWR, { mutate } from "swr";
import { FileIcon } from "./file-icon";
+import { FileExplorerTopMenu } from "./file-explorer-top-menu";
type TOCProps = {
toc: TreeViewElement[];
@@ -116,7 +117,12 @@ const FileExplorer = () => {
if (!toc) return Loading...
;
- return ;
+ return (
+ <>
+
+
+ >
+ );
};
/**
diff --git a/components/workspace/new-file-form.tsx b/components/workspace/new-file-form.tsx
new file mode 100644
index 0000000..e21e911
--- /dev/null
+++ b/components/workspace/new-file-form.tsx
@@ -0,0 +1,91 @@
+"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 { usePathname } from "next/navigation";
+import { db } from "@/data/db";
+import { Loader2 } from "lucide-react";
+import { useRefreshFileExplorer } from "./file-explorer";
+
+const FormSchema = z.object({
+ path: z.string(),
+});
+
+export function NewFileForm({
+ onSubmit,
+}: {
+ onSubmit?: (path: string) => void;
+}) {
+ const pathname = usePathname();
+ const refreshFileExplorer = useRefreshFileExplorer();
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ path: "",
+ },
+ });
+
+ async function _onSubmit(data: z.infer) {
+ form.clearErrors();
+ try {
+ await db.files.add({
+ path: `${pathname}/${encodeURIComponent(data.path)}`,
+ contents: "",
+ });
+ await refreshFileExplorer();
+ onSubmit?.(data.path);
+ } catch (err) {
+ form.setError("path", { message: String(err) });
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/components/workspace/new-file.tsx b/components/workspace/new-file.tsx
new file mode 100644
index 0000000..5d87578
--- /dev/null
+++ b/components/workspace/new-file.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+
+import { useState } from "react";
+import { FilePlus } from "lucide-react";
+import { NewFileForm } from "./new-file-form";
+
+export default function NewFile() {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+ );
+}