diff --git a/components/build-deploy-panel.tsx b/components/build-deploy-panel.tsx index 9841ea2..f188083 100644 --- a/components/build-deploy-panel.tsx +++ b/components/build-deploy-panel.tsx @@ -6,7 +6,8 @@ import { useCliCommands } from "./workspace/use-cli-commands"; import useSWR, { mutate } from "swr"; import { db } from "@/data/db"; import { useWorkspaceId } from "./workspace/use-workspace-id"; -import { Download, Rocket, ShieldCheck, Wrench } from "lucide-react"; +import { Download, Rocket, ShieldCheck, Wrench, Upload } from "lucide-react"; +import UploadModal from "./workspace/upload-modal"; export function BuildDeployPanel() { const commands = useCliCommands(); @@ -99,6 +100,7 @@ export function BuildDeployPanel() { ))} + ); } diff --git a/components/webcontainer/load-files.tsx b/components/webcontainer/load-files.tsx index ad02617..1192888 100644 --- a/components/webcontainer/load-files.tsx +++ b/components/webcontainer/load-files.tsx @@ -1,56 +1,10 @@ -import { useCallback, useEffect } from "react"; -import { useWebContainer } from "./use-web-container"; -import { useWorkspaceId } from "../workspace/use-workspace-id"; -import { usePathname } from "next/navigation"; -import { db } from "@/data/db"; -import { DirectoryNode, FileSystemTree } from "@webcontainer/api"; +"use client"; -export function LoadFiles() { - const id = useWorkspaceId(); - const pathname = usePathname(); - - const webContainer = useWebContainer(); - - const loadFiles = useCallback(async () => { - if (typeof id !== "string") throw new Error("id is not string"); - const start = `${pathname}/`; - const files = ( - await db.files.filter((file) => file.path.startsWith(start)).toArray() - ).map((file) => ({ - path: decodeURIComponent(file.path.replace(start, "")), - contents: file.contents, - })); - - const root: FileSystemTree = {}; +import { useEffect } from "react"; +import { useLoadFiles } from "./use-load-files"; - files.forEach((file) => { - const parts = file.path.split("/"); - let current = root; - - parts.forEach((part, index) => { - if (index === parts.length - 1) { - // Last part, assign file content under the "file" key - current[part] = { - file: { - contents: file.contents, - }, - }; - } else { - // Traverse or create sub-directory under "directory" key - if (!current[part]) { - current[part] = { - directory: {}, - }; - } - current = (current[part] as DirectoryNode).directory; - } - }); - }); - - if (!!webContainer) { - webContainer.mount(root); - } - }, [id, pathname]); +export function LoadFiles() { + const loadFiles = useLoadFiles(); useEffect(() => { loadFiles(); diff --git a/components/webcontainer/use-load-files.ts b/components/webcontainer/use-load-files.ts new file mode 100644 index 0000000..511c6ba --- /dev/null +++ b/components/webcontainer/use-load-files.ts @@ -0,0 +1,62 @@ +"use client"; + +import { useCallback } from "react"; +import { useWebContainer } from "./use-web-container"; +import { useWorkspaceId } from "../workspace/use-workspace-id"; +import { usePathname } from "next/navigation"; +import { db } from "@/data/db"; +import { DirectoryNode, FileSystemTree } from "@webcontainer/api"; + +export function useLoadFiles() { + const id = useWorkspaceId(); + const pathname = usePathname(); + + const webContainer = useWebContainer(); + + const loadFiles = useCallback(async () => { + if (typeof id !== "string") throw new Error("id is not string"); + const start = `${pathname}/`; + const files = ( + await db.files.filter((file) => file.path.startsWith(start)).toArray() + ).map((file) => ({ + path: decodeURIComponent(file.path.replace(start, "")), + contents: file.contents, + })); + + const root: FileSystemTree = {}; + + files.forEach((file) => { + const parts = file.path.split("/"); + let current = root; + + parts.forEach((part, index) => { + if (index === parts.length - 1) { + // Last part, assign file content under the "file" key + current[part] = { + file: { + contents: file.contents, + }, + }; + } else { + // Traverse or create sub-directory under "directory" key + if (!current[part]) { + current[part] = { + directory: {}, + }; + } + current = (current[part] as DirectoryNode).directory; + } + }); + }); + + if (!!webContainer) { + webContainer.fs.rm(webContainer.workdir, { + recursive: true, + force: true, + }); + webContainer.mount(root); + } + }, [id, pathname]); + + return loadFiles; +} diff --git a/components/workspace/file-explorer.tsx b/components/workspace/file-explorer.tsx index b46cc05..89377bc 100644 --- a/components/workspace/file-explorer.tsx +++ b/components/workspace/file-explorer.tsx @@ -10,7 +10,7 @@ import { import { db } from "@/data/db"; import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; import { FileIcon } from "./file-icon"; type TOCProps = { @@ -119,4 +119,13 @@ const FileExplorer = () => { return ; }; +/** + * + * @returns Function to refresh the file explorer in the current view + */ +export const useRefreshFileExplorer = () => { + const pathname = usePathname(); + return () => mutate(`file-explorer-${pathname}`); +}; + export default FileExplorer; diff --git a/components/workspace/upload-modal.tsx b/components/workspace/upload-modal.tsx new file mode 100644 index 0000000..fb0828e --- /dev/null +++ b/components/workspace/upload-modal.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Upload } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +import { db, FileContent } from "@/data/db"; +import { useCallback, useState } from "react"; +import { useDropzone, DropzoneOptions } from "react-dropzone"; +import { usePathname } from "next/navigation"; +import { useRefreshFileExplorer } from "./file-explorer"; +import { useLoadFiles } from "../webcontainer/use-load-files"; + +export default function UploadModal() { + const refreshFileExplorer = useRefreshFileExplorer(); + const pathname = usePathname(); + const [isOpen, setIsOpen] = useState(false); + const loadFiles = useLoadFiles(); + + const onDrop = useCallback>( + async (acceptedFiles) => { + let all: FileContent[] = []; + + acceptedFiles.forEach((file) => { + const reader = new FileReader(); + + reader.onabort = () => console.log("file reading was aborted"); + reader.onerror = () => console.log("file reading has failed"); + reader.onload = () => { + // Do whatever you want with the file contents + const binaryStr = reader.result; + if (binaryStr instanceof ArrayBuffer) { + const contents = Buffer.from(binaryStr).toString("ascii"); + // @ts-expect-error + const filename = file.path!; + + all.push({ path: filename.slice(1), contents }); + } + }; + reader.readAsArrayBuffer(file); + }); + + try { + await db.workspaces.delete(pathname); + await db.files.bulkDelete( + (await db.files.toArray()) + .map((i) => i.path) + .filter((i) => i.startsWith(pathname + "/")) + ); + + await db.workspaces.add({ + name: pathname, + template: "file-upload", + dll: "", + }); + + const templateData: { path: string; contents: string }[] = all; + + await db.files.bulkAdd( + templateData.map(({ path, contents }) => ({ + path: `${pathname}/${encodeURIComponent(path)}`, + contents, + })) + ); + + await refreshFileExplorer(); + loadFiles(); + setIsOpen(false); + } catch (err) { + alert(String(err)); + } + }, + [] + ); + const { getRootProps, getInputProps } = useDropzone({ onDrop }); + + return ( + + + setIsOpen(true)}> + + + + + + Upload Files + + + + + Drag 'n' drop some files here, or click to select + files + + + + + + + ); +}
+ Drag 'n' drop some files here, or click to select + files +