feat: fix panel persist over server rendering, etc

This commit is contained in:
2024-02-22 23:05:02 +07:00
parent 2c9faa332a
commit efe51a9b5e
27 changed files with 962 additions and 150 deletions
+3 -2
View File
@@ -8,7 +8,6 @@ import { usePortrait } from "~/hooks/usePortrait";
import Editor from "./components/editor";
import ProjectContext from "./context/project";
import { cn } from "~/lib/utils";
import { withClientOnly } from "~/renderer/client-only";
import { useParams, useSearchParams } from "~/renderer/hooks";
import { BASE_URL } from "~/lib/consts";
@@ -29,6 +28,7 @@ const ViewProjectPage = () => {
className={cn("w-full !h-dvh bg-slate-600", !isCompact ? "md:p-4" : "")}
>
<ResizablePanel
panelId={0}
defaultSize={isPortrait ? 50 : 60}
collapsible
collapsedSize={0}
@@ -45,6 +45,7 @@ const ViewProjectPage = () => {
}
/>
<ResizablePanel
panelId={1}
defaultSize={isPortrait ? 50 : 40}
collapsible
collapsedSize={0}
@@ -57,4 +58,4 @@ const ViewProjectPage = () => {
);
};
export default withClientOnly(ViewProjectPage);
export default ViewProjectPage;
+12
View File
@@ -0,0 +1,12 @@
import { PageContext } from "vike/types";
import trpcServer from "~/server/api/trpc/trpc";
export const data = async (ctx: PageContext) => {
const trpc = await trpcServer(ctx);
const pinnedFiles = await trpc.file.getAll({ isPinned: true });
const files = await trpc.file.getAll();
return { files, pinnedFiles };
};
export type Data = Awaited<ReturnType<typeof data>>;
+30 -37
View File
@@ -9,7 +9,6 @@ import FileViewer from "./file-viewer";
import trpc from "~/lib/trpc";
import EditorContext from "../context/editor";
import type { FileSchema } from "~/server/db/schema/file";
import { usePortrait } from "~/hooks/usePortrait";
import Panel from "~/components/ui/panel";
import { previewStore } from "../stores/web-preview";
import { useProjectContext } from "../context/project";
@@ -19,27 +18,26 @@ import useCommandKey from "~/hooks/useCommandKey";
import { Button } from "~/components/ui/button";
import { FaCompress, FaCompressArrowsAlt } from "react-icons/fa";
import ConsoleLogger from "./console-logger";
import { useData } from "~/renderer/hooks";
import { Data } from "../+data";
const Editor = () => {
const isPortrait = usePortrait();
const { pinnedFiles } = useData<Data>();
const trpcUtils = trpc.useUtils();
const [isMounted, setMounted] = useState(false);
const project = useProjectContext();
const sidebarPanel = useRef<ImperativePanelHandle>(null);
const [sidebarExpanded, setSidebarExpanded] = useState(false);
const [curTabIdx, setCurTabIdx] = useState(0);
const [curOpenFiles, setOpenFiles] = useState<number[]>([]);
const pinnedFiles = trpc.file.getAll.useQuery(
{ isPinned: true },
{ enabled: !isMounted }
const [curOpenFiles, setOpenFiles] = useState<number[]>(
pinnedFiles.map((i) => i.id)
);
const openedFilesData = trpc.file.getAll.useQuery(
{ id: curOpenFiles },
{ enabled: curOpenFiles.length > 0 }
{ enabled: curOpenFiles.length > 0, initialData: pinnedFiles }
);
const [openedFiles, setOpenedFiles] = useState<any[]>([]);
const [openedFiles, setOpenedFiles] = useState<any[]>(pinnedFiles);
const deleteFile = trpc.file.delete.useMutation({
onSuccess: (file) => {
@@ -53,10 +51,6 @@ const Editor = () => {
},
});
useEffect(() => {
setMounted(true);
}, []);
const toggleSidebar = useCallback(() => {
const sidebar = sidebarPanel.current;
if (!sidebar) {
@@ -74,15 +68,19 @@ const Editor = () => {
useCommandKey("b", toggleSidebar);
useEffect(() => {
if (!pinnedFiles.data?.length || curOpenFiles.length > 0) {
if (!pinnedFiles?.length || curOpenFiles.length > 0) {
return;
}
pinnedFiles.data.forEach((file) => {
pinnedFiles.forEach((file) => {
onOpenFile(file.id, false);
});
return () => {
setOpenFiles([]);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pinnedFiles.data]);
}, [pinnedFiles]);
useEffect(() => {
if (openedFilesData.data) {
@@ -170,7 +168,8 @@ const Editor = () => {
<ResizablePanelGroup autoSaveId="veditor-panel" direction="horizontal">
<ResizablePanel
ref={sidebarPanel}
defaultSize={isPortrait ? 0 : 25}
panelId={0}
defaultSize={25}
minSize={10}
collapsible
collapsedSize={0}
@@ -183,12 +182,9 @@ const Editor = () => {
<ResizableHandle className="bg-slate-900" />
<ResizablePanel defaultSize={isPortrait ? 100 : 75}>
<ResizablePanelGroup
autoCapitalize="code-editor"
direction="vertical"
>
<ResizablePanel defaultSize={isPortrait ? 100 : 80} minSize={20}>
<ResizablePanel panelId={1} defaultSize={75}>
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
<ResizablePanel panelId={0} defaultSize={80} minSize={20}>
<Tabs
tabs={openFileList}
current={curTabIdx}
@@ -197,19 +193,16 @@ const Editor = () => {
/>
</ResizablePanel>
{!isPortrait ? (
<>
<ResizableHandle />
<ResizablePanel
defaultSize={20}
collapsible
collapsedSize={0}
minSize={10}
>
<ConsoleLogger />
</ResizablePanel>
</>
) : null}
<ResizableHandle />
<ResizablePanel
panelId={1}
defaultSize={20}
collapsible
collapsedSize={0}
minSize={10}
>
<ConsoleLogger />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
@@ -24,12 +24,17 @@ import {
import { cn, getUrl } from "~/lib/utils";
import FileIcon from "~/components/ui/file-icon";
import copy from "copy-to-clipboard";
import { useParams } from "~/renderer/hooks";
import { useData, useParams } from "~/renderer/hooks";
import Spinner from "~/components/ui/spinner";
import { Data } from "../+data";
const FileListing = () => {
const pageData = useData<Data>();
const { onOpenFile, onFileChanged } = useEditorContext();
const createFileDlg = useDisclose<CreateFileSchema>();
const files = trpc.file.getAll.useQuery();
const files = trpc.file.getAll.useQuery(undefined, {
initialData: pageData.files,
});
const fileList = useMemo(() => groupFiles(files.data, null), [files.data]);
@@ -62,11 +67,17 @@ const FileListing = () => {
</DropdownMenu>
</div>
<div className="flex flex-col items-stretch flex-1 overflow-y-auto">
{fileList.map((file) => (
<FileItem key={file.id} file={file} createFileDlg={createFileDlg} />
))}
</div>
{files.isLoading ? (
<div className="flex-1 flex items-center justify-center">
<Spinner />
</div>
) : (
<div className="flex flex-col items-stretch flex-1 overflow-y-auto">
{fileList.map((file) => (
<FileItem key={file.id} file={file} createFileDlg={createFileDlg} />
))}
</div>
)}
<CreateFileDialog
disclose={createFileDlg}
+38 -8
View File
@@ -4,6 +4,10 @@ import { getFileExt } from "~/lib/utils";
import React from "react";
import CodeEditor from "../../../../components/ui/code-editor";
import trpc from "~/lib/trpc";
import { useData } from "~/renderer/hooks";
import { Data } from "../+data";
import ClientOnly from "~/renderer/client-only";
import Spinner from "~/components/ui/spinner";
type Props = {
id: number;
@@ -11,7 +15,12 @@ type Props = {
};
const FileViewer = ({ id, onFileContentChange }: Props) => {
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id);
const { pinnedFiles } = useData<Data>();
const initialData = pinnedFiles.find((i) => i.id === id);
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, {
initialData,
});
const updateFileContent = trpc.file.update.useMutation({
onSuccess: () => {
if (onFileContentChange) onFileContentChange();
@@ -20,8 +29,9 @@ const FileViewer = ({ id, onFileContentChange }: Props) => {
});
if (isLoading) {
return <p>Loading...</p>;
return <LoadingLayout />;
}
if (!data || data.isDirectory) {
return <p>File not found.</p>;
}
@@ -32,16 +42,36 @@ const FileViewer = ({ id, onFileContentChange }: Props) => {
const ext = getFileExt(filename);
return (
<CodeEditor
lang={ext}
value={data?.content || ""}
formatOnSave
onChange={(val) => updateFileContent.mutate({ id, content: val })}
/>
<ClientOnly fallback={<SSRCodeEditor value={data?.content} />}>
<CodeEditor
lang={ext}
value={data?.content || ""}
formatOnSave
onChange={(val) => updateFileContent.mutate({ id, content: val })}
/>
</ClientOnly>
);
}
return null;
};
const LoadingLayout = () => {
return (
<div className="w-full h-full flex items-center justify-center">
<Spinner />
</div>
);
};
const SSRCodeEditor = ({ value }: { value?: string | null }) => {
return (
<textarea
className="w-full h-full py-3 pl-11 pr-2 overflow-x-auto text-nowrap font-mono text-sm md:text-[16px] md:leading-[22px] bg-[#1a1b26] text-[#787c99]"
value={value || ""}
readOnly
/>
);
};
export default FileViewer;
+1 -12
View File
@@ -31,6 +31,7 @@ const WebPreview = ({ url }: WebPreviewProps) => {
return (
<PanelComponent className="h-full flex flex-col bg-slate-800">
<div className="h-10 flex items-center">
<p className="flex-1 truncate text-xs uppercase pl-4">Preview</p>
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
@@ -38,18 +39,6 @@ const WebPreview = ({ url }: WebPreviewProps) => {
>
<FaRedo />
</Button>
<Input
className="flex-1 dark:bg-gray-900 dark:hover:bg-gray-950 h-8 rounded-full"
value={url || ""}
readOnly
/>
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
onClick={() => {}}
>
<FaEllipsisV />
</Button>
</div>
{url != null ? (