mirror of
https://github.com/khairul169/code-share.git
synced 2025-05-15 00:49:34 +07:00
feat: layout fix
This commit is contained in:
parent
735fa9354b
commit
8bb3d2bd84
@ -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 = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button>Log in</Button>
|
||||
<Button href={"/auth/login?return=" + urlPathname}>Log in</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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<typeof Button> & {
|
||||
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<React.ComponentProps<typeof Button>, "size"> &
|
||||
VariantProps<typeof variants> & {
|
||||
icon: IconType;
|
||||
};
|
||||
};
|
||||
|
||||
const ActionButton = forwardRef(
|
||||
({ icon: Icon, className, onClick, ...props }: Props, ref: any) => {
|
||||
({ icon: Icon, className, size, onClick, ...props }: Props, ref: any) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"text-slate-400 hover:bg-transparent hover:dark:bg-transparent h-8 w-6 p-0 flex-shrink-0",
|
||||
className
|
||||
)}
|
||||
className={cn(variants({ size }), className)}
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
|
@ -41,16 +41,18 @@ type ResizablePanelProps = Omit<
|
||||
"defaultSize"
|
||||
> & {
|
||||
defaultSize: number | BreakpointValues<number>;
|
||||
defaultCollapsed?: boolean | BreakpointValues<boolean>;
|
||||
};
|
||||
|
||||
const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => {
|
||||
const { defaultSize, ...restProps } = props;
|
||||
const { defaultSize, defaultCollapsed, ...restProps } = props;
|
||||
const initialSize = useBreakpointValue(defaultSize);
|
||||
const initialCollapsed = useBreakpointValue(defaultCollapsed);
|
||||
|
||||
return (
|
||||
<ResizablePrimitive.Panel
|
||||
ref={ref}
|
||||
defaultSize={initialSize}
|
||||
defaultSize={initialCollapsed ? 0 : initialSize}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ export type Tab = {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
render?: () => React.ReactNode;
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -76,7 +77,7 @@ const Tabs = ({ tabs, current = 0, onChange, onClose }: Props) => {
|
||||
icon={tab.icon}
|
||||
isActive={idx === current}
|
||||
onSelect={() => onChange && onChange(idx)}
|
||||
onClose={() => onClose && onClose(idx)}
|
||||
onClose={!tab.locked ? () => onClose && onClose(idx) : null}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
@ -93,12 +94,13 @@ type TabItemProps = {
|
||||
icon?: React.ReactNode;
|
||||
isActive?: boolean;
|
||||
onSelect: () => void;
|
||||
onClose: () => void;
|
||||
onClose?: (() => void) | null;
|
||||
};
|
||||
|
||||
const TabItem = ({
|
||||
index,
|
||||
title,
|
||||
icon,
|
||||
isActive,
|
||||
onSelect,
|
||||
onClose,
|
||||
@ -116,19 +118,28 @@ const TabItem = ({
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<button className="pl-4 pr-0 truncate flex items-center self-stretch">
|
||||
<FileIcon
|
||||
file={{ isDirectory: false, filename: title }}
|
||||
className="mr-1"
|
||||
/>
|
||||
<span className="truncate">{filename}</span>
|
||||
<button
|
||||
className={cn(
|
||||
"pl-4 pr-4 truncate flex items-center self-stretch",
|
||||
onClose ? "pr-0" : ""
|
||||
)}
|
||||
>
|
||||
{icon != null ? (
|
||||
icon
|
||||
) : (
|
||||
<FileIcon file={{ isDirectory: false, filename: title }} />
|
||||
)}
|
||||
<span className="inline-block ml-2 truncate">{filename}</span>
|
||||
<span>{ext}</span>
|
||||
</button>
|
||||
|
||||
{onClose ? (
|
||||
<ActionButton
|
||||
icon={FiX}
|
||||
className="opacity-0 group-hover:opacity-100 transition-colors"
|
||||
onClick={onClose}
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
34
hooks/useConsoleLogger.ts
Normal file
34
hooks/useConsoleLogger.ts
Normal file
@ -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<Store>(() => ({ logs: [] }));
|
||||
|
||||
export const useConsoleLogger = () => {
|
||||
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;
|
||||
}
|
||||
|
||||
store.setState((i) => ({ logs: [data, ...i.logs] }));
|
||||
};
|
||||
|
||||
window.addEventListener("message", onMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", onMessage);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useConsoleLogs = () => useStore(store, (i) => i.logs);
|
@ -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"
|
||||
}
|
||||
/>
|
||||
<ResizablePanel
|
||||
<WebPreview
|
||||
defaultSize={40}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
minSize={10}
|
||||
>
|
||||
<WebPreview url={previewUrl} />
|
||||
</ResizablePanel>
|
||||
url={previewUrl}
|
||||
/>
|
||||
</ResizablePanelGroup>
|
||||
</ProjectContext.Provider>
|
||||
);
|
||||
|
@ -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) => {
|
||||
// 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} />
|
||||
),
|
||||
render: () => <FileViewer id={fileId} />,
|
||||
};
|
||||
}) satisfies Tab[];
|
||||
}, [curOpenFiles, openedFiles, refreshPreview]);
|
||||
})
|
||||
);
|
||||
|
||||
// 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 (
|
||||
<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 />
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
55
pages/project/@slug/components/status-bar.tsx
Normal file
55
pages/project/@slug/components/status-bar.tsx
Normal file
@ -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,36 +1,73 @@
|
||||
/* 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 (
|
||||
<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 flex items-center">
|
||||
<p className="flex-1 truncate text-xs uppercase pl-4">Preview</p>
|
||||
<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"
|
||||
@ -49,6 +86,7 @@ const WebPreview = ({ url }: WebPreviewProps) => {
|
||||
/>
|
||||
) : null}
|
||||
</PanelComponent>
|
||||
</ResizablePanel>
|
||||
);
|
||||
};
|
||||
|
||||
|
11
pages/project/@slug/stores/sidebar.ts
Normal file
11
pages/project/@slug/stores/sidebar.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createStore } from "zustand";
|
||||
|
||||
type SidebarStore = {
|
||||
expanded: boolean;
|
||||
toggle: (toggle?: boolean) => void;
|
||||
};
|
||||
|
||||
export const sidebarStore = createStore<SidebarStore>(() => ({
|
||||
expanded: false,
|
||||
toggle() {},
|
||||
}));
|
@ -1,9 +1,13 @@
|
||||
import { createStore } from "zustand";
|
||||
|
||||
type PreviewStore = {
|
||||
open: boolean;
|
||||
toggle: (toggle?: boolean) => void;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
export const previewStore = createStore<PreviewStore>(() => ({
|
||||
open: false,
|
||||
toggle() {},
|
||||
refresh: () => {},
|
||||
}));
|
||||
|
Loading…
x
Reference in New Issue
Block a user