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

This commit is contained in:
shiva raj badu 2025-07-23 23:23:39 +05:45
commit d062730c6e
No known key found for this signature in database
12 changed files with 1987 additions and 0 deletions

View file

@ -0,0 +1,112 @@
import React from "react";
import * as z from "zod";
export const layoutId = "intro-pitchdeck-slide";
export const layoutName = "Intro Pitch Deck Slide";
export const layoutDescription =
"A visually appealing introduction slide for a pitch deck, featuring a large title, company name, date, and contact information with a modern design.";
const introPitchDeckSchema = z.object({
title: z.string().min(2).max(15).default("Pitch Deck").meta({
description: "Main title of the slide",
}),
description: z.string().default("").meta({
description: "Empty description as per the design",
}),
contactNumber: z.string().default("+123-456-7890").meta({
description: "Contact phone number displayed in footer",
}),
contactAddress: z
.string()
.default("123 Anywhere St., Any City, ST 123")
.meta({
description: "Contact address displayed in footer",
}),
contactWebsite: z.string().default("www.reallygreatsite.com").meta({
description: "Contact website URL displayed in footer",
}),
companyName: z.string().default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().default("June 13, 2038").meta({
description: "Date of the presentation",
}),
});
export const Schema = introPitchDeckSchema;
export type IntroPitchDeckData = z.infer<typeof introPitchDeckSchema>;
interface IntroSlideLayoutProps {
data?: Partial<IntroPitchDeckData>;
}
const IntroPitchDeckSlide: React.FC<IntroSlideLayoutProps> = ({
data: slideData,
}) => {
return (
<>
{/* Montserrat Font */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full max-w-[1280px] aspect-video mx-auto relative overflow-hidden rounded-md"
style={{
fontFamily: "Montserrat, sans-serif",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
{/* 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>
</div>
{/* Main Title */}
<div
className="absolute left-10"
style={{
top: "50%",
transform: "translateY(-50%)",
}}
>
<h1
className="text-[9.5rem] font-bold text-[#1E4CD9] leading-none inline-block relative"
id="pitchdeck-title"
style={{ display: "inline-block" }}
>
{slideData?.title}
{/* Blue underline */}
<span
className="block bg-[#1E4CD9] h-[4px] absolute left-0"
style={{
width: "100%",
bottom: "-0.5em",
transition: "width 0.3s",
}}
/>
</h1>
</div>
{/* Bottom Contact Row */}
<div className="absolute bottom-8 left-10 right-10 flex flex-wrap items-center gap-10 text-[#1E4CD9] text-sm font-medium">
<div className="flex items-center gap-2">
<span className="text-lg">📞</span>
<span>{slideData?.contactNumber}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-lg">📍</span>
<span>{slideData?.contactAddress}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-lg">🌐</span>
<span>{slideData?.contactWebsite}</span>
</div>
</div>
</div>
</>
);
};
export default IntroPitchDeckSlide;

View file

@ -0,0 +1,140 @@
import React from "react";
import * as z from "zod";
import { ImageSchema, IconSchema } from "@/presentation-layouts/defaultSchemes";
export const layoutId = "about-company-slide";
export const layoutName = "About Our Company Slide";
export const layoutDescription =
"A slide layout providing an overview of the company, its background, and key information.";
const aboutCompanySlideSchema = z.object({
title: z.string().min(3).max(60).default("About Our Company").meta({
description: "Main title of the slide",
}),
content: z
.string()
.min(50)
.max(500)
.default(
"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.",
)
.meta({
description: "Main content text describing the company or topic",
}),
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(30).default("June 13, 2038").meta({
description: "Date displayed in header",
}),
image: ImageSchema.optional().meta({
description:
"Optional supporting image for the slide (building, office, etc.)",
}),
});
export const Schema = aboutCompanySlideSchema;
export type AboutCompanySlideData = z.infer<typeof aboutCompanySlideSchema>;
interface AboutCompanySlideLayoutProps {
data?: Partial<AboutCompanySlideData>;
}
const AboutCompanySlideLayout: React.FC<AboutCompanySlideLayoutProps> = ({
data: slideData,
}) => {
return (
<>
{/* Import fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;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: "Montserrat, sans-serif",
}}
>
{/* 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>
</div>
{/* Main content area */}
<div className="flex h-full px-16 pb-16">
{/* Left side - Image */}
<div className="flex-1 pr-16 flex items-center pt-8">
<div className="w-full h-96 overflow-hidden">
{slideData?.image ? (
<img
src={slideData.image.__image_url__}
alt={slideData.image.__image_prompt__}
className="w-full h-full object-cover"
/>
) : (
/* Default building facade */
<div className="w-full h-full bg-gradient-to-br from-gray-200 to-gray-300 relative">
{/* Building structure simulation */}
<div className="absolute inset-0 bg-gradient-to-r from-gray-300 to-gray-200"></div>
{/* Horizontal lines (building floors) */}
<div className="absolute inset-0">
{[...Array(12)].map((_, i) => (
<div
key={i}
className="absolute w-full border-t border-gray-400 opacity-60"
style={{ top: `${(i + 1) * 8}%` }}
></div>
))}
</div>
{/* Vertical lines (building columns) */}
<div className="absolute inset-0">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="absolute h-full border-l border-gray-400 opacity-40"
style={{ left: `${(i + 1) * 16}%` }}
></div>
))}
</div>
{/* Windows */}
<div className="absolute inset-0 grid grid-cols-4 gap-2 p-4">
{[...Array(32)].map((_, i) => (
<div
key={i}
className="bg-blue-100 opacity-60 rounded-sm border border-gray-300"
></div>
))}
</div>
{/* Building edge highlight */}
<div className="absolute right-0 top-0 w-1 h-full bg-white opacity-80"></div>
</div>
)}
</div>
</div>
{/* 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>
<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>
</div>
</div>
</div>
</>
);
};
export default AboutCompanySlideLayout;

View file

@ -0,0 +1,161 @@
import React from "react";
import * as z from "zod";
import { ImageSchema, IconSchema } from "@/presentation-layouts/defaultSchemes";
export const layoutId = "problem-statement-slide";
export const layoutName = "Problem Statement Slide";
export const layoutDescription =
"A slide layout designed to present a clear problem statement, including categories of problems, company information, and an optional image.";
const problemStatementSlideSchema = z.object({
title: z.string().min(3).max(40).default("Problem").meta({
description: "Main title of the problem statement slide",
}),
description: z
.string()
.min(50)
.max(500)
.default(
"A problem needs to be discussed further and in detail because this problem is the main foundation in the initial development of a product, service, and decision making. Without a well-defined problem, it will have an impact on a job that is unfocused, unmanaged, and less relevant.",
)
.meta({
description: "Main content text describing the problem statement",
}),
problemCategories: z
.array(
z.object({
title: z.string().min(3).max(30).meta({
description: "Title of the problem category",
}),
description: z.string().min(20).max(200).meta({
description: "Description of the problem category",
}),
icon: IconSchema.optional().meta({
description: "Optional icon for the problem category",
}),
}),
)
.min(2)
.max(4)
.default([
{
title: "Inefficiency",
description:
"Businesses struggle to find digital tools that meet their needs, causing operational slowdowns.",
icon: {
__icon_url__:
"https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/alert-triangle.js",
__icon_query__: "warning alert inefficiency",
},
},
{
title: "High Costs",
description:
"Outdated systems increase expenses, while small businesses struggle to expand their market reach.",
icon: {
__icon_url__:
"https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/trending-up.js",
__icon_query__: "trending up costs chart",
},
},
])
.meta({
description:
"List of problem categories with titles, descriptions, and optional icons",
}),
companyName: z.string().min(2).max(50).default("Rimberio").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(30).default("June 13, 2038").meta({
description: "Date displayed in header",
}),
});
export const Schema = problemStatementSlideSchema;
export type ProblemStatementSlideData = z.infer<
typeof problemStatementSlideSchema
>;
interface ProblemStatementSlideLayoutProps {
data?: Partial<ProblemStatementSlideData>;
}
const ProblemStatementSlideLayout: React.FC<
ProblemStatementSlideLayoutProps
> = ({ data: slideData }) => {
const problemCategories = slideData?.problemCategories || [];
return (
<>
{/* Import fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-gradient-to-br from-blue-600 to-blue-700 relative z-20 mx-auto overflow-hidden text-white"
style={{
fontFamily: "Montserrat, sans-serif",
}}
>
{/* Header */}
<div className="absolute top-8 left-10 right-10 flex justify-between items-center text-white text-sm font-semibold">
<span>{slideData?.companyName}</span>
<span>{slideData?.date}</span>
</div>
{/* Main content area */}
<div className="flex h-full px-16 pb-16">
{/* Left side - Main Problem */}
<div className="flex-1 pr-16 flex flex-col justify-center">
<div className="flex flex-col items-start justify-center h-full">
<h2 className="text-7xl font-bold text-white mb-8 leading-tight text-left">
{slideData?.title}
</h2>
<div className="text-lg text-white leading-relaxed font-normal mb-12 max-w-lg text-left">
{slideData?.description}
</div>
</div>
</div>
{/* Right side - Problem Categories with Icons */}
<div className="flex-1 pl-16 flex flex-col justify-center">
<div className="w-full max-w-xl mx-auto grid grid-cols-1 gap-8">
{problemCategories.map((category, index) => (
<div
key={index}
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)" }}
/>
</div>
<div>
<h3 className="text-xl font-semibold text-white mb-1">
{category.title}
</h3>
<p className="text-sm text-blue-100 leading-relaxed max-w-md">
{category.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* Bottom border line */}
<div className="absolute bottom-0 left-0 w-full h-1 bg-white"></div>
</div>
</>
);
};
export default ProblemStatementSlideLayout;

View file

@ -0,0 +1,169 @@
import React from "react";
import * as z from "zod";
import { IconSchema } from "@/presentation-layouts/defaultSchemes";
export const layoutId = "solution-slide";
export const layoutName = "Solution Slide";
export const layoutDescription =
"A slide layout designed to present a solution to previously identified problems, showcasing key aspects of the solution with sections and icons.";
const solutionSlideSchema = z.object({
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Date displayed in header",
}),
title: z.string().min(3).max(40).default("Solution").meta({
description: "Main title of the slide",
}),
mainDescription: z
.string()
.min(20)
.max(300)
.default(
"Show that we offer a solution that solves the problems previously described and identified. Make sure that the solutions we offer uphold the values of effectiveness, efficiency, and are highly relevant to the market situation and society.",
)
.meta({
description: "Main content text describing the solution",
}),
sections: z
.array(
z.object({
title: z.string().min(3).max(30).meta({
description: "Section title",
}),
description: z.string().min(5).max(80).meta({
description: "Section description",
}),
icon: IconSchema.optional().meta({
description: "Icon for the section",
}),
}),
)
.min(2)
.max(4)
.default([
{
title: "Market",
description: "Innovative and widely accepted.",
icon: {
__icon_query__: "market innovation",
__icon_url__:
"https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/globe.js",
},
},
{
title: "Industry",
description: "Based on sound market decisions.",
icon: {
__icon_query__: "industry building",
__icon_url__:
"https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/building.js",
},
},
{
title: "SEM",
description: "Driven by precise data and analysis.",
icon: {
__icon_query__: "SEM data analysis",
__icon_url__:
"https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/chart-bar.js",
},
},
{
title: "End User",
description: "Focused on real user impact.",
icon: {
__icon_query__: "end user impact",
__icon_url__:
"https://cdn.jsdelivr.net/npm/lucide@latest/dist/esm/icons/user.js",
},
},
])
.meta({
description:
"List of solution sections with titles, descriptions, and optional icons",
}),
});
export const Schema = solutionSlideSchema;
export type SolutionSlideData = z.infer<typeof solutionSlideSchema>;
interface SolutionSlideLayoutProps {
data?: Partial<SolutionSlideData>;
}
const SolutionSlideLayout: React.FC<SolutionSlideLayoutProps> = ({
data: slideData,
}) => {
const sections = slideData?.sections || [];
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;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 border-2 border-gray-800"
style={{
fontFamily: "Montserrat, sans-serif",
}}
>
{/* 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>
</div>
{/* Main Content */}
<div className="flex justify-center items-center h-full px-16 pb-16 gap-4">
{/* Title and Description */}
<div className="w-full flex flex-col items-start mb-4">
<h1 className="text-7xl font-bold text-blue-600 mb-8 leading-tight text-left">
{slideData?.title}
</h1>
<p className="text-blue-600 text-lg leading-relaxed font-normal mb-12 max-w-lg text-left">
{slideData?.mainDescription}
</p>
</div>
{/* Four Small Boxes in a Row */}
<div className="grid grid-cols-2 gap-4 w-full max-w-5xl">
{sections.map((section, idx) => (
<div
key={idx}
className="flex flex-col items-center text-center bg-[#F5F8FE] rounded-lg shadow px-3 py-4 min-h-[140px] max-h-[160px]"
>
<div className="mb-2">
{section?.icon?.__icon_url__ && (
<img
src={section.icon.__icon_url__}
alt={section.icon.__icon_query__}
className="w-12 h-12 mb-2"
/>
)}
</div>
<h2 className="text-lg font-semibold text-blue-600 mb-1">
{section.title}
</h2>
<div className="w-8 h-1 bg-blue-600 mb-2"></div>
<p className="text-blue-600 text-xs leading-snug">
{section.description}
</p>
</div>
))}
</div>
</div>
{/* Bottom Border */}
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600"></div>
</div>
</>
);
};
export default SolutionSlideLayout;

View file

@ -0,0 +1,301 @@
import React from "react";
import * as z from "zod";
import { ImageSchema } from "@/presentation-layouts/defaultSchemes";
export const layoutId = "product-overview-slide";
export const layoutName = "Product Overview Slide";
export const layoutDescription =
"A slide layout designed to showcase a company's products or services, highlighting their features and benefits in a structured format.";
const productOverviewSlideSchema = z.object({
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Date displayed in header",
}),
title: z.string().min(3).max(40).default("Product Overview").meta({
description: "Main title of the slide",
}),
mainDescription: z
.string()
.min(50)
.max(400)
.default(
"Provide an explanation of the general profile of the services we have. Arrange information about our products services in a systematic and fact-based manner. Also express our pride in the service that we have done well.",
)
.meta({
description: "Main content text describing the product overview",
}),
products: z
.array(
z.object({
title: z.string().min(3).max(50).meta({
description: "Product title",
}),
description: z.string().min(30).max(200).meta({
description: "Product description",
}),
image: ImageSchema.meta({
description: "Product image",
}),
isBlueBackground: z.boolean().default(false).meta({
description: "Whether the product box has a blue background",
}),
}),
)
.min(2)
.max(4)
.default([
{
title: "Internet of Things",
description:
"Detail and explain each product. Our examination of community and market issues increases with additional products/services.",
image: {
__image_url__:
"https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&h=200&fit=crop",
__image_prompt__: "Person working on electronics with headphones",
},
isBlueBackground: true,
},
{
title: "Smart Home Platform",
description:
"Our alternate product category is available. Our products must work together to solve social and economic issues.",
image: {
__image_url__:
"https://images.unsplash.com/photo-1573164713988-8665fc963095?w=300&h=200&fit=crop",
__image_prompt__:
"Woman working at computer with technical equipment",
},
isBlueBackground: true,
},
])
.meta({
description: "List of products or services to showcase",
}),
});
export const Schema = productOverviewSlideSchema;
export type ProductOverviewSlideData = z.infer<
typeof productOverviewSlideSchema
>;
interface ProductOverviewSlideLayoutProps {
data?: Partial<ProductOverviewSlideData>;
}
const ProductOverviewSlideLayout: React.FC<ProductOverviewSlideLayoutProps> = ({
data: slideData,
}) => {
const products = slideData?.products || [];
// Make the product boxes smaller
const PRODUCT_BOX_HEIGHT = 400; // px (smaller than before)
const PRODUCT_BOX_WIDTH = 200; // px (smaller than before)
const TEXT_SECTION_HEIGHT = Math.round(PRODUCT_BOX_HEIGHT * 0.56); // ~190px
const IMAGE_SECTION_HEIGHT = PRODUCT_BOX_HEIGHT - TEXT_SECTION_HEIGHT; // ~150px
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;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: "Montserrat, sans-serif",
}}
>
{/* 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>
</div>
6+
{/* Main Content */}
<div className="flex h-full px-16 pb-16">
{/* Title and Description on the left */}
<div className="flex flex-col items-start justify-center w-[48%] pr-8">
<h1 className="text-7xl font-bold text-blue-600 mb-8 leading-tight text-left">
{slideData?.title}
</h1>
<p className="text-blue-600 text-lg leading-relaxed font-normal mb-12 max-w-lg text-left">
{slideData?.mainDescription}
</p>
</div>
{/* Product Grid on the right */}
<div className="flex flex-row items-center justify-end w-[62%] gap-8">
{/* First Column: Normal order (Text above, Image below) */}
<div className="flex flex-col items-center gap-4 justify-center h-full">
{products[0] && (
<div
className="flex flex-col items-stretch"
style={{
width: `${PRODUCT_BOX_WIDTH + 40}px`,
height: `${PRODUCT_BOX_HEIGHT + 60}px`,
}}
>
{/* Top Section - Blue background with text */}
<div
className={`${products[0].isBlueBackground ? "bg-blue-600" : "bg-gray-100"} p-5 flex flex-col justify-center text-center rounded-t-md`}
style={{ height: `${TEXT_SECTION_HEIGHT + 32}px` }}
>
<h2
className={`text-xl font-semibold mb-3 ${products[0].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[0].title}
</h2>
<p
className={`text-sm leading-relaxed ${products[0].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[0].description}
</p>
</div>
{/* Bottom Section - Image */}
<div
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"
/>
</div>
</div>
)}
{products[2] && (
<div
className="flex flex-col items-stretch"
style={{
width: `${PRODUCT_BOX_WIDTH + 40}px`,
height: `${PRODUCT_BOX_HEIGHT + 60}px`,
}}
>
<div
className={`${products[2].isBlueBackground ? "bg-blue-600" : "bg-gray-100"} p-5 flex flex-col justify-center text-center rounded-t-md`}
style={{ height: `${TEXT_SECTION_HEIGHT + 32}px` }}
>
<h2
className={`text-xl font-semibold mb-3 ${products[2].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[2].title}
</h2>
<p
className={`text-sm leading-relaxed ${products[2].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[2].description}
</p>
</div>
<div
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"
/>
</div>
</div>
)}
</div>
{/* Second Column: Reverse order (Image above, Text below) */}
<div className="flex flex-col items-center gap-4 justify-center h-full">
{products[1] && (
<div
className="flex flex-col items-stretch"
style={{
width: `${PRODUCT_BOX_WIDTH + 40}px`,
height: `${PRODUCT_BOX_HEIGHT + 60}px`,
}}
>
{/* Top Section - Image */}
<div
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"
/>
</div>
{/* Bottom Section - Blue background with text */}
<div
className={`${products[1].isBlueBackground ? "bg-blue-600" : "bg-gray-100"} p-5 flex flex-col justify-center text-center rounded-b-md`}
style={{ height: `${TEXT_SECTION_HEIGHT + 32}px` }}
>
<h2
className={`text-xl font-semibold mb-3 ${products[1].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[1].title}
</h2>
<p
className={`text-sm leading-relaxed ${products[1].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[1].description}
</p>
</div>
</div>
)}
{products[3] && (
<div
className="flex flex-col items-stretch"
style={{
width: `${PRODUCT_BOX_WIDTH + 40}px`,
height: `${PRODUCT_BOX_HEIGHT + 60}px`,
}}
>
<div
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"
/>
</div>
<div
className={`${products[3].isBlueBackground ? "bg-blue-600" : "bg-gray-100"} p-5 flex flex-col justify-center text-center rounded-b-md`}
style={{ height: `${TEXT_SECTION_HEIGHT + 32}px` }}
>
<h2
className={`text-xl font-semibold mb-3 ${products[3].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[3].title}
</h2>
<p
className={`text-sm leading-relaxed ${products[3].isBlueBackground ? "text-white" : "text-blue-600"}`}
>
{products[3].description}
</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Bottom Border */}
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600"></div>
</div>
</>
);
};
export default ProductOverviewSlideLayout;

View file

@ -0,0 +1,153 @@
import React from "react";
import * as z from "zod";
import { ImageSchema } from "@/presentation-layouts/defaultSchemes";
export const layoutId = "market-size-pitchdeck-slide";
export const layoutName = "Market Size Pitch Deck Slide";
export const layoutDescription =
"A professional slide layout designed to present market size statistics, including TAM, SAM, and SOM, with a world map and key metrics.";
const marketSizeSlideSchema = z.object({
title: z.string().default("Market Size").meta({
description: "Main slide title",
}),
companyName: z.string().default("Rimberio").meta({
description: "Presenter's name",
}),
date: z.string().default("June 13, 2038").meta({
description: "Presentation date",
}),
mapImage: ImageSchema.default({
__image_url__:
"https://upload.wikimedia.org/wikipedia/commons/8/80/World_map_-_low_resolution.svg", // You can quickly find a world map image via a Google search or use a free resource like Wikimedia Commons
__image_prompt__: "World map with location pins or points",
}),
marketStats: z
.array(
z.object({
label: z.string(),
value: z.string(),
description: z.string(),
}),
)
.min(1)
.max(3)
.default([
{
label: "Total Available Market (TAM)",
value: "1.4 Billion",
description:
"In the TAM Section, we can fill in the potential of any person who can buy an offer or the maximum amount of revenue a business can earn by selling their offer.",
},
{
label: "Serviceable Available Market (SAM)",
value: "194 Million",
description:
"It is a part of TAM that has the potential to become a target market for the company by considering the type of product, technology available and geographical conditions.",
},
{
label: "Serviceable Obtainable Market (SOM)",
value: "167 Million",
description:
"The SOM is a smaller fraction of the SAM that is the target of a serviceable and realistically achievable market in the short to medium term.",
},
]),
description: z
.string()
.default(
"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. This is very useful, especially for new companies that will offer services to those who are interested in our services.",
)
.meta({
description: "Main description text for the slide",
}),
});
export const Schema = marketSizeSlideSchema;
export type MarketSizeSlideData = z.infer<typeof marketSizeSlideSchema>;
interface MarketSizeSlideProps {
data?: Partial<MarketSizeSlideData>;
}
const MarketSizeSlideLayout: React.FC<MarketSizeSlideProps> = ({
data: slideData,
}) => {
const stats = slideData?.marketStats || [];
return (
<>
{/* Montserrat Font */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;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: "Montserrat, sans-serif",
}}
>
{/* Header */}
<div className="absolute top-8 left-10 right-10 flex justify-between items-center text-[#1E4CD9] text-sm font-semibold">
<span>{slideData?.companyName || "Rimberio"}</span>
<span>{slideData?.date || "June 13, 2038"}</span>
</div>
{/* Main Content */}
<div className="flex h-full px-16 pb-16">
{/* Title and Map on the left */}
<div className="flex flex-col items-center justify-center w-[48%] pr-8 h-full">
<div className="flex flex-col items-left justify-center h-full w-full">
{/* Move the title down to align with the top of the market stats */}
<h1
className="text-7xl font-bold text-blue-600 mb-8 leading-tight text-left"
style={{ marginTop: "112px" }} // 112px matches top-36 (9rem) of stats
>
{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 }}
/>
</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>
</div>
</div>
{/* Market Stats on the right */}
<div className="flex flex-col items-start justify-center w-[52%] gap-8">
<div className="absolute top-36 right-10 w-[42%] space-y-10">
{stats.map((stat, index) => (
<div key={index}>
<div className="space-y-2">
<div className="bg-[#1E4CD9] text-white text-sm font-semibold px-3 py-1 inline-block rounded-sm">
{stat.label}
</div>
<div className="text-2xl font-bold text-[#1E4CD9]">
{stat.value}
</div>
</div>
<p className="text-sm text-gray-700 leading-snug">
{stat.description}
</p>
</div>
))}
</div>
</div>
</div>
</div>
</>
);
};
export default MarketSizeSlideLayout;

View file

@ -0,0 +1,193 @@
import React from "react";
import * as z from "zod";
import { Card } from "@/components/ui/card";
import { Table, TableHeader, TableBody } from "@/components/ui/table";
import { ChartContainer } from "@/components/ui/chart";
import { ImageSchema } from "@/presentation-layouts/defaultSchemes";
import { BarChart, Bar, XAxis, YAxis, Tooltip, Cell } from "recharts";
export const layoutId = "market-validation-slide";
export const layoutName = "Market Validation Slide";
export const layoutDescription =
"A slide layout designed to present market validation data, including flexible market validation metrics, comparisons, and an optional decorative image.";
// Make the schema generic: allow any label/value pairs for comparison
const marketValidationSchema = z.object({
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Date in header",
}),
title: z.string().min(3).max(40).default("Market Validation").meta({
description: "Title of the slide",
}),
description: z
.string()
.min(50)
.max(400)
.default(
"Its a market testing stage to ensure that the products produced by the company can be accepted and effectively used by the broad market. For start-up companies, we can use data already achieved by similar products from other companies.",
)
.meta({
description:
"Main description text for the slide explaining market validation",
}),
// Generic comparisonData: label for row, label for metric, and value
comparisonData: z
.array(
z.object({
label: z.string().min(2).max(50).meta({
description:
"Name of comparison entity (e.g., company, product, etc.)",
}),
metricLabel: z.string().min(2).max(50).meta({
description:
"Label for the metric being compared (e.g., Users, Revenue, etc.)",
}),
value: z.number().min(0).meta({
description: "Numeric value for the metric",
}),
}),
)
.min(2)
.max(5)
.default([
{ label: "Thynk Unlimited", metricLabel: "Revenue ($K)", value: 2650 },
{ label: "Salford & Co.", metricLabel: "Revenue ($K)", value: 1850 },
{ label: "Liceria & Co.", metricLabel: "Revenue ($K)", value: 1010 },
])
.meta({
description: "Market benchmark data (generic metric)",
}),
image: ImageSchema.optional().meta({
description: "Optional decorative image",
}),
});
export const Schema = marketValidationSchema;
export type MarketValidationSlideData = z.infer<typeof marketValidationSchema>;
interface MarketValidationSlideLayoutProps {
data?: Partial<MarketValidationSlideData>;
}
const MarketValidationSlideLayout: React.FC<
MarketValidationSlideLayoutProps
> = ({ data: slideData }) => {
const comparisonData = slideData?.comparisonData || [];
// Chart color palette (shadcn blue and gray)
const chartColors = ["#2563eb", "#1e40af", "#60a5fa", "#93c5fd", "#dbeafe"];
// Determine the metric label to use (assume all rows use the same metricLabel)
const metricLabel =
comparisonData.length > 0 ? comparisonData[0].metricLabel : "Metric";
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full max-w-[1280px] max-h-[720px] aspect-video bg-white mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "Montserrat, sans-serif",
}}
>
{/* 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>
</div>
{/* Main Content */}
<div className="px-16 py-16 flex h-full gap-8">
{/* Left Column */}
<div className="flex-1 pr-12 flex flex-col justify-center">
<h1 className="text-7xl font-bold text-blue-600 mb-8 leading-tight text-left">
{slideData?.title}
</h1>
<p className="text-blue-600 text-sm leading-relaxed font-normal mb-12 max-w-lg text-left">
{slideData?.description}
</p>
</div>
{/* Right Column - Chart on top, Table on bottom */}
<div className="flex-1 flex flex-col justify-center items-center gap-6">
{/* Bar Chart */}
<Card className="w-full p-4 flex flex-col items-center">
<div className="w-full h-64">
<ChartContainer
config={{
value: { label: metricLabel, color: "#2563eb" },
}}
>
<BarChart
data={comparisonData}
layout="vertical"
margin={{ left: 32, right: 16, top: 16, bottom: 16 }}
>
<XAxis type="number" hide />
<YAxis
dataKey="label"
type="category"
width={120}
tick={{ fill: "#1e40af", fontWeight: 600 }}
/>
<Tooltip />
{/* Legend removed */}
<Bar
dataKey="value"
name={metricLabel}
radius={[8, 8, 8, 8]}
>
{comparisonData.map((entry, idx) => (
<Cell
key={entry.label}
fill={chartColors[idx % chartColors.length]}
/>
))}
</Bar>
</BarChart>
</ChartContainer>
</div>
</Card>
{/* Table of comparison data */}
<Card className="w-full">
<Table>
<TableHeader>
<tr>
<th className="text-left px-4 py-2 text-blue-700">
{comparisonData.length > 0 ? "Name" : "Name"}
</th>
<th className="text-left px-4 py-2 text-blue-700">
{metricLabel}
</th>
</tr>
</TableHeader>
<TableBody>
{comparisonData.map((entry) => (
<tr key={entry.label}>
<td className="px-4 py-2">{entry.label}</td>
<td className="px-4 py-2">
{entry.value.toLocaleString()}
</td>
</tr>
))}
</TableBody>
</Table>
</Card>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600" />
</div>
</>
);
};
export default MarketValidationSlideLayout;

View file

@ -0,0 +1,267 @@
import React from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
import * as z from "zod";
export const layoutId = "company-traction-slide";
export const layoutName = "Company Traction Slide";
export const layoutDescription =
"A slide layout designed to present company traction data, including growth statistics over the years, a chart visualization, and key metrics in a visually appealing format.";
// growthStats: list of dicts, each dict is { year: string, <metric1>: number, <metric2>: number, ... }
const tractionSchema = z.object({
companyName: z.string().default("presention"),
date: z.string().default("June 13, 2038"),
title: z.string().default("Company Traction"),
description: z
.string()
.default(
"Traction is a period where the company is feeling momentum during its development period. If traction momentum is not harnessed, sales figures can decline and the customer base can shrink. In general, companies will judge success by the amount of revenue and new customers they receive.",
),
// growthStats is a list of objects, each with a 'year' and any number of metric keys (all numbers)
growthStats: z
.array(
z
.object({
year: z.string(),
})
.catchall(z.number()),
)
.min(1)
.max(20)
.default([
{
year: "2020",
artificialIntelligence: 5,
internetOfThings: 10,
others: 8,
},
{
year: "2021",
artificialIntelligence: 10,
internetOfThings: 20,
others: 15,
},
{
year: "2022",
artificialIntelligence: 20,
internetOfThings: 30,
others: 22,
},
{
year: "2023",
artificialIntelligence: 28,
internetOfThings: 38,
others: 29,
},
{
year: "2024",
artificialIntelligence: 35,
internetOfThings: 45,
others: 34,
},
{
year: "2025",
artificialIntelligence: 45,
internetOfThings: 53,
others: 42,
},
{
year: "2026",
artificialIntelligence: 55,
internetOfThings: 65,
others: 52,
},
{
year: "2029",
artificialIntelligence: 55,
internetOfThings: 65,
others: 52,
},
]),
});
export const Schema = tractionSchema;
export type CompanyTractionData = z.infer<typeof tractionSchema>;
interface Props {
data?: Partial<CompanyTractionData>;
}
// Helper: assign colors to series
const defaultColors = [
"#1E4CD9",
"#3b82f6",
"#f59e0b",
"#10b981",
"#ef4444",
"#a21caf",
"#6366f1",
"#f43f5e",
"#fbbf24",
"#14b8a6",
];
function getSeriesKeys(
growthStats: Array<Record<string, string | number>>,
): string[] {
if (!growthStats.length) return [];
// Exclude 'year' or any non-numeric keys
const first = growthStats[0];
return Object.keys(first).filter(
(key) => key !== "year" && typeof first[key] === "number",
);
}
// Compute stats for right column, generic for all series
function computeStats(
growthStats: Array<Record<string, string | number>>,
seriesKeys: string[],
) {
if (!growthStats.length) return [];
const first = growthStats[0];
const last = growthStats[growthStats.length - 1];
return seriesKeys.map((key) => {
const start = typeof first[key] === "number" ? (first[key] as number) : 0;
const end = typeof last[key] === "number" ? (last[key] as number) : 0;
const growth = start === 0 ? 0 : ((end - start) / Math.abs(start)) * 100;
return {
label: key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase()),
value: `${growth >= 0 ? "+" : ""}${Math.round(growth)}% growth`,
description: `${key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())} growth over the period.`,
};
});
}
const CompanyTractionSlideLayout: React.FC<Props> = ({ data }) => {
const growthStats = data?.growthStats || [];
// Dynamically determine series keys
const seriesKeys = getSeriesKeys(growthStats);
// Prepare stats for the right column, generic for all series
const stats = computeStats(growthStats, seriesKeys);
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full max-w-[1280px] max-h-[720px] aspect-video bg-white mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "Montserrat, sans-serif",
}}
>
{/* Header */}
<div className="absolute top-8 left-10 right-10 flex justify-between items-center text-[#1E4CD9] text-sm font-semibold">
<span>{data?.companyName}</span>
<span>{data?.date}</span>
</div>
{/* Main Content */}
<div className="px-16 py-16 flex h-full gap-8">
{/* Left Column - Chart with Title Below */}
<div className="flex-1 pr-12 flex flex-col justify-center">
<h1 className="text-7xl font-bold text-blue-600 mb-4 leading-tight text-left">
{data?.title}
</h1>
<div className="bg-white rounded-lg shadow p-4 mb-8">
<div className="w-full h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={growthStats}>
<CartesianGrid stroke="#e5eafe" />
<XAxis
dataKey="year"
stroke="#1E4CD9"
tick={{ fill: "#1E4CD9", fontWeight: 600 }}
/>
<YAxis
stroke="#1E4CD9"
tick={{ fill: "#1E4CD9", fontWeight: 600 }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1E4CD9",
border: "none",
color: "#fff",
}}
labelStyle={{ color: "#fff" }}
itemStyle={{ color: "#fff" }}
/>
<Legend
wrapperStyle={{ color: "#1E4CD9", fontWeight: 600 }}
iconType="circle"
/>
{seriesKeys.map((key, idx) => (
<Line
key={key}
type="monotone"
dataKey={key}
stroke={defaultColors[idx % defaultColors.length]}
strokeWidth={3}
name={key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())}
dot={{
r: 4,
fill: defaultColors[idx % defaultColors.length],
}}
activeDot={{
r: 6,
fill: defaultColors[idx % defaultColors.length],
}}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Right Column - Description and Stats */}
<div className="flex flex-col items-start justify-center w-[52%] gap-8">
<p className="text-blue-600 text-base leading-relaxed font-normal mb-6 max-w-xl text-left">
{data?.description ||
"Traction is a period where the company is feeling momentum during its development period. If traction momentum is not harnessed, sales figures can decline and the customer base can shrink. In general, companies will judge success by the amount of revenue and new customers they receive."}
</p>
<div className="flex flex-row w-full gap-6">
{stats.map((stat, index) => (
<div
key={index}
className="flex-1 bg-[#f5f8ff] rounded-lg shadow-sm px-5 py-4 flex flex-col items-start"
>
<div className="bg-[#1E4CD9] text-white text-xs font-semibold px-3 py-1 rounded-sm mb-2">
{stat.label}
</div>
<div className="text-2xl font-bold text-[#1E4CD9] mb-1">
{stat.value}
</div>
<p className="text-sm text-gray-700 leading-snug">
{stat.description}
</p>
</div>
))}
</div>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600" />
</div>
</>
);
};
export default CompanyTractionSlideLayout;

View file

@ -0,0 +1,140 @@
import React from "react";
import * as z from "zod";
import {
BarChart,
Bar,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
export const layoutId = "business-model-slide";
export const layoutName = "Business Model Slide";
export const layoutDescription =
"A business model presentation slide displaying CAC metrics and monetization strategy.";
const businessModelSchema = z.object({
companyName: z.string().default("presenton"),
date: z.string().default("June 13, 2038"),
title: z.string().default("Business Model"),
description: z
.string()
.default(
"Describe how you monetize, who your customers are, your distribution channels or fee structure. The goal is to give an idea of how this business will sustain your product or service and explain how your company will make money and achieve its goals. This can be shown with graphs, statistics, or charts. Use the Lifetime Value (LTV) and Customer Acquisition Cost (CAC) metrics to provide a clearer picture.",
),
cacChart: z
.array(
z.object({
label: z.string(),
percentage: z.number(),
}),
)
.default([
{ label: "Internet of Things", percentage: 70 },
{ label: "Artificial Intelligence", percentage: 60 },
]),
});
export const Schema = businessModelSchema;
export type BusinessModelData = z.infer<typeof businessModelSchema>;
interface Props {
data?: Partial<BusinessModelData>;
}
const BusinessModelSlide: React.FC<Props> = ({ data }) => {
const cacChartData =
data?.cacChart && Array.isArray(data.cacChart) && data.cacChart.length > 0
? data.cacChart
: [
{ label: "Internet of Things", percentage: 70 },
{ label: "Artificial Intelligence", percentage: 60 },
];
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full max-w-[1280px] max-h-[720px] aspect-video bg-white mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "Montserrat, sans-serif",
}}
>
{/* Header */}
<div className="absolute top-8 left-10 right-10 flex justify-between items-center text-[#1E4CD9] text-sm font-semibold">
<span>{data?.companyName}</span>
<span>{data?.date}</span>
</div>
{/* Main Content */}
<div className="px-16 py-16 flex h-full gap-8">
{/* Left Column - Chart with Title Below */}
<div className="flex-1 pr-12 flex flex-col justify-center">
<h1 className="text-7xl font-bold text-blue-600 mb-4 leading-tight text-left">
{data?.title}
</h1>
<div className="bg-white rounded-lg shadow p-4 mb-8">
<div className="w-full h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={cacChartData}
margin={{ top: 20, right: 30, left: 10, bottom: 20 }}
barCategoryGap="30%"
>
<CartesianGrid stroke="#e5eafe" />
<XAxis
dataKey="label"
tick={{ fill: "#1E4CD9", fontWeight: 600 }}
/>
<YAxis
tick={{ fill: "#1E4CD9", fontWeight: 600 }}
domain={[0, 100]}
ticks={[0, 20, 40, 60, 80, 100]}
width={40}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1E4CD9",
border: "none",
color: "#fff",
}}
labelStyle={{ color: "#fff" }}
itemStyle={{ color: "#fff" }}
/>
<Legend
wrapperStyle={{ color: "#1E4CD9", fontWeight: 600 }}
iconType="circle"
/>
<Bar
dataKey="percentage"
fill="#3b82f6"
name="CAC %"
maxBarSize={48}
radius={[8, 8, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Right Column - Description and Optional Image */}
<div className="flex flex-col items-start justify-center w-[52%] gap-8">
<p className="text-blue-600 text-base leading-relaxed font-normal mb-6 max-w-xl text-left">
{data?.description}
</p>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600" />
</div>
</>
);
};
export default BusinessModelSlide;

View file

@ -0,0 +1,5 @@
{
"description": "Modern white and blue business pitch deck layouts with clean, professional design",
"ordered": false,
"isDefault": false
}

View file

@ -0,0 +1,199 @@
import React from "react";
import * as z from "zod";
import { ImageSchema } from "@/presentation-layouts/defaultSchemes";
export const layoutId = "modern-team-slide";
export const layoutName = "Modern Team Slide";
export const layoutDescription =
"A clean modern team slide showcasing team members with professional profiles and blue-white design.";
const teamMemberSchema = z.object({
name: z.string().min(2).max(50).meta({
description: "Team member's full name",
}),
position: z.string().min(2).max(50).meta({
description: "Job title or position",
}),
description: z.string().min(20).max(120).meta({
description: "Brief professional description of the team member",
}),
image: ImageSchema,
linkedIn: z.string().optional().meta({
description: "LinkedIn profile URL (optional)",
}),
});
const modernTeamSlideSchema = z.object({
title: z.string().min(3).max(40).default("Our Team").meta({
description: "Main title of the slide",
}),
subtitle: z.string().min(10).max(120).optional().meta({
description: "Optional subtitle describing the team",
}),
teamMembers: z
.array(teamMemberSchema)
.min(2)
.max(4)
.default([
{
name: "Sarah Johnson",
position: "CEO & Founder",
description:
"Strategic leader with 15+ years experience in technology and business development. Former VP at Fortune 500 company.",
image: {
__image_url__:
"https://plus.unsplash.com/premium_photo-1661589856899-6dd0871f9db6?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8YnVzaW5lc3N3b21lbnxlbnwwfHwwfHx8MA%3D%3D",
__image_prompt__: "Professional businesswoman CEO headshot",
},
},
{
name: "Michael Chen",
position: "CTO",
description:
"Technology expert specializing in scalable architecture and AI solutions. PhD in Computer Science from MIT.",
image: {
__image_url__:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80",
__image_prompt__: "Professional businessman CTO headshot",
},
},
{
name: "Emily Rodriguez",
position: "VP of Sales",
description:
"Sales leader with proven track record of building high-performing teams and driving revenue growth in B2B markets.",
image: {
__image_url__:
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80",
__image_prompt__: "Professional businesswoman VP headshot",
},
},
{
name: "David Kim",
position: "Head of Product",
description:
"Product strategist focused on user experience and market-driven solutions. Former product manager at leading tech companies.",
image: {
__image_url__:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80",
__image_prompt__: "Professional businessman product manager headshot",
},
},
])
.meta({
description: "List of team members with their information",
}),
companyName: z.string().default("presenton").meta({
description: "Company name to display in the header",
}),
date: z.string().default("June 13, 2038").meta({
description: "Date to display in the header",
}),
});
export const Schema = modernTeamSlideSchema;
export type ModernTeamSlideData = z.infer<typeof modernTeamSlideSchema>;
interface ModernTeamSlideLayoutProps {
data?: Partial<ModernTeamSlideData>;
}
const ModernTeamSlideLayout: React.FC<ModernTeamSlideLayoutProps> = ({
data: slideData,
}) => {
return (
<>
{/* Import Montserrat Font */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<div
className="w-full max-w-[1280px] max-h-[720px] aspect-video bg-white mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "Montserrat, sans-serif",
}}
>
{/* 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>
</div>
{/* Main Content */}
<div className="relative z-10 flex flex-col items-start justify-center h-full px-16 pt-24 pb-10">
{/* Title */}
<h1
className="text-7xl font-bold text-blue-600 mb-4 leading-tight text-left"
style={{ letterSpacing: "-0.03em" }}
>
{slideData?.title}
</h1>
{/* Subtitle */}
<p className="text-blue-600 text-lg leading-relaxed font-normal mb-12 max-w-lg text-left">
{slideData?.subtitle}
</p>
{/* Team Members Row */}
<div className="flex flex-row w-full justify-between items-start gap-6 mt-2">
{slideData?.teamMembers?.map((member, idx) => (
<div
key={idx}
className="flex flex-col items-center bg-[#f7f9fc] rounded-lg shadow-md px-6 pt-6 pb-4 w-1/4 min-w-[210px] max-w-[240px] mx-auto"
style={{ minHeight: 340 }}
>
{/* 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"
/>
</div>
{/* Name */}
<div className="text-lg font-bold text-blue-700 mb-1">
{member.name}
</div>
{/* Position Badge */}
<div className="bg-blue-600 text-white text-xs font-semibold px-3 py-1 rounded-sm mb-2 uppercase tracking-wide">
{member.position}
</div>
{/* Description */}
<div className="text-sm text-gray-700 text-center mb-2 min-h-[48px]">
{member.description}
</div>
{/* LinkedIn Link (if provided) */}
{member.linkedIn && (
<a
href={member.linkedIn}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 transition-colors duration-200 mt-1"
>
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.338 16.338H13.67V12.16c0-.995-.017-2.277-1.387-2.277-1.39 0-1.601 1.086-1.601 2.207v4.248H8.014v-8.59h2.559v1.174h.037c.356-.675 1.227-1.387 2.526-1.387 2.703 0 3.203 1.778 3.203 4.092v4.711zM5.005 6.575a1.548 1.548 0 11-.003-3.096 1.548 1.548 0 01.003 3.096zm-1.337 9.763H6.34v-8.59H3.667v8.59zM17.668 1H2.328C1.595 1 1 1.581 1 2.298v15.403C1 18.418 1.595 19 2.328 19h15.34c.734 0 1.332-.582 1.332-1.299V2.298C19 1.581 18.402 1 17.668 1z"
clipRule="evenodd"
/>
</svg>
LinkedIn
</a>
)}
</div>
))}
</div>
</div>
{/* Bottom Divider */}
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600" />
</div>
</>
);
};
export default ModernTeamSlideLayout;

View file

@ -0,0 +1,147 @@
import React from "react";
import * as z from "zod";
export const layoutId = "thank-you-slide";
export const layoutName = "Thank You Slide";
export const layoutDescription =
"A simple, plain thank you slide for closing presentations.";
const thankYouSlideSchema = z.object({
title: z.string().min(3).max(40).default("Thank You!").meta({
description: "Main thank you message",
}),
subtitle: z.string().min(0).max(100).default("").meta({
description: "Optional subtitle or closing remark",
}),
companyName: z.string().min(2).max(50).default("Rimberio").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(30).default("June 13, 2038").meta({
description: "Date displayed in header",
}),
address: z
.string()
.min(5)
.max(100)
.default("123 Anywhere St., Any City, ST 12345")
.meta({
description: "Company address for contact section",
}),
phone: z.string().min(5).max(30).default("+123-456-7890").meta({
description: "Company phone number for contact section",
}),
website: z.string().min(5).max(100).default("www.reallygreatsite.com").meta({
description: "Company website for contact section",
}),
email: z.string().default("info@reallygreatsite.com").meta({
description: "Company email address for contact section",
}),
});
export const Schema = thankYouSlideSchema;
export type ThankYouSlideData = z.infer<typeof thankYouSlideSchema>;
interface ThankYouSlideLayoutProps {
data?: Partial<ThankYouSlideData>;
}
const ThankYouSlideLayout: React.FC<ThankYouSlideLayoutProps> = ({ data }) => {
return (
<>
{/* Import fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video relative z-20 mx-auto overflow-hidden flex flex-col"
style={{
fontFamily: "Montserrat, sans-serif",
backgroundColor: "#1E4CD9", // blue background
}}
>
{/* Header */}
<div className="absolute top-8 left-10 right-10 flex justify-between items-center text-white text-sm font-semibold">
<span>{data?.companyName || "Rimberio"}</span>
<span>{data?.date || "June 13, 2038"}</span>
</div>
{/* Main content area */}
<div className="flex flex-1 flex-col px-16 pb-16 justify-between">
{/* Thank You and description */}
<div className="flex flex-col items-start w-full pt-16">
<h1
className="font-bold text-white mb-6 mt-8 text-left w-full"
style={{
fontSize: "8.5rem", // Increase size beyond text-7xl
lineHeight: 1.05,
}}
>
{data?.title || "Thank You!"}
</h1>
{data?.subtitle && (
<div className="text-xl text-blue-100 font-normal text-left w-full mb-2">
{data.subtitle}
</div>
)}
<div className="text-xl text-white text-left w-full max-w-3xl mb-0">
Write down your hopes for the future of your company. Don't forget
to thank the company for the opportunity and convince related
parties to support your company.
</div>
</div>
{/* Footer area */}
<div className="flex w-full items-end justify-between mt-auto">
{/* Left: We are ready to assist you */}
<div className="flex flex-col">
<div
className="font-bold text-white text-left mb-3"
style={{
fontSize: "2rem",
marginBottom: 0,
}}
>
We are ready to assist you
</div>
</div>
{/* Right: Contacts */}
<div className="flex flex-col items-end text-white text-sm space-y-2 min-w-[220px]">
<div className="flex items-center gap-2">
<span role="img" aria-label="address">
📍
</span>
{data?.address}
</div>
<div className="flex items-center gap-2">
<span role="img" aria-label="phone">
📞
</span>
{data?.phone}
</div>
<div className="flex items-center gap-2">
<span role="img" aria-label="website">
🌐
</span>
{data?.website}
</div>
<div className="flex items-center gap-2">
<span role="img" aria-label="email">
</span>
{data?.email}
</div>
</div>
</div>
</div>
{/* Bottom border line */}
<div className="absolute bottom-0 left-0 w-full h-1 bg-white"></div>
</div>
</>
);
};
export default ThankYouSlideLayout;