chore(nextjs)
This commit is contained in:
parent
e084782328
commit
df7102d18b
4 changed files with 189 additions and 116 deletions
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import useLayoutSchema from "./useLayoutSchema";
|
||||
|
||||
// Global layout cache to persist across component unmounts
|
||||
const layoutCache = new Map<string, React.ComponentType<{ data: any }>>();
|
||||
|
||||
const useLayoutCache = () => {
|
||||
const { idMapFileNames, loading } = useLayoutSchema();
|
||||
const [isPreloading, setIsPreloading] = useState(false);
|
||||
const preloadedRef = useRef(false);
|
||||
|
||||
// Pre-load all layouts when schema is available
|
||||
useEffect(() => {
|
||||
if (!loading && idMapFileNames && Object.keys(idMapFileNames).length > 0 && !preloadedRef.current) {
|
||||
preloadLayouts();
|
||||
preloadedRef.current = true;
|
||||
}
|
||||
}, [idMapFileNames, loading]);
|
||||
|
||||
const preloadLayouts = async () => {
|
||||
if (isPreloading) return;
|
||||
|
||||
setIsPreloading(true);
|
||||
|
||||
try {
|
||||
const layoutPromises = Object.values(idMapFileNames).map(async (layoutName) => {
|
||||
if (!layoutCache.has(layoutName)) {
|
||||
const Layout = dynamic(
|
||||
() => import(`@/components/layouts/${layoutName}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
}
|
||||
) as React.ComponentType<{ data: any }>;
|
||||
|
||||
layoutCache.set(layoutName, Layout);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(layoutPromises);
|
||||
} catch (error) {
|
||||
console.error('Error preloading layouts:', error);
|
||||
} finally {
|
||||
setIsPreloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getLayout = (layoutId: string): React.ComponentType<{ data: any }> | null => {
|
||||
const layoutName = idMapFileNames[layoutId];
|
||||
if (!layoutName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return cached layout if available
|
||||
if (layoutCache.has(layoutName)) {
|
||||
return layoutCache.get(layoutName)!;
|
||||
}
|
||||
|
||||
// Create and cache layout if not available
|
||||
const Layout = dynamic(
|
||||
() => import(`@/components/layouts/${layoutName}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
}
|
||||
) as React.ComponentType<{ data: any }>;
|
||||
|
||||
layoutCache.set(layoutName, Layout);
|
||||
return Layout;
|
||||
};
|
||||
|
||||
const clearCache = () => {
|
||||
layoutCache.clear();
|
||||
preloadedRef.current = false;
|
||||
};
|
||||
|
||||
return {
|
||||
getLayout,
|
||||
isPreloading,
|
||||
clearCache,
|
||||
cacheSize: layoutCache.size,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLayoutCache;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { LayoutList, ListTree, PanelRightOpen, X } from "lucide-react";
|
||||
import ToolTip from "@/components/ToolTip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -22,7 +22,7 @@ import {
|
|||
import { setPresentationData } from "@/store/slices/presentationGeneration";
|
||||
import { SortableSlide } from "./SortableSlide";
|
||||
import { SortableListItem } from "./SortableListItem";
|
||||
import { renderSlideContent } from "../../components/slide_config";
|
||||
import useLayoutCache from "../../hooks/useLayoutCache";
|
||||
|
||||
interface SidePanelProps {
|
||||
selectedSlide: number;
|
||||
|
|
@ -49,6 +49,18 @@ const SidePanel = ({
|
|||
(state: RootState) => state.theme
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const { getLayout } = useLayoutCache();
|
||||
|
||||
// Memoized slide renderer using layout cache
|
||||
const renderSlideContent = useMemo(() => {
|
||||
return (slide: any) => {
|
||||
const Layout = getLayout(slide.layout);
|
||||
if (!Layout) {
|
||||
return <div>Layout not found</div>;
|
||||
}
|
||||
return <Layout data={slide.content} />;
|
||||
};
|
||||
}, [getLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth < 768) {
|
||||
|
|
@ -56,7 +68,6 @@ const SidePanel = ({
|
|||
}
|
||||
}, [isMobilePanelOpen]);
|
||||
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
|
|
@ -71,7 +82,6 @@ const SidePanel = ({
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDragEnd = (event: any) => {
|
||||
const { active, over } = event;
|
||||
|
||||
|
|
@ -114,104 +124,73 @@ const SidePanel = ({
|
|||
presentationData?.slides.length === 0
|
||||
) {
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Toggle Button - Always visible when panel is closed */}
|
||||
{!isOpen && (
|
||||
<div className="hidden xl:block fixed left-4 top-1/2 -translate-y-1/2 z-50">
|
||||
<ToolTip content="Open Panel">
|
||||
<Button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="bg-white hover:bg-gray-50 shadow-lg"
|
||||
>
|
||||
<PanelRightOpen className="text-black" size={20} />
|
||||
</Button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Toggle Button */}
|
||||
{!isMobilePanelOpen && (
|
||||
<div className="xl:hidden fixed left-4 bottom-4 z-50">
|
||||
<ToolTip content="Show Panel">
|
||||
<Button
|
||||
onClick={() => setIsMobilePanelOpen(true)}
|
||||
className="bg-[#5146E5] text-white p-3 rounded-full shadow-lg"
|
||||
>
|
||||
<PanelRightOpen className="text-white" size={20} />
|
||||
</Button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
<div className="md:hidden fixed top-20 left-4 z-40">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsMobilePanelOpen(true)}
|
||||
className="bg-white shadow-md"
|
||||
>
|
||||
<PanelRightOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Backdrop for mobile */}
|
||||
{isMobilePanelOpen && (
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-black/50 z-30"
|
||||
onClick={() => setIsMobilePanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Side Panel */}
|
||||
<div
|
||||
className={`
|
||||
fixed xl:relative h-full z-50 xl:z-auto
|
||||
transition-all duration-300 ease-in-out
|
||||
${isOpen ? "ml-0" : "-ml-[300px]"}
|
||||
${isMobilePanelOpen
|
||||
? "translate-x-0"
|
||||
: "-translate-x-full xl:translate-x-0"
|
||||
}
|
||||
`}
|
||||
className={`${isOpen ? "w-72" : "w-0"} ${isMobilePanelOpen ? "translate-x-0" : "-translate-x-full"
|
||||
} md:translate-x-0 fixed md:relative left-0 top-0 h-full bg-white border-r border-gray-200 transition-all duration-300 ease-in-out z-40 md:z-auto flex-shrink-0`}
|
||||
style={{
|
||||
background: currentColors.background,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-theme={currentTheme}
|
||||
style={{
|
||||
backgroundColor: currentColors.slideBg,
|
||||
}}
|
||||
className="min-w-[300px] max-w-[300px] h-[calc(100vh-120px)] rounded-[20px] hide-scrollbar overflow-hidden slide-theme shadow-xl"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: currentColors.slideBg,
|
||||
}}
|
||||
className="sticky top-0 z-40 px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center justify-start gap-4">
|
||||
<ToolTip content="Image Preview">
|
||||
<Button
|
||||
className={`${active === "grid"
|
||||
? "bg-[#5141e5] hover:bg-[#4638c7]"
|
||||
: "bg-white hover:bg-white"
|
||||
}`}
|
||||
onClick={() => setActive("grid")}
|
||||
>
|
||||
<LayoutList
|
||||
className={`${active === "grid" ? "text-white" : "text-black"
|
||||
}`}
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
</ToolTip>
|
||||
<ToolTip content="List Preview">
|
||||
<Button
|
||||
className={`${active === "list"
|
||||
? "bg-[#5141e5] hover:bg-[#4638c7]"
|
||||
: "bg-white hover:bg-white"
|
||||
}`}
|
||||
onClick={() => setActive("list")}
|
||||
>
|
||||
<ListTree
|
||||
className={`${active === "list" ? "text-white" : "text-black"
|
||||
}`}
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
<X
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-color flex items-center justify-between">
|
||||
<h3 className="font-semibold slide-title">Slides</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<ToolTip content="List view">
|
||||
<Button
|
||||
variant={active === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setActive("list")}
|
||||
className="p-2"
|
||||
>
|
||||
<ListTree className="w-4 h-4" />
|
||||
</Button>
|
||||
</ToolTip>
|
||||
<ToolTip content="Grid view">
|
||||
<Button
|
||||
variant={active === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setActive("grid")}
|
||||
className="p-2"
|
||||
>
|
||||
<LayoutList className="w-4 h-4" />
|
||||
</Button>
|
||||
</ToolTip>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClose}
|
||||
className="text-[#6c7081] cursor-pointer hover:text-gray-600"
|
||||
size={20}
|
||||
/>
|
||||
className="md:hidden p-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
|
|
@ -274,7 +253,7 @@ const SidePanel = ({
|
|||
<div className=" bg-white relative overflow-hidden aspect-video">
|
||||
<div className="absolute bg-gray-100/5 z-40 top-0 left-0 w-full h-full" />
|
||||
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
|
||||
{renderSlideContent(slide, 'English')}
|
||||
{renderSlideContent(slide)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -294,6 +273,7 @@ const SidePanel = ({
|
|||
index={index}
|
||||
selectedSlide={selectedSlide}
|
||||
onSlideClick={onSlideClick}
|
||||
renderSlideContent={renderSlideContent}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState, useMemo } from "react";
|
||||
import { Slide } from "../../types/slide";
|
||||
import { Loader2, PlusIcon, Trash2, WandSparkles } from "lucide-react";
|
||||
import {
|
||||
|
|
@ -16,23 +16,18 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import { addSlide, updateSlide } from "@/store/slices/presentationGeneration";
|
||||
import NewSlide from "../../components/slide_layouts/NewSlide";
|
||||
import { getEmptySlideContent } from "../../utils/NewSlideContent";
|
||||
import useLayoutSchema from "../../hooks/useLayoutSchema";
|
||||
import dynamic from "next/dynamic";
|
||||
import useLayoutCache from "../../hooks/useLayoutCache";
|
||||
|
||||
interface SlideContentProps {
|
||||
slide: Slide;
|
||||
slide: any;
|
||||
index: number;
|
||||
|
||||
presentationId: string;
|
||||
|
||||
onDeleteSlide: (index: number) => void;
|
||||
}
|
||||
|
||||
|
||||
const SlideContent = ({
|
||||
slide,
|
||||
index,
|
||||
|
||||
presentationId,
|
||||
onDeleteSlide,
|
||||
}: SlideContentProps) => {
|
||||
|
|
@ -42,7 +37,16 @@ const SlideContent = ({
|
|||
const { presentationData, isStreaming } = useSelector(
|
||||
(state: RootState) => state.presentationGeneration
|
||||
);
|
||||
const { idMapFileNames, idMapSchema } = useLayoutSchema();
|
||||
const { getLayout } = useLayoutCache();
|
||||
|
||||
// Memoized layout component to prevent re-renders
|
||||
const LayoutComponent = useMemo(() => {
|
||||
const Layout = getLayout(slide.layout);
|
||||
if (!Layout) {
|
||||
return () => <div>Layout not found</div>;
|
||||
}
|
||||
return Layout;
|
||||
}, [slide.layout, getLayout]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const element = document.getElementById(
|
||||
|
|
@ -96,6 +100,7 @@ const SlideContent = ({
|
|||
dispatch(addSlide({ slide: newSlide, index: index + 1 }));
|
||||
setShowNewSlideSelection(false);
|
||||
};
|
||||
|
||||
// Scroll to the new slide when the presentationData is updated
|
||||
// useEffect(() => {
|
||||
// if (
|
||||
|
|
@ -114,17 +119,10 @@ const SlideContent = ({
|
|||
// }
|
||||
// }, [presentationData?.slides, isStreaming]);
|
||||
|
||||
const renderLayout = (slide: any) => {
|
||||
console.log(slide)
|
||||
console.log(idMapFileNames)
|
||||
const layoutName = idMapFileNames[slide.layout];
|
||||
if (!layoutName) {
|
||||
return <div>Layout not found</div>
|
||||
}
|
||||
console.log(layoutName)
|
||||
const Layout = dynamic(() => import(`@/components/layouts/${layoutName}`)) as React.ComponentType<{ data: any }>;
|
||||
return <Layout data={slide.content} />
|
||||
};
|
||||
// Memoized slide content rendering to prevent unnecessary re-renders
|
||||
const slideContent = useMemo(() => {
|
||||
return <LayoutComponent data={slide.content} />;
|
||||
}, [LayoutComponent, slide.content]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -137,7 +135,7 @@ const SlideContent = ({
|
|||
)}
|
||||
<div className={` w-full group `}>
|
||||
{/* render slides */}
|
||||
{renderLayout(slide)}
|
||||
{slideContent}
|
||||
|
||||
{!showNewSlideSelection && (
|
||||
<div className="group-hover:opacity-100 hidden md:block opacity-0 transition-opacity my-4 duration-300">
|
||||
|
|
@ -230,4 +228,13 @@ const SlideContent = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default SlideContent;
|
||||
export default React.memo(SlideContent, (prevProps, nextProps) => {
|
||||
// Only re-render if these specific props change
|
||||
return (
|
||||
prevProps.slide.layout === nextProps.slide.layout &&
|
||||
JSON.stringify(prevProps.slide.content) === JSON.stringify(nextProps.slide.content) &&
|
||||
prevProps.slide.index === nextProps.slide.index &&
|
||||
prevProps.index === nextProps.index &&
|
||||
prevProps.presentationId === nextProps.presentationId
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { renderSlideContent } from '../../components/slide_config';
|
||||
import { Slide } from '../../types/slide';
|
||||
import { useState } from 'react';
|
||||
|
||||
|
|
@ -9,9 +8,10 @@ interface SortableSlideProps {
|
|||
index: number;
|
||||
selectedSlide: number;
|
||||
onSlideClick: (index: number) => void;
|
||||
renderSlideContent: (slide: any) => React.ReactElement;
|
||||
}
|
||||
|
||||
export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: SortableSlideProps) {
|
||||
export function SortableSlide({ slide, index, selectedSlide, onSlideClick, renderSlideContent }: SortableSlideProps) {
|
||||
const [mouseDownTime, setMouseDownTime] = useState(0);
|
||||
|
||||
const {
|
||||
|
|
@ -57,7 +57,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor
|
|||
<div className=" slide-box relative overflow-hidden aspect-video">
|
||||
<div className="absolute bg-transparent z-40 top-0 left-0 w-full h-full" />
|
||||
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
|
||||
{renderSlideContent(slide, 'English')}
|
||||
{renderSlideContent(slide)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue