Skip to content

Commit

Permalink
feat: node cli
Browse files Browse the repository at this point in the history
  • Loading branch information
yongenaelf committed Aug 29, 2024
1 parent c80e1b3 commit 566c806
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 53 deletions.
4 changes: 3 additions & 1 deletion components/build-deploy-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -99,6 +100,7 @@ export function BuildDeployPanel() {
<button.icon className="w-4 h-4" />
</Button>
))}
<UploadModal />
</div>
);
}
56 changes: 5 additions & 51 deletions components/webcontainer/load-files.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
62 changes: 62 additions & 0 deletions components/webcontainer/use-load-files.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 10 additions & 1 deletion components/workspace/file-explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -119,4 +119,13 @@ const FileExplorer = () => {
return <TOC toc={toc} pathname={pathname} />;
};

/**
*
* @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;
107 changes: 107 additions & 0 deletions components/workspace/upload-modal.tsx
Original file line number Diff line number Diff line change
@@ -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<NonNullable<DropzoneOptions["onDrop"]>>(
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 (
<Dialog open={isOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => setIsOpen(true)}>
<Upload className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Files</DialogTitle>
<DialogDescription>
<div {...getRootProps()} className="p-8 border">
<input {...getInputProps()} />
<p>
Drag &apos;n&apos; drop some files here, or click to select
files
</p>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}

0 comments on commit 566c806

Please sign in to comment.