feat: layout fix

This commit is contained in:
2024-02-23 23:57:14 +00:00
parent 735fa9354b
commit 8bb3d2bd84
14 changed files with 325 additions and 157 deletions
@@ -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<any[]>([]);
useEffect(() => {
const onMessage = (event: MessageEvent<any>) => {
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 (
<div className="h-full flex flex-col bg-[#242424] border-t border-t-gray-600">
+41 -58
View File
@@ -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<Data>();
const trpcUtils = trpc.useUtils();
const projectCtx = useProjectContext();
const sidebarPanel = useRef<ImperativePanelHandle>(null);
const [breakpoint] = useBreakpoint();
const [sidebarExpanded, setSidebarExpanded] = useState(false);
const [curTabIdx, setCurTabIdx] = useState(0);
const [curOpenFiles, setOpenFiles] = useState<number[]>(
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: () => (
<FileViewer id={fileId} onFileContentChange={refreshPreview} />
),
};
}) satisfies Tab[];
}, [curOpenFiles, openedFiles, refreshPreview]);
return {
title: fileData?.filename || "...",
render: () => <FileViewer id={fileId} />,
};
})
);
// show console tab on mobile
if (breakpoint < 2) {
tabs.push({
title: "Console",
icon: <FiTerminal />,
render: () => <ConsoleLogger />,
locked: true,
});
}
return tabs;
}, [curOpenFiles, openedFiles, breakpoint]);
const PanelComponent = !projectCtx.isCompact ? Panel : "div";
@@ -166,20 +156,19 @@ const Editor = () => {
onDeleteFile,
}}
>
<PanelComponent className="h-full relative">
<ResizablePanelGroup autoSaveId="veditor-panel" direction="horizontal">
<ResizablePanel
ref={sidebarPanel}
defaultSize={{ sm: 0, md: 25 }}
<PanelComponent className="h-full relative flex flex-col">
<ResizablePanelGroup
autoSaveId="veditor-panel"
direction="horizontal"
className="flex-1 order-2 md:order-1"
>
<Sidebar
defaultSize={{ sm: 50, md: 25 }}
defaultCollapsed={{ sm: true, md: false }}
minSize={10}
collapsible
collapsedSize={0}
className="bg-[#1e2536]"
onExpand={() => setSidebarExpanded(true)}
onCollapse={() => setSidebarExpanded(false)}
>
<Sidebar />
</ResizablePanel>
/>
<ResizableHandle className="bg-slate-900" />
@@ -187,8 +176,8 @@ const Editor = () => {
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
<ResizablePanel defaultSize={{ sm: 100, md: 80 }} minSize={20}>
<Tabs
tabs={openFileList}
current={curTabIdx}
tabs={tabs}
current={Math.min(Math.max(curTabIdx, 0), tabs.length - 1)}
onChange={setCurTabIdx}
onClose={onCloseFile}
/>
@@ -212,13 +201,7 @@ const Editor = () => {
</ResizablePanel>
</ResizablePanelGroup>
<Button
variant="ghost"
className="absolute bottom-0 left-0 w-12 h-12 rounded-none flex items-center justify-center"
onClick={toggleSidebar}
>
{sidebarExpanded ? <FaCompressArrowsAlt /> : <FaCompress />}
</Button>
<StatusBar className="order-1 md:order-2" />
</PanelComponent>
</EditorContext.Provider>
);
@@ -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<Data>();
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();
},
});
+47 -16
View File
@@ -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<typeof ResizablePanel>;
const Sidebar = (props: SidebarProps) => {
const sidebarPanel = useRef<ImperativePanelHandle>(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 (
<aside className="flex flex-col items-stretch h-full">
<FileListing />
<div className="h-12 bg-[#1a1b26] pl-12">
<Button
variant="ghost"
className="h-12 w-full truncate flex justify-start text-left uppercase text-xs rounded-none"
>
<FaUserCircle className="mr-2 text-xl" />
<span className="truncate">Log in</span>
</Button>
</div>
</aside>
<ResizablePanel
ref={sidebarPanel}
className="bg-[#1e2536]"
onExpand={() => sidebarStore.setState({ expanded: true })}
onCollapse={() => sidebarStore.setState({ expanded: false })}
{...props}
>
<aside className="flex flex-col items-stretch h-full">
<FileListing />
</aside>
</ResizablePanel>
);
};
@@ -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 (
<div
className={cn(
"h-10 flex items-center gap-1 pl-2 pr-3 w-full bg-slate-900 md:bg-[#242424] border-b md:border-b-0 md:border-t border-slate-900 md:border-black/30",
className
)}
>
<ActionButton
title="Toggle Sidebar"
icon={FiSidebar}
className={sidebarExpanded ? "text-white" : ""}
onClick={() => sidebarStore.getState().toggle()}
/>
<ActionButton
title="Toggle Preview Window"
icon={FiSmartphone}
className={previewExpanded ? "text-white" : ""}
onClick={() => previewStore.getState().toggle()}
/>
<div className="flex-1"></div>
<Button
href={user ? "/user" : "/auth/login?return=" + urlPathname}
className="h-full p-0 gap-2 text-xs"
variant="link"
>
<FiUser className="text-sm" />
{user?.name || "Log in"}
</Button>
</div>
);
};
export default StatusBar;
+63 -25
View File
@@ -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<typeof ResizablePanel> & {
url?: string | null;
};
const WebPreview = ({ url }: WebPreviewProps) => {
const WebPreview = ({ url, ...props }: WebPreviewProps) => {
const frameRef = useRef<HTMLIFrameElement>(null);
const panelRef = useRef<ImperativePanelHandle>(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 (
<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"
onClick={refresh}
>
<FaRedo />
</Button>
</div>
<ResizablePanel
ref={panelRef}
onExpand={() => previewStore.setState({ open: true })}
onCollapse={() => previewStore.setState({ open: false })}
{...props}
>
<PanelComponent className="h-full flex flex-col bg-slate-800">
<div className="h-10 hidden md:flex items-center pl-4">
<p className="flex-1 truncate text-xs uppercase">Preview</p>
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
onClick={refresh}
>
<FaRedo />
</Button>
</div>
{url != null ? (
<iframe
id="web-preview"
ref={frameRef}
className="border-none w-full flex-1 overflow-hidden bg-white"
sandbox="allow-scripts"
/>
) : null}
</PanelComponent>
{url != null ? (
<iframe
id="web-preview"
ref={frameRef}
className="border-none w-full flex-1 overflow-hidden bg-white"
sandbox="allow-scripts"
/>
) : null}
</PanelComponent>
</ResizablePanel>
);
};