feat: update layout

This commit is contained in:
Khairul Hidayat 2024-03-03 18:24:00 +07:00
parent f1f6d65798
commit e007dd4ac4
12 changed files with 169 additions and 121 deletions

View File

@ -18,7 +18,7 @@ const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-slate-200 px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/40 bg-transparent dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300", "flex h-10 w-full rounded-md border border-slate-200 px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/40 bg-transparent dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
inputClassName label || error ? inputClassName : className
)} )}
ref={ref} ref={ref}
{...props} {...props}

View File

@ -58,26 +58,28 @@ const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => {
); );
}); });
const ResizableHandle = ({ type ResizableHandleProps = React.ComponentProps<
withHandle, typeof ResizablePrimitive.PanelResizeHandle
className, > & {
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean; withHandle?: boolean;
}) => ( };
const ResizableHandle = (props: ResizableHandleProps) => {
const { withHandle, className, ...restProps } = props;
return (
<ResizablePrimitive.PanelResizeHandle <ResizablePrimitive.PanelResizeHandle
className={cn( className={cn(
"relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", "relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
withHandle && "w-3 bg-black group",
className className
)} )}
{...props} {...restProps}
> >
{withHandle && ( {withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-800"> <div className="z-10 flex h-full max-h-[100px] w-1.5 items-center justify-center rounded-full bg-white/30 group-hover:bg-white/40"></div>
<GripVertical className="h-2.5 w-2.5" />
</div>
)} )}
</ResizablePrimitive.PanelResizeHandle> </ResizablePrimitive.PanelResizeHandle>
); );
};
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -16,17 +16,9 @@ type Props = {
onChange?: (idx: number) => void; onChange?: (idx: number) => void;
onClose?: (idx: number) => void; onClose?: (idx: number) => void;
className?: string; className?: string;
containerClassName?: string;
}; };
const Tabs = ({ const Tabs = ({ tabs, current = 0, onChange, onClose, className }: Props) => {
tabs,
current = 0,
onChange,
onClose,
className,
containerClassName,
}: Props) => {
const tabContainerRef = useRef<HTMLDivElement>(null); const tabContainerRef = useRef<HTMLDivElement>(null);
const onWheel = (e: WheelEvent) => { const onWheel = (e: WheelEvent) => {
@ -64,23 +56,13 @@ const Tabs = ({
container.scrollTo({ left: scrollX, behavior: "smooth" }); container.scrollTo({ left: scrollX, behavior: "smooth" });
}, [tabs, current]); }, [tabs, current]);
const tabView = useMemo(() => { return tabs.length > 0 ? (
const tab = tabs[current];
const element = tab?.render ? tab.render() : null;
return element;
}, [tabs, current]);
return (
<div
className={cn(
"w-full flex flex-col items-stretch bg-slate-800",
className
)}
>
{tabs.length > 0 ? (
<nav <nav
ref={tabContainerRef} ref={tabContainerRef}
className="flex items-stretch overflow-x-auto w-full h-10 min-h-10 hide-scrollbar" className={cn(
"flex items-stretch overflow-x-auto h-10 gap-1 hide-scrollbar",
className
)}
> >
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<TabItem <TabItem
@ -94,13 +76,24 @@ const Tabs = ({
/> />
))} ))}
</nav> </nav>
) : null} ) : null;
};
<main className={cn("flex-1 overflow-hidden", containerClassName)}> type TabViewProps = {
{tabView} tabs: Tab[];
</main> current: number;
</div> className?: string;
); };
export const TabView = ({ tabs, current, className }: TabViewProps) => {
const tabView = useMemo(() => {
const tab = tabs[current];
const element = tab?.render ? tab.render() : null;
return element;
}, [tabs, current]);
return <main className={cn("overflow-hidden", className)}>{tabView}</main>;
}; };
type TabItemProps = { type TabItemProps = {
@ -128,8 +121,8 @@ const TabItem = ({
<div <div
data-idx={index} data-idx={index}
className={cn( className={cn(
"group border-b-2 border-transparent truncate flex-shrink-0 text-white/70 transition-all hover:text-white text-center max-w-[140px] md:max-w-[180px] text-sm flex items-center gap-0 relative z-[1]", "group truncate flex-shrink-0 h-full bg-white/10 rounded-lg text-white/70 transition-all hover:text-white text-center max-w-[140px] md:max-w-[180px] text-sm flex items-center gap-0 relative z-[1]",
isActive ? "border-slate-500 text-white" : "" isActive ? "bg-white/20 text-white" : ""
)} )}
onClick={onSelect} onClick={onSelect}
> >

View File

@ -23,12 +23,9 @@ const ViewProjectPage = () => {
return ( return (
<ProjectContext.Provider value={{ project, isCompact, isEmbed }}> <ProjectContext.Provider value={{ project, isCompact, isEmbed }}>
<ResizablePanelGroup <ResizablePanelGroup
autoSaveId="main-panel" autoSaveId={!isEmbed ? "main-panel" : null}
direction={{ sm: "vertical", md: "horizontal" }} direction={{ sm: "vertical", md: "horizontal" }}
className={cn( className={cn("w-full !h-screen")}
"w-full !h-dvh bg-slate-600",
!isCompact && !isEmbed ? "md:p-4" : ""
)}
> >
<ResizablePanel <ResizablePanel
defaultSize={hidePreview ? 100 : 60} defaultSize={hidePreview ? 100 : 60}
@ -38,14 +35,7 @@ const ViewProjectPage = () => {
> >
<Editor /> <Editor />
</ResizablePanel> </ResizablePanel>
<ResizableHandle <ResizableHandle withHandle={!isEmbed && !isCompact} />
withHandle
className={
!isCompact && !isEmbed
? "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"
}
/>
<WebPreview <WebPreview
defaultSize={40} defaultSize={40}
defaultCollapsed={hidePreview} defaultCollapsed={hidePreview}

View File

@ -12,7 +12,7 @@ export const data = async (ctx: PageContext) => {
throw render(404, "Project not found!"); throw render(404, "Project not found!");
} }
const files = await trpc.file.getAll({ projectId: project.id }); const files = await trpc.file.getAll({ projectId: project.id! });
const initialFiles = files.filter((i) => const initialFiles = files.filter((i) =>
filesParam != null ? filesParam.includes(i.path) : i.isPinned filesParam != null ? filesParam.includes(i.path) : i.isPinned
); );

View File

@ -4,12 +4,11 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "~/components/ui/resizable"; } from "~/components/ui/resizable";
import Tabs, { Tab } from "~/components/ui/tabs"; import Tabs, { Tab, TabView } from "~/components/ui/tabs";
import FileViewer from "./file-viewer"; import FileViewer from "./file-viewer";
import trpc from "~/lib/trpc"; 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 { useProjectContext } from "../context/project"; import { useProjectContext } from "../context/project";
import Sidebar from "./sidebar"; import Sidebar from "./sidebar";
import ConsoleLogger from "./console-logger"; import ConsoleLogger from "./console-logger";
@ -22,11 +21,14 @@ import SettingsDialog from "./settings-dialog";
import FileIcon from "~/components/ui/file-icon"; import FileIcon from "~/components/ui/file-icon";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { Button } from "~/components/ui/button";
import { FaExternalLinkAlt } from "react-icons/fa";
import { BASE_URL } from "~/lib/consts";
const Editor = () => { const Editor = () => {
const { project, initialFiles } = useData<Data>(); const { project, initialFiles } = useData<Data>();
const trpcUtils = trpc.useUtils(); const trpcUtils = trpc.useUtils();
const projectCtx = useProjectContext(); const { isEmbed } = useProjectContext();
const [breakpoint] = useBreakpoint(); const [breakpoint] = useBreakpoint();
const [curTabIdx, setCurTabIdx] = useState(0); const [curTabIdx, setCurTabIdx] = useState(0);
@ -178,8 +180,7 @@ const Editor = () => {
return tabs; return tabs;
}, [curOpenFiles, openedFiles, breakpoint]); }, [curOpenFiles, openedFiles, breakpoint]);
const PanelComponent = const currentTab = Math.min(Math.max(curTabIdx, 0), tabs.length - 1);
!projectCtx.isCompact || !projectCtx.isEmbed ? Panel : "div";
return ( return (
<EditorContext.Provider <EditorContext.Provider
@ -189,9 +190,9 @@ const Editor = () => {
onDeleteFile, onDeleteFile,
}} }}
> >
<PanelComponent className="h-full relative flex flex-col"> <div className="h-full relative flex flex-col">
<ResizablePanelGroup <ResizablePanelGroup
autoSaveId="veditor-panel" autoSaveId={!isEmbed ? "veditor-panel" : null}
direction="horizontal" direction="horizontal"
className="flex-1 order-2 md:order-1" className="flex-1 order-2 md:order-1"
> >
@ -206,15 +207,36 @@ const Editor = () => {
<ResizableHandle className="w-0" /> <ResizableHandle className="w-0" />
<ResizablePanel defaultSize={{ md: 100, lg: 75 }}> <ResizablePanel defaultSize={{ md: 100, lg: 75 }}>
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical"> <ResizablePanelGroup
autoSaveId={!isEmbed ? "code-editor" : null}
direction="vertical"
>
<ResizablePanel defaultSize={{ sm: 100, md: 80 }} minSize={20}> <ResizablePanel defaultSize={{ sm: 100, md: 80 }} minSize={20}>
<div className="w-full h-full flex flex-col items-stretch bg-slate-800">
<div className="flex items-center">
<Tabs <Tabs
tabs={tabs} tabs={tabs}
current={Math.min(Math.max(curTabIdx, 0), tabs.length - 1)} current={currentTab}
onChange={setCurTabIdx} onChange={setCurTabIdx}
onClose={onCloseFile} onClose={onCloseFile}
className="h-full" className="flex-1 p-1 md:h-12 md:p-1.5 md:gap-1.5"
/> />
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
onClick={() =>
window.open(`${BASE_URL}/${project.slug}`, "_blank")
}
>
<FaExternalLinkAlt />
</Button>
</div>
<TabView
tabs={tabs}
current={currentTab}
className="flex-1"
/>
</div>
</ResizablePanel> </ResizablePanel>
{breakpoint >= 2 ? ( {breakpoint >= 2 ? (
@ -236,7 +258,7 @@ const Editor = () => {
</ResizablePanelGroup> </ResizablePanelGroup>
<StatusBar className="order-1 md:order-2" /> <StatusBar className="order-1 md:order-2" />
</PanelComponent> </div>
<SettingsDialog /> <SettingsDialog />
</EditorContext.Provider> </EditorContext.Provider>

View File

@ -39,7 +39,7 @@ const FileListing = () => {
return ( return (
<Fragment> <Fragment>
<div className="h-10 flex items-center pl-4 pr-1"> <div className="h-10 md:h-12 flex items-center pl-4 pr-1">
<p className="text-xs uppercase truncate flex-1">{project.title}</p> <p className="text-xs uppercase truncate flex-1">{project.title}</p>
<ActionButton <ActionButton
icon={FiFilePlus} icon={FiFilePlus}

View File

@ -15,7 +15,7 @@ type Props = {
const FileViewer = ({ id }: Props) => { const FileViewer = ({ id }: Props) => {
const { project } = useProjectContext(); const { project } = useProjectContext();
const { initialFiles } = useData<Data>(); const { initialFiles } = useData<Data>();
const initialData = initialFiles.find((i) => i.id === id); const initialData = initialFiles.find((i) => i.id === id) as any;
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, { const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, {
initialData, initialData,

View File

@ -6,7 +6,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { useMemo, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import { useProjectContext } from "../context/project"; import { useProjectContext } from "../context/project";
import { useForm, useFormReturn } from "~/hooks/useForm"; import { useForm, useFormReturn } from "~/hooks/useForm";
import Input from "~/components/ui/input"; import Input from "~/components/ui/input";
@ -21,8 +21,10 @@ import {
import trpc from "~/lib/trpc"; import trpc from "~/lib/trpc";
import { toast } from "~/lib/utils"; import { toast } from "~/lib/utils";
import Checkbox from "~/components/ui/checkbox"; import Checkbox from "~/components/ui/checkbox";
import Tabs, { Tab } from "~/components/ui/tabs"; import Tabs, { Tab, TabView } from "~/components/ui/tabs";
import { navigate } from "vike/client/router"; import { navigate } from "vike/client/router";
import { useFieldArray } from "react-hook-form";
import { FaTrashAlt } from "react-icons/fa";
const defaultValues: ProjectSettingsSchema = { const defaultValues: ProjectSettingsSchema = {
title: "", title: "",
@ -102,8 +104,9 @@ const SettingsDialog = () => {
tabs={tabs} tabs={tabs}
current={tab} current={tab}
onChange={setTab} onChange={setTab}
containerClassName="mt-4" className="rounded-md overflow-hidden"
/> />
<TabView current={tab} tabs={tabs} className="mt-4" />
<div className="flex flex-col sm:flex-row items-stretch sm:items-center sm:justify-end gap-4 mt-8"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center sm:justify-end gap-4 mt-8">
<Button variant="outline" onClick={onClose}> <Button variant="outline" onClick={onClose}>
@ -157,17 +160,55 @@ const CSSTab = ({ form }: TabProps) => {
}; };
const JSTab = ({ form }: TabProps) => { const JSTab = ({ form }: TabProps) => {
const packages = useFieldArray({
control: form.control,
name: "settings.js.packages",
});
return ( return (
<div className="space-y-3"> <div>
<Select <Select
form={form} form={form}
name="settings.js.transpiler" name="settings.js.transpiler"
label="Transpiler" label="Transpiler"
items={jsTranspilerList} items={jsTranspilerList}
/> />
<p className="text-sm"> <p className="text-sm mt-1">
* Set transpiler to <strong>SWC</strong> to use JSX * Set transpiler to <strong>SWC</strong> to use JSX
</p> </p>
<p className="text-sm mt-8">External Packages</p>
<div>
{packages.fields.map((field, index) => (
<div key={field.id} className="flex items-center gap-2 mt-2">
<Input
key={field.id}
form={form}
name={`settings.js.packages.${index}.name`}
placeholder={`Package alias`}
className="flex-1"
/>
<Input
key={field.id}
form={form}
name={`settings.js.packages.${index}.url`}
placeholder={`URL`}
className="flex-1"
/>
<Button size="icon" onClick={() => packages.remove(index)}>
<FaTrashAlt />
</Button>
</div>
))}
</div>
<Button
onClick={() => packages.append({ name: "", url: "" })}
className="mt-3"
>
Add Package
</Button>
</div> </div>
); );
}; };

View File

@ -17,11 +17,11 @@ import { useProjectContext } from "../context/project";
const StatusBar = ({ className }: React.ComponentProps<"div">) => { const StatusBar = ({ className }: React.ComponentProps<"div">) => {
const { user, urlPathname } = usePageContext(); const { user, urlPathname } = usePageContext();
const { isCompact, project } = useProjectContext(); const { isEmbed, project } = useProjectContext();
const sidebarExpanded = useStore(sidebarStore, (i) => i.expanded); const sidebarExpanded = useStore(sidebarStore, (i) => i.expanded);
const previewExpanded = useStore(previewStore, (i) => i.open); const previewExpanded = useStore(previewStore, (i) => i.open);
if (isCompact) { if (isEmbed) {
return null; return null;
} }

View File

@ -1,5 +1,4 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import Panel from "~/components/ui/panel";
import { ComponentProps, 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";
@ -56,8 +55,6 @@ const WebPreview = ({ url, ...props }: WebPreviewProps) => {
refresh(); refresh();
}, [refresh, togglePanel]); }, [refresh, togglePanel]);
const PanelComponent = !project.isCompact || !project.isEmbed ? Panel : "div";
return ( return (
<ResizablePanel <ResizablePanel
ref={panelRef} ref={panelRef}
@ -65,8 +62,8 @@ const WebPreview = ({ url, ...props }: WebPreviewProps) => {
onCollapse={() => previewStore.setState({ open: false })} onCollapse={() => previewStore.setState({ open: false })}
{...props} {...props}
> >
<PanelComponent className="h-full flex flex-col bg-slate-800"> <div className="h-full flex flex-col bg-slate-800">
<div className="h-10 hidden md:flex items-center pl-4"> <div className="h-10 md:h-12 hidden md:flex items-center pl-4">
<p className="flex-1 truncate text-xs uppercase">Preview</p> <p className="flex-1 truncate text-xs uppercase">Preview</p>
<Button <Button
variant="ghost" variant="ghost"
@ -75,6 +72,8 @@ const WebPreview = ({ url, ...props }: WebPreviewProps) => {
> >
<FaRedo /> <FaRedo />
</Button> </Button>
{!project.isEmbed ? (
<Button <Button
variant="ghost" variant="ghost"
className="dark:hover:bg-slate-700" className="dark:hover:bg-slate-700"
@ -82,6 +81,7 @@ const WebPreview = ({ url, ...props }: WebPreviewProps) => {
> >
<FaExternalLinkAlt /> <FaExternalLinkAlt />
</Button> </Button>
) : null}
</div> </div>
{url != null ? ( {url != null ? (
@ -92,7 +92,7 @@ const WebPreview = ({ url, ...props }: WebPreviewProps) => {
sandbox="allow-scripts allow-forms" sandbox="allow-scripts allow-forms"
/> />
) : null} ) : null}
</PanelComponent> </div>
</ResizablePanel> </ResizablePanel>
); );
}; };

View File

@ -55,20 +55,20 @@ const projectRouter = router({
isNull(project.deletedAt) isNull(project.deletedAt)
); );
const result = await db.query.project.findFirst({ const projectData = await db.query.project.findFirst({
where, where,
with: { with: {
user: { columns: { password: false } }, user: { columns: { password: false } },
}, },
}); });
if (!hasPermission(ctx, result, "r")) { if (!projectData || !hasPermission(ctx, projectData, "r")) {
throw new TRPCError({ code: "FORBIDDEN" }); return null;
} }
const isMutable = hasPermission(ctx, result, "w"); const isMutable = hasPermission(ctx, projectData, "w");
return { ...result, isMutable }; return { ...projectData!, isMutable };
}), }),
create: procedure create: procedure