chore(nextjs)

This commit is contained in:
shiva raj badu 2025-07-16 22:08:28 +05:45
parent e084782328
commit df7102d18b
4 changed files with 189 additions and 116 deletions

View file

@ -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;

View file

@ -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>

View file

@ -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
);
});

View file

@ -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>