Merge branch 'feat/custom_schema_and_layout' of github.com:presenton/presenton into feat/custom_schema_and_layout

This commit is contained in:
sauravniraula 2025-07-26 00:06:13 +05:45
commit b3815493c9
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
12 changed files with 482 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

@ -0,0 +1,116 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'quote-slide'
export const layoutName = 'Quote'
export const layoutDescription = 'A slide layout with a heading, inspirational quote, and background image with overlay for text visibility.'
const quoteSlideSchema = z.object({
heading: z.string().min(3).max(60).default('Words of Wisdom').meta({
description: "Main heading of the slide",
}),
quote: z.string().min(10).max(200).default('Success is not final, failure is not fatal: it is the courage to continue that counts. The future belongs to those who believe in the beauty of their dreams.').meta({
description: "The main quote text content",
}),
author: z.string().min(2).max(50).default('Winston Churchill').meta({
description: "Author of the quote",
}),
backgroundImage: ImageSchema.default({
__image_url__: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2000&q=80',
__image_prompt__: 'Inspirational mountain landscape with dramatic sky and clouds'
}).meta({
description: "Background image for the slide",
})
})
export const Schema = quoteSlideSchema
export type QuoteSlideData = z.infer<typeof quoteSlideSchema>
interface QuoteSlideLayoutProps {
data?: Partial<QuoteSlideData>
}
const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData }) => {
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'Poppins, sans-serif'
}}
>
{/* Background Image */}
<div
className="absolute inset-0 w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${slideData?.backgroundImage?.__image_url__ || ''}')`,
}}
/>
{/* Background Overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-black/60 via-black/40 to-black/60"></div>
{/* Decorative Elements */}
<div className="absolute top-0 left-0 w-32 h-32 bg-purple-600/20 rounded-full blur-3xl"></div>
<div className="absolute bottom-0 right-0 w-40 h-40 bg-purple-400/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/2 left-1/4 w-24 h-24 bg-white/10 rounded-full blur-2xl"></div>
{/* Main Content */}
<div className="relative z-10 px-8 sm:px-12 lg:px-20 py-12 flex-1 flex flex-col justify-center h-full">
<div className="text-center space-y-8 max-w-4xl mx-auto">
{/* Heading */}
<div className="space-y-4">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white leading-tight">
{slideData?.heading || 'Words of Wisdom'}
</h1>
{/* Purple accent line */}
<div className="w-20 h-1 bg-purple-400 mx-auto"></div>
</div>
{/* Quote Section */}
<div className="space-y-6">
{/* Quote Icon */}
<div className="flex justify-center">
<svg
className="w-12 h-12 text-purple-300 opacity-80"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/>
</svg>
</div>
{/* Quote Text */}
<blockquote className="text-xl sm:text-2xl lg:text-3xl font-medium text-white leading-relaxed italic">
"{slideData?.quote || 'Success is not final, failure is not fatal: it is the courage to continue that counts. The future belongs to those who believe in the beauty of their dreams.'}"
</blockquote>
{/* Author */}
<div className="flex justify-center items-center space-x-4">
<div className="w-16 h-px bg-purple-300"></div>
<cite className="text-base sm:text-lg text-purple-200 font-semibold not-italic">
{slideData?.author || 'Winston Churchill'}
</cite>
<div className="w-16 h-px bg-purple-300"></div>
</div>
</div>
</div>
</div>
{/* Bottom Decorative Border */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-r from-purple-600 via-purple-400 to-purple-600"></div>
</div>
</>
)
}
export default QuoteSlideLayout

View file

@ -0,0 +1,145 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'table-info-slide'
export const layoutName = 'Table with Info'
export const layoutDescription = 'A slide layout with a title at the top, structured table in the middle, and descriptive text at the bottom.'
const tableInfoSlideSchema = z.object({
title: z.string().min(3).max(40).default('Market Comparison').meta({
description: "Main title of the slide",
}),
tableData: z.object({
headers: z.array(z.string().min(1).max(30)).min(2).max(5).meta({
description: "Table column headers"
}),
rows: z.array(z.array(z.string().min(1).max(50))).min(2).max(6).meta({
description: "Table rows data - each row should match the number of headers"
})
}).default({
headers: ['Company', 'Revenue', 'Growth', 'Market Share'],
rows: [
['Company A', '$2.5M', '15%', '25%'],
['Company B', '$1.8M', '12%', '18%'],
['Company C', '$3.2M', '20%', '32%'],
['Our Company', '$1.2M', '35%', '12%']
]
}).meta({
description: "Table structure with headers and rows"
}),
description: z.string().min(10).max(200).default('This comparison shows our competitive position in the market. While we currently have a smaller market share, our growth rate significantly exceeds competitors, indicating strong potential for future expansion.').meta({
description: "Descriptive text that appears below the table",
})
})
export const Schema = tableInfoSlideSchema
export type TableInfoSlideData = z.infer<typeof tableInfoSlideSchema>
interface TableInfoSlideLayoutProps {
data?: Partial<TableInfoSlideData>
}
const TableInfoSlideLayout: React.FC<TableInfoSlideLayoutProps> = ({ data: slideData }) => {
const tableHeaders = slideData?.tableData?.headers || ['Company', 'Revenue', 'Growth', 'Market Share']
const tableRows = slideData?.tableData?.rows || [
['Company A', '$2.5M', '15%', '25%'],
['Company B', '$1.8M', '12%', '18%'],
['Company C', '$3.2M', '20%', '32%'],
['Our Company', '$1.2M', '35%', '12%']
]
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden flex flex-col"
style={{
fontFamily: 'Poppins, sans-serif'
}}
>
{/* Decorative Wave Patterns */}
<div className="absolute top-0 left-0 w-64 h-full opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 400" fill="none">
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="#8b5cf6" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="#8b5cf6" opacity="0.1" />
</svg>
</div>
<div className="absolute top-0 right-0 w-64 h-full opacity-10 overflow-hidden transform scale-x-[-1]">
<svg className="w-full h-full" viewBox="0 0 200 400" fill="none">
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="#8b5cf6" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="#8b5cf6" opacity="0.1" />
</svg>
</div>
{/* Main Content */}
<div className="relative z-10 px-8 sm:px-12 lg:px-20 py-8 flex-1 flex flex-col justify-between">
{/* Title Section */}
<div className="text-center space-y-4">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900">
{slideData?.title || 'Market Comparison'}
</h1>
{/* Purple accent line */}
<div className="w-20 h-1 bg-purple-600 mx-auto"></div>
</div>
{/* Table Section */}
<div className="flex-1 flex items-center justify-center py-8">
<div className="w-full max-w-4xl">
<div className="bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden">
{/* Table Header */}
<div className="bg-purple-600 text-white">
<div className="grid gap-px" style={{ gridTemplateColumns: `repeat(${tableHeaders.length}, 1fr)` }}>
{tableHeaders.map((header, index) => (
<div key={index} className="px-6 py-4 font-semibold text-center text-sm sm:text-base">
{header}
</div>
))}
</div>
</div>
{/* Table Body */}
<div className="divide-y divide-gray-200">
{tableRows.map((row, rowIndex) => (
<div
key={rowIndex}
className={`grid gap-px ${rowIndex % 2 === 0 ? 'bg-gray-50' : 'bg-white'} hover:bg-purple-50 transition-colors duration-200`}
style={{ gridTemplateColumns: `repeat(${tableHeaders.length}, 1fr)` }}
>
{row.slice(0, tableHeaders.length).map((cell, cellIndex) => (
<div key={cellIndex} className="px-6 py-4 text-center text-sm sm:text-base text-gray-800">
{cell}
</div>
))}
</div>
))}
</div>
</div>
</div>
</div>
{/* Description Section */}
<div className="text-center space-y-4">
<div className="max-w-4xl mx-auto">
<p className="text-sm sm:text-base text-gray-700 leading-relaxed">
{slideData?.description || 'This comparison shows our competitive position in the market. While we currently have a smaller market share, our growth rate significantly exceeds competitors, indicating strong potential for future expansion.'}
</p>
</div>
</div>
</div>
</div>
</>
)
}
export default TableInfoSlideLayout

View file

@ -68,8 +68,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

@ -111,20 +111,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">