269 lines
7.3 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "~/components/ui/resizable";
import Tabs, { Tab, TabView } from "~/components/ui/tabs";
import FileViewer from "./file-viewer";
import trpc from "~/lib/trpc";
import EditorContext from "../context/editor";
import type { FileSchema } from "~/server/db/schema/file";
import { useProjectContext } from "../context/project";
import Sidebar from "./sidebar";
import ConsoleLogger from "./console-logger";
import { useData } from "~/renderer/hooks";
import { Data } from "../+data";
import { useBreakpoint } from "~/hooks/useBreakpoint";
import StatusBar from "./status-bar";
import { FiTerminal } from "react-icons/fi";
import SettingsDialog from "./settings-dialog";
import FileIcon from "~/components/ui/file-icon";
import { api } from "~/lib/api";
import { useMutation } from "@tanstack/react-query";
import { Button } from "~/components/ui/button";
import { FaExternalLinkAlt } from "react-icons/fa";
import { BASE_URL } from "~/lib/consts";
const Editor = () => {
const { project, initialFiles } = useData<Data>();
const trpcUtils = trpc.useUtils();
const { isEmbed } = useProjectContext();
const [breakpoint] = useBreakpoint();
const [curTabIdx, setCurTabIdx] = useState(0);
const [curOpenFiles, setOpenFiles] = useState<number[]>(
initialFiles.map((i) => i.id)
);
const openedFilesData = trpc.file.getAll.useQuery(
{ projectId: project.id!, id: curOpenFiles },
{ enabled: curOpenFiles.length > 0, initialData: initialFiles }
);
const [openedFiles, setOpenedFiles] = useState<any[]>(initialFiles);
const generateThumbnail = useMutation({
mutationFn: () => {
return api(`/thumbnail/${project.slug!}`, { method: "PATCH" });
},
});
const deleteFile = trpc.file.delete.useMutation({
onSuccess: (file) => {
trpcUtils.file.getAll.invalidate();
onFileChanged(file);
const openFileIdx = curOpenFiles.indexOf(file.id);
if (openFileIdx >= 0) {
onCloseFile(openFileIdx);
}
},
});
useEffect(() => {
if (!initialFiles?.length || curOpenFiles.length > 0) {
return;
}
initialFiles.forEach((file) => {
onOpenFile(file.id, false);
});
return () => {
setOpenFiles([]);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialFiles]);
useEffect(() => {
if (openedFilesData.data) {
setOpenedFiles(openedFilesData.data);
}
}, [openedFilesData.data]);
// useEffect(() => {
// // start API sandbox
// api(`/sandbox/${project.slug}/start`, { method: "POST" }).catch(() => {});
// }, [project]);
useEffect(() => {
const itv = setInterval(() => generateThumbnail.mutate(), 60000);
return () => {
clearInterval(itv);
};
}, []);
const onOpenFile = useCallback(
(fileId: number, autoSwitchTab = true) => {
const idx = curOpenFiles.indexOf(fileId);
if (idx >= 0) {
return setCurTabIdx(idx);
}
setOpenFiles((state) => {
if (autoSwitchTab) {
setCurTabIdx(state.length);
}
return [...state, fileId];
});
},
[curOpenFiles]
);
const onDeleteFile = useCallback(
(fileId: number) => {
if (
window.confirm("Are you sure want to delete this files?") &&
!deleteFile.isPending
) {
deleteFile.mutate(fileId);
}
},
[deleteFile]
);
const onCloseFile = useCallback(
(idx: number) => {
const _f = [...curOpenFiles];
_f.splice(idx, 1);
setOpenFiles(_f);
if (curTabIdx === idx) {
setCurTabIdx(Math.max(0, idx - 1));
}
},
[curOpenFiles, curTabIdx]
);
const onFileChanged = useCallback(
(_file: Omit<FileSchema, "content">) => {
openedFilesData.refetch();
},
[openedFilesData]
);
const tabs = useMemo(() => {
let tabs: Tab[] = [];
// opened files
tabs = tabs.concat(
curOpenFiles.map((fileId) => {
const fileData = openedFiles?.find((i) => i.id === fileId);
const filename = fileData?.filename || "...";
return {
title: filename,
icon: <FileIcon file={{ isDirectory: false, filename }} />,
render: () => <FileViewer id={fileId} />,
};
})
);
// show console tab on mobile
if (breakpoint < 2) {
tabs.push({
title: "Console",
icon: <FiTerminal />,
render: () => <ConsoleLogger />,
locked: true,
});
}
// tabs.push({
// title: "API",
// icon: <FiServer />,
// render: () => <APIManager />,
// locked: true,
// });
return tabs;
}, [curOpenFiles, openedFiles, breakpoint]);
const currentTab = Math.min(Math.max(curTabIdx, 0), tabs.length - 1);
return (
<EditorContext.Provider
value={{
onOpenFile,
onFileChanged,
onDeleteFile,
}}
>
<div className="h-full relative flex flex-col">
<ResizablePanelGroup
autoSaveId={!isEmbed ? "veditor-panel" : null}
direction="horizontal"
className="flex-1 order-2 md:order-1"
>
<Sidebar
defaultSize={{ md: 50, lg: 25 }}
defaultCollapsed={{ md: true, lg: false }}
minSize={10}
collapsible
collapsedSize={0}
/>
<ResizableHandle className="w-0" />
<ResizablePanel defaultSize={{ md: 100, lg: 75 }}>
<ResizablePanelGroup
autoSaveId={!isEmbed ? "code-editor" : null}
direction="vertical"
>
<ResizablePanel defaultSize={{ sm: 100, md: 80 }} minSize={20}>
<div className="w-full h-full flex flex-col items-stretch bg-slate-800">
<div className="flex items-center">
<Tabs
tabs={tabs}
current={currentTab}
onChange={setCurTabIdx}
onClose={onCloseFile}
className="flex-1 p-1 md:h-12 md:p-1.5 md:gap-1.5"
/>
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
onClick={() =>
window.open(`${BASE_URL}/${project.slug}`, "_blank")
}
>
<FaExternalLinkAlt />
</Button>
</div>
<TabView
tabs={tabs}
current={currentTab}
className="flex-1"
/>
</div>
</ResizablePanel>
{breakpoint >= 2 ? (
<>
<ResizableHandle className="!h-0" />
<ResizablePanel
defaultSize={{ sm: 0, md: 20 }}
collapsible
collapsedSize={0}
minSize={10}
>
<ConsoleLogger />
</ResizablePanel>
</>
) : null}
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
<StatusBar className="order-1 md:order-2" />
</div>
<SettingsDialog />
</EditorContext.Provider>
);
};
export default Editor;