feat(Nextjs): enchance editableLayoutWrapper

This commit is contained in:
shiva raj badu 2025-07-25 23:58:15 +05:45
parent ec398bbbee
commit 9ce7ed23ee
No known key found for this signature in database
10 changed files with 221 additions and 96 deletions

View file

@ -194,15 +194,15 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
// Add hover effects without changing layout
htmlImg.style.cursor = 'pointer';
htmlImg.style.transition = 'filter 0.2s, transform 0.2s';
htmlImg.style.transition = 'opacity 0.2s, transform 0.2s';
const mouseEnterHandler = () => {
htmlImg.style.filter = 'brightness(0.9)';
htmlImg.style.opacity = '0.8';
};
const mouseLeaveHandler = () => {
htmlImg.style.filter = 'brightness(1)';
htmlImg.style.opacity = '1';
};
@ -216,7 +216,7 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
htmlImg.removeEventListener('mouseleave', mouseLeaveHandler);
htmlImg.style.cursor = '';
htmlImg.style.transition = '';
htmlImg.style.filter = '';
htmlImg.style.opacity = '';
htmlImg.style.transform = '';
htmlImg.removeAttribute('data-editable-processed');
};
@ -326,6 +326,18 @@ 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;
}
};
return (
<div ref={containerRef} className="editable-layout-wrapper">
@ -341,6 +353,7 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
properties={null}
onClose={handleEditorClose}
onImageChange={handleImageChange}
onFocusPointClick={handleFocusPointClick}
>
</ImageEditor>
)}
@ -359,4 +372,49 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
);
};
export default EditableLayoutWrapper;
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

@ -27,7 +27,7 @@ interface ImageEditorProps {
properties?: null | any;
onClose?: () => void;
onImageChange?: (newImageUrl: string, prompt?: string) => void;
onFocusPointClick?: (propertiesData: any) => void;
}
const ImageEditor = ({
@ -36,6 +36,7 @@ const ImageEditor = ({
promptContent,
properties,
onClose,
onFocusPointClick,
onImageChange,
}: ImageEditorProps) => {
@ -69,9 +70,6 @@ const ImageEditor = ({
// Refs
const imageRef = useRef<HTMLImageElement>(null);
const imageContainerRef = useRef<HTMLDivElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const popoverContentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setPreviewImages(initialImage);
@ -105,28 +103,7 @@ const ImageEditor = ({
}
}
// Close toolbar when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
imageContainerRef.current &&
!imageContainerRef.current.contains(event.target as Node) &&
toolbarRef.current &&
!toolbarRef.current.contains(event.target as Node) &&
!popoverContentRef.current
) {
if (isFocusPointMode) {
saveImageProperties(objectFit, focusPoint);
}
setIsFocusPointMode(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isFocusPointMode, focusPoint, objectFit]);
/**
* Handles image selection and calls the parent callback
@ -200,6 +177,7 @@ const ImageEditor = ({
initialFocusPoint: focusPoint,
};
// TODO: Save to Redux store if needed
onFocusPointClick?.(propertiesData);
};
/**
@ -298,6 +276,7 @@ const ImageEditor = ({
<TabsTrigger className="font-medium" value="upload">
Upload
</TabsTrigger>
{/* <TabsTrigger className="font-medium" value="edit">Edit</TabsTrigger> */}
</TabsList>
{/* Generate Tab */}
<TabsContent value="generate" className="mt-4 space-y-4">
@ -455,6 +434,79 @@ const ImageEditor = ({
)}
</div>
</TabsContent>
<TabsContent value="edit" className="mt-4 space-y-4">
<div className="space-y-4">
<h3 className="text-sm font-medium mb-2">Current Image</h3>
<div onClick={(e) => {
if (isFocusPointMode) {
handleFocusPointClick(e);
} else {
}
}}
className="aspect-[4/3] group rounded-lg overflow-hidden relative border border-gray-200">
<p className="group-hover:opacity-100 opacity-0 transition-opacity absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm text-center font-medium bg-black/50 text-white px-2 py-1 rounded">Click to Change Focus Point</p>
{previewImages && <img ref={imageRef} onClick={
() => {
setIsFocusPointMode(true);
}
} src={previewImages} style={{ objectFit: objectFit, objectPosition: `${focusPoint.x}% ${focusPoint.y}%`, }} alt={`Preview`} className="w-full h-full " />}
{isFocusPointMode && <div className="absolute inset-0 bg-black/20 flex items-center justify-center">
<div className="text-white text-center p-2 bg-black/50 rounded">
<p className="text-sm font-medium pointer-events-none">
Click anywhere to set focus point
</p>
<button
className="mt-2 px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600"
onClick={(e) => {
e.stopPropagation();
toggleFocusPointMode();
}}
>
Done
</button>
</div>
<div
className="absolute w-8 h-8 border-2 border-white rounded-full transform -translate-x-1/2 -translate-y-1/2 pointer-events-none"
style={{
left: `${focusPoint.x}%`,
top: `${focusPoint.y}%`,
boxShadow: "0 0 0 2px rgba(0,0,0,0.5)",
}}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
<div className="absolute w-16 h-0.5 bg-white/70 left-1/2 -translate-x-1/2"></div>
<div className="absolute w-0.5 h-16 bg-white/70 top-1/2 -translate-y-1/2"></div>
</div>
</div>}
</div>
{/* Edit Image */}
{/* Object Fit */}
{
<div>
<h3 className="text-sm font-medium mb-2">Object Fit</h3>
<div className="flex gap-4">
<Button variant="outline" className={cn(objectFit === "cover" && "bg-blue-50 border-blue-500")} onClick={() => handleFitChange("cover")}>Cover</Button>
<Button variant="outline" className={cn(objectFit === "contain" && "bg-blue-50 border-blue-500")} onClick={() => handleFitChange("contain")}>Contain</Button>
<Button variant="outline" className={cn(objectFit === "fill" && "bg-blue-50 border-blue-500")} onClick={() => handleFitChange("fill")}>Fill</Button>
</div>
</div>
}
{/* Focus Point */}
{
}
</div>
</TabsContent>
</Tabs>
</div>
</SheetContent>

View file

@ -26,7 +26,7 @@ const SlideComponent = ({ data }: { data: Partial<SchemaType> }) => {
// Validate each data field before rendering using && operator or optional chaining.
// These layouts are exported as PDF and PPTX so they must be optimized for both formats.
// Content must properly fit in the container, specify min and max constraints in the schema.
// You can check out ExampleComponent.tsx for more details.
// You can check out ExampleSlideLayout.tsx for more details.
};
export default SlideComponent;

View file

@ -29,7 +29,7 @@ const bulletIconsOnlySlideSchema = z.object({
title: 'Custom Software',
subtitle: 'We create tailored software to optimize processes and boost efficiency.',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/code.js',
__icon_url__: '/static/icons/placeholder.png',
__icon_query__: 'code software development'
}
},
@ -37,7 +37,7 @@ const bulletIconsOnlySlideSchema = z.object({
title: 'Digital Consulting',
subtitle: 'Our consultants guide organizations in leveraging the latest technologies.',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/users.js',
__icon_url__: '/static/icons/placeholder.png',
__icon_query__: 'users consulting team'
}
},
@ -45,7 +45,7 @@ const bulletIconsOnlySlideSchema = z.object({
title: 'Support Services',
subtitle: 'We provide ongoing support to help businesses adapt and maintain performance.',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/headphones.js',
__icon_url__: '/static/icons/placeholder.png',
__icon_query__: 'headphones support service'
}
},
@ -53,7 +53,7 @@ const bulletIconsOnlySlideSchema = z.object({
title: 'Scalable Marketing',
subtitle: 'Our data-driven strategies help businesses expand their reach and engagement.',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/trending-up.js',
__icon_url__: '/static/icons/placeholder.png',
__icon_query__: 'trending up marketing growth'
}
}

View file

@ -60,8 +60,8 @@ const IntroPitchDeckSlide: React.FC<IntroSlideLayoutProps> = ({
>
{/* Top Header */}
<div className="absolute top-8 left-10 right-10 flex justify-between items-center text-[#1E4CD9] text-sm font-semibold">
<span>{slideData?.companyName}</span>
<span>{slideData?.date}</span>
<p>{slideData?.companyName}</p>
<p>{slideData?.date}</p>
</div>
{/* Main Title */}

View file

@ -122,14 +122,17 @@ const AboutCompanySlideLayout: React.FC<AboutCompanySlideLayoutProps> = ({
{/* Right side - Content */}
<div className="flex-1 pl-16 flex flex-col justify-center">
<h2 className="text-6xl font-bold text-blue-600 mb-12 leading-tight">
{slideData?.title || "About Our Company"}
</h2>
{slideData?.title && (
<h2 className="text-6xl font-bold text-blue-600 mb-12 leading-tight">
{slideData?.title}
</h2>
)}
<div className="text-lg text-blue-600 leading-relaxed font-normal max-w-lg">
{slideData?.content ||
"In the presentation session, the background/introduction can be filled with information that is arranged systematically and effectively with respect to an interesting topic to be used as material for discussion at the opening of the presentation session. The introduction can provide a general overview for those who are listening to your presentation so that the key words on the topic of discussion are emphasized during this background/introductory presentation session."}
</div>
{slideData?.content && (
<div className="text-lg text-blue-600 leading-relaxed font-normal max-w-lg">
{slideData?.content}
</div>
)}
</div>
</div>
</div>

View file

@ -150,12 +150,14 @@ const ProblemStatementSlideLayout: React.FC<
className="flex items-start gap-5 bg-white bg-opacity-5 rounded-lg p-5"
>
<div className="flex-shrink-0">
<img
src={category.icon?.__icon_url__}
alt={category.icon?.__icon_query__}
className="w-12 h-12"
style={{ filter: "invert(1)" }}
/>
{category.icon?.__icon_url__ && (
<img
src={category.icon?.__icon_url__}
alt={category.icon?.__icon_query__}
className="w-12 h-12"
style={{ filter: "invert(1)" }}
/>
)}
</div>
<div>
<h3 className="text-xl font-semibold text-white mb-1">

View file

@ -162,13 +162,15 @@ const ProductOverviewSlideLayout: React.FC<ProductOverviewSlideLayoutProps> = ({
className="rounded-b-md overflow-hidden"
style={{ height: `${IMAGE_SECTION_HEIGHT + 28}px` }}
>
<img
src={products[0].image.__image_url__}
alt={
products[0].image.__image_prompt__ || products[0].title
}
className="w-full h-full object-cover"
/>
{products[0].image.__image_url__ && (
<img
src={products[0].image.__image_url__}
alt={
products[0].image.__image_prompt__ || products[0].title
}
className="w-full h-full object-cover"
/>
)}
</div>
</div>
)}
@ -199,13 +201,15 @@ const ProductOverviewSlideLayout: React.FC<ProductOverviewSlideLayoutProps> = ({
className="rounded-b-md overflow-hidden"
style={{ height: `${IMAGE_SECTION_HEIGHT + 28}px` }}
>
<img
src={products[2].image.__image_url__}
alt={
products[2].image.__image_prompt__ || products[2].title
}
className="w-full h-full object-cover"
/>
{products[2].image.__image_url__ && (
<img
src={products[2].image.__image_url__}
alt={
products[2].image.__image_prompt__ || products[2].title
}
className="w-full h-full object-cover"
/>
)}
</div>
</div>
)}
@ -225,13 +229,15 @@ const ProductOverviewSlideLayout: React.FC<ProductOverviewSlideLayoutProps> = ({
className="rounded-t-md overflow-hidden"
style={{ height: `${IMAGE_SECTION_HEIGHT + 28}px` }}
>
<img
src={products[1].image.__image_url__}
alt={
products[1].image.__image_prompt__ || products[1].title
}
className="w-full h-full object-cover"
/>
{products[1].image.__image_url__ && (
<img
src={products[1].image.__image_url__}
alt={
products[1].image.__image_prompt__ || products[1].title
}
className="w-full h-full object-cover"
/>
)}
</div>
{/* Bottom Section - Blue background with text */}
<div
@ -263,13 +269,15 @@ const ProductOverviewSlideLayout: React.FC<ProductOverviewSlideLayoutProps> = ({
className="rounded-t-md overflow-hidden"
style={{ height: `${IMAGE_SECTION_HEIGHT + 28}px` }}
>
<img
src={products[3].image.__image_url__}
alt={
products[3].image.__image_prompt__ || products[3].title
}
className="w-full h-full object-cover"
/>
{products[3].image.__image_url__ && (
<img
src={products[3].image.__image_url__}
alt={
products[3].image.__image_prompt__ || products[3].title
}
className="w-full h-full object-cover"
/>
)}
</div>
<div
className={`${products[3].isBlueBackground ? "bg-blue-600" : "bg-gray-100"} p-5 flex flex-col justify-center text-center rounded-b-md`}

View file

@ -107,20 +107,20 @@ const MarketSizeSlideLayout: React.FC<MarketSizeSlideProps> = ({
{slideData?.title || "Market Size"}
</h1>
<div className="w-full bg-[#CBE3CC] rounded-md mb-8 flex items-center justify-center">
<img
src={
slideData?.mapImage?.__image_url__ ||
"https://upload.wikimedia.org/wikipedia/commons/8/80/World_map_-_low_resolution.svg"
}
alt="Market World Map with Points"
className="w-full object-contain rounded-md"
style={{ maxHeight: 220 }}
/>
{slideData?.mapImage?.__image_url__ && (
<img
src={slideData?.mapImage?.__image_url__}
alt="Market World Map with Points"
className="w-full object-contain rounded-md"
style={{ maxHeight: 220 }}
/>
)}
</div>
<p className="text-blue-600 text-sm leading-relaxed font-normal mb-12 max-w-lg text-left">
{slideData?.description ||
"Market size is the total amount of all sales and customers that can be seen directly by stakeholders. This technique is usually calculated at the end of the year, the market size can be used by companies to determine the potential of their market and business in the future."}
</p>
{slideData?.description && (
<p className="text-blue-600 text-sm leading-relaxed font-normal mb-12 max-w-lg text-left">
{slideData?.description}
</p>
)}
</div>
</div>

View file

@ -145,11 +145,13 @@ const ModernTeamSlideLayout: React.FC<ModernTeamSlideLayoutProps> = ({
>
{/* Photo */}
<div className="relative w-28 h-28 mb-4 rounded overflow-hidden bg-white border-2 border-blue-100 flex items-center justify-center">
<img
src={member.image.__image_url__ || ""}
alt={member.image.__image_prompt__ || member.name}
className="w-full h-full object-cover"
/>
{member.image.__image_url__ && (
<img
src={member.image.__image_url__}
alt={member.image.__image_prompt__ || member.name}
className="w-full h-full object-cover"
/>
)}
</div>
{/* Name */}
<div className="text-lg font-bold text-blue-700 mb-1">