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";
|
} from "../ui/dropdown-menu";
|
||||||
import { FaChevronDown } from "react-icons/fa";
|
import { FaChevronDown } from "react-icons/fa";
|
||||||
import trpc from "~/lib/trpc";
|
import trpc from "~/lib/trpc";
|
||||||
|
import { usePageContext } from "~/renderer/context";
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const { user } = useAuth();
|
const {user, urlPathname } = usePageContext();
|
||||||
const logout = trpc.auth.logout.useMutation({
|
const logout = trpc.auth.logout.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@ -51,7 +52,7 @@ const Navbar = () => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<Button>Log in</Button>
|
<Button href={"/auth/login?return=" + urlPathname}>Log in</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,22 +3,37 @@ import { Button } from "~/components/ui/button";
|
|||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import { IconType } from "react-icons/lib";
|
import { IconType } from "react-icons/lib";
|
||||||
|
import { VariantProps, cva } from "class-variance-authority";
|
||||||
|
|
||||||
type Props = React.ComponentProps<typeof Button> & {
|
const variants = cva(
|
||||||
icon: IconType;
|
"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(
|
const ActionButton = forwardRef(
|
||||||
({ icon: Icon, className, onClick, ...props }: Props, ref: any) => {
|
({ icon: Icon, className, size, onClick, ...props }: Props, ref: any) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn(
|
className={cn(variants({ size }), className)}
|
||||||
"text-slate-400 hover:bg-transparent hover:dark:bg-transparent h-8 w-6 p-0 flex-shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -41,16 +41,18 @@ type ResizablePanelProps = Omit<
|
|||||||
"defaultSize"
|
"defaultSize"
|
||||||
> & {
|
> & {
|
||||||
defaultSize: number | BreakpointValues<number>;
|
defaultSize: number | BreakpointValues<number>;
|
||||||
|
defaultCollapsed?: boolean | BreakpointValues<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => {
|
const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => {
|
||||||
const { defaultSize, ...restProps } = props;
|
const { defaultSize, defaultCollapsed, ...restProps } = props;
|
||||||
const initialSize = useBreakpointValue(defaultSize);
|
const initialSize = useBreakpointValue(defaultSize);
|
||||||
|
const initialCollapsed = useBreakpointValue(defaultCollapsed);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizablePrimitive.Panel
|
<ResizablePrimitive.Panel
|
||||||
ref={ref}
|
ref={ref}
|
||||||
defaultSize={initialSize}
|
defaultSize={initialCollapsed ? 0 : initialSize}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,7 @@ export type Tab = {
|
|||||||
title: string;
|
title: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
render?: () => React.ReactNode;
|
render?: () => React.ReactNode;
|
||||||
|
locked?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -76,7 +77,7 @@ const Tabs = ({ tabs, current = 0, onChange, onClose }: Props) => {
|
|||||||
icon={tab.icon}
|
icon={tab.icon}
|
||||||
isActive={idx === current}
|
isActive={idx === current}
|
||||||
onSelect={() => onChange && onChange(idx)}
|
onSelect={() => onChange && onChange(idx)}
|
||||||
onClose={() => onClose && onClose(idx)}
|
onClose={!tab.locked ? () => onClose && onClose(idx) : null}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
@ -93,12 +94,13 @@ type TabItemProps = {
|
|||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onClose: () => void;
|
onClose?: (() => void) | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TabItem = ({
|
const TabItem = ({
|
||||||
index,
|
index,
|
||||||
title,
|
title,
|
||||||
|
icon,
|
||||||
isActive,
|
isActive,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
onClose,
|
||||||
@ -116,19 +118,28 @@ const TabItem = ({
|
|||||||
)}
|
)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
<button className="pl-4 pr-0 truncate flex items-center self-stretch">
|
<button
|
||||||
<FileIcon
|
className={cn(
|
||||||
file={{ isDirectory: false, filename: title }}
|
"pl-4 pr-4 truncate flex items-center self-stretch",
|
||||||
className="mr-1"
|
onClose ? "pr-0" : ""
|
||||||
/>
|
)}
|
||||||
<span className="truncate">{filename}</span>
|
>
|
||||||
|
{icon != null ? (
|
||||||
|
icon
|
||||||
|
) : (
|
||||||
|
<FileIcon file={{ isDirectory: false, filename: title }} />
|
||||||
|
)}
|
||||||
|
<span className="inline-block ml-2 truncate">{filename}</span>
|
||||||
<span>{ext}</span>
|
<span>{ext}</span>
|
||||||
</button>
|
</button>
|
||||||
<ActionButton
|
|
||||||
icon={FiX}
|
{onClose ? (
|
||||||
className="opacity-0 group-hover:opacity-100 transition-colors"
|
<ActionButton
|
||||||
onClick={onClose}
|
icon={FiX}
|
||||||
/>
|
className="opacity-0 group-hover:opacity-100 transition-colors"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</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
|
withHandle
|
||||||
className={
|
className={
|
||||||
!isCompact
|
!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"
|
: "bg-slate-800"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ResizablePanel
|
<WebPreview
|
||||||
defaultSize={40}
|
defaultSize={40}
|
||||||
collapsible
|
collapsible
|
||||||
collapsedSize={0}
|
collapsedSize={0}
|
||||||
minSize={10}
|
minSize={10}
|
||||||
>
|
url={previewUrl}
|
||||||
<WebPreview url={previewUrl} />
|
/>
|
||||||
</ResizablePanel>
|
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</ProjectContext.Provider>
|
</ProjectContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,31 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { Console } from "console-feed";
|
||||||
import { Console, Decode } from "console-feed";
|
|
||||||
import type { Message } from "console-feed/lib/definitions/Console";
|
|
||||||
import ErrorBoundary from "~/components/containers/error-boundary";
|
import ErrorBoundary from "~/components/containers/error-boundary";
|
||||||
|
import { useConsoleLogs } from "~/hooks/useConsoleLogger";
|
||||||
|
|
||||||
const ConsoleLogger = () => {
|
const ConsoleLogger = () => {
|
||||||
const [logs, setLogs] = useState<any[]>([]);
|
const logs = useConsoleLogs();
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-[#242424] border-t border-t-gray-600">
|
<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 {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
@ -10,26 +10,21 @@ import trpc from "~/lib/trpc";
|
|||||||
import EditorContext from "../context/editor";
|
import EditorContext from "../context/editor";
|
||||||
import type { FileSchema } from "~/server/db/schema/file";
|
import type { FileSchema } from "~/server/db/schema/file";
|
||||||
import Panel from "~/components/ui/panel";
|
import Panel from "~/components/ui/panel";
|
||||||
import { previewStore } from "../stores/web-preview";
|
|
||||||
import { useProjectContext } from "../context/project";
|
import { useProjectContext } from "../context/project";
|
||||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
|
||||||
import Sidebar from "./sidebar";
|
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 ConsoleLogger from "./console-logger";
|
||||||
import { useData } from "~/renderer/hooks";
|
import { useData } from "~/renderer/hooks";
|
||||||
import { Data } from "../+data";
|
import { Data } from "../+data";
|
||||||
import { useBreakpoint } from "~/hooks/useBreakpoint";
|
import { useBreakpoint } from "~/hooks/useBreakpoint";
|
||||||
|
import StatusBar from "./status-bar";
|
||||||
|
import { FiTerminal } from "react-icons/fi";
|
||||||
|
|
||||||
const Editor = () => {
|
const Editor = () => {
|
||||||
const { project, pinnedFiles } = useData<Data>();
|
const { project, pinnedFiles } = useData<Data>();
|
||||||
const trpcUtils = trpc.useUtils();
|
const trpcUtils = trpc.useUtils();
|
||||||
const projectCtx = useProjectContext();
|
const projectCtx = useProjectContext();
|
||||||
const sidebarPanel = useRef<ImperativePanelHandle>(null);
|
|
||||||
const [breakpoint] = useBreakpoint();
|
const [breakpoint] = useBreakpoint();
|
||||||
|
|
||||||
const [sidebarExpanded, setSidebarExpanded] = useState(false);
|
|
||||||
const [curTabIdx, setCurTabIdx] = useState(0);
|
const [curTabIdx, setCurTabIdx] = useState(0);
|
||||||
const [curOpenFiles, setOpenFiles] = useState<number[]>(
|
const [curOpenFiles, setOpenFiles] = useState<number[]>(
|
||||||
pinnedFiles.map((i) => i.id)
|
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(() => {
|
useEffect(() => {
|
||||||
if (!pinnedFiles?.length || curOpenFiles.length > 0) {
|
if (!pinnedFiles?.length || curOpenFiles.length > 0) {
|
||||||
return;
|
return;
|
||||||
@ -139,22 +118,33 @@ const Editor = () => {
|
|||||||
[openedFilesData]
|
[openedFilesData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshPreview = useCallback(() => {
|
const tabs = useMemo(() => {
|
||||||
previewStore.getState().refresh();
|
let tabs: Tab[] = [];
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openFileList = useMemo(() => {
|
// opened files
|
||||||
return curOpenFiles.map((fileId) => {
|
tabs = tabs.concat(
|
||||||
const fileData = openedFiles?.find((i) => i.id === fileId);
|
curOpenFiles.map((fileId) => {
|
||||||
|
const fileData = openedFiles?.find((i) => i.id === fileId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: fileData?.filename || "...",
|
title: fileData?.filename || "...",
|
||||||
render: () => (
|
render: () => <FileViewer id={fileId} />,
|
||||||
<FileViewer id={fileId} onFileContentChange={refreshPreview} />
|
};
|
||||||
),
|
})
|
||||||
};
|
);
|
||||||
}) 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";
|
const PanelComponent = !projectCtx.isCompact ? Panel : "div";
|
||||||
|
|
||||||
@ -166,20 +156,19 @@ const Editor = () => {
|
|||||||
onDeleteFile,
|
onDeleteFile,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PanelComponent className="h-full relative">
|
<PanelComponent className="h-full relative flex flex-col">
|
||||||
<ResizablePanelGroup autoSaveId="veditor-panel" direction="horizontal">
|
<ResizablePanelGroup
|
||||||
<ResizablePanel
|
autoSaveId="veditor-panel"
|
||||||
ref={sidebarPanel}
|
direction="horizontal"
|
||||||
defaultSize={{ sm: 0, md: 25 }}
|
className="flex-1 order-2 md:order-1"
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
defaultSize={{ sm: 50, md: 25 }}
|
||||||
|
defaultCollapsed={{ sm: true, md: false }}
|
||||||
minSize={10}
|
minSize={10}
|
||||||
collapsible
|
collapsible
|
||||||
collapsedSize={0}
|
collapsedSize={0}
|
||||||
className="bg-[#1e2536]"
|
/>
|
||||||
onExpand={() => setSidebarExpanded(true)}
|
|
||||||
onCollapse={() => setSidebarExpanded(false)}
|
|
||||||
>
|
|
||||||
<Sidebar />
|
|
||||||
</ResizablePanel>
|
|
||||||
|
|
||||||
<ResizableHandle className="bg-slate-900" />
|
<ResizableHandle className="bg-slate-900" />
|
||||||
|
|
||||||
@ -187,8 +176,8 @@ const Editor = () => {
|
|||||||
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
|
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
|
||||||
<ResizablePanel defaultSize={{ sm: 100, md: 80 }} minSize={20}>
|
<ResizablePanel defaultSize={{ sm: 100, md: 80 }} minSize={20}>
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={openFileList}
|
tabs={tabs}
|
||||||
current={curTabIdx}
|
current={Math.min(Math.max(curTabIdx, 0), tabs.length - 1)}
|
||||||
onChange={setCurTabIdx}
|
onChange={setCurTabIdx}
|
||||||
onClose={onCloseFile}
|
onClose={onCloseFile}
|
||||||
/>
|
/>
|
||||||
@ -212,13 +201,7 @@ const Editor = () => {
|
|||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
|
||||||
<Button
|
<StatusBar className="order-1 md:order-2" />
|
||||||
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>
|
|
||||||
</PanelComponent>
|
</PanelComponent>
|
||||||
</EditorContext.Provider>
|
</EditorContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -4,22 +4,28 @@ import trpc from "~/lib/trpc";
|
|||||||
import { useData } from "~/renderer/hooks";
|
import { useData } from "~/renderer/hooks";
|
||||||
import { Data } from "../+data";
|
import { Data } from "../+data";
|
||||||
import Spinner from "~/components/ui/spinner";
|
import Spinner from "~/components/ui/spinner";
|
||||||
|
import { previewStore } from "../stores/web-preview";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number;
|
id: number;
|
||||||
onFileContentChange?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FileViewer = ({ id, onFileContentChange }: Props) => {
|
const FileViewer = ({ id }: Props) => {
|
||||||
const { pinnedFiles } = useData<Data>();
|
const { pinnedFiles } = useData<Data>();
|
||||||
const initialData = pinnedFiles.find((i) => i.id === id);
|
const initialData = pinnedFiles.find((i) => i.id === id);
|
||||||
|
|
||||||
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, {
|
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, {
|
||||||
initialData,
|
initialData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onFileContentChange = () => {
|
||||||
|
// refresh preview
|
||||||
|
previewStore.getState().refresh();
|
||||||
|
};
|
||||||
|
|
||||||
const updateFileContent = trpc.file.update.useMutation({
|
const updateFileContent = trpc.file.update.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (onFileContentChange) onFileContentChange();
|
onFileContentChange();
|
||||||
refetch();
|
refetch();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,22 +1,53 @@
|
|||||||
|
import { ComponentProps, useCallback, useEffect, useRef } from "react";
|
||||||
import FileListing from "./file-listing";
|
import FileListing from "./file-listing";
|
||||||
import { FaUserCircle } from "react-icons/fa";
|
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||||
import { Button } from "~/components/ui/button";
|
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 (
|
return (
|
||||||
<aside className="flex flex-col items-stretch h-full">
|
<ResizablePanel
|
||||||
<FileListing />
|
ref={sidebarPanel}
|
||||||
|
className="bg-[#1e2536]"
|
||||||
<div className="h-12 bg-[#1a1b26] pl-12">
|
onExpand={() => sidebarStore.setState({ expanded: true })}
|
||||||
<Button
|
onCollapse={() => sidebarStore.setState({ expanded: false })}
|
||||||
variant="ghost"
|
{...props}
|
||||||
className="h-12 w-full truncate flex justify-start text-left uppercase text-xs rounded-none"
|
>
|
||||||
>
|
<aside className="flex flex-col items-stretch h-full">
|
||||||
<FaUserCircle className="mr-2 text-xl" />
|
<FileListing />
|
||||||
<span className="truncate">Log in</span>
|
</aside>
|
||||||
</Button>
|
</ResizablePanel>
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,54 +1,92 @@
|
|||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
import Panel from "~/components/ui/panel";
|
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 { useProjectContext } from "../context/project";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { FaRedo } from "react-icons/fa";
|
import { FaRedo } from "react-icons/fa";
|
||||||
import { previewStore } from "../stores/web-preview";
|
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;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WebPreview = ({ url }: WebPreviewProps) => {
|
const WebPreview = ({ url, ...props }: WebPreviewProps) => {
|
||||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const panelRef = useRef<ImperativePanelHandle>(null);
|
||||||
const project = useProjectContext();
|
const project = useProjectContext();
|
||||||
|
|
||||||
|
// hook into the console
|
||||||
|
useConsoleLogger();
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
if (frameRef.current) {
|
if (frameRef.current) {
|
||||||
frameRef.current.src = `${url}?t=${Date.now()}`;
|
frameRef.current.src = `${url}?t=${Date.now()}`;
|
||||||
}
|
}
|
||||||
}, [url]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
previewStore.setState({ refresh });
|
previewStore.setState({ refresh, toggle: togglePanel });
|
||||||
refresh();
|
refresh();
|
||||||
}, [refresh]);
|
}, [refresh, togglePanel]);
|
||||||
|
|
||||||
const PanelComponent = !project.isCompact ? Panel : "div";
|
const PanelComponent = !project.isCompact ? Panel : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelComponent className="h-full flex flex-col bg-slate-800">
|
<ResizablePanel
|
||||||
<div className="h-10 flex items-center">
|
ref={panelRef}
|
||||||
<p className="flex-1 truncate text-xs uppercase pl-4">Preview</p>
|
onExpand={() => previewStore.setState({ open: true })}
|
||||||
<Button
|
onCollapse={() => previewStore.setState({ open: false })}
|
||||||
variant="ghost"
|
{...props}
|
||||||
className="dark:hover:bg-slate-700"
|
>
|
||||||
onClick={refresh}
|
<PanelComponent className="h-full flex flex-col bg-slate-800">
|
||||||
>
|
<div className="h-10 hidden md:flex items-center pl-4">
|
||||||
<FaRedo />
|
<p className="flex-1 truncate text-xs uppercase">Preview</p>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
variant="ghost"
|
||||||
|
className="dark:hover:bg-slate-700"
|
||||||
|
onClick={refresh}
|
||||||
|
>
|
||||||
|
<FaRedo />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{url != null ? (
|
{url != null ? (
|
||||||
<iframe
|
<iframe
|
||||||
id="web-preview"
|
id="web-preview"
|
||||||
ref={frameRef}
|
ref={frameRef}
|
||||||
className="border-none w-full flex-1 overflow-hidden bg-white"
|
className="border-none w-full flex-1 overflow-hidden bg-white"
|
||||||
sandbox="allow-scripts"
|
sandbox="allow-scripts"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</PanelComponent>
|
</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";
|
import { createStore } from "zustand";
|
||||||
|
|
||||||
type PreviewStore = {
|
type PreviewStore = {
|
||||||
|
open: boolean;
|
||||||
|
toggle: (toggle?: boolean) => void;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const previewStore = createStore<PreviewStore>(() => ({
|
export const previewStore = createStore<PreviewStore>(() => ({
|
||||||
|
open: false,
|
||||||
|
toggle() {},
|
||||||
refresh: () => {},
|
refresh: () => {},
|
||||||
}));
|
}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user