mirror of
https://github.com/khairul169/code-share.git
synced 2026-10-24 11:53:58 +07:00
feat: fix panel persist over server rendering, etc
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>>;
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user