feat(nextjs): Image edit with object fit & focal point added

This commit is contained in:
shiva raj badu 2025-07-26 02:42:54 +05:45
parent b8c81a38d5
commit fc68faea43
No known key found for this signature in database
4 changed files with 53 additions and 53 deletions

View file

@ -2,7 +2,7 @@
import React, { ReactNode, useRef, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { updateSlideImage, updateSlideIcon } from '@/store/slices/presentationGeneration';
import { updateSlideImage, updateSlideIcon, updateImageProperties } from '@/store/slices/presentationGeneration';
import ImageEditor from './ImageEditor';
import IconsEditor from './IconsEditor';
@ -170,10 +170,10 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
htmlImg.setAttribute('data-editable-processed', 'true');
// Add a unique identifier to help with debugging
htmlImg.setAttribute('data-editable-id', `${type}-${dataPath}-${index}`);
htmlImg.setAttribute('data-editable-id', `${slideIndex}-${type}-${dataPath}-${index}`);
const editableElement: EditableElement = {
id: `${type}-${dataPath}-${index}`,
id: `${slideIndex}-${type}-${dataPath}-${index}`,
type,
src,
dataPath,
@ -192,9 +192,14 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
htmlImg.addEventListener('click', clickHandler);
const itemIndex = parseInt(`${slideIndex}-${type}-${dataPath}-${index}`.split('-').pop() || '0');
const properties = slideData.properties?.[itemIndex];
// Add hover effects without changing layout
htmlImg.style.cursor = 'pointer';
htmlImg.style.transition = 'opacity 0.2s, transform 0.2s';
htmlImg.style.objectFit = properties?.objectFit;
htmlImg.style.objectPosition = `${properties?.focusPoint?.x}% ${properties?.focusPoint?.y}%`;
const mouseEnterHandler = () => {
htmlImg.style.opacity = '0.8';
@ -327,16 +332,22 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
}
};
const handleFocusPointClick = (propertiesData: any) => {
console.log('activeEditor', activeEditor);
const id = activeEditor?.id;
const editableId = document.querySelector(`[data-editable-id="${id}"]`);
console.log('editableId', editableId);
if (editableId) {
const editableElement = editableId as HTMLImageElement;
editableElement.style.objectPosition = `${propertiesData.initialFocusPoint.x}px ${propertiesData.initialFocusPoint.y}px`;
editableElement.style.objectFit = propertiesData.initialObjectFit;
editableElement.style.objectPosition = `${propertiesData.initialFocusPoint.x}% ${propertiesData.initialFocusPoint.y}%`;
}
dispatch(updateImageProperties({
slideIndex,
itemIndex: parseInt(activeEditor?.id.split('-').pop() || '0'),
properties: propertiesData
}));
};
return (
@ -374,47 +385,3 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
export default EditableLayoutWrapper;
const setNestedImageValue = (obj: any, path: string, url: string, promptText?: string) => {
const keys = path.split(/[.\[\]]+/).filter(Boolean);
let current = obj;
// Navigate to the parent object
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (isNaN(Number(key))) {
if (!current[key]) {
current[key] = {};
}
current = current[key];
} else {
const index = Number(key);
if (!current[index]) {
current[index] = {};
}
current = current[index];
}
}
// 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] = updatedValue;
} else {
current[Number(finalKey)] = updatedValue;
}
// Add debugging
console.log('Redux: Updated slide image at path:', path, 'with URL:', url);
};

View file

@ -269,14 +269,14 @@ const ImageEditor = ({
<div className="mt-6">
<Tabs defaultValue="generate" className="w-full">
<TabsList className="grid bg-blue-100 border border-blue-300 w-full grid-cols-2 mx-auto">
<TabsList className="grid bg-blue-100 border border-blue-300 w-full grid-cols-3 mx-auto">
<TabsTrigger className="font-medium" value="generate">
AI Generate
</TabsTrigger>
<TabsTrigger className="font-medium" value="upload">
Upload
</TabsTrigger>
{/* <TabsTrigger className="font-medium" value="edit">Edit</TabsTrigger> */}
<TabsTrigger className="font-medium" value="edit">Edit</TabsTrigger>
</TabsList>
{/* Generate Tab */}
<TabsContent value="generate" className="mt-4 space-y-4">

View file

@ -32,7 +32,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
eventSource.addEventListener("response", (event) => {
const data = JSON.parse(event.data);
console.log('data', data);
switch (data.type) {
case "chunk":
accumulatedChunks += data.chunk;
@ -66,6 +66,15 @@ export const useOutlineStreaming = (presentationId: string | null) => {
setStreamState({ isStreaming: false, isLoading: false });
eventSource.close();
break;
case "error":
setStreamState({ isStreaming: false, isLoading: false });
eventSource.close();
toast.error('Error in outline streaming',
{
description: data.detail || 'Failed to connect to the server. Please try again.',
}
);
break;
}
});

View file

@ -289,6 +289,29 @@ const presentationGenerationSlice = createSlice({
}
},
updateImageProperties: (
state,
action: PayloadAction<{
slideIndex: number;
itemIndex: number;
properties: any;
}>
) => {
if (
state.presentationData &&
state.presentationData.slides &&
state.presentationData.slides[action.payload.slideIndex]
) {
const slide = state.presentationData.slides[action.payload.slideIndex];
const { itemIndex, properties } = action.payload;
slide['properties'] = {
...slide.properties,
[itemIndex]: properties
};
}
},
// Update slide icon at specific data path
updateSlideIcon: (
state,
@ -385,6 +408,7 @@ export const {
deletePresentationSlide,
updateSlideContent,
updateSlideImage,
updateImageProperties,
updateSlideIcon,
addNewSlide,
} = presentationGenerationSlice.actions;