feat(Nextjs): enchance editableLayoutWrapper
This commit is contained in:
parent
ec398bbbee
commit
9ce7ed23ee
10 changed files with 221 additions and 96 deletions
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue