import type React from "react"; import type { Layout } from "react-resizable-panels"; import type { BuilderLayout } from "./-store/sidebar"; import { useSuspenseQuery } from "@tanstack/react-query"; import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; import Cookies from "js-cookie"; import { useEffect, useRef } from "react"; import { usePanelRef } from "react-resizable-panels"; import { ResizableGroup, ResizablePanel, ResizableSeparator } from "@reactive-resume/ui/components/resizable"; import { useBuilderResumeUpdateSubscription, useInitializeResumeStore, useMergeResumeMetadata, useResumeCleanup, useResumeStore, } from "@/features/resume/builder/draft"; import { useIsMobile } from "@/hooks/use-mobile"; import { orpc } from "@/libs/orpc/client"; import { createNoindexFollowMeta } from "@/libs/seo"; import { BuilderHeader } from "./-components/header"; import { BuilderSidebarLeft } from "./-sidebar/left"; import { BuilderSidebarRight } from "./-sidebar/right"; import { BUILDER_LAYOUT_COOKIE_NAME, DEFAULT_BUILDER_LAYOUT, mapPanelLayoutToBuilderLayout, parseBuilderLayoutCookie, useBuilderSidebar, useBuilderSidebarStore, } from "./-store/sidebar"; export const Route = createFileRoute("/builder/$resumeId")({ component: RouteComponent, beforeLoad: async ({ context }) => { if (!context.session) throw redirect({ to: "/auth/login", replace: true }); return { session: context.session }; }, loader: async ({ params, context }) => { const [layout, resume] = await Promise.all([ getBuilderLayout(), context.queryClient.ensureQueryData(orpc.resume.getById.queryOptions({ input: { id: params.resumeId } })), ]); return { layout, name: resume.name }; }, head: ({ loaderData }) => ({ meta: loaderData ? [{ title: `${loaderData.name} - Reactive Resume` }, createNoindexFollowMeta()] : [createNoindexFollowMeta()], }), }); function RouteComponent() { const { layout: initialLayout } = Route.useLoaderData(); const { resumeId } = Route.useParams(); const { data: resume } = useSuspenseQuery(orpc.resume.getById.queryOptions({ input: { id: resumeId } })); const initializeResumeStore = useInitializeResumeStore(); const mergeResumeMetadata = useMergeResumeMetadata(); const isReady = useResumeStore((state) => state.isReady); const initializedResumeId = useResumeStore((state) => state.resumeId); const isInitialized = isReady && initializedResumeId === resumeId; useResumeCleanup(); useBuilderResumeUpdateSubscription(); useEffect(() => { if (isInitialized) return; initializeResumeStore(resume); }, [initializeResumeStore, isInitialized, resume]); useEffect(() => { mergeResumeMetadata(resume); }, [ mergeResumeMetadata, resume.id, resume.name, resume.slug, resume.tags, resume.isLocked, resume.isPublic, resume.hasPassword, resume.updatedAt, resume, ]); if (!isInitialized) return null; return ; } type BuilderLayoutShellProps = React.ComponentProps<"div"> & { initialLayout: BuilderLayout; }; function BuilderLayoutShell({ initialLayout }: BuilderLayoutShellProps) { const isMobile = useIsMobile(); const canPersistLayoutRef = useRef(false); const leftSidebarRef = usePanelRef(); const rightSidebarRef = usePanelRef(); const setLeftSidebar = useBuilderSidebarStore((state) => state.setLeftSidebar); const setRightSidebar = useBuilderSidebarStore((state) => state.setRightSidebar); const setLayout = useBuilderSidebarStore((state) => state.setLayout); const { maxSidebarSize, collapsedSidebarSize } = useBuilderSidebar((state) => ({ maxSidebarSize: state.maxSidebarSize, collapsedSidebarSize: state.collapsedSidebarSize, })); useEffect(() => { setLayout(initialLayout); canPersistLayoutRef.current = true; }, [initialLayout, setLayout]); const onLayoutChanged = (layout: Layout) => { const nextLayout = mapPanelLayoutToBuilderLayout(layout); if (!canPersistLayoutRef.current) return; setLayout(nextLayout); setBuilderLayout(nextLayout); }; useEffect(() => { if (!leftSidebarRef || !rightSidebarRef) return; setLeftSidebar(leftSidebarRef); setRightSidebar(rightSidebarRef); }, [leftSidebarRef, rightSidebarRef, setLeftSidebar, setRightSidebar]); const sidebarMinSize = isMobile ? "0%" : `${collapsedSidebarSize * 2}px`; const sidebarCollapsedSize = isMobile ? "0%" : `${collapsedSidebarSize}px`; const leftSidebarSize = isMobile ? "0%" : `${initialLayout.left}%`; const rightSidebarSize = isMobile ? "0%" : `${initialLayout.right}%`; const artboardSize = isMobile ? "100%" : `${initialLayout.artboard}%`; return (
); } const setBuilderLayout = (data: BuilderLayout) => { const layout = parseBuilderLayoutCookie(JSON.stringify(data)); Cookies.set(BUILDER_LAYOUT_COOKIE_NAME, JSON.stringify(layout), { path: "/" }); }; const getBuilderLayout = (): BuilderLayout => { const layout = Cookies.get(BUILDER_LAYOUT_COOKIE_NAME); if (!layout) return DEFAULT_BUILDER_LAYOUT; return parseBuilderLayoutCookie(layout); };