From 8bb3d2bd8434b1c1874da8aa651d6811a5861593 Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Fri, 23 Feb 2024 23:57:14 +0000 Subject: [PATCH] feat: layout fix --- components/containers/navbar.tsx | 5 +- components/ui/action-button.tsx | 31 ++++-- components/ui/resizable.tsx | 6 +- components/ui/tabs.tsx | 37 ++++--- hooks/useConsoleLogger.ts | 34 +++++++ pages/project/@slug/+Page.tsx | 9 +- .../@slug/components/console-logger.tsx | 28 +----- pages/project/@slug/components/editor.tsx | 99 ++++++++----------- .../project/@slug/components/file-viewer.tsx | 12 ++- pages/project/@slug/components/sidebar.tsx | 63 +++++++++--- pages/project/@slug/components/status-bar.tsx | 55 +++++++++++ .../project/@slug/components/web-preview.tsx | 88 ++++++++++++----- pages/project/@slug/stores/sidebar.ts | 11 +++ pages/project/@slug/stores/web-preview.ts | 4 + 14 files changed, 325 insertions(+), 157 deletions(-) create mode 100644 hooks/useConsoleLogger.ts create mode 100644 pages/project/@slug/components/status-bar.tsx create mode 100644 pages/project/@slug/stores/sidebar.ts diff --git a/components/containers/navbar.tsx b/components/containers/navbar.tsx index 27e7bd2..22a88af 100644 --- a/components/containers/navbar.tsx +++ b/components/containers/navbar.tsx @@ -12,9 +12,10 @@ import { } from "../ui/dropdown-menu"; import { FaChevronDown } from "react-icons/fa"; import trpc from "~/lib/trpc"; +import { usePageContext } from "~/renderer/context"; const Navbar = () => { - const { user } = useAuth(); + const {user, urlPathname } = usePageContext(); const logout = trpc.auth.logout.useMutation({ onSuccess() { window.location.reload(); @@ -51,7 +52,7 @@ const Navbar = () => { ) : ( - + )} diff --git a/components/ui/action-button.tsx b/components/ui/action-button.tsx index 9a73e21..9926416 100644 --- a/components/ui/action-button.tsx +++ b/components/ui/action-button.tsx @@ -3,22 +3,37 @@ import { Button } from "~/components/ui/button"; import { cn } from "~/lib/utils"; import React, { forwardRef } from "react"; import { IconType } from "react-icons/lib"; +import { VariantProps, cva } from "class-variance-authority"; -type Props = React.ComponentProps & { - icon: IconType; -}; +const variants = cva( + "text-slate-400 hover:bg-transparent hover:dark:bg-transparent p-0 flex-shrink-0", + { + variants: { + size: { + sm: "h-8 w-6", + md: "h-8 w-8", + lg: "h-10 w-10", + }, + }, + defaultVariants: { + size: "sm", + }, + } +); + +type Props = Omit, "size"> & + VariantProps & { + icon: IconType; + }; const ActionButton = forwardRef( - ({ icon: Icon, className, onClick, ...props }: Props, ref: any) => { + ({ icon: Icon, className, size, onClick, ...props }: Props, ref: any) => { return ( - + + {onClose ? ( + onClose()} + /> + ) : null} ); }; diff --git a/hooks/useConsoleLogger.ts b/hooks/useConsoleLogger.ts new file mode 100644 index 0000000..727b167 --- /dev/null +++ b/hooks/useConsoleLogger.ts @@ -0,0 +1,34 @@ +import { createStore, useStore } from "zustand"; +import { Decode } from "console-feed"; +import { useEffect } from "react"; + +type Store = { + logs: any[]; +}; + +const store = createStore(() => ({ logs: [] })); + +export const useConsoleLogger = () => { + useEffect(() => { + const onMessage = (event: MessageEvent) => { + const { data: eventData } = event; + if (!eventData || eventData.type !== "console") { + return; + } + + const data = Decode(eventData.data); + if (!data || !data.method || !data.data) { + return; + } + + store.setState((i) => ({ logs: [data, ...i.logs] })); + }; + + window.addEventListener("message", onMessage); + return () => { + window.removeEventListener("message", onMessage); + }; + }, []); +}; + +export const useConsoleLogs = () => useStore(store, (i) => i.logs); diff --git a/pages/project/@slug/+Page.tsx b/pages/project/@slug/+Page.tsx index 4b25176..caf5c5e 100644 --- a/pages/project/@slug/+Page.tsx +++ b/pages/project/@slug/+Page.tsx @@ -38,18 +38,17 @@ const ViewProjectPage = () => { withHandle className={ !isCompact - ? "bg-slate-800 md:bg-transparent hover:bg-slate-500 transition-colors md:mx-1 w-2 md:data-[panel-group-direction=vertical]:h-2 rounded-lg" + ? "bg-slate-800 md:bg-transparent hover:bg-slate-500 transition-colors md:mx-1 w-2 md:data-[panel-group-direction=vertical]:h-2 md:rounded-lg" : "bg-slate-800" } /> - - - + url={previewUrl} + /> ); diff --git a/pages/project/@slug/components/console-logger.tsx b/pages/project/@slug/components/console-logger.tsx index bc1354b..e342571 100644 --- a/pages/project/@slug/components/console-logger.tsx +++ b/pages/project/@slug/components/console-logger.tsx @@ -1,31 +1,9 @@ -import { useEffect, useState } from "react"; -import { Console, Decode } from "console-feed"; -import type { Message } from "console-feed/lib/definitions/Console"; +import { Console } from "console-feed"; import ErrorBoundary from "~/components/containers/error-boundary"; +import { useConsoleLogs } from "~/hooks/useConsoleLogger"; const ConsoleLogger = () => { - const [logs, setLogs] = useState([]); - - useEffect(() => { - const onMessage = (event: MessageEvent) => { - const { data: eventData } = event; - if (!eventData || eventData.type !== "console") { - return; - } - - const data = Decode(eventData.data); - if (!data || !data.method || !data.data) { - return; - } - - setLogs((i) => [data, ...i]); - }; - - window.addEventListener("message", onMessage); - return () => { - window.removeEventListener("message", onMessage); - }; - }, []); + const logs = useConsoleLogs(); return (
diff --git a/pages/project/@slug/components/editor.tsx b/pages/project/@slug/components/editor.tsx index b5c2b63..399de08 100644 --- a/pages/project/@slug/components/editor.tsx +++ b/pages/project/@slug/components/editor.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { ResizableHandle, ResizablePanel, @@ -10,26 +10,21 @@ import trpc from "~/lib/trpc"; import EditorContext from "../context/editor"; import type { FileSchema } from "~/server/db/schema/file"; import Panel from "~/components/ui/panel"; -import { previewStore } from "../stores/web-preview"; import { useProjectContext } from "../context/project"; -import { ImperativePanelHandle } from "react-resizable-panels"; import Sidebar from "./sidebar"; -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"; import { useBreakpoint } from "~/hooks/useBreakpoint"; +import StatusBar from "./status-bar"; +import { FiTerminal } from "react-icons/fi"; const Editor = () => { const { project, pinnedFiles } = useData(); const trpcUtils = trpc.useUtils(); const projectCtx = useProjectContext(); - const sidebarPanel = useRef(null); const [breakpoint] = useBreakpoint(); - const [sidebarExpanded, setSidebarExpanded] = useState(false); const [curTabIdx, setCurTabIdx] = useState(0); const [curOpenFiles, setOpenFiles] = useState( pinnedFiles.map((i) => i.id) @@ -53,22 +48,6 @@ const Editor = () => { }, }); - const toggleSidebar = useCallback(() => { - const sidebar = sidebarPanel.current; - if (!sidebar) { - return; - } - - if (sidebar.isExpanded()) { - sidebar.collapse(); - } else { - sidebar.expand(); - sidebar.resize(25); - } - }, [sidebarPanel]); - - useCommandKey("b", toggleSidebar); - useEffect(() => { if (!pinnedFiles?.length || curOpenFiles.length > 0) { return; @@ -139,22 +118,33 @@ const Editor = () => { [openedFilesData] ); - const refreshPreview = useCallback(() => { - previewStore.getState().refresh(); - }, []); + const tabs = useMemo(() => { + let tabs: Tab[] = []; - const openFileList = useMemo(() => { - return curOpenFiles.map((fileId) => { - const fileData = openedFiles?.find((i) => i.id === fileId); + // opened files + tabs = tabs.concat( + curOpenFiles.map((fileId) => { + const fileData = openedFiles?.find((i) => i.id === fileId); - return { - title: fileData?.filename || "...", - render: () => ( - - ), - }; - }) satisfies Tab[]; - }, [curOpenFiles, openedFiles, refreshPreview]); + return { + title: fileData?.filename || "...", + render: () => , + }; + }) + ); + + // show console tab on mobile + if (breakpoint < 2) { + tabs.push({ + title: "Console", + icon: , + render: () => , + locked: true, + }); + } + + return tabs; + }, [curOpenFiles, openedFiles, breakpoint]); const PanelComponent = !projectCtx.isCompact ? Panel : "div"; @@ -166,20 +156,19 @@ const Editor = () => { onDeleteFile, }} > - - - + + setSidebarExpanded(true)} - onCollapse={() => setSidebarExpanded(false)} - > - - + /> @@ -187,8 +176,8 @@ const Editor = () => { @@ -212,13 +201,7 @@ const Editor = () => { - + ); diff --git a/pages/project/@slug/components/file-viewer.tsx b/pages/project/@slug/components/file-viewer.tsx index e80be35..0deb112 100644 --- a/pages/project/@slug/components/file-viewer.tsx +++ b/pages/project/@slug/components/file-viewer.tsx @@ -4,22 +4,28 @@ import trpc from "~/lib/trpc"; import { useData } from "~/renderer/hooks"; import { Data } from "../+data"; import Spinner from "~/components/ui/spinner"; +import { previewStore } from "../stores/web-preview"; type Props = { id: number; - onFileContentChange?: () => void; }; -const FileViewer = ({ id, onFileContentChange }: Props) => { +const FileViewer = ({ id }: Props) => { const { pinnedFiles } = useData(); const initialData = pinnedFiles.find((i) => i.id === id); const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, { initialData, }); + + const onFileContentChange = () => { + // refresh preview + previewStore.getState().refresh(); + }; + const updateFileContent = trpc.file.update.useMutation({ onSuccess: () => { - if (onFileContentChange) onFileContentChange(); + onFileContentChange(); refetch(); }, }); diff --git a/pages/project/@slug/components/sidebar.tsx b/pages/project/@slug/components/sidebar.tsx index a0d7049..77728e5 100644 --- a/pages/project/@slug/components/sidebar.tsx +++ b/pages/project/@slug/components/sidebar.tsx @@ -1,22 +1,53 @@ +import { ComponentProps, useCallback, useEffect, useRef } from "react"; import FileListing from "./file-listing"; -import { FaUserCircle } from "react-icons/fa"; -import { Button } from "~/components/ui/button"; +import { ImperativePanelHandle } from "react-resizable-panels"; +import useCommandKey from "~/hooks/useCommandKey"; +import { sidebarStore } from "../stores/sidebar"; +import { ResizablePanel } from "~/components/ui/resizable"; +import { useBreakpointValue } from "~/hooks/useBreakpointValue"; + +type SidebarProps = ComponentProps; + +const Sidebar = (props: SidebarProps) => { + const sidebarPanel = useRef(null); + const defaultSize = useBreakpointValue(props.defaultSize); + + const toggleSidebar = useCallback( + (toggle?: boolean) => { + const sidebar = sidebarPanel.current; + if (!sidebar) { + return; + } + + const expand = toggle != null ? toggle : !sidebar.isCollapsed(); + + if (expand) { + sidebar.collapse(); + } else { + sidebar.expand(); + sidebar.resize(defaultSize); + } + }, + [sidebarPanel, defaultSize] + ); + + useCommandKey("b", toggleSidebar); + useEffect(() => { + sidebarStore.setState({ toggle: toggleSidebar }); + }, [toggleSidebar]); -const Sidebar = () => { return ( - + sidebarStore.setState({ expanded: true })} + onCollapse={() => sidebarStore.setState({ expanded: false })} + {...props} + > + + ); }; diff --git a/pages/project/@slug/components/status-bar.tsx b/pages/project/@slug/components/status-bar.tsx new file mode 100644 index 0000000..cda7e44 --- /dev/null +++ b/pages/project/@slug/components/status-bar.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { FiSidebar, FiSmartphone, FiUser } from "react-icons/fi"; +import { useStore } from "zustand"; +import { Button } from "~/components/ui/button"; +import { cn } from "~/lib/utils"; +import { usePageContext } from "~/renderer/context"; +import { sidebarStore } from "../stores/sidebar"; +import ActionButton from "~/components/ui/action-button"; +import { previewStore } from "../stores/web-preview"; +import { useProjectContext } from "../context/project"; + +const StatusBar = ({ className }: React.ComponentProps<"div">) => { + const { user, urlPathname } = usePageContext(); + const { isCompact } = useProjectContext(); + const sidebarExpanded = useStore(sidebarStore, (i) => i.expanded); + const previewExpanded = useStore(previewStore, (i) => i.open); + + if (isCompact) { + return null; + } + + return ( +
+ sidebarStore.getState().toggle()} + /> + previewStore.getState().toggle()} + /> + +
+ +
+ ); +}; + +export default StatusBar; diff --git a/pages/project/@slug/components/web-preview.tsx b/pages/project/@slug/components/web-preview.tsx index 646d78c..88b1361 100644 --- a/pages/project/@slug/components/web-preview.tsx +++ b/pages/project/@slug/components/web-preview.tsx @@ -1,54 +1,92 @@ /* eslint-disable react/display-name */ import Panel from "~/components/ui/panel"; -import { useCallback, useEffect, useRef } from "react"; +import { ComponentProps, useCallback, useEffect, useRef } from "react"; import { useProjectContext } from "../context/project"; import { Button } from "~/components/ui/button"; import { FaRedo } from "react-icons/fa"; import { previewStore } from "../stores/web-preview"; +import { ImperativePanelHandle } from "react-resizable-panels"; +import useCommandKey from "~/hooks/useCommandKey"; +import { ResizablePanel } from "~/components/ui/resizable"; +import { useConsoleLogger } from "~/hooks/useConsoleLogger"; -type WebPreviewProps = { +type WebPreviewProps = ComponentProps & { url?: string | null; }; -const WebPreview = ({ url }: WebPreviewProps) => { +const WebPreview = ({ url, ...props }: WebPreviewProps) => { const frameRef = useRef(null); + const panelRef = useRef(null); const project = useProjectContext(); + // hook into the console + useConsoleLogger(); + const refresh = useCallback(() => { if (frameRef.current) { frameRef.current.src = `${url}?t=${Date.now()}`; } }, [url]); + const togglePanel = useCallback( + (toggle?: boolean) => { + const panel = panelRef.current; + if (!panel) { + return; + } + + const expand = toggle != null ? toggle : !panel.isCollapsed(); + + if (expand) { + panel.collapse(); + } else { + panel.expand(); + panel.resize( + typeof props.defaultSize === "number" ? props.defaultSize : 25 + ); + } + }, + [panelRef, props.defaultSize] + ); + + useCommandKey("p", togglePanel); + useEffect(() => { - previewStore.setState({ refresh }); + previewStore.setState({ refresh, toggle: togglePanel }); refresh(); - }, [refresh]); + }, [refresh, togglePanel]); const PanelComponent = !project.isCompact ? Panel : "div"; return ( - -
-

Preview

- -
+ previewStore.setState({ open: true })} + onCollapse={() => previewStore.setState({ open: false })} + {...props} + > + +
+

Preview

+ +
- {url != null ? ( -