feat(nextjs): New slide add featture added
This commit is contained in:
parent
2d49e536ca
commit
5f58828d02
12 changed files with 138 additions and 232 deletions
|
|
@ -107,6 +107,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
const root = ReactDOM.createRoot(tiptapContainer);
|
||||
root.render(
|
||||
<TiptapText
|
||||
key={trimmedText}
|
||||
content={trimmedText}
|
||||
onContentChange={(content: string) => {
|
||||
if (dataPath && onContentChange) {
|
||||
|
|
|
|||
|
|
@ -1,152 +1,46 @@
|
|||
import { Trash2 } from 'lucide-react';
|
||||
import React from 'react'
|
||||
import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addNewSlide } from '@/store/slices/presentationGeneration';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface NewSlideProps {
|
||||
onSelectLayout: (type: number) => void;
|
||||
setShowNewSlideSelection: (show: boolean) => void;
|
||||
group: string;
|
||||
index: number;
|
||||
presentationId: string;
|
||||
}
|
||||
|
||||
const LayoutPreview = ({ type }: { type: string }) => {
|
||||
switch (type) {
|
||||
case 'type1':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex items-center gap-2">
|
||||
<div className="w-1/2 space-y-1.5">
|
||||
<div className="h-3 bg-gray-200 w-3/4"></div>
|
||||
<div className="h-2 bg-gray-100 w-3/4"></div>
|
||||
</div>
|
||||
<div className="w-1/2 h-full bg-gray-100 flex items-center justify-center">
|
||||
<p className='text-gray-500 text-sm'>image</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type2':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="flex gap-2 flex-1">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex-1 bg-gray-100 p-1.5">
|
||||
<div className="h-2 bg-gray-200 w-3/4 mb-1"></div>
|
||||
<div className="h-1.5 bg-gray-50 w-full"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type4':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="grid grid-cols-3 gap-2 flex-1">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-gray-100 p-1.5">
|
||||
<div className="h-8 bg-gray-200 mb-1 flex items-center justify-center">
|
||||
<p className='text-gray-500 text-xs'>image</p>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-50 w-3/4"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type5':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="h-2 bg-gray-200 w-1/2 "></div>
|
||||
<div className="w-full grid grid-cols-2 h-full items-center gap-2">
|
||||
<div className=" bg-gray-100 h-full w-full flex items-center justify-center">
|
||||
<p className='text-gray-500 text-xs'>chart</p>
|
||||
</div>
|
||||
<div className="h-4 bg-gray-100 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type6':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex gap-2">
|
||||
<div className="w-1/2 space-y-1.5">
|
||||
<div className="h-3 bg-gray-200 w-3/4"></div>
|
||||
<div className="h-2 bg-gray-100 w-full"></div>
|
||||
</div>
|
||||
<div className="w-1/2 space-y-1.5">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex gap-1.5 bg-gray-50 p-1.5">
|
||||
<div className="w-4 h-4 bg-gray-200 rounded-full flex-shrink-0"></div>
|
||||
<div className="flex-1">
|
||||
<div className="h-1.5 bg-gray-200 w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type7':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="flex justify-between px-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="text-center bg-gray-100 h-full flex flex-col items-center justify-center">
|
||||
<div className="w-4 h-4 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-1">
|
||||
<p className='text-gray-500 text-xs'>Icon</p>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-200 w-12 mb-1"></div>
|
||||
<div className="h-5 bg-gray-200 w-12"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type8':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex gap-2">
|
||||
<div className="w-1/2 space-y-1.5">
|
||||
<div className="h-3 bg-gray-200 w-3/4"></div>
|
||||
<div className="h-2 bg-gray-100 w-full"></div>
|
||||
</div>
|
||||
<div className="w-1/2 space-y-2">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex gap-1.5 bg-gray-50 p-1.5">
|
||||
<div className="w-6 h-6 bg-gray-200 flex-shrink-0 flex items-center justify-center">
|
||||
<p className='text-gray-500 text-[10px]'>Icon</p>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-1.5 bg-gray-200 w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type9':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex gap-2">
|
||||
<div className="w-1/2 space-y-1.5">
|
||||
<div className="h-2 bg-gray-200 w-3/4"></div>
|
||||
<div className="flex-1 bg-gray-100 h-full flex items-center justify-center">
|
||||
<p className='text-gray-500 text-sm'>chart</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 space-y-1.5">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex gap-1.5 bg-gray-50 p-1.5">
|
||||
<div className="w-4 h-4 bg-gray-200 rounded-full flex-shrink-0"></div>
|
||||
<div className="flex-1">
|
||||
<div className="h-1.5 bg-gray-200 w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
const NewSlide = ({ setShowNewSlideSelection, group, index, presentationId }: NewSlideProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleNewSlide = (sampleData: any, id: string) => {
|
||||
const newSlide = {
|
||||
id: crypto.randomUUID(),
|
||||
index: index,
|
||||
content: sampleData,
|
||||
layout_group: group,
|
||||
layout: `${group}:${id}`,
|
||||
presentation: presentationId
|
||||
}
|
||||
dispatch(addNewSlide({ slideData: newSlide, index }));
|
||||
setShowNewSlideSelection(false);
|
||||
}
|
||||
const { layoutGroup, loading } = useGroupLayoutLoader(group)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className='my-6 w-full bg-gray-50 p-8 max-w-[1280px]'>
|
||||
<div className='flex justify-between items-center mb-8'>
|
||||
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
|
||||
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
|
||||
</div>
|
||||
<div className='flex items-center justify-center h-32'>
|
||||
<Loader2 className="w-8 h-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const NewSlide = ({ onSelectLayout, setShowNewSlideSelection }: NewSlideProps) => {
|
||||
return (
|
||||
<div className='my-6 w-full bg-gray-50 p-8 max-w-[1280px]'>
|
||||
<div className='flex justify-between items-center mb-8'>
|
||||
|
|
@ -155,22 +49,17 @@ const NewSlide = ({ onSelectLayout, setShowNewSlideSelection }: NewSlideProps) =
|
|||
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
|
||||
</div>
|
||||
<div className='grid grid-cols-4 gap-4'>
|
||||
{['type1', 'type2', 'type4', 'type5', 'type6', 'type7', 'type8', 'type9'].map((type) => (
|
||||
<div
|
||||
key={type}
|
||||
className="transform hover:scale-105 transition-transform cursor-pointer"
|
||||
onClick={() => onSelectLayout(parseInt(type.replace('type', '')))}
|
||||
>
|
||||
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div className="p-2 border-b">
|
||||
<h3 className="text-xs font-medium">Layout {type.replace('type', '')}</h3>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<LayoutPreview type={type} />
|
||||
{layoutGroup && layoutGroup?.layouts.map((layout: any, index: number) => {
|
||||
const { component: LayoutComponent, sampleData, layoutId } = layout
|
||||
return (
|
||||
<div onClick={() => handleNewSlide(sampleData, layoutId)} key={`${layoutGroup?.groupName}-${index}`} className=" relative cursor-pointer overflow-hidden aspect-video">
|
||||
<div className="absolute cursor-pointer 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%]">
|
||||
<LayoutComponent data={sampleData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const useGroupLayouts = () => {
|
|||
|
||||
// Render slide content with group validation, automatic Tiptap text editing, and editable images/icons
|
||||
const renderSlideContent = useMemo(() => {
|
||||
return (slide: any, isEditMode: boolean = true) => {
|
||||
return (slide: any, isEditMode: boolean) => {
|
||||
const Layout = getGroupLayout(slide.layout, slide.layout_group);
|
||||
if (!Layout) {
|
||||
return (
|
||||
|
|
@ -55,6 +55,7 @@ export const useGroupLayouts = () => {
|
|||
isEditMode={isEditMode}
|
||||
>
|
||||
<TiptapTextReplacer
|
||||
key={slide.id}
|
||||
slideData={slide.content}
|
||||
slideIndex={slide.index}
|
||||
isEditMode={isEditMode}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
|
|||
});
|
||||
|
||||
// Custom hooks
|
||||
const { fetchUserSlides, handleDeleteSlide } = usePresentationData(
|
||||
const { fetchUserSlides } = usePresentationData(
|
||||
presentation_id,
|
||||
setLoading,
|
||||
setError
|
||||
|
|
@ -72,9 +72,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
|
|||
fetchUserSlides
|
||||
);
|
||||
|
||||
const onDeleteSlide = (index: number) => {
|
||||
handleDeleteSlide(index, presentationData);
|
||||
};
|
||||
|
||||
|
||||
const onSlideChange = (newSlide: number) => {
|
||||
handleSlideChange(newSlide, presentationData);
|
||||
|
|
@ -172,7 +170,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
|
|||
slide={slide}
|
||||
index={index}
|
||||
presentationId={presentation_id}
|
||||
onDeleteSlide={onDeleteSlide}
|
||||
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,23 +13,21 @@ import { PresentationGenerationApi } from "../../services/api/presentation-gener
|
|||
import ToolTip from "@/components/ToolTip";
|
||||
import { RootState } from "@/store/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { addSlide, updateSlide } from "@/store/slices/presentationGeneration";
|
||||
import { deletePresentationSlide, updateSlide } from "@/store/slices/presentationGeneration";
|
||||
import NewSlide from "../../components/slide_layouts/NewSlide";
|
||||
import { getEmptySlideContent } from "../../utils/NewSlideContent";
|
||||
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
|
||||
|
||||
interface SlideContentProps {
|
||||
slide: any;
|
||||
index: number;
|
||||
presentationId: string;
|
||||
onDeleteSlide: (index: number) => void;
|
||||
}
|
||||
|
||||
const SlideContent = ({
|
||||
slide,
|
||||
index,
|
||||
presentationId,
|
||||
onDeleteSlide,
|
||||
|
||||
}: SlideContentProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
|
@ -82,29 +80,16 @@ const SlideContent = ({
|
|||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewSlide = (type: number, index: number) => {
|
||||
const newSlide: Slide = getEmptySlideContent(
|
||||
type,
|
||||
index + 1,
|
||||
presentationData?.id!
|
||||
);
|
||||
|
||||
dispatch(addSlide({ slide: newSlide, index: index + 1 }));
|
||||
setShowNewSlideSelection(false);
|
||||
|
||||
// Scroll to the newly added slide after a short delay to ensure it's rendered
|
||||
setTimeout(() => {
|
||||
const newSlideElement = document.getElementById(`slide-${newSlide.id}`);
|
||||
if (newSlideElement) {
|
||||
newSlideElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
const onDeleteSlide = async () => {
|
||||
try {
|
||||
dispatch(deletePresentationSlide(slide.index));
|
||||
} catch (error) {
|
||||
console.error("Error deleting slide:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Scroll to the new slide when streaming and new slides are being generated
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -139,16 +124,16 @@ const SlideContent = ({
|
|||
{isStreaming && (
|
||||
<Loader2 className="w-8 h-8 absolute right-2 top-2 z-30 text-blue-800 animate-spin" />
|
||||
)}
|
||||
<div data-layout={slide.layout} data-group={slide.layout_group} className={` w-full group mb-6`}>
|
||||
<div data-layout={slide.layout} data-group={slide.layout_group} className={` w-full group `}>
|
||||
{/* render slides */}
|
||||
{loading ? <div className="flex flex-col bg-white aspect-video items-center justify-center h-full">
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
</div> : slideContent}
|
||||
|
||||
{/* {!showNewSlideSelection && (
|
||||
{!showNewSlideSelection && (
|
||||
<div className="group-hover:opacity-100 hidden md:block opacity-0 transition-opacity my-4 duration-300">
|
||||
<ToolTip content="Add new slide below">
|
||||
{!isStreaming && (
|
||||
{!isStreaming && !loading && (
|
||||
<div
|
||||
onClick={() => setShowNewSlideSelection(true)}
|
||||
className=" bg-white shadow-md w-[80px] py-2 border hover:border-[#5141e5] duration-300 flex items-center justify-center rounded-lg cursor-pointer mx-auto"
|
||||
|
|
@ -159,16 +144,18 @@ const SlideContent = ({
|
|||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
{showNewSlideSelection && (
|
||||
{showNewSlideSelection && !loading && (
|
||||
<NewSlide
|
||||
onSelectLayout={(type) => handleNewSlide(type, slide.index)}
|
||||
index={index}
|
||||
group={slide.layout_group}
|
||||
setShowNewSlideSelection={setShowNewSlideSelection}
|
||||
presentationId={presentationId}
|
||||
/>
|
||||
)} */}
|
||||
{!isStreaming && (
|
||||
)}
|
||||
{!isStreaming && !loading && (
|
||||
<ToolTip content="Delete slide">
|
||||
<div
|
||||
onClick={() => onDeleteSlide(slide.index)}
|
||||
onClick={onDeleteSlide}
|
||||
className="absolute top-2 z-20 sm:top-4 right-2 sm:right-4 hidden md:block transition-transform"
|
||||
>
|
||||
<Trash2 className="text-gray-500 text-xl cursor-pointer" />
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { useCallback } from "react";
|
|||
import { useDispatch } from "react-redux";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { DashboardApi } from "@/app/dashboard/api/dashboard";
|
||||
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
|
||||
import { setPresentationData, deletePresentationSlide } from "@/store/slices/presentationGeneration";
|
||||
import { setPresentationData } from "@/store/slices/presentationGeneration";
|
||||
|
||||
export const usePresentationData = (
|
||||
presentationId: string,
|
||||
|
|
@ -32,25 +31,10 @@ export const usePresentationData = (
|
|||
}
|
||||
}, [presentationId, dispatch, setLoading, setError]);
|
||||
|
||||
const handleDeleteSlide = useCallback(async (index: number, presentationData: any) => {
|
||||
dispatch(deletePresentationSlide(index));
|
||||
try {
|
||||
await PresentationGenerationApi.deleteSlide(
|
||||
presentationId,
|
||||
presentationData?.slides[index].id!
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error deleting slide:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to delete slide",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [presentationId, dispatch]);
|
||||
|
||||
|
||||
return {
|
||||
fetchUserSlides,
|
||||
handleDeleteSlide,
|
||||
|
||||
};
|
||||
};
|
||||
|
|
@ -63,11 +63,6 @@ export const PresentationCard = ({
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={handlePreview}
|
||||
|
|
@ -82,11 +77,11 @@ export const PresentationCard = ({
|
|||
{new Date(created_at).toLocaleDateString()}
|
||||
</p>
|
||||
<Popover>
|
||||
<PopoverTrigger onClick={(e) => e.stopPropagation()}>
|
||||
<button className="w-6 h-6 rounded-full flex items-center justify-center text-gray-500 hover:text-gray-700" >
|
||||
<PopoverTrigger className="w-6 h-6 rounded-full flex items-center justify-center text-gray-500 hover:text-gray-700" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
|
||||
<DotsVerticalIcon className="w-4 h-4 text-gray-500" />
|
||||
|
||||
<DotsVerticalIcon className="w-4 h-4 text-gray-500" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="bg-white w-[200px]">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const GroupLayoutPreview = () => {
|
|||
{/* Layout Grid */}
|
||||
<main className="max-w-7xl mx-auto px-6 py-8">
|
||||
<div className="space-y-8">
|
||||
{layoutGroup.layouts.map((layout, index) => {
|
||||
{layoutGroup.layouts.map((layout: any, index: number) => {
|
||||
const { component: LayoutComponent, sampleData, name, fileName } = layout
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
|
@ -11,16 +11,34 @@ interface UseGroupLayoutLoaderReturn {
|
|||
retry: () => void
|
||||
}
|
||||
|
||||
// Global cache to store layout groups and avoid re-fetching
|
||||
const layoutGroupCache = new Map<string, LayoutGroup>()
|
||||
const loadingGroupsCache = new Set<string>()
|
||||
|
||||
export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => {
|
||||
const [layoutGroup, setLayoutGroup] = useState<LayoutGroup | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const hasMountedRef = useRef(false)
|
||||
|
||||
const loadGroupLayouts = async () => {
|
||||
// Check cache first
|
||||
if (layoutGroupCache.has(groupSlug)) {
|
||||
setLayoutGroup(layoutGroupCache.get(groupSlug)!)
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent multiple simultaneous requests for the same group
|
||||
if (loadingGroupsCache.has(groupSlug)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setLayoutGroup(null)
|
||||
loadingGroupsCache.add(groupSlug)
|
||||
|
||||
const response = await fetch('/api/layouts')
|
||||
if (!response.ok) {
|
||||
|
|
@ -76,6 +94,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
|
|||
|
||||
// Use empty object to let schema apply its default values
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
|
|
@ -83,7 +102,8 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
|
|||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: targetGroupData.groupName
|
||||
groupName: targetGroupData.groupName,
|
||||
layoutId
|
||||
}
|
||||
|
||||
groupLayouts.push(layoutInfo)
|
||||
|
|
@ -98,13 +118,15 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
|
|||
|
||||
if (module.default && module.Schema) {
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
component: module.default,
|
||||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: targetGroupData.groupName
|
||||
groupName: targetGroupData.groupName,
|
||||
layoutId
|
||||
}
|
||||
groupLayouts.push(layoutInfo)
|
||||
} else {
|
||||
|
|
@ -123,11 +145,15 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
|
|||
})
|
||||
setError(`No valid layouts found in "${groupSlug}" group.`)
|
||||
} else {
|
||||
setLayoutGroup({
|
||||
const group: LayoutGroup = {
|
||||
groupName: targetGroupData.groupName,
|
||||
layouts: groupLayouts,
|
||||
settings: groupSettings
|
||||
})
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
layoutGroupCache.set(groupSlug, group)
|
||||
setLayoutGroup(group)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
|
|
@ -136,15 +162,19 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
|
|||
setError(error instanceof Error ? error.message : 'Failed to load group layouts')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
loadingGroupsCache.delete(groupSlug)
|
||||
}
|
||||
}
|
||||
|
||||
const retry = () => {
|
||||
// Clear cache for this group to force reload
|
||||
layoutGroupCache.delete(groupSlug)
|
||||
loadGroupLayouts()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (groupSlug) {
|
||||
if (groupSlug && !hasMountedRef.current) {
|
||||
hasMountedRef.current = true
|
||||
loadGroupLayouts()
|
||||
}
|
||||
}, [groupSlug])
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
// Use empty object to let schema apply its default values
|
||||
// User will need to provide actual data when using the layouts
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
|
|
@ -80,7 +81,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: groupData.groupName
|
||||
groupName: groupData.groupName,
|
||||
layoutId
|
||||
}
|
||||
|
||||
groupLayouts.push(layoutInfo)
|
||||
|
|
@ -97,13 +99,15 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
if (module.default && module.Schema) {
|
||||
// Use empty object to let schema apply its default values
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
component: module.default,
|
||||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: groupData.groupName
|
||||
groupName: groupData.groupName,
|
||||
layoutId
|
||||
}
|
||||
groupLayouts.push(layoutInfo)
|
||||
allLayouts.push(layoutInfo)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface LayoutInfo {
|
|||
sampleData: any
|
||||
fileName: string
|
||||
groupName: string
|
||||
layoutId: string
|
||||
}
|
||||
|
||||
export interface GroupSetting {
|
||||
|
|
|
|||
|
|
@ -193,6 +193,21 @@ const presentationGenerationSlice = createSlice({
|
|||
}
|
||||
},
|
||||
|
||||
addNewSlide: (state, action: PayloadAction<{ slideData: any; index: number }>) => {
|
||||
if (state.presentationData?.slides) {
|
||||
// Insert the new slide at the specified index + 1 (after current slide)
|
||||
state.presentationData.slides.splice(action.payload.index +1, 0, action.payload.slideData);
|
||||
|
||||
// Update indices for all slides to ensure they remain sequential
|
||||
state.presentationData.slides = state.presentationData.slides.map(
|
||||
(slide: any, idx: number) => ({
|
||||
...slide,
|
||||
index: idx,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Update slide image at specific data path
|
||||
updateSlideImage: (
|
||||
state,
|
||||
|
|
@ -365,6 +380,7 @@ export const {
|
|||
updateSlideContent,
|
||||
updateSlideImage,
|
||||
updateSlideIcon,
|
||||
addNewSlide,
|
||||
} = presentationGenerationSlice.actions;
|
||||
|
||||
export default presentationGenerationSlice.reducer;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue