refactor(nextjs): enhance image and icon handling in EditableLayoutWrapper

This commit is contained in:
shiva raj badu 2025-07-19 20:47:08 +05:45
parent 9cf5ba3906
commit 2d49e536ca
No known key found for this signature in database
12 changed files with 164 additions and 476 deletions

View file

@ -34,18 +34,20 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
const [activeEditor, setActiveEditor] = useState<EditableElement | null>(null);
/**
* Recursively searches for image/icon data in the slide data structure
* Recursively searches for ALL image/icon data paths in the slide data structure
*/
const findDataPath = (targetUrl: string, data: any, path: string = ''): { path: string; type: 'image' | 'icon'; data: any } | null => {
if (!data || typeof data !== 'object') return null;
const findAllDataPaths = (targetUrl: string, data: any, path: string = ''): { path: string; type: 'image' | 'icon'; data: any }[] => {
if (!data || typeof data !== 'object') return [];
const matches: { path: string; type: 'image' | 'icon'; data: any }[] = [];
// Check current level for __image_url__ or __icon_url__
if (data.__image_url__ && isMatchingUrl(data.__image_url__, targetUrl)) {
return { path, type: 'image', data };
matches.push({ path, type: 'image', data });
}
if (data.__icon_url__ && isMatchingUrl(data.__icon_url__, targetUrl)) {
return { path, type: 'icon', data };
matches.push({ path, type: 'icon', data });
}
// Recursively check nested objects and arrays
@ -54,16 +56,53 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
const result = findDataPath(targetUrl, value[i], `${newPath}[${i}]`);
if (result) return result;
const results = findAllDataPaths(targetUrl, value[i], `${newPath}[${i}]`);
matches.push(...results);
}
} else if (value && typeof value === 'object') {
const result = findDataPath(targetUrl, value, newPath);
if (result) return result;
const results = findAllDataPaths(targetUrl, value, newPath);
matches.push(...results);
}
}
return null;
return matches;
};
/**
* Finds the best matching data path for a specific DOM element
*/
const findBestDataPath = (targetUrl: string, imgElement: HTMLImageElement, data: any): { path: string; type: 'image' | 'icon'; data: any } | null => {
const allMatches = findAllDataPaths(targetUrl, data);
if (allMatches.length === 0) return null;
if (allMatches.length === 1) return allMatches[0];
// If multiple matches, use DOM position to find the correct one
const allImagesInContainer = containerRef.current?.querySelectorAll('img') || [];
const imgIndex = Array.from(allImagesInContainer).indexOf(imgElement);
// Find images with the same URL pattern
const sameUrlImages: HTMLImageElement[] = [];
allImagesInContainer.forEach((img) => {
if (isMatchingUrl((img as HTMLImageElement).src, targetUrl)) {
sameUrlImages.push(img as HTMLImageElement);
}
});
const sameUrlIndex = sameUrlImages.indexOf(imgElement);
// Try to match based on position in the same URL group
if (sameUrlIndex >= 0 && sameUrlIndex < allMatches.length) {
return allMatches[sameUrlIndex];
}
// Fallback: try to match based on overall DOM position
if (imgIndex >= 0 && imgIndex < allMatches.length) {
return allMatches[imgIndex];
}
// Last resort: return the first match
return allMatches[0];
};
/**
@ -81,30 +120,32 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
if (cleanUrl1 === cleanUrl2) return true;
// Handle app_data paths and placeholder URLs
if (url1.includes('/app_data/') || url2.includes('/app_data/') ||
url1.includes('placeholder') || url2.includes('placeholder')) {
// Handle placeholder URLs - be more specific
if ((url1.includes('placeholder') && url2.includes('placeholder')) ||
(url1.includes('/static/images/') && url2.includes('/static/images/'))) {
return url1 === url2; // Require exact match for placeholders
}
// Handle app_data paths - be more specific about filename matching
if (url1.includes('/app_data/') || url2.includes('/app_data/')) {
const getFilename = (path: string) => path.split('/').pop() || '';
const filename1 = getFilename(url1);
const filename2 = getFilename(url2);
if (filename1 === filename2 && filename1 !== '') return true;
if (filename1 === filename2 && filename1 !== '' && filename1.length > 10) { // Ensure significant filename
return true;
}
}
// Extract and compare filenames for other URLs
// Extract and compare filenames for other URLs - be more restrictive
const getFilename = (path: string) => path.split('/').pop() || '';
const filename1 = getFilename(url1);
const filename2 = getFilename(url2);
if (filename1 === filename2 && filename1 !== '') {
if (filename1 === filename2 && filename1 !== '' && filename1.length > 10) { // Ensure significant filename
return true;
}
// Check if one URL is contained in another (for partial matches)
if (url1.includes(url2) || url2.includes(url1)) {
return true;
}
return false;
return false; // Remove the overly permissive substring matching
};
/**
@ -121,7 +162,7 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
const src = htmlImg.src;
if (src) {
const result = findDataPath(src, slideData);
const result = findBestDataPath(src, htmlImg, slideData);
if (result) {
const { path: dataPath, type, data } = result;
@ -129,6 +170,9 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
// Mark as processed to prevent re-processing
htmlImg.setAttribute('data-editable-processed', 'true');
// Add a unique identifier to help with debugging
htmlImg.setAttribute('data-editable-id', `${type}-${dataPath}-${index}`);
const editableElement: EditableElement = {
id: `${type}-${dataPath}-${index}`,
type,
@ -248,6 +292,8 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
*/
const handleImageChange = (newImageUrl: string, prompt?: string) => {
if (activeEditor && activeEditor.element) {
// Update the DOM element immediately for visual feedback
activeEditor.element.src = newImageUrl;
@ -259,10 +305,8 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
prompt: prompt || activeEditor.data?.__image_prompt__ || ''
}));
setActiveEditor(null);
}
};
/**
* Handles icon change from IconsEditor
*/
@ -279,7 +323,6 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
query: query || activeEditor.data?.__icon_query__ || ''
}));
setActiveEditor(null);
}
};
@ -305,8 +348,6 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
{/* Render IconsEditor when an icon is being edited */}
{activeEditor && activeEditor.type === 'icon' && (
<IconsEditor
icon={activeEditor.src}
index={0}
icon_prompt={activeEditor.data?.__icon_query__ ? [activeEditor.data.__icon_query__] : []}
onClose={handleEditorClose}
onIconChange={handleIconChange}

View file

@ -8,23 +8,18 @@ import {
} from "@/components/ui/sheet";
import { Input } from "@/components/ui/input";
import { Search } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { PresentationGenerationApi } from "../services/api/presentation-generation";
import { getStaticFileUrl } from "../utils/others";
interface IconsEditorProps {
icon: string;
index: number;
className?: string;
icon_prompt?: string[] | null;
onClose?: () => void;
onIconChange?: (newIconUrl: string, query?: string) => void;
}
const IconsEditor = ({
icon: initialIcon,
icon_prompt,
onClose,
onIconChange,
@ -36,9 +31,7 @@ const IconsEditor = ({
icon_prompt?.[0] || ""
);
const [loading, setLoading] = useState(true);
const [isOpen, setIsOpen] = useState(true);
// Search for icons when component opens
useEffect(() => {
@ -59,7 +52,6 @@ const IconsEditor = ({
query,
limit: 40,
});
console.log("icons search data", data);
setIcons(data);
} catch (error) {
console.error("Error fetching icons:", error);
@ -79,11 +71,20 @@ const IconsEditor = ({
}
};
// Handle close with animation
const handleClose = () => {
setIsOpen(false);
// Delay the actual close to allow animation to complete
setTimeout(() => {
onClose?.();
}, 300); // Match the Sheet animation duration
};
return (
<div className="icons-editor-container">
<Sheet open={true} onOpenChange={() => onClose?.()}>
<Sheet open={isOpen} onOpenChange={() => handleClose()}>
<SheetContent
side="right"
className="w-[400px]"

View file

@ -12,15 +12,10 @@ import { Textarea } from "@/components/ui/textarea";
import {
Wand2,
Upload,
Move,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useSelector } from "react-redux";
import { PresentationGenerationApi } from "../services/api/presentation-generation";
import { RootState } from "@/store/store";
import { useSearchParams } from "next/navigation";
import { Skeleton } from "@/components/ui/skeleton";
import { ThemeImagePrompt } from "../utils/others";
interface ImageEditorProps {
initialImage: string | null;
@ -44,16 +39,15 @@ const ImageEditor = ({
onImageChange,
}: ImageEditorProps) => {
// State management
const [image, setImage] = useState(initialImage);
const [previewImages, setPreviewImages] = useState([initialImage]);
const [previewImages, setPreviewImages] = useState(initialImage);
const [prompt, setPrompt] = useState<string>("");
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(true);
// Focus point and object fit for image editing
const [isFocusPointMode, setIsFocusPointMode] = useState(false);
@ -71,6 +65,7 @@ const ImageEditor = ({
properties[imageIdx].initialObjectFit) ||
"cover"
);
console.log("previewImages", previewImages);
// Refs
const imageRef = useRef<HTMLImageElement>(null);
@ -78,12 +73,19 @@ const ImageEditor = ({
const toolbarRef = useRef<HTMLDivElement>(null);
const popoverContentRef = useRef<HTMLDivElement>(null);
// Update local state when initial image changes
useEffect(() => {
setImage(initialImage);
setPreviewImages([initialImage]);
setPreviewImages(initialImage);
}, [initialImage]);
// Handle close with animation
const handleClose = () => {
setIsOpen(false);
// Delay the actual close to allow animation to complete
setTimeout(() => {
onClose?.();
}, 300); // Match the Sheet animation duration
};
// Close toolbar when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@ -111,10 +113,11 @@ const ImageEditor = ({
* Handles image selection and calls the parent callback
*/
const handleImageChange = (newImage: string) => {
setImage(newImage);
if (onImageChange) {
onImageChange(newImage, promptContent);
setPreviewImages(newImage);
}
};
@ -188,7 +191,6 @@ const ImageEditor = ({
setError("Please enter a prompt");
return;
}
console.log("prompt", prompt);
try {
setIsGenerating(true);
setError(null);
@ -196,7 +198,7 @@ const ImageEditor = ({
prompt: prompt,
});
setPreviewImages(response.paths);
setPreviewImages(response);
} catch (err) {
console.error("Error in image generation", err);
setError("Failed to generate image. Please try again.");
@ -257,7 +259,7 @@ const ImageEditor = ({
<div className="image-editor-container">
<Sheet open={true} onOpenChange={() => onClose?.()}>
<Sheet open={isOpen} onOpenChange={() => handleClose()}>
<SheetContent
side="right"
className="w-[600px]"
@ -308,28 +310,26 @@ const ImageEditor = ({
{error && <p className="text-red-500 text-sm">{error}</p>}
<div className="grid grid-cols-2 gap-4">
{isGenerating || previewImages.length === 0
{isGenerating || !previewImages
? Array.from({ length: 4 }).map((_, index) => (
<Skeleton
key={index}
className="aspect-[4/3] w-full rounded-lg"
/>
))
: previewImages.map((image, index) => (
<div
key={index}
onClick={() => handleImageChange(image as string)}
className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer hover:border-blue-500 transition-colors"
>
{image && (
<img
src={image}
alt={`Preview ${index + 1}`}
className="w-full h-full object-cover"
/>
)}
</div>
))}
: <div
onClick={() => handleImageChange(previewImages)}
className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer hover:border-blue-500 transition-colors"
>
{previewImages && (
<img
src={previewImages}
alt={`Preview`}
className="w-full h-full object-cover"
/>
)}
</div>
}
</div>
</div>
</TabsContent>

View file

@ -1,329 +0,0 @@
"use client";
import React, { createContext, useContext, useRef, useEffect, ReactNode, useState } from 'react';
import ReactDOM from 'react-dom';
import { useDispatch } from 'react-redux';
import { updateSlideImage, updateSlideIcon } from '../../../store/slices/presentationGeneration';
import ImageEditor from './ImageEditor';
import IconsEditor from './IconsEditor';
interface EditableElement {
type: 'image' | 'icon';
element: HTMLElement;
dataPath?: string;
props: any;
}
interface SmartEditableContextType {
slideIndex: number;
slideId: string;
isEditMode: boolean;
slideData: any;
}
interface SmartEditableProviderProps {
children: ReactNode;
slideIndex: number;
slideId: string;
slideData: any;
isEditMode?: boolean;
}
const SmartEditableContext = createContext<SmartEditableContextType | null>(null);
export const useSmartEditable = () => {
const context = useContext(SmartEditableContext);
if (!context) {
throw new Error('useSmartEditable must be used within SmartEditableProvider');
}
return context;
};
export const SmartEditableProvider: React.FC<SmartEditableProviderProps> = ({
children,
slideIndex,
slideId,
slideData,
isEditMode = true,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [editableElements, setEditableElements] = useState<EditableElement[]>([]);
const [activeEditor, setActiveEditor] = useState<{
type: 'image' | 'icon';
element: HTMLElement;
props: any;
rect: DOMRect;
} | null>(null);
const dispatch = useDispatch();
useEffect(() => {
if (!isEditMode || !containerRef.current || !slideData) return;
const container = containerRef.current;
const findEditableElements = () => {
const elements: EditableElement[] = [];
console.log('🔍 Starting smart detection with slideData:', slideData);
// Detect Images and Icons only (text is now handled by TiptapTextReplacer)
const detectEditableElementsFromData = (data: any, path: string = '') => {
if (!data || typeof data !== 'object') return;
// Check for __image_url__ pattern
if (data.__image_url__) {
console.log(`📸 Found __image_url__ at ${path}:`, data.__image_url__);
const imgElement = findDOMElementByImageUrl(container, data.__image_url__);
if (imgElement) {
elements.push({
type: 'image',
element: imgElement,
dataPath: path,
props: {
slideIndex,
initialImage: data.__image_url__,
promptContent: data.__image_prompt__ || '',
imageIdx: elements.filter(e => e.type === 'image').length,
onImageChange: (newImageUrl: string, prompt?: string) => {
console.log(`🖼️ Image changed at ${path}:`, newImageUrl);
dispatch(updateSlideImage({
slideIndex,
dataPath: path,
imageUrl: newImageUrl,
prompt: prompt
}));
}
}
});
console.log(`✅ Matched image to DOM element:`, imgElement);
}
}
// Check for __icon_url__ pattern
if (data.__icon_url__) {
console.log(`🎯 Found __icon_url__ at ${path}:`, data.__icon_url__);
const imgElement = findDOMElementByImageUrl(container, data.__icon_url__);
if (imgElement) {
elements.push({
type: 'icon',
element: imgElement,
dataPath: path,
props: {
slideIndex,
elementId: `icon-${path.replace(/[^\w]/g, '-')}`,
icon: data.__icon_url__,
index: elements.filter(e => e.type === 'icon').length,
backgroundColor: '#3B82F6',
hasBg: false,
icon_prompt: data.__icon_query__ ? [data.__icon_query__] : [],
onIconChange: (newIconUrl: string, query?: string) => {
console.log(`🎯 Icon changed at ${path}:`, newIconUrl);
dispatch(updateSlideIcon({
slideIndex,
dataPath: path,
iconUrl: newIconUrl,
query: query
}));
}
}
});
console.log(`✅ Matched icon to DOM element:`, imgElement);
}
}
// Recursively scan nested objects and arrays
Object.keys(data).forEach(key => {
const value = data[key];
const newPath = path ? `${path}.${key}` : key;
if (Array.isArray(value)) {
value.forEach((item, index) => {
detectEditableElementsFromData(item, `${newPath}[${index}]`);
});
} else if (value && typeof value === 'object') {
detectEditableElementsFromData(value, newPath);
}
});
};
detectEditableElementsFromData(slideData);
console.log('🎉 Final detected elements:', elements);
setEditableElements(elements);
};
const findDOMElementByImageUrl = (container: HTMLElement, targetUrl: string): HTMLImageElement | null => {
const allImages = Array.from(container.getElementsByTagName('img'));
for (const img of allImages) {
if (isMatchingImageUrl(img.src, targetUrl)) {
return img;
}
}
return null;
};
const isMatchingImageUrl = (domSrc: string, dataSrc: string): boolean => {
// Direct match
if (domSrc === dataSrc) return true;
// Handle app_data paths
if (dataSrc.includes('/app_data/images/') || domSrc.includes('/app_data/images/')) {
const getFilename = (path: string) => path.split('/').pop() || '';
return getFilename(domSrc) === getFilename(dataSrc);
}
// Handle placeholder URLs
if (dataSrc.includes('placeholder') || domSrc.includes('placeholder')) {
return true;
}
// Extract and compare filenames
const getFilename = (path: string) => path.split('/').pop() || '';
return getFilename(domSrc) === getFilename(dataSrc) && getFilename(domSrc) !== '';
};
// Set up event listeners after elements are found
const timer = setTimeout(() => {
findEditableElements();
}, 500);
return () => {
clearTimeout(timer);
};
}, [slideIndex, slideId, slideData, isEditMode, dispatch]);
// Set up event listeners when editableElements change
useEffect(() => {
if (!containerRef.current || editableElements.length === 0) return;
const container = containerRef.current;
const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Handle image/icon clicks only
if (target.tagName === 'IMG') {
const imgElement = target as HTMLImageElement;
const editableElement = editableElements.find(el => el.element === imgElement);
if (editableElement && (editableElement.type === 'image' || editableElement.type === 'icon')) {
event.preventDefault();
event.stopPropagation();
const rect = imgElement.getBoundingClientRect();
setActiveEditor({
type: editableElement.type,
element: imgElement,
props: editableElement.props,
rect
});
}
}
};
const handleMouseEnter = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Handle image/icon hover only
if (target.tagName === 'IMG') {
const imgElement = target as HTMLImageElement;
const isEditable = editableElements.some(el => el.element === imgElement);
if (isEditable) {
imgElement.style.cursor = 'pointer';
imgElement.style.filter = 'brightness(0.9)';
imgElement.style.transition = 'filter 0.2s ease';
}
}
};
const handleMouseLeave = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Handle image/icon hover only
if (target.tagName === 'IMG') {
const imgElement = target as HTMLImageElement;
const isEditable = editableElements.some(el => el.element === imgElement);
if (isEditable) {
imgElement.style.filter = '';
}
}
};
container.addEventListener('click', handleClick);
container.addEventListener('mouseenter', handleMouseEnter, true);
container.addEventListener('mouseleave', handleMouseLeave, true);
return () => {
container.removeEventListener('click', handleClick);
container.removeEventListener('mouseenter', handleMouseEnter, true);
container.removeEventListener('mouseleave', handleMouseLeave, true);
};
}, [editableElements]);
return (
<SmartEditableContext.Provider value={{ slideIndex, slideId, isEditMode, slideData }}>
<div ref={containerRef} className="smart-editable-container">
{children}
</div>
{/* Render active editor as a modal/overlay */}
{activeEditor && (
<EditorOverlay
activeEditor={activeEditor}
onClose={() => setActiveEditor(null)}
/>
)}
</SmartEditableContext.Provider>
);
};
// Simple overlay component for editors
const EditorOverlay: React.FC<{
activeEditor: {
type: 'image' | 'icon';
element: HTMLElement;
props: any;
rect: DOMRect;
};
onClose: () => void;
}> = ({ activeEditor, onClose }) => {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
const handleClickOutside = (e: MouseEvent) => {
// Close if clicked outside the editor
const target = e.target as HTMLElement;
if (!target.closest('.editor-modal')) {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('keydown', handleEscape);
document.removeEventListener('click', handleClickOutside);
};
}, [onClose]);
// Handle image/icon editing in modal
const EditorComponent = activeEditor.type === 'image' ? ImageEditor : IconsEditor;
return ReactDOM.createPortal(
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div className="editor-modal">
<EditorComponent
{...activeEditor.props}
onClose={onClose}
/>
</div>
</div>,
document.body
);
};

View file

@ -166,7 +166,6 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
currentElement = currentElement.parentElement;
}
return false;
};

View file

@ -60,7 +60,6 @@ export const useGroupLayouts = () => {
isEditMode={isEditMode}
layout={Layout}
onContentChange={(content: string, dataPath: string, slideIndex?: number) => {
console.log(`Text content changed at slide ${slideIndex}, path ${dataPath}:`, content);
// Dispatch Redux action to update slide content
if (dataPath && slideIndex !== undefined) {

View file

@ -21,10 +21,7 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
const isDisabled =
loadingState.isLoading ||
streamState.isLoading ||
streamState.isStreaming ||
!outlines ||
outlines.length === 0 ||
!selectedLayoutGroup;
streamState.isStreaming
const getButtonText = () => {
if (loadingState.isLoading) return loadingState.message;

View file

@ -31,7 +31,8 @@ const OutlinePage: React.FC = () => {
const { loadingState, handleSubmit } = usePresentationGeneration(
presentation_id,
outlines,
selectedLayoutGroup
selectedLayoutGroup,
setActiveTab
);
if (!presentation_id) {

View file

@ -5,7 +5,7 @@ import { toast } from "@/hooks/use-toast";
import { clearPresentationData, setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { useLayout } from "../../context/LayoutContext";
import { LayoutGroup, LoadingState } from "../types/index";
import { LayoutGroup, LoadingState, TABS } from "../types/index";
const DEFAULT_LOADING_STATE: LoadingState = {
message: "",
@ -17,7 +17,8 @@ const DEFAULT_LOADING_STATE: LoadingState = {
export const usePresentationGeneration = (
presentationId: string | null,
outlines: SlideOutline[] | null,
selectedLayoutGroup: LayoutGroup | null
selectedLayoutGroup: LayoutGroup | null,
setActiveTab: (tab: string) => void
) => {
const dispatch = useDispatch();
const router = useRouter();
@ -69,7 +70,10 @@ export const usePresentationGeneration = (
}, [selectedLayoutGroup, getLayoutById]);
const handleSubmit = useCallback(async () => {
if (!selectedLayoutGroup) {
setActiveTab(TABS.LAYOUTS);
return;
}
if (!validateInputs()) return;

View file

@ -1,5 +1,5 @@
"use client";
import React, { useEffect, useState, useCallback, useRef } from "react";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { Skeleton } from "@/components/ui/skeleton";
@ -8,30 +8,21 @@ import { Skeleton } from "@/components/ui/skeleton";
import { DashboardApi } from "@/app/dashboard/api/dashboard";
import {
setPresentationData,
} from "@/store/slices/presentationGeneration";
import { toast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import { AlertCircle } from "lucide-react";
import { setThemeColors, ThemeColors } from "../store/themeSlice";
import { ThemeType } from "../upload/type";
import { renderSlideContent } from "../components/slide_config";
import { useGroupLayouts } from "../hooks/useGroupLayouts";
import { setPresentationData } from "@/store/slices/presentationGeneration";
const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const { renderSlideContent, loading } = useGroupLayouts();
const [contentLoading, setContentLoading] = useState(true);
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const { currentTheme, currentColors } = useSelector(
(state: RootState) => state.theme
);
const { presentationData } = useSelector(
(state: RootState) => state.presentationGeneration
);
@ -45,22 +36,8 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const fetchUserSlides = async () => {
try {
const data = await DashboardApi.getPresentation(presentation_id);
if (data) {
if (data.presentation.theme) {
dispatch(
setThemeColors({
...data.presentation.theme.colors,
theme: data.presentation.theme.name as ThemeType,
})
);
setColorsVariables(
data.presentation.theme.colors,
data.presentation.theme.name as ThemeType
);
}
dispatch(setPresentationData(data));
setLoading(false);
}
dispatch(setPresentationData(data));
setContentLoading(false);
} catch (error) {
setError(true);
toast({
@ -68,19 +45,11 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
description: "Failed to load presentation",
variant: "destructive",
});
console.error("Error fetching user slides:", error);
setLoading(false);
setContentLoading(false);
}
};
const setColorsVariables = (colors: ThemeColors, theme: ThemeType) => {
const root = document.documentElement;
Object.entries(colors).forEach(([key, value]) => {
const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
root.style.setProperty(`--${theme}-${cssKey}`, value);
});
};
const language = presentationData?.presentation?.language || "English";
console.log("presentationData", presentationData);
// Regular view
return (
<div className="flex overflow-hidden flex-col">
@ -109,15 +78,14 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
) : (
<div style={{
background: currentColors.background,
}} className="">
<div className="">
<div
className="mx-auto flex flex-col items-center overflow-hidden justify-center slide-theme"
data-theme={currentTheme}
className="mx-auto flex flex-col items-center overflow-hidden justify-center "
>
{!presentationData ||
loading ||
contentLoading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
<div className="relative w-full h-[calc(100vh-120px)] mx-auto ">
@ -136,10 +104,10 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
{presentationData &&
presentationData.slides &&
presentationData.slides.length > 0 &&
presentationData.slides.map((slide, index) => (
presentationData.slides.map((slide: any, index: number) => (
<div key={index} className="w-full">
{renderSlideContent(slide, language)}
{renderSlideContent(slide, false)}
</div>
))}
</>

View file

@ -52,7 +52,6 @@ export const PresentationCard = ({
description: "The presentation has been deleted successfully",
variant: "default",
});
// Call the onDeleted callback to update the parent state
if (onDeleted) {
onDeleted(id);
}
@ -63,7 +62,6 @@ export const PresentationCard = ({
variant: "destructive",
});
}
// Removed window.location.reload() - no longer needed
};

View file

@ -69,9 +69,6 @@ const presentationGenerationSlice = createSlice({
},
// Clear presentation data
clearPresentationData: (state) => {
state.presentation_id = null;
state.error = null;
state.isLoading = false;
state.presentationData = null;
},
clearOutlines: (state) => {
@ -238,17 +235,23 @@ const presentationGenerationSlice = createSlice({
// Set the image properties
const finalKey = keys[keys.length - 1];
const target = isNaN(Number(finalKey)) ? current[finalKey] : current[Number(finalKey)];
// Preserve existing properties if the target already exists
const updatedValue = {
...(target && typeof target === 'object' ? target : {}),
__image_url__: url,
__image_prompt__: promptText || (target?.__image_prompt__) || ''
};
if (isNaN(Number(finalKey))) {
current[finalKey] = {
__image_url__: url,
__image_prompt__: promptText || ''
};
current[finalKey] = updatedValue;
} else {
current[Number(finalKey)] = {
__image_url__: url,
__image_prompt__: promptText || ''
};
current[Number(finalKey)] = updatedValue;
}
// Add debugging
console.log('Redux: Updated slide image at path:', path, 'with URL:', url);
};
// Update the slide image
@ -308,17 +311,23 @@ const presentationGenerationSlice = createSlice({
// Set the icon properties
const finalKey = keys[keys.length - 1];
const target = isNaN(Number(finalKey)) ? current[finalKey] : current[Number(finalKey)];
// Preserve existing properties if the target already exists
const updatedValue = {
...(target && typeof target === 'object' ? target : {}),
__icon_url__: url,
__icon_query__: queryText || (target?.__icon_query__) || ''
};
if (isNaN(Number(finalKey))) {
current[finalKey] = {
__icon_url__: url,
__icon_query__: queryText || ''
};
current[finalKey] = updatedValue;
} else {
current[Number(finalKey)] = {
__icon_url__: url,
__icon_query__: queryText || ''
};
current[Number(finalKey)] = updatedValue;
}
// Add debugging
console.log('Redux: Updated slide icon at path:', path, 'with URL:', url);
};
// Update the slide icon