diff --git a/servers/nextjs/app/(presentation-generator)/components/ChartEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/ChartEditor.tsx deleted file mode 100644 index f672d01d..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/ChartEditor.tsx +++ /dev/null @@ -1,505 +0,0 @@ -import React, { useState } from 'react'; -import { - Sheet, - SheetContent, - SheetTitle, - SheetHeader, -} from "@/components/ui/sheet"; -import { Button } from '@/components/ui/button'; -import { Plus, ChevronDown, Trash, BarChart3, PieChart as PieChartIcon, LineChart as LineChartIcon } from 'lucide-react'; -import { Input } from '@/components/ui/input'; -import { StoreChartData } from '../utils/chartDataTransforms'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, - DropdownMenuItem, -} from "@/components/ui/dropdown-menu"; -import { renderChart } from './slide_config'; -import { useSelector } from 'react-redux'; -import { RootState } from '@/store/store'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ChartSettings } from '@/store/slices/presentationGeneration'; - -interface ChartEditorProps { - isOpen: boolean; - onClose: () => void; - chartData: StoreChartData; - onChartDataChange: (newData: StoreChartData) => void; - chartSettings: ChartSettings; - setChartSettings: (newSettings: ChartSettings) => void; -} - -const ChartEditor = ({ isOpen, onClose, chartData, onChartDataChange, chartSettings, setChartSettings }: ChartEditorProps) => { - const [selectedCell, setSelectedCell] = useState<{ row: number; col: number } | null>(null); - const { currentColors } = useSelector((state: RootState) => state.theme); - - const handleCategoryChange = (index: number, value: string) => { - const newData = { - ...chartData, - data: { - ...chartData.data, - categories: [ - ...chartData.data.categories.slice(0, index), - value, - ...chartData.data.categories.slice(index + 1) - ] - } - }; - onChartDataChange(newData); - }; - - - const handleValueChange = (categoryIndex: number, seriesIndex: number, value: string) => { - const newData = { - ...chartData, - data: { - ...chartData.data, - series: chartData.data.series.map((series, idx) => { - if (idx === seriesIndex) { - return { - ...series, - data: [...series.data.slice(0, categoryIndex), Number(value), ...series.data.slice(categoryIndex + 1)] - }; - } - return series; - }) - } - }; - onChartDataChange(newData); - }; - - const addCategory = () => { - - const newData = { - ...chartData, - data: { - ...chartData.data, - categories: [...chartData.data.categories, ''], - series: chartData.data.series.map(series => ({ - ...series, - data: [...series.data, 0] - })) - } - }; - onChartDataChange(newData); - }; - - const addSeriesBefore = (index: number) => { - if (chartData.type === 'pie' && chartData.data.series.length >= 1) { - return; - } else { - if (chartData.data.series.length >= 4) { - return; - } - } - const newData = { - ...chartData, - data: { - ...chartData.data, - series: [ - ...chartData.data.series.slice(0, index), - { - name: `Series ${chartData.data.series.length + 1}`, - data: new Array(chartData.data.categories.length).fill(0) - }, - ...chartData.data.series.slice(index) - ] - } - }; - onChartDataChange(newData); - }; - - const addSeriesAfter = (index: number) => { - if (chartData.type === 'pie' && chartData.data.series.length >= 1) { - return; - } else { - if (chartData.data.series.length >= 4) { - return; - } - } - const newData = { - ...chartData, - data: { - ...chartData.data, - series: [ - ...chartData.data.series.slice(0, index + 1), - { - name: `Series ${chartData.data.series.length + 1}`, - data: new Array(chartData.data.categories.length).fill(0) - }, - ...chartData.data.series.slice(index + 1) - ] - } - }; - onChartDataChange(newData); - }; - - const removeCategory = (index: number) => { - const newData = { - ...chartData, - data: { - ...chartData.data, - categories: chartData.data.categories.filter((_, idx) => idx !== index), - series: chartData.data.series.map(series => ({ - ...series, - data: series.data.filter((_, idx) => idx !== index) - })) - } - }; - onChartDataChange(newData); - }; - - const removeSeries = (index: number) => { - const newData = { - ...chartData, - data: { - ...chartData.data, - series: chartData.data.series.filter((_, idx) => idx !== index) - } - }; - onChartDataChange(newData); - }; - - const getColumnLetter = (index: number) => { - return String.fromCharCode(65 + index); - }; - - const isColumnSelected = (colIndex: number) => { - return selectedCell?.col === colIndex; - }; - - const isRowSelected = (rowIndex: number) => { - return selectedCell?.row === rowIndex; - }; - - const isCellSelected = (rowIndex: number, colIndex: number) => { - return selectedCell?.row === rowIndex && selectedCell?.col === colIndex; - }; - const disableAddSeries = (chartType: string) => { - if (chartType === 'pie') { - return chartData.data.series.length >= 1; - } else { - return chartData.data.series.length >= 4; - } - } - - return ( - - e.preventDefault()}> - - Chart Editor - -
-
- {/* Spreadsheet Table */} -
-
- - - - - {/* First column for categories */} - - {/* Data columns for each series */} - {chartData && chartData.data.series && chartData.data.series.map((_, index) => ( - - ))} - - - {/* New row for series names */} - - - - {chartData.data.series.map((series, index) => ( - - ))} - - - - - - {chartData.data.categories.map((category, rowIndex) => ( - - {/* Row Numbers */} - - - {/* Category Cell */} - - - - {/* Series Data Cells */} - {/* series name */} - {chartData.data.series.map((series, seriesIndex) => ( - - ))} - - - - ))} - -
- -
- A -
-
-
- - {getColumnLetter(index + 1)} - - - - - - - addSeriesBefore(index)} disabled={disableAddSeries(chartData.type)}> - - Add Column before - - addSeriesAfter(index)} disabled={disableAddSeries(chartData.type)}> - - Add Column after - - removeSeries(index)}> - - Delete Column - - - -
-
- -
- { - const newSeries = chartData.data.series.map((s, i) => - i === index ? { ...s, name: e.target.value } : s - ); - onChartDataChange({ - ...chartData, - data: { - ...chartData.data, - series: newSeries - } - }); - }} - className="border-0 focus-visible:ring-0 focus:ring-0 h-7 text-[13px] bg-transparent" - /> -
- {rowIndex + 1} - setSelectedCell({ row: rowIndex, col: 0 })} - > - handleCategoryChange(rowIndex, e.target.value)} - className="border-0 focus-visible:ring-0 focus:ring-0 h-7 text-[13px] bg-transparent" - /> - setSelectedCell({ row: rowIndex, col: seriesIndex + 1 })} - > - handleValueChange(rowIndex, seriesIndex, e.target.value)} - className="border-0 focus-visible:ring-0 focus:ring-0 h-7 text-[13px] bg-transparent text-right" - /> - - -
- - {/* Add Row Button */} -
- -
-
-
-
- - {/* Add the chart preview section */} -
-

Preview

-
- {renderChart(chartData, false, currentColors, chartSettings)} -
- - {/* Add chart type selection */} -
-

Chart Type

-
- - - -
-
- {chartData.type !== 'line' && ( -
-
- - setChartSettings({ ...chartSettings, showDataLabel: checked })} - /> -
- - {chartSettings.showDataLabel && ( -
- - - - setChartSettings({ - ...chartSettings, dataLabel: { - ...chartSettings.dataLabel, - dataLabelPosition: 'Inside' - } - })} value="inside">Inside - setChartSettings({ - ...chartSettings, dataLabel: { - ...chartSettings.dataLabel, - dataLabelPosition: 'Outside' - } - })} value="outside">Outside - - {chartData.type === 'bar' && - - - - setChartSettings({ - ...chartSettings, dataLabel: { - ...chartSettings.dataLabel, - dataLabelAlignment: 'Base' - } - })} value="base">Base - setChartSettings({ - ...chartSettings, dataLabel: { - ...chartSettings.dataLabel, - dataLabelAlignment: 'Center' - } - })} value="center">Center - setChartSettings({ - ...chartSettings, dataLabel: { - ...chartSettings.dataLabel, - dataLabelAlignment: 'End' - } - })} value="end">End - - - } - -
- )} -
- )} -
- - setChartSettings({ ...chartSettings, showLegend: checked })} - /> -
- - {chartData.type !== 'pie' &&
- - setChartSettings({ ...chartSettings, showGrid: checked })} - /> -
} - - {chartData.type !== 'pie' &&
- - setChartSettings({ ...chartSettings, showAxisLabel: checked })} - /> -
} -
-
-
-
-
-
- ); -}; - -export default ChartEditor; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/components/EditableLayoutWrapper.tsx b/servers/nextjs/app/(presentation-generator)/components/EditableLayoutWrapper.tsx new file mode 100644 index 00000000..9e0117b2 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/components/EditableLayoutWrapper.tsx @@ -0,0 +1,321 @@ +"use client"; + +import React, { ReactNode, useRef, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { updateSlideImage, updateSlideIcon } from '@/store/slices/presentationGeneration'; +import ImageEditor from './ImageEditor'; +import IconsEditor from './IconsEditor'; + +interface EditableLayoutWrapperProps { + children: ReactNode; + slideIndex: number; + slideData: any; + isEditMode?: boolean; +} + +interface EditableElement { + id: string; + type: 'image' | 'icon'; + src: string; + dataPath: string; + data: any; + element: HTMLImageElement; +} + +const EditableLayoutWrapper: React.FC = ({ + children, + slideIndex, + slideData, + isEditMode = true, +}) => { + const dispatch = useDispatch(); + const containerRef = useRef(null); + const [editableElements, setEditableElements] = useState([]); + const [activeEditor, setActiveEditor] = useState(null); + + /** + * Recursively searches for image/icon data 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; + + // Check current level for __image_url__ or __icon_url__ + if (data.__image_url__ && isMatchingUrl(data.__image_url__, targetUrl)) { + return { path, type: 'image', data }; + } + + if (data.__icon_url__ && isMatchingUrl(data.__icon_url__, targetUrl)) { + return { path, type: 'icon', data }; + } + + // Recursively check nested objects and arrays + for (const [key, value] of Object.entries(data)) { + const newPath = path ? `${path}.${key}` : key; + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const result = findDataPath(targetUrl, value[i], `${newPath}[${i}]`); + if (result) return result; + } + } else if (value && typeof value === 'object') { + const result = findDataPath(targetUrl, value, newPath); + if (result) return result; + } + } + + return null; + }; + + /** + * Checks if two URLs match using various comparison strategies + */ + const isMatchingUrl = (url1: string, url2: string): boolean => { + if (!url1 || !url2) return false; + + // Direct match + if (url1 === url2) return true; + + // Remove protocol and domain differences + const cleanUrl1 = url1.replace(/^https?:\/\/[^\/]+/, '').replace(/^\/+/, ''); + const cleanUrl2 = url2.replace(/^https?:\/\/[^\/]+/, '').replace(/^\/+/, ''); + + 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')) { + const getFilename = (path: string) => path.split('/').pop() || ''; + const filename1 = getFilename(url1); + const filename2 = getFilename(url2); + if (filename1 === filename2 && filename1 !== '') return true; + } + + // Extract and compare filenames for other URLs + const getFilename = (path: string) => path.split('/').pop() || ''; + const filename1 = getFilename(url1); + const filename2 = getFilename(url2); + + if (filename1 === filename2 && filename1 !== '') { + return true; + } + + // Check if one URL is contained in another (for partial matches) + if (url1.includes(url2) || url2.includes(url1)) { + return true; + } + + return false; + }; + + /** + * Finds and processes images in the DOM, making them editable + */ + const findAndProcessImages = () => { + if (!containerRef.current || !isEditMode) return; + + const imgElements = containerRef.current.querySelectorAll('img:not([data-editable-processed])'); + const newEditableElements: EditableElement[] = []; + + imgElements.forEach((img, index) => { + const htmlImg = img as HTMLImageElement; + const src = htmlImg.src; + + if (src) { + const result = findDataPath(src, slideData); + + if (result) { + const { path: dataPath, type, data } = result; + + // Mark as processed to prevent re-processing + htmlImg.setAttribute('data-editable-processed', 'true'); + + const editableElement: EditableElement = { + id: `${type}-${dataPath}-${index}`, + type, + src, + dataPath, + data, + element: htmlImg + }; + + newEditableElements.push(editableElement); + + // Add click handler directly to the image + const clickHandler = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + setActiveEditor(editableElement); + }; + + htmlImg.addEventListener('click', clickHandler); + + // Add hover effects without changing layout + htmlImg.style.cursor = 'pointer'; + htmlImg.style.transition = 'filter 0.2s, transform 0.2s'; + + const mouseEnterHandler = () => { + htmlImg.style.filter = 'brightness(0.9)'; + + }; + + const mouseLeaveHandler = () => { + htmlImg.style.filter = 'brightness(1)'; + + }; + + htmlImg.addEventListener('mouseenter', mouseEnterHandler); + htmlImg.addEventListener('mouseleave', mouseLeaveHandler); + + // Store cleanup functions + (htmlImg as any)._editableCleanup = () => { + htmlImg.removeEventListener('click', clickHandler); + htmlImg.removeEventListener('mouseenter', mouseEnterHandler); + htmlImg.removeEventListener('mouseleave', mouseLeaveHandler); + htmlImg.style.cursor = ''; + htmlImg.style.transition = ''; + htmlImg.style.filter = ''; + htmlImg.style.transform = ''; + htmlImg.removeAttribute('data-editable-processed'); + }; + } + } + }); + + setEditableElements(prev => [...prev, ...newEditableElements]); + }; + + /** + * Cleanup function to remove event listeners and reset styles + */ + const cleanupElements = () => { + editableElements.forEach(({ element }) => { + if ((element as any)._editableCleanup) { + (element as any)._editableCleanup(); + } + }); + setEditableElements([]); + }; + + // Wait for LoadableComponent to render and then process images + useEffect(() => { + const timer = setTimeout(() => { + findAndProcessImages(); + }, 300); + + return () => { + clearTimeout(timer); + cleanupElements(); + }; + }, [slideData, children]); + + // Re-run when container content changes + useEffect(() => { + if (!containerRef.current) return; + + const observer = new MutationObserver((mutations) => { + const hasNewImages = mutations.some(mutation => + Array.from(mutation.addedNodes).some(node => + node.nodeType === Node.ELEMENT_NODE && + ( + (node as Element).tagName === 'IMG' || + (node as Element).querySelector('img:not([data-editable-processed])') + ) + ) + ); + + if (hasNewImages) { + setTimeout(findAndProcessImages, 100); + } + }); + + observer.observe(containerRef.current, { + childList: true, + subtree: true + }); + + return () => observer.disconnect(); + }, [slideData]); + + /** + * Handles closing the active editor + */ + const handleEditorClose = () => { + setActiveEditor(null); + }; + + /** + * Handles image change from ImageEditor + */ + const handleImageChange = (newImageUrl: string, prompt?: string) => { + if (activeEditor && activeEditor.element) { + // Update the DOM element immediately for visual feedback + activeEditor.element.src = newImageUrl; + + // Update Redux store + dispatch(updateSlideImage({ + slideIndex, + dataPath: activeEditor.dataPath, + imageUrl: newImageUrl, + prompt: prompt || activeEditor.data?.__image_prompt__ || '' + })); + + setActiveEditor(null); + } + }; + + /** + * Handles icon change from IconsEditor + */ + const handleIconChange = (newIconUrl: string, query?: string) => { + if (activeEditor && activeEditor.element) { + // Update the DOM element immediately for visual feedback + activeEditor.element.src = newIconUrl; + + // Update Redux store + dispatch(updateSlideIcon({ + slideIndex, + dataPath: activeEditor.dataPath, + iconUrl: newIconUrl, + query: query || activeEditor.data?.__icon_query__ || '' + })); + + setActiveEditor(null); + } + }; + + return ( +
+ {children} + + {/* Render ImageEditor when an image is being edited */} + {activeEditor && activeEditor.type === 'image' && ( + +
+ + )} + + {/* Render IconsEditor when an icon is being edited */} + {activeEditor && activeEditor.type === 'icon' && ( + +
+ + )} +
+ ); +}; + +export default EditableLayoutWrapper; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx index ca8d6e17..1cfb6568 100644 --- a/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx @@ -7,66 +7,52 @@ import { SheetTitle, } from "@/components/ui/sheet"; import { Input } from "@/components/ui/input"; -import { PlusIcon, Search } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { useDispatch, useSelector } from "react-redux"; -import { PresentationGenerationApi } from "../services/api/presentation-generation"; -import { RootState } from "@/store/store"; -import { usePathname, useSearchParams } from "next/navigation"; +import { Search } from "lucide-react"; +import { useSearchParams } from "next/navigation"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; -import { updateSlideIcon } from "@/store/slices/presentationGeneration"; +import { PresentationGenerationApi } from "../services/api/presentation-generation"; import { getStaticFileUrl } from "../utils/others"; interface IconsEditorProps { icon: string; index: number; - backgroundColor: string; - hasBg: boolean; - slideIndex: number; - elementId: string; - isWhite?: boolean; className?: string; icon_prompt?: string[] | null; onClose?: () => void; + onIconChange?: (newIconUrl: string, query?: string) => void; } const IconsEditor = ({ icon: initialIcon, - index, - backgroundColor, - hasBg, - className, - slideIndex, - elementId, icon_prompt, onClose, -}: IconsEditorProps) => { - const dispatch = useDispatch(); + onIconChange, +}: IconsEditorProps) => { + // State management const [icon, setIcon] = useState(initialIcon); const [icons, setIcons] = useState([]); - const [isEditorOpen, setIsEditorOpen] = useState(false); const [searchQuery, setSearchQuery] = useState( icon_prompt?.[0] || "" ); const [loading, setLoading] = useState(true); + const searchParams = useSearchParams(); + // Update local state when initial icon changes useEffect(() => { setIcon(initialIcon); }, [initialIcon]); + // Search for icons when component opens useEffect(() => { - if (isEditorOpen) { - handleIconSearch(); - } - }, [isEditorOpen]); - - const handleIconClick = () => { - setIsEditorOpen(true); - }; + handleIconSearch(); + }, []); + /** + * Searches for icons based on the current query + */ const handleIconSearch = async () => { setLoading(true); const presentation_id = searchParams.get("id"); @@ -88,94 +74,100 @@ const IconsEditor = ({ } }; + /** + * Handles icon selection and calls the parent callback + */ const handleIconChange = (newIcon: string) => { - - setIcon(newIcon); - dispatch( - updateSlideIcon({ index: slideIndex, iconIdx: index, icon: newIcon }) - ); - setIsEditorOpen(false); + + if (onIconChange) { + onIconChange(newIcon, searchQuery || icon_prompt?.[0] || ''); + } }; return ( - onClose?.()}> - e.preventDefault()} - onClick={(e) => e.stopPropagation()} - > - - Choose Icon - -
-
{ - e.preventDefault(); - e.stopPropagation(); - handleIconSearch(); - }} - > -
- +
- setSearchQuery(e.target.value)} - onClick={(e) => e.stopPropagation()} - className="pl-10" - /> -
- - +
+ + setSearchQuery(e.target.value)} + onClick={(e) => e.stopPropagation()} + className="pl-10" + /> +
+ + - {/* Icons grid */} -
- {loading ? ( -
- {Array.from({ length: 40 }).map((_, index) => ( - - ))} -
- ) : icons.length > 0 ? ( -
- {icons.map((iconSrc, idx) => ( -
{ - e.stopPropagation(); - handleIconChange(iconSrc); - }} - className="w-12 h-12 cursor-pointer group relative rounded-lg overflow-hidden hover:bg-gray-100 p-2" - > - {`Icon -
- ))} -
- ) : ( -
- -

No icons found for your search.

-

Try refining your search query.

-
- )} + {/* Icons Grid */} +
+ {loading ? ( +
+ {Array.from({ length: 40 }).map((_, index) => ( + + ))} +
+ ) : icons.length > 0 ? ( +
+ {icons.map((iconSrc, idx) => ( +
{ + e.stopPropagation(); + handleIconChange(iconSrc); + }} + className="w-12 h-12 cursor-pointer group relative rounded-lg overflow-hidden hover:bg-gray-100 p-2 transition-colors" + > + {`Icon +
+ ))} +
+ ) : ( +
+ +

No icons found for your search.

+

Try refining your search query.

+
+ )} +
-
- - + + +
); }; diff --git a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx index 14349032..cdf5a3c7 100644 --- a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx @@ -13,32 +13,25 @@ import { Wand2, Upload, Move, - } from "lucide-react"; import { cn } from "@/lib/utils"; -import { useDispatch, useSelector } from "react-redux"; +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 { - updateSlideImage, - updateSlideProperties, -} from "@/store/slices/presentationGeneration"; -import { getStaticFileUrl, ThemeImagePrompt } from "../utils/others"; - - +import { ThemeImagePrompt } from "../utils/others"; interface ImageEditorProps { initialImage: string | null; imageIdx?: number; - slideIndex: number; - className?: string; promptContent?: string; properties?: null | any; onClose?: () => void; + onImageChange?: (newImageUrl: string, prompt?: string) => void; + } const ImageEditor = ({ @@ -48,12 +41,13 @@ const ImageEditor = ({ promptContent, properties, onClose, + onImageChange, + }: ImageEditorProps) => { - const dispatch = useDispatch(); - const { currentTheme } = useSelector((state: RootState) => state.theme); - const searchParams = useSearchParams(); + + // State management const [image, setImage] = useState(initialImage); const [previewImages, setPreviewImages] = useState([initialImage]); const [prompt, setPrompt] = useState(""); @@ -62,6 +56,8 @@ const ImageEditor = ({ const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [uploadedImageUrl, setUploadedImageUrl] = useState(null); + + // Focus point and object fit for image editing const [isFocusPointMode, setIsFocusPointMode] = useState(false); const [focusPoint, setFocusPoint] = useState( (properties && @@ -77,11 +73,14 @@ const ImageEditor = ({ properties[imageIdx].initialObjectFit) || "cover" ); + + // Refs const imageRef = useRef(null); const imageContainerRef = useRef(null); const toolbarRef = useRef(null); const popoverContentRef = useRef(null); + // Update local state when initial image changes useEffect(() => { setImage(initialImage); setPreviewImages([initialImage]); @@ -97,9 +96,7 @@ const ImageEditor = ({ !toolbarRef.current.contains(event.target as Node) && !popoverContentRef.current ) { - if (isFocusPointMode) { - // saveFocusPoint(); // Save focus point before closing saveImageProperties(objectFit, focusPoint); } setIsFocusPointMode(false); @@ -110,21 +107,22 @@ const ImageEditor = ({ return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, [isFocusPointMode, focusPoint]); - - + }, [isFocusPointMode, focusPoint, objectFit]); + /** + * Handles image selection and calls the parent callback + */ const handleImageChange = (newImage: string) => { setImage(newImage); - dispatch( - updateSlideImage({ - index: slideIndex, - imageIdx: imageIdx, - image: newImage, - }) - ); + + if (onImageChange) { + onImageChange(newImage, promptContent); + } }; + /** + * Handles focus point adjustment when clicking on the image + */ const handleFocusPointClick = (e: React.MouseEvent) => { if (!isFocusPointMode || !imageRef.current) return; @@ -147,14 +145,19 @@ const ImageEditor = ({ } }; + /** + * Toggles focus point adjustment mode + */ const toggleFocusPointMode = () => { if (isFocusPointMode) { - // If turning off focus point mode, save the current focus point - // saveFocusPoint(); + saveImageProperties(objectFit, focusPoint); } setIsFocusPointMode(!isFocusPointMode); }; + /** + * Handles object fit change + */ const handleFitChange = (fit: "cover" | "contain" | "fill") => { setObjectFit(fit); @@ -162,10 +165,12 @@ const ImageEditor = ({ imageRef.current.style.objectFit = fit; } - // Save the fit change to your state saveImageProperties(fit, focusPoint); }; + /** + * Saves image properties (focus point and object fit) + */ const saveImageProperties = ( fit: "cover" | "contain" | "fill", focusPoint: { x: number; y: number } @@ -174,16 +179,12 @@ const ImageEditor = ({ initialObjectFit: fit, initialFocusPoint: focusPoint, }; - - dispatch( - updateSlideProperties({ - index: slideIndex, - itemIdx: imageIdx, - properties: propertiesData, - }) - ); + // TODO: Save to Redux store if needed }; + /** + * Generates new images using AI + */ const handleGenerateImage = async () => { try { setIsGenerating(true); @@ -208,26 +209,24 @@ const ImageEditor = ({ } }; + /** + * Handles file upload + */ const handleFileUpload = async ( event: React.ChangeEvent ) => { - const presentation_id = searchParams.get("id"); const file = event.target.files?.[0]; if (!file) return; - // Check file size (e.g., 5MB limit) + // Validate file size (5MB limit) if (file.size > 5 * 1024 * 1024) { - const error_message = "File size should be less than 5MB"; - - setUploadError(error_message); + setUploadError("File size should be less than 5MB"); return; } - // Check file type + // Validate file type if (!file.type.startsWith("image/")) { - const error_message = "Please upload an image file"; - - setUploadError(error_message); + setUploadError("Please upload an image file"); return; } @@ -249,356 +248,191 @@ const ImageEditor = ({ throw new Error(result.error || 'Upload failed'); } - // Update state with the returned path setUploadedImageUrl(result.filePath); } catch (err) { - const error_message = "Failed to upload image. Please try again."; - - setUploadError(error_message); + setUploadError("Failed to upload image. Please try again."); console.error("Upload error:", err); } finally { setIsUploading(false); } }; - - return ( - onClose?.()}> - e.preventDefault()} - onClick={(e) => e.stopPropagation()} - > - - Update Image - +
-
- - - - Edit - - - AI Generate - - - Upload - - - -
- {/* Current Image Preview */} -
-

Current Image

-
- {image ? ( - Current image { - e.stopPropagation(); - handleFocusPointClick(e); - }} - onError={(e) => { - console.error('Image failed to load:', image); - e.currentTarget.src = '/placeholder-image.png'; - }} - /> - ) : ( -
-
- -

No image selected

-
-
- )} + onClose?.()}> + e.preventDefault()} + onClick={(e) => e.stopPropagation()} + > + + Update Image + - {/* Focus Point Indicator */} - {isFocusPointMode && image && ( -
- )} -
- {/* Debug info */} - {image && ( -
-

Image Path: {image}

-

Resolved URL: {image}

-

Focus Point: {focusPoint.x.toFixed(1)}%, {focusPoint.y.toFixed(1)}%

-

Object Fit: {objectFit}

-
- )} -
- - {/* Editing Controls */} +
+ + + + AI Generate + + + Upload + + + {/* Generate Tab */} +
- {/* Focus Point Controls */} -
-
-

Focus Point

- -
- {isFocusPointMode && ( -

- Click on the image above to set the focus point -

- )} +
+

Current Prompt

+

{promptContent}

- {/* Object Fit Controls */} -
-

Image Fit

-
- - - -
-
-

Cover: Fill container, may crop image

-

Contain: Fit entire image, may show empty space

-

Fill: Stretch to fill container exactly

-
+
+

Image Description

+