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