diff --git a/components/ui/code-editor.tsx b/components/ui/code-editor.tsx index a821cbe..5065da6 100644 --- a/components/ui/code-editor.tsx +++ b/components/ui/code-editor.tsx @@ -50,6 +50,7 @@ const CodeEditor = (props: Props) => { parser, plugins, cursorOffset: cursor, + printWidth: 64, } ); diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 6bc160b..b1fae9b 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -1,5 +1,3 @@ -"use client"; - import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { X } from "lucide-react"; diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index 8da56a9..6dbf25b 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -1,5 +1,3 @@ -"use client"; - import * as React from "react"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import { Check, ChevronRight, Circle } from "lucide-react"; diff --git a/components/ui/resizable.tsx b/components/ui/resizable.tsx index 53fdab1..b4b1fb6 100644 --- a/components/ui/resizable.tsx +++ b/components/ui/resizable.tsx @@ -1,24 +1,82 @@ -"use client"; - import { GripVertical } from "lucide-react"; +import { createContext, forwardRef, useContext } from "react"; import * as ResizablePrimitive from "react-resizable-panels"; +import cookieJs from "cookiejs"; import { cn } from "~/lib/utils"; +import { usePageContext } from "~/renderer/context"; +import { useDebounce } from "~/hooks/useDebounce"; + +const ResizableContext = createContext<{ initialSize: number[] }>(null!); const ResizablePanelGroup = ({ className, + autoSaveId, + direction, ...props -}: React.ComponentProps) => ( - -); +}: React.ComponentProps) => { + const { cookies } = usePageContext(); + const [debouncePersistLayout] = useDebounce((sizes: number[]) => { + if (autoSaveId && typeof window !== "undefined") { + cookieJs.set(panelKey, JSON.stringify(sizes)); + } + }, 500); -const ResizablePanel = ResizablePrimitive.Panel; + const panelKey = ["panel", direction, autoSaveId].join(":"); + let initialSize: number[] = []; + + if (autoSaveId && cookies && cookies[panelKey]) { + initialSize = JSON.parse(cookies[panelKey]) || []; + } + + const onLayout = (sizes: number[]) => { + if (props.onLayout) { + props.onLayout(sizes); + } + debouncePersistLayout(sizes); + }; + + return ( + + + + ); +}; + +type ResizablePanelProps = React.ComponentProps< + typeof ResizablePrimitive.Panel +> & { + panelId: number; +}; + +const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => { + const { panelId, defaultSize, ...restProps } = props; + const ctx = useContext(ResizableContext); + let initialSize = defaultSize; + + if (panelId != null) { + const size = ctx?.initialSize[panelId]; + if (size != null) { + initialSize = size; + } + } + + return ( + + ); +}); const ResizableHandle = ({ withHandle, diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/package.json b/package.json index aac5756..1da0611 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "license": "ISC", "devDependencies": { "@swc/cli": "^0.3.9", + "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.21", "@types/node": "^20.11.19", "@types/nprogress": "^0.2.3", @@ -29,7 +30,6 @@ "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.0.1", "drizzle-kit": "^0.20.14", - "postcss": "^8", "tailwindcss": "^3.3.0", "tsx": "^4.7.1", "typescript": "^5.3.3", @@ -58,13 +58,17 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "console-feed": "^3.5.0", + "cookie-parser": "^1.4.6", + "cookiejs": "^2.1.3", "copy-to-clipboard": "^3.3.3", + "cssnano": "^6.0.3", "drizzle-orm": "^0.29.3", "drizzle-zod": "^0.5.1", "express": "^4.18.2", "lucide-react": "^0.331.0", "mime": "^4.0.1", "nprogress": "^0.2.0", + "postcss": "^8", "prettier": "^3.2.5", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pages/project/@slug/+Page.tsx b/pages/project/@slug/+Page.tsx index e3faf59..d546433 100644 --- a/pages/project/@slug/+Page.tsx +++ b/pages/project/@slug/+Page.tsx @@ -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" : "")} > { } /> { ); }; -export default withClientOnly(ViewProjectPage); +export default ViewProjectPage; diff --git a/pages/project/@slug/+data.ts b/pages/project/@slug/+data.ts new file mode 100644 index 0000000..4598444 --- /dev/null +++ b/pages/project/@slug/+data.ts @@ -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>; diff --git a/pages/project/@slug/components/editor.tsx b/pages/project/@slug/components/editor.tsx index c1dd0f2..47d8d77 100644 --- a/pages/project/@slug/components/editor.tsx +++ b/pages/project/@slug/components/editor.tsx @@ -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(); const trpcUtils = trpc.useUtils(); - const [isMounted, setMounted] = useState(false); const project = useProjectContext(); const sidebarPanel = useRef(null); const [sidebarExpanded, setSidebarExpanded] = useState(false); const [curTabIdx, setCurTabIdx] = useState(0); - const [curOpenFiles, setOpenFiles] = useState([]); - - const pinnedFiles = trpc.file.getAll.useQuery( - { isPinned: true }, - { enabled: !isMounted } + const [curOpenFiles, setOpenFiles] = useState( + 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([]); + const [openedFiles, setOpenedFiles] = useState(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 = () => { { - - - + + + { /> - {!isPortrait ? ( - <> - - - - - - ) : null} + + + + diff --git a/pages/project/@slug/components/file-listing.tsx b/pages/project/@slug/components/file-listing.tsx index 47e0fb1..5fac227 100644 --- a/pages/project/@slug/components/file-listing.tsx +++ b/pages/project/@slug/components/file-listing.tsx @@ -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(); const { onOpenFile, onFileChanged } = useEditorContext(); const createFileDlg = useDisclose(); - 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 = () => { -
- {fileList.map((file) => ( - - ))} -
+ {files.isLoading ? ( +
+ +
+ ) : ( +
+ {fileList.map((file) => ( + + ))} +
+ )} { - const { data, isLoading, refetch } = trpc.file.getById.useQuery(id); + const { pinnedFiles } = useData(); + 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

Loading...

; + return ; } + if (!data || data.isDirectory) { return

File not found.

; } @@ -32,16 +42,36 @@ const FileViewer = ({ id, onFileContentChange }: Props) => { const ext = getFileExt(filename); return ( - updateFileContent.mutate({ id, content: val })} - /> + }> + updateFileContent.mutate({ id, content: val })} + /> + ); } return null; }; +const LoadingLayout = () => { + return ( +
+ +
+ ); +}; + +const SSRCodeEditor = ({ value }: { value?: string | null }) => { + return ( +