fix: responsive fix

This commit is contained in:
Khairul Hidayat 2024-02-22 19:49:13 +00:00
parent efe51a9b5e
commit b232410d83
13 changed files with 209 additions and 129 deletions

View File

@ -1,73 +1,51 @@
import { GripVertical } from "lucide-react"; import { GripVertical } from "lucide-react";
import { createContext, forwardRef, useContext } from "react"; import { forwardRef } from "react";
import * as ResizablePrimitive from "react-resizable-panels"; import * as ResizablePrimitive from "react-resizable-panels";
import cookieJs from "cookiejs";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { usePageContext } from "~/renderer/context"; import {
import { useDebounce } from "~/hooks/useDebounce"; BreakpointValues,
useBreakpointValue,
} from "~/hooks/useBreakpointValue";
const ResizableContext = createContext<{ initialSize: number[] }>(null!); type Direction = "horizontal" | "vertical";
type ResizablePanelGroupProps = Omit<
React.ComponentProps<typeof ResizablePrimitive.PanelGroup>,
"direction"
> & {
direction: Direction | BreakpointValues<Direction>;
};
const ResizablePanelGroup = ({ const ResizablePanelGroup = ({
className, className,
autoSaveId,
direction, direction,
...props ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => { }: ResizablePanelGroupProps) => {
const { cookies } = usePageContext(); const directionValue = useBreakpointValue(direction);
const [debouncePersistLayout] = useDebounce((sizes: number[]) => {
if (autoSaveId && typeof window !== "undefined") {
cookieJs.set(panelKey, JSON.stringify(sizes));
}
}, 500);
const panelKey = ["panel", direction, autoSaveId].join(":");
let initialSize: number[] = [];
if (autoSaveId && cookies && cookies[panelKey]) {
initialSize = JSON.parse(cookies[panelKey]) || [];
}
const onLayout = (sizes: number[]) => {
if (props.onLayout) {
props.onLayout(sizes);
}
debouncePersistLayout(sizes);
};
return ( return (
<ResizableContext.Provider value={{ initialSize }}>
<ResizablePrimitive.PanelGroup <ResizablePrimitive.PanelGroup
className={cn( className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col", "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className className
)} )}
direction={directionValue}
{...props} {...props}
direction={direction}
onLayout={onLayout}
/> />
</ResizableContext.Provider>
); );
}; };
type ResizablePanelProps = React.ComponentProps< type ResizablePanelProps = Omit<
typeof ResizablePrimitive.Panel React.ComponentProps<typeof ResizablePrimitive.Panel>,
"defaultSize"
> & { > & {
panelId: number; defaultSize: number | BreakpointValues<number>;
}; };
const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => { const ResizablePanel = forwardRef((props: ResizablePanelProps, ref: any) => {
const { panelId, defaultSize, ...restProps } = props; const { defaultSize, ...restProps } = props;
const ctx = useContext(ResizableContext); const initialSize = useBreakpointValue(defaultSize);
let initialSize = defaultSize;
if (panelId != null) {
const size = ctx?.initialSize[panelId];
if (size != null) {
initialSize = size;
}
}
return ( return (
<ResizablePrimitive.Panel <ResizablePrimitive.Panel

57
hooks/useBreakpoint.ts Normal file
View File

@ -0,0 +1,57 @@
import { useCallback, useEffect, useRef, useState } from "react";
export const breakpoints = {
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
"2xl": 1536,
};
export type Breakpoint = keyof typeof breakpoints;
export const breakpointKeys = Object.keys(breakpoints);
export const breakpointValues = Object.values(breakpoints);
export const useBreakpoint = (onChange?: (breakpoint: number) => void) => {
const prevRef = useRef<number>(0);
const [curBreakpoint, setBreakpoint] = useState<number>(
getScreenBreakpoint()
);
const onResize = useCallback(() => {
const breakpointIdx = getScreenBreakpoint();
if (breakpointIdx >= 0 && prevRef.current !== breakpointIdx) {
if (onChange) {
onChange(breakpointIdx);
} else {
setBreakpoint(breakpointIdx);
}
prevRef.current = breakpointIdx;
}
}, [onChange]);
useEffect(() => {
window.addEventListener("resize", onResize);
onResize();
return () => {
window.removeEventListener("resize", onResize);
};
}, [onResize]);
const breakpoint = breakpointKeys[curBreakpoint];
return [curBreakpoint, breakpoint] as [number, Breakpoint];
};
export function getScreenBreakpoint() {
const width = typeof window !== "undefined" ? window.innerWidth : 0;
let breakpointIdx = breakpointValues.findIndex((i) => width <= i);
if (breakpointIdx < 0) {
breakpointIdx = breakpointKeys.length - 1;
}
return breakpointIdx;
}

View File

@ -0,0 +1,55 @@
import { useState } from "react";
import {
Breakpoint,
breakpointKeys,
getScreenBreakpoint,
useBreakpoint,
} from "./useBreakpoint";
export type BreakpointValues<T> = Partial<Record<Breakpoint, T | null>>;
export const useBreakpointValue = <T>(values: T | BreakpointValues<T>) => {
const [value, setValue] = useState(
typeof values === "object"
? getValueByBreakpoint(
values as BreakpointValues<T>,
getScreenBreakpoint()
)
: values
);
useBreakpoint((breakpoint) => {
if (typeof values !== "object") {
return;
}
const newValue = getValueByBreakpoint(
values as BreakpointValues<T>,
breakpoint
);
if (newValue !== value) {
setValue(newValue);
}
});
return value as T;
};
export function getValueByBreakpoint<T>(
values: BreakpointValues<T>,
breakpoint: number
) {
const valueEntries = Object.entries(values as never);
let resIdx = valueEntries.findIndex(([key]) => {
const bpIdx = breakpointKeys.indexOf(key);
return breakpoint <= bpIdx;
});
if (resIdx < 0) {
resIdx = valueEntries.length - 1;
}
const value = valueEntries[resIdx]?.[1] as T;
return value;
}

View File

@ -59,7 +59,6 @@
"clsx": "^2.1.0", "clsx": "^2.1.0",
"console-feed": "^3.5.0", "console-feed": "^3.5.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cookiejs": "^2.1.3",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"cssnano": "^6.0.3", "cssnano": "^6.0.3",
"drizzle-orm": "^0.29.3", "drizzle-orm": "^0.29.3",

View File

@ -4,6 +4,7 @@ import Link from "~/renderer/link";
const HomePage = () => { const HomePage = () => {
const { posts } = useData<Data>(); const { posts } = useData<Data>();
if (!posts?.length) { if (!posts?.length) {
return <p>No posts.</p>; return <p>No posts.</p>;
} }

View File

@ -4,15 +4,15 @@ import {
ResizablePanelGroup, ResizablePanelGroup,
} from "~/components/ui/resizable"; } from "~/components/ui/resizable";
import WebPreview from "./components/web-preview"; import WebPreview from "./components/web-preview";
import { usePortrait } from "~/hooks/usePortrait";
import Editor from "./components/editor"; import Editor from "./components/editor";
import ProjectContext from "./context/project"; import ProjectContext from "./context/project";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useParams, useSearchParams } from "~/renderer/hooks"; import { useParams, useSearchParams } from "~/renderer/hooks";
import { BASE_URL } from "~/lib/consts"; import { BASE_URL } from "~/lib/consts";
import { withClientOnly } from "~/renderer/client-only";
import Spinner from "~/components/ui/spinner";
const ViewProjectPage = () => { const ViewProjectPage = () => {
const isPortrait = usePortrait();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const params = useParams(); const params = useParams();
const isCompact = const isCompact =
@ -24,15 +24,14 @@ const ViewProjectPage = () => {
<ProjectContext.Provider value={{ slug, isCompact }}> <ProjectContext.Provider value={{ slug, isCompact }}>
<ResizablePanelGroup <ResizablePanelGroup
autoSaveId="main-panel" autoSaveId="main-panel"
direction={isPortrait ? "vertical" : "horizontal"} direction={{ sm: "vertical", md: "horizontal" }}
className={cn("w-full !h-dvh bg-slate-600", !isCompact ? "md:p-4" : "")} className={cn("w-full !h-dvh bg-slate-600", !isCompact ? "md:p-4" : "")}
> >
<ResizablePanel <ResizablePanel
panelId={0} defaultSize={60}
defaultSize={isPortrait ? 50 : 60}
collapsible collapsible
collapsedSize={0} collapsedSize={0}
minSize={isPortrait ? 10 : 30} minSize={30}
> >
<Editor /> <Editor />
</ResizablePanel> </ResizablePanel>
@ -45,8 +44,7 @@ const ViewProjectPage = () => {
} }
/> />
<ResizablePanel <ResizablePanel
panelId={1} defaultSize={40}
defaultSize={isPortrait ? 50 : 40}
collapsible collapsible
collapsedSize={0} collapsedSize={0}
minSize={10} minSize={10}
@ -58,4 +56,12 @@ const ViewProjectPage = () => {
); );
}; };
export default ViewProjectPage; const LoadingPage = () => {
return (
<div className="flex w-full h-dvh items-center justify-center">
<Spinner />
</div>
);
};
export default withClientOnly(ViewProjectPage, LoadingPage);

View File

@ -20,12 +20,14 @@ 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";
const Editor = () => { const Editor = () => {
const { pinnedFiles } = useData<Data>(); const { pinnedFiles } = useData<Data>();
const trpcUtils = trpc.useUtils(); const trpcUtils = trpc.useUtils();
const project = useProjectContext(); const project = useProjectContext();
const sidebarPanel = useRef<ImperativePanelHandle>(null); const sidebarPanel = useRef<ImperativePanelHandle>(null);
const [breakpoint] = useBreakpoint();
const [sidebarExpanded, setSidebarExpanded] = useState(false); const [sidebarExpanded, setSidebarExpanded] = useState(false);
const [curTabIdx, setCurTabIdx] = useState(0); const [curTabIdx, setCurTabIdx] = useState(0);
@ -168,8 +170,7 @@ const Editor = () => {
<ResizablePanelGroup autoSaveId="veditor-panel" direction="horizontal"> <ResizablePanelGroup autoSaveId="veditor-panel" direction="horizontal">
<ResizablePanel <ResizablePanel
ref={sidebarPanel} ref={sidebarPanel}
panelId={0} defaultSize={{ sm: 0, md: 25 }}
defaultSize={25}
minSize={10} minSize={10}
collapsible collapsible
collapsedSize={0} collapsedSize={0}
@ -182,9 +183,9 @@ const Editor = () => {
<ResizableHandle className="bg-slate-900" /> <ResizableHandle className="bg-slate-900" />
<ResizablePanel panelId={1} defaultSize={75}> <ResizablePanel defaultSize={{ sm: 100, md: 75 }}>
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical"> <ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
<ResizablePanel panelId={0} defaultSize={80} minSize={20}> <ResizablePanel defaultSize={{ sm: 100, md: 80 }} minSize={20}>
<Tabs <Tabs
tabs={openFileList} tabs={openFileList}
current={curTabIdx} current={curTabIdx}
@ -193,16 +194,20 @@ const Editor = () => {
/> />
</ResizablePanel> </ResizablePanel>
{breakpoint >= 2 ? (
<>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel <ResizablePanel
panelId={1} defaultSize={{ sm: 0, md: 20 }}
defaultSize={20}
collapsible collapsible
collapsedSize={0} collapsedSize={0}
minSize={10} minSize={10}
> >
<ConsoleLogger /> <ConsoleLogger />
</ResizablePanel> </ResizablePanel>
</>
) : null}
</ResizablePanelGroup> </ResizablePanelGroup>
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>

View File

@ -1,12 +1,10 @@
"use client"; "use client";
import { getFileExt } from "~/lib/utils"; import { getFileExt } from "~/lib/utils";
import React from "react";
import CodeEditor from "../../../../components/ui/code-editor"; import CodeEditor from "../../../../components/ui/code-editor";
import trpc from "~/lib/trpc"; import trpc from "~/lib/trpc";
import { useData } from "~/renderer/hooks"; import { useData } from "~/renderer/hooks";
import { Data } from "../+data"; import { Data } from "../+data";
import ClientOnly from "~/renderer/client-only";
import Spinner from "~/components/ui/spinner"; import Spinner from "~/components/ui/spinner";
type Props = { type Props = {
@ -42,14 +40,12 @@ const FileViewer = ({ id, onFileContentChange }: Props) => {
const ext = getFileExt(filename); const ext = getFileExt(filename);
return ( return (
<ClientOnly fallback={<SSRCodeEditor value={data?.content} />}>
<CodeEditor <CodeEditor
lang={ext} lang={ext}
value={data?.content || ""} value={data?.content || ""}
formatOnSave formatOnSave
onChange={(val) => updateFileContent.mutate({ id, content: val })} onChange={(val) => updateFileContent.mutate({ id, content: val })}
/> />
</ClientOnly>
); );
} }
@ -64,14 +60,4 @@ const LoadingLayout = () => {
); );
}; };
const SSRCodeEditor = ({ value }: { value?: string | null }) => {
return (
<textarea
className="w-full h-full py-3 pl-11 pr-2 overflow-x-auto text-nowrap font-mono text-sm md:text-[16px] md:leading-[22px] bg-[#1a1b26] text-[#787c99]"
value={value || ""}
readOnly
/>
);
};
export default FileViewer; export default FileViewer;

7
pnpm-lock.yaml generated
View File

@ -74,9 +74,6 @@ dependencies:
cookie-parser: cookie-parser:
specifier: ^1.4.6 specifier: ^1.4.6
version: 1.4.6 version: 1.4.6
cookiejs:
specifier: ^2.1.3
version: 2.1.3
copy-to-clipboard: copy-to-clipboard:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
@ -2925,10 +2922,6 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: false dev: false
/cookiejs@2.1.3:
resolution: {integrity: sha512-pA/nRQVka2eTXm1/Dq8pNt1PN+e1PJNItah0vL15qwpet81/tUfrAp8e0iiVM8WEAzDcTGK5/1hDyR6BdBZMVg==}
dev: false
/copy-anything@3.0.5: /copy-anything@3.0.5:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'} engines: {node: '>=12.13'}

View File

@ -3,7 +3,7 @@ import type { ReactNode } from "react";
type ClientOnlyProps = { type ClientOnlyProps = {
children: ReactNode; children: ReactNode;
fallback?: ReactNode | null; fallback?: () => JSX.Element | null;
}; };
const ClientOnly = ({ children, fallback }: ClientOnlyProps) => { const ClientOnly = ({ children, fallback }: ClientOnlyProps) => {
@ -14,15 +14,19 @@ const ClientOnly = ({ children, fallback }: ClientOnlyProps) => {
}, []); }, []);
if (typeof window === "undefined") { if (typeof window === "undefined") {
return fallback; return fallback ? fallback() : null;
} }
return isMounted ? children : fallback; if (isMounted) {
return children;
}
return fallback ? fallback() : null;
}; };
export const withClientOnly = <T extends unknown>( export const withClientOnly = <T extends unknown>(
Component: React.ComponentType<T>, Component: React.ComponentType<T>,
fallback?: ReactNode | null fallback?: () => JSX.Element | null
): React.ComponentType<T> => { ): React.ComponentType<T> => {
return (props: any) => ( return (props: any) => (
<ClientOnly fallback={fallback}> <ClientOnly fallback={fallback}>

View File

@ -1,20 +1,14 @@
import postcssPlugin from "postcss"; import postcssPlugin from "postcss";
import tailwindcss from "tailwindcss"; import tailwindcss from "tailwindcss";
import cssnano from "cssnano"; import cssnano from "cssnano";
import { fileExists, getProjectDir } from "~/server/lib/utils";
import { FileSchema } from "~/server/db/schema/file"; import { FileSchema } from "~/server/db/schema/file";
import { unpackProject } from "~/server/lib/unpack-project"; import { unpackProject } from "~/server/lib/unpack-project";
export const postcss = async (fileData: FileSchema) => { export const postcss = async (fileData: FileSchema) => {
const content = fileData.content || ""; const content = fileData.content || "";
const projectDir = getProjectDir();
if (!fileExists(projectDir)) {
return content;
}
try { try {
await unpackProject({ ext: "ts,tsx,js,jsx,html" }); const projectDir = await unpackProject({ ext: "ts,tsx,js,jsx,html" });
const result = await postcssPlugin([ const result = await postcssPlugin([
tailwindcss({ tailwindcss({
@ -29,6 +23,7 @@ export const postcss = async (fileData: FileSchema) => {
return result.css; return result.css;
} catch (err) { } catch (err) {
console.error("postcss error", err);
return content; return content;
} }
}; };

View File

@ -23,6 +23,7 @@ export const unpackProject = async (
const projectDir = getProjectDir(); const projectDir = getProjectDir();
if (!fileExists(projectDir)) { if (!fileExists(projectDir)) {
console.log("not exist", projectDir);
await fs.mkdir(projectDir, { recursive: true }); await fs.mkdir(projectDir, { recursive: true });
} }