Merge pull request #407 from presenton/feat/new_templates_and_refactor_template_loading

feat: New Templates and refactor template loading approach
This commit is contained in:
Shiva Raj Badu 2026-02-11 20:52:27 +05:45 committed by GitHub
commit acb850b50c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 25109 additions and 3471 deletions

View file

@ -24,7 +24,7 @@ ENV TEMP_DIRECTORY=/tmp/presenton
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Install ollama
RUN curl -fsSL http://ollama.com/install.sh | sh
# RUN curl -fsSL http://ollama.com/install.sh | sh
# Install dependencies for FastAPI
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \

View file

@ -1,32 +1,60 @@
import React from "react";
'use client'
import React, { useEffect, useState, memo, useCallback } from "react";
import { useDispatch } from "react-redux";
import { addNewSlide } from "@/store/slices/presentationGeneration";
import { Loader2 } from "lucide-react";
import { useLayout, FullDataInfo } from "../context/LayoutContext";
import { v4 as uuidv4 } from "uuid";
import { Trash2 } from 'lucide-react';
import { toast } from 'sonner';
interface NewSlideProps {
import { getCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { getTemplatesByTemplateName } from "@/app/presentation-templates";
interface LayoutItemProps {
layout: any;
onSelect: (sampleData: any, layoutId: string) => void;
}
const LayoutItem = memo(({ layout, onSelect }: LayoutItemProps) => {
const { component: LayoutComponent, sampleData, layoutId } = layout;
return (
<div
onClick={() => onSelect(sampleData, layoutId)}
className="relative cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
});
LayoutItem.displayName = 'LayoutItem';
interface NewSlideV1Props {
setShowNewSlideSelection: (show: boolean) => void;
templateID: string;
index: number;
presentationId: string;
}
const NewSlide = ({
const NewSlideV1 = ({
setShowNewSlideSelection,
templateID,
index,
presentationId,
}: NewSlideProps) => {
}: NewSlideV1Props) => {
const dispatch = useDispatch();
const handleNewSlide = (sampleData: any, id: string) => {
const [layouts, setLayouts] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const isCustomTemplate = templateID.startsWith("custom-");
const handleNewSlide = useCallback((sampleData: any, id: string) => {
try {
const newSlide = {
id: uuidv4(),
index: index,
content: sampleData,
layout_group: templateID,
layout: id,
layout: isCustomTemplate ? `${templateID}:${id}` : id,
presentation: presentationId,
};
dispatch(addNewSlide({ slideData: newSlide, index }));
@ -35,9 +63,32 @@ const NewSlide = ({
console.error(error);
toast.error("Error adding new slide");
}
};
const { getFullDataByTemplateID, loading } = useLayout();
const fullData = getFullDataByTemplateID(templateID);
}, [index, templateID, presentationId, dispatch, setShowNewSlideSelection]);
useEffect(() => {
if (layouts.length > 0 || loading) return;
const fetchLayouts = async () => {
if (isCustomTemplate) {
setLoading(true);
const customTemplateId = templateID.split("custom-")[1];
const templateDetails = await getCustomTemplateDetails(customTemplateId, "Custom Template", "User-created template");
setLayouts(templateDetails?.layouts || []);
setLoading(false);
} else {
setLoading(true);
const templateDetails = getTemplatesByTemplateName(templateID);
setLayouts(templateDetails || []);
setLoading(false);
}
}
fetchLayouts();
}, []);
if (loading) {
return (
@ -66,24 +117,20 @@ const NewSlide = ({
/>
</div>
<div className="grid grid-cols-4 gap-4">
{fullData.map((layout: FullDataInfo, index: number) => {
const { component: LayoutComponent, sampleData, layoutId } = layout;
return (
<div
onClick={() => handleNewSlide(sampleData, layoutId)}
key={`${layoutId}-${index}`}
className=" relative cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
{layouts.map((layout: any) => (
<LayoutItem
key={layout.layoutId}
layout={layout}
onSelect={handleNewSlide}
/>
))}
</div>
</div>
);
};
export default NewSlide;
export default NewSlideV1;

View file

@ -1,5 +1,5 @@
"use client";
import React, { useCallback, useEffect } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
ChevronLeft,
ChevronRight,
@ -9,7 +9,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Slide } from "../types/slide";
import { useTemplateLayouts } from "../hooks/useTemplateLayouts";
import { V1ContentRender } from "./V1ContentRender";
interface PresentationModeProps {
@ -32,8 +32,33 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
onExit,
onSlideChange,
}) => {
const { renderSlideContent } = useTemplateLayouts();
if (slides === undefined || slides === null || slides.length === 0) {
return null;
}
const recomputeScale = useCallback(() => {
if (typeof window === "undefined") return;
const padding = isFullscreen ? 0 : 64; // match p-8 when not fullscreen
const fullscreenMargin = isFullscreen ? 16 : 0; // small safety margin to prevent clipping
const availableWidth = Math.max(window.innerWidth - padding - fullscreenMargin, 0);
const availableHeight = Math.max(window.innerHeight - padding - fullscreenMargin, 0);
const baseW = 1280;
const baseH = 720;
const s = Math.min(availableWidth / baseW, availableHeight / baseH);
}, [isFullscreen]);
useEffect(() => {
recomputeScale();
window.addEventListener("resize", recomputeScale);
return () => window.removeEventListener("resize", recomputeScale);
}, [recomputeScale]);
// Modify the handleKeyPress to prevent default behavior
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
@ -54,6 +79,11 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
}
break;
case "Escape":
// If fullscreen is active, only exit fullscreen on first ESC. Second ESC exits present mode.
if (document.fullscreenElement) {
try { document.exitFullscreen(); } catch (_) { }
return;
}
onExit();
break;
case "f":
@ -62,7 +92,7 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
break;
}
},
[currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle]
[currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle, isFullscreen]
);
// Add both keydown and keyup listeners
@ -118,7 +148,8 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
return (
<div
className="fixed inset-0 bg-black flex flex-col"
className="fixed inset-0 flex flex-col"
style={{ backgroundColor: "var(--page-background-color,#c8c7c9)" }}
tabIndex={0}
onClick={handleSlideClick}
>
@ -128,6 +159,7 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
<div className="presentation-controls absolute top-4 right-4 flex items-center gap-2 z-50">
<Button
variant="ghost"
style={{ color: "var(--text-body-color,#000000)" }}
size="icon"
onClick={(e) => {
e.stopPropagation();
@ -143,6 +175,7 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
</Button>
<Button
variant="ghost"
style={{ color: "var(--text-body-color,#000000)" }}
size="icon"
onClick={(e) => {
e.stopPropagation();
@ -157,6 +190,7 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
<div className="presentation-controls absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-4 z-50">
<Button
variant="ghost"
style={{ color: "var(--text-body-color,#000000)" }}
size="icon"
onClick={(e) => {
e.stopPropagation();
@ -165,13 +199,16 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
disabled={currentSlide === 0}
className="text-white hover:bg-white/20"
>
<ChevronLeft className="h-5 w-5" />
<ChevronLeft className="h-5 w-5" style={{ color: "var(--text-body-color,#000000)" }} />
</Button>
<span className="text-white">
<span className="text-white"
style={{ color: "var(--text-body-color,#000000)" }}
>
{currentSlide + 1} / {slides.length}
</span>
<Button
variant="ghost"
style={{ color: "var(--text-body-color,#000000)" }}
size="icon"
onClick={(e) => {
e.stopPropagation();
@ -180,19 +217,28 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
disabled={currentSlide === slides.length - 1}
className="text-white hover:bg-white/20"
>
<ChevronRight className="h-5 w-5" />
<ChevronRight className="h-5 w-5" style={{ color: "var(--text-body-color,#000000)" }} />
</Button>
</div>
</>
)}
{/* Current Slide */}
<div className="flex-1 flex items-center justify-center p-8">
<div
className={`w-full max-w-[1280px] scale-110 aspect-video slide-theme slide-container border rounded-sm font-inter shadow-lg bg-white`}
>
{slides[currentSlide] &&
renderSlideContent(slides[currentSlide], false)}
{/* Slides (all mounted, only current visible) */}
<div className={`flex-1 flex items-center justify-center ${isFullscreen ? "p-0" : "p-8"}`}>
<div className="w-full h-full flex items-center justify-center relative" >
<div
className={` rounded-sm font-inter relative w-full h-full flex items-center justify-center`}
>
{slides.length > 0 && slides.map((slide, index) => (
<div
key={slide.id}
className={index === currentSlide ? " w-full h-full flex items-center justify-center" : "hidden w-full h-full"}
>
<V1ContentRender slide={slide} isEditMode={true} />
</div>
))}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,80 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { V1ContentRender } from '../../(presentation-generator)/components/V1ContentRender';
const BASE_WIDTH = 1280;
const BASE_HEIGHT = 720;
const SlideScale = ({ slide }: { slide: any }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [containerWidth, setContainerWidth] = useState<number>(0);
const scale = useMemo(() => {
// Slight padding to avoid overflow due to borders/scrollbars
const safeWidth = Math.max(0, containerWidth + 20);
if (!safeWidth) return 1;
return Math.min((safeWidth / BASE_WIDTH) * 0.98, 1);
}, [containerWidth]);
useEffect(() => {
if (!containerRef.current) return;
const el = containerRef.current;
const ro = new ResizeObserver(() => {
// Use clientWidth so we match the actual available column width
setContainerWidth(el.clientWidth);
});
ro.observe(el);
// Initial measure
setContainerWidth(el.clientWidth);
return () => ro.disconnect();
}, []);
return (<div
ref={containerRef}
className="relative w-full shadow-md"
>
<div
className="relative mx-auto max-w-[1280px] "
style={{ height: `${BASE_HEIGHT * scale}px`, overflow: "hidden" }}
>
<div
className="absolute top-0 left-0"
style={{
width: BASE_WIDTH,
height: BASE_HEIGHT,
transformOrigin: "top left",
transform: `scale(${scale})`,
}}
>
<div
className="relative w-full h-full select-none"
data-testid="slide-content"
style={{
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
} as React.CSSProperties}
>
<div
className="absolute inset-0 bg-transparent z-30 w-full h-full select-none"
aria-hidden="true"
/>
<V1ContentRender slide={slide} isEditMode={true} />
</div>
</div>
</div>
</div>
)
}
export default SlideScale

View file

@ -0,0 +1,139 @@
"use client";
import React, { useMemo, useRef } from "react";
import EditableLayoutWrapper from "../components/EditableLayoutWrapper";
import SlideErrorBoundary from "../components/SlideErrorBoundary";
import TiptapTextReplacer from "../components/TiptapTextReplacer";
import { validate as uuidValidate } from 'uuid';
import { getLayoutByLayoutId } from "@/app/presentation-templates";
import { useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { updateSlideContent } from "@/store/slices/presentationGeneration";
import { useDispatch } from "react-redux";
import { Loader2 } from "lucide-react";
export const V1ContentRender = ({ slide, isEditMode, theme }: { slide: any, isEditMode: boolean, theme?: any, enableEditMode?: boolean }) => {
const dispatch = useDispatch();
const containerRef = useRef<HTMLDivElement | null>(null);
const customTemplateId = slide.layout_group.startsWith("custom-") ? slide.layout_group.split("custom-")[1] : slide.layout_group;
const isCustomTemplate = uuidValidate(customTemplateId) || slide.layout_group.startsWith("custom-");
// Always call the hook (React hooks rule), but with empty id when not a custom template
const { template: customTemplate, loading: customLoading, fonts } = useCustomTemplateDetails({
id: isCustomTemplate ? customTemplateId : "",
name: isCustomTemplate ? slide.layout_group : "",
description: ""
});
if (fonts && typeof fonts === 'object') {
// useFontLoader(fonts as unknown as Record<string, string>);
}
// Memoize layout resolution to prevent unnecessary recalculations
const Layout = useMemo(() => {
if (isCustomTemplate) {
if (customTemplate) {
const layoutId = slide.layout.startsWith("custom-") ? slide.layout.split(":")[1] : slide.layout;
const compiledLayout = customTemplate.layouts.find(
(layout) => layout.layoutId === layoutId
);
return compiledLayout?.component ?? null;
}
return null;
} else {
const template = getLayoutByLayoutId(slide.layout);
return template?.component ?? null;
}
}, [isCustomTemplate, customTemplate, slide.layout]);
// Show loading state for custom templates
if (isCustomTemplate && customLoading) {
return (
<div className="flex flex-col items-center justify-center aspect-video h-full bg-gray-100 rounded-lg">
<Loader2 className="w-4 h-4 animate-spin" />
</div>
);
}
if (!Layout) {
if (Object.keys(slide.content).length === 0) {
return (
<div className="flex flex-col items-center cursor-pointer justify-center aspect-video h-full bg-gray-100 rounded-lg">
<p className="text-gray-600 text-center text-base">Blank Slide</p>
<p className="text-gray-600 text-center text-sm">This slide is empty. Please add content to it using the edit button.</p>
</div>
)
}
return (
<div className="flex flex-col items-center justify-center aspect-video h-full bg-gray-100 rounded-lg">
<p className="text-gray-600 text-center text-base">
Layout &quot;{slide.layout}&quot; not found in &quot;
{slide.layout_group}&quot; Template
</p>
</div>
);
}
const LayoutComp = Layout as React.ComponentType<{ data: any }>;
if (isEditMode) {
return (
<SlideErrorBoundary label={`Slide ${slide.index + 1}`}>
<div ref={containerRef} className={`w-full h-full `}>
<EditableLayoutWrapper
slideIndex={slide.index}
slideData={slide.content}
properties={slide.properties}
>
<TiptapTextReplacer
key={slide.id}
slideData={slide.content}
slideIndex={slide.index}
onContentChange={(
content: string,
dataPath: string,
slideIndex?: number
) => {
if (dataPath && slideIndex !== undefined) {
dispatch(
updateSlideContent({
slideIndex: slideIndex,
dataPath: dataPath,
content: content,
})
);
}
}}
>
<LayoutComp data={{
...slide.content,
_logo_url__: theme ? theme.logo_url : null,
__companyName__: (theme && theme.company_name) ? theme.company_name : null,
}} />
</TiptapTextReplacer>
</EditableLayoutWrapper>
</div>
</SlideErrorBoundary>
);
}
return (
<LayoutComp data={{
...slide.content,
_logo_url__: theme ? theme.logo_url : null,
__companyName__: (theme && theme.company_name) ? theme.company_name : null,
}} />
)
};

View file

@ -1,713 +0,0 @@
"use client";
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from "react";
import dynamic from "next/dynamic";
import { toast } from "sonner";
import * as z from "zod";
import { useDispatch } from "react-redux";
import { setLayoutLoading } from "@/store/slices/presentationGeneration";
import * as Babel from "@babel/standalone";
import * as Recharts from "recharts";
import * as d3 from 'd3';
import { getHeader } from "../services/api/header";
export interface LayoutInfo {
id: string;
name?: string;
description?: string;
json_schema: any;
templateID: string;
templateName?: string;
}
export interface FullDataInfo {
name: string;
component: React.ComponentType<any>;
schema: any;
sampleData: any;
fileName: string;
templateID: string;
layoutId: string;
}
export interface TemplateSetting {
description: string;
ordered: boolean;
default?: boolean;
}
export interface TemplateResponse {
templateID: string;
templateName?: string;
files: string[];
settings: TemplateSetting | null;
}
export interface LayoutData {
layoutsById: Map<string, LayoutInfo>;
layoutsByTemplateID: Map<string, Set<string>>;
templateSettings: Map<string, TemplateSetting>;
fileMap: Map<string, { fileName: string; templateID: string }>;
templateLayouts: Map<string, LayoutInfo[]>;
layoutSchema: LayoutInfo[];
fullDataByTemplateID: Map<string, FullDataInfo[]>;
}
export interface LayoutContextType {
getLayoutById: (layoutId: string) => LayoutInfo | null;
getLayoutsByTemplateID: (templateID: string) => LayoutInfo[];
getTemplateSetting: (templateID: string) => TemplateSetting | null;
getAllTemplateIDs: () => string[];
getAllLayouts: () => LayoutInfo[];
getFullDataByTemplateID: (templateID: string) => FullDataInfo[];
loading: boolean;
error: string | null;
getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null;
isPreloading: boolean;
cacheSize: number;
refetch: () => Promise<void>;
getCustomTemplateFonts: (presentationId: string) => string[] | null;
}
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
const layoutCache = new Map<string, React.ComponentType<{ data: any }>>();
const createCacheKey = (templateID: string, fileName: string): string =>
`${templateID}/${fileName}`;
// Extract Babel compilation logic into a utility function
const compileCustomLayout = (layoutCode: string, React: any, z: any) => {
const cleanCode = layoutCode
.replace(/import\s+React\s+from\s+'react';?/g, "")
.replace(/import\s*{\s*z\s*}\s*from\s+'zod';?/g, "")
.replace(/import\s+.*\s+from\s+['"]zod['"];?/g, "")
// remove every zod import (any style)
.replace(/import\s+.*\s+from\s+['"]zod['"];?/g, "")
.replace(/const\s+[^=]*=\s*require\(['"]zod['"]\);?/g, "")
.replace(/typescript/g, "")
const compiled = Babel.transform(cleanCode, {
presets: [
["react", { runtime: "classic" }],
["typescript", { isTSX: true, allExtensions: true }],
],
sourceType: "script",
}).code;
const factory = new Function(
"React",
"_z",
"Recharts",
`
const z = _z;
const useRef= React.useRef;
const useEffect= React.useEffect;
// Expose commonly used Recharts components to compiled layouts
const { ResponsiveContainer, LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, PieChart, Pie, Cell, AreaChart, Area, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ComposedChart, ScatterChart, Scatter, FunnelChart, Funnel, TreemapChart, Treemap, SankeyChart, Sankey, RadialBarChart, RadialBar, ReferenceLine, ReferenceDot, ReferenceArea, Brush, ErrorBar, LabelList, Label } = Recharts || {};
${compiled}
/* everything declared in the string is in scope here */
return {
__esModule: true,
default: typeof dynamicSlideLayout !== 'undefined' ? dynamicSlideLayout : (typeof DefaultLayout !== 'undefined' ? DefaultLayout : undefined),
layoutName,
layoutId,
layoutDescription,
Schema
};
`
);
return factory(React, z, Recharts);
};
export const LayoutProvider: React.FC<{
children: ReactNode;
}> = ({ children }) => {
const [layoutData, setLayoutData] = useState<LayoutData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isPreloading, setIsPreloading] = useState(false);
const [customTemplateFonts, setCustomTemplateFonts] = useState<Map<string, string[]>>(new Map());
const dispatch = useDispatch();
const buildData = async (templateData: TemplateResponse[]) => {
const layouts: LayoutInfo[] = [];
const layoutsById = new Map<string, LayoutInfo>();
const layoutsByTemplateID = new Map<string, Set<string>>();
const templateSettingsMap = new Map<string, TemplateSetting>();
const fileMap = new Map<string, { fileName: string; templateID: string }>();
const templateLayoutsCache = new Map<string, LayoutInfo[]>();
const fullDataByTemplateID = new Map<string, FullDataInfo[]>();
// Start preloading process
setIsPreloading(true);
try {
for (const template of templateData) {
// Initialize template
if (!layoutsByTemplateID.has(template.templateID)) {
layoutsByTemplateID.set(template.templateID, new Set());
}
fullDataByTemplateID.set(template.templateID, []);
// template settings or default settings
const settings = template.settings || {
templateName: template.templateName,
description: `${template.templateID} presentation layouts`,
ordered: false,
default: false,
};
templateSettingsMap.set(template.templateID, settings);
const templateLayouts: LayoutInfo[] = [];
const templateFullData: FullDataInfo[] = [];
for (const fileName of template.files) {
try {
const file = fileName.replace(".tsx", "").replace(".ts", "");
const module = await import(
`@/presentation-templates/${template.templateID}/${file}`
);
if (!module.default) {
toast.error(`${file} has no default export`, {
description:
"Please ensure the layout file exports a default component",
});
console.warn(`${file} has no default export`);
continue;
}
if (!module.Schema) {
toast.error(`${file} has no Schema export`, {
description: "Please ensure the layout file exports a Schema",
});
console.warn(`${file} has no Schema export`);
continue;
}
// Cache the layout component immediately after import
const cacheKey = createCacheKey(template.templateID, fileName);
if (!layoutCache.has(cacheKey)) {
layoutCache.set(cacheKey, module.default);
}
const originalLayoutId =
module.layoutId || file.toLowerCase().replace(/layout$/, "");
const uniqueKey = `${template.templateID}:${originalLayoutId}`;
const layoutName =
module.layoutName || file.replace(/([A-Z])/g, " $1").trim();
const layoutDescription =
module.layoutDescription ||
`${layoutName} layout for presentations`;
const jsonSchema = z.toJSONSchema(module.Schema, {
override: (ctx) => {
delete ctx.jsonSchema.default;
},
});
const layout: LayoutInfo = {
id: uniqueKey,
name: layoutName,
description: layoutDescription,
json_schema: jsonSchema,
templateID: template.templateID,
templateName: template.templateName,
};
const sampleData = module.Schema.parse({});
const fullData: FullDataInfo = {
name: layoutName,
component: module.default,
schema: jsonSchema,
sampleData: sampleData,
fileName,
templateID: template.templateID,
layoutId: uniqueKey,
};
templateFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByTemplateID.get(template.templateID)!.add(uniqueKey);
fileMap.set(uniqueKey, {
fileName,
templateID: template.templateID,
});
templateLayouts.push(layout);
layouts.push(layout);
} catch (error) {
console.error(
`💥 Error extracting schema for ${fileName} from ${template.templateID}:`,
error
);
}
}
fullDataByTemplateID.set(template.templateID, templateFullData);
// Cache template layouts
templateLayoutsCache.set(template.templateID, templateLayouts);
}
} catch (err: any) {
console.error("Compilation error:", err);
}
return {
layoutsById,
layoutsByTemplateID,
templateSettings: templateSettingsMap,
fileMap,
templateLayoutsCache,
layoutSchema: layouts,
fullDataByTemplateID,
};
};
const loadLayouts = async () => {
try {
setLoading(true);
setError(null);
dispatch(setLayoutLoading(true));
const templateResponse = await fetch("/api/templates");
if (!templateResponse.ok) {
throw new Error(
`Failed to fetch layouts: ${templateResponse.statusText}`
);
}
const templateData: TemplateResponse[] =
await templateResponse.json();
if (!templateData || templateData.length === 0) {
setError("No template found");
return;
}
const data = await buildData(templateData);
const customLayouts = await LoadCustomLayouts();
setIsPreloading(false);
const combinedData = {
layoutsById: mergeMaps(data.layoutsById, customLayouts.layoutsById),
layoutsByTemplateID: mergeMaps(
data.layoutsByTemplateID,
customLayouts.layoutsByTemplateID
),
templateSettings: mergeMaps(
data.templateSettings,
customLayouts.templateSettings
),
fileMap: mergeMaps(data.fileMap, customLayouts.fileMap),
templateLayouts: mergeMaps(
data.templateLayoutsCache,
customLayouts.templateLayoutsCache
),
layoutSchema: [...data.layoutSchema, ...customLayouts.layoutSchema],
fullDataByTemplateID: mergeMaps(
data.fullDataByTemplateID,
customLayouts.fullDataByTemplateID
),
};
setLayoutData(combinedData);
// The preloading is now handled within buildData
} catch (err: unknown) {
const errorMessage =
err instanceof Error ? err.message : "Failed to load layouts";
setError(errorMessage);
console.error("💥 Error loading layouts:", err);
} finally {
dispatch(setLayoutLoading(false));
setLoading(false);
}
};
function mergeMaps<K, V>(map1: Map<K, V>, map2: Map<K, V>): Map<K, V> {
const merged = new Map(map1);
map2.forEach((value, key) => {
merged.set(key, value);
});
return merged;
}
const LoadCustomLayouts = async () => {
const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
const layouts: LayoutInfo[] = [];
const layoutsById = new Map<string, LayoutInfo>();
const layoutsByTemplateID = new Map<string, Set<string>>();
const templateSettingsMap = new Map<string, TemplateSetting>();
const fileMap = new Map<string, { fileName: string; templateID: string }>();
const templateLayoutsCache = new Map<string, LayoutInfo[]>();
const fullDataByTemplateID = new Map<string, FullDataInfo[]>();
try {
const customTemplateResponse = await fetch(
`/api/v1/ppt/template-management/summary`,
{
headers: {
...getHeader(),
...(token ? { Authorization: `Bearer ${token}` } : {}),
}
}
);
const customTemplateData = await customTemplateResponse.json();
const customFonts = new Map<string, string[]>();
const customTemplates = customTemplateData.presentations || [];
for (const templateInfo of customTemplates) {
const pid =
(templateInfo && (templateInfo.presentation_id || templateInfo.presentation || templateInfo.id)) ||
"";
if (!pid) {
// skip invalid entries
continue;
}
const templateID = `custom-${pid}`;
const templateName = templateInfo.template?.name || templateID;
fullDataByTemplateID.set(templateID, []);
if (!layoutsByTemplateID.has(templateID)) {
layoutsByTemplateID.set(templateID, new Set());
}
const presentationId = pid;
const customLayoutResponse = await fetch(
`/api/v1/ppt/template-management/get-templates/${presentationId}`,
{
headers: {
...getHeader(),
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
}
);
const customLayoutsData = await customLayoutResponse.json();
const allLayout = customLayoutsData.layouts;
const settings = {
templateName: templateName,
description: `Custom presentation layouts`,
ordered: false,
default: false,
};
templateSettingsMap.set(`custom-${presentationId}`, settings);
const templateLayouts: LayoutInfo[] = [];
const templateFullData: FullDataInfo[] = [];
// Helper to create an inline error component for this specific slide
const createErrorComponent = (title: string, message: string): React.ComponentType<{ data: any }> => {
const ErrorSlide: React.FC<{ data: any }> = () => (
<div className="aspect-video w-full h-full bg-red-50 text-red-700 flex flex-col items-start justify-start p-4 space-y-2">
<div className="text-sm font-semibold">{title}</div>
<pre className="text-xs whitespace-pre-wrap break-words max-h-full overflow-auto bg-red-100 rounded-md p-2 border border-red-200">{message}</pre>
</div>
);
ErrorSlide.displayName = "CustomTemplateErrorSlide";
return ErrorSlide;
};
for (const i of allLayout) {
try {
/* ---------- 1. compile JSX to plain script ------------------ */
const module = compileCustomLayout(i.layout_code, React, z);
// Determine identifiers even if subsequent steps fail
const originalLayoutId =
(module && (module as any).layoutId) ||
i.layout_name.toLowerCase().replace(/layout$/, "");
const uniqueKey = `${`custom-${presentationId}`}:${originalLayoutId}`;
const layoutName =
(module && (module as any).layoutName) ||
i.layout_name.replace(/([A-Z])/g, " $1").trim();
const layoutDescription =
(module && (module as any).layoutDescription) ||
`${layoutName} layout for presentations`;
let fullData: FullDataInfo | null = null;
let jsonSchema: any = null;
let componentToUse: React.ComponentType<{ data: any } | any> | null = null;
let sampleData: any = {};
// Validate exports
if (!module || !(module as any).default) {
const errorComp = createErrorComponent(
`Invalid export in ${i.layout_name}`,
"Default export not found. Please export a default React component."
);
componentToUse = errorComp;
jsonSchema = {};
} else if (!(module as any).Schema) {
const errorComp = createErrorComponent(
`Schema missing in ${i.layout_name}`,
"Schema export not found. Please export a Zod Schema as 'Schema'."
);
componentToUse = errorComp;
jsonSchema = {};
} else {
// Cache valid component
const cacheKey = createCacheKey(
`custom-${presentationId}`,
i.layout_name
);
if (!layoutCache.has(cacheKey)) {
layoutCache.set(cacheKey, (module as any).default);
}
componentToUse = (module as any).default;
// Build schema and sample data with guards
try {
jsonSchema = z.toJSONSchema((module as any).Schema, {
override: (ctx) => {
delete ctx.jsonSchema.default;
},
});
} catch (schemaErr: any) {
const errorComp = createErrorComponent(
`Schema generation failed for ${i.layout_name}`,
schemaErr?.message || String(schemaErr)
);
componentToUse = errorComp;
jsonSchema = {};
}
if (componentToUse !== null && componentToUse !== (module as any).default) {
// componentToUse already replaced with error component
sampleData = {};
} else {
try {
sampleData = (module as any).Schema.parse({});
} catch (parseErr: any) {
const errorComp = createErrorComponent(
`Schema.parse failed for ${i.layout_name}`,
parseErr?.message || String(parseErr)
);
componentToUse = errorComp;
sampleData = {};
jsonSchema = jsonSchema || {};
}
}
}
customFonts.set(presentationId, i.fonts);
const layout: LayoutInfo = {
id: uniqueKey,
name: layoutName,
description: layoutDescription,
json_schema: jsonSchema,
templateID: templateID,
templateName: templateName,
};
fullData = {
name: layoutName,
component: componentToUse as React.ComponentType<any>,
schema: jsonSchema,
sampleData: sampleData,
fileName: i.layout_name,
templateID: templateID,
layoutId: uniqueKey,
};
templateFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByTemplateID.get(templateID)!.add(uniqueKey);
fileMap.set(uniqueKey, {
fileName: i.layout_name,
templateID: templateID,
});
templateLayouts.push(layout);
layouts.push(layout);
} catch (e: any) {
// Handle compilation/runtime errors during transformation
const uniqueKey = `${`custom-${presentationId}`}:${i.layout_name.toLowerCase().replace(/layout$/, "")}`;
const layoutName = i.layout_name.replace(/([A-Z])/g, " $1").trim();
const errorComp = createErrorComponent(
`Compilation error in ${i.layout_name}`,
e?.message || String(e)
);
const layout: LayoutInfo = {
id: uniqueKey,
name: layoutName,
description: `Failed to compile ${i.layout_name}`,
json_schema: {},
templateID: templateID,
templateName: templateName,
};
const fullData: FullDataInfo = {
name: layoutName,
component: errorComp,
schema: {},
sampleData: {},
fileName: i.layout_name,
templateID: templateID,
layoutId: uniqueKey,
};
templateFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByTemplateID.get(templateID)!.add(uniqueKey);
fileMap.set(uniqueKey, {
fileName: i.layout_name,
templateID: templateID,
});
templateLayouts.push(layout);
layouts.push(layout);
}
}
setCustomTemplateFonts(customFonts);
// Cache template layouts
templateLayoutsCache.set(templateID, templateLayouts);
fullDataByTemplateID.set(templateID, templateFullData);
}
} catch (err: any) {
console.error("Compilation error:", err);
}
return {
layoutsById,
layoutsByTemplateID,
templateSettings: templateSettingsMap,
fileMap,
templateLayoutsCache,
layoutSchema: layouts,
fullDataByTemplateID,
};
};
const getLayout = (
layoutId: string
): React.ComponentType<{ data: any }> | null => {
if (!layoutData) return null;
let fileInfo: { fileName: string; templateID: string } | undefined;
// Search through all fileMap entries to find the layout
for (const [key, info] of Array.from(layoutData.fileMap.entries())) {
if (key === layoutId) {
fileInfo = info;
break;
}
}
if (!fileInfo) {
console.warn(`No file info found for layout: ${layoutId}`);
return null;
}
const cacheKey = createCacheKey(fileInfo.templateID, fileInfo.fileName);
// Return cached layout if available
if (layoutCache.has(cacheKey)) {
return layoutCache.get(cacheKey)!;
}
// Create and cache layout if not available
const file = fileInfo.fileName.replace(".tsx", "").replace(".ts", "");
const Layout = dynamic(
() => import(`@/presentation-templates/${fileInfo.templateID}/${file}`),
{
loading: () => (
<div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />
),
ssr: false,
}
) as React.ComponentType<{ data: any }>;
layoutCache.set(cacheKey, Layout);
return Layout;
};
// Updated accessor methods to handle templateID-specific lookups
const getLayoutById = (layoutId: string): LayoutInfo | null => {
if (!layoutData) return null;
// Search through all entries to find the layout (since we don't know the templateID)
for (const [key, layout] of Array.from(layoutData.layoutsById.entries())) {
if (key === layoutId) {
return layout;
}
}
return null;
};
const getLayoutsByTemplateID = (templateID: string): LayoutInfo[] => {
return layoutData?.templateLayouts.get(templateID) || [];
};
const getTemplateSetting = (templateID: string): TemplateSetting | null => {
return layoutData?.templateSettings.get(templateID) || null;
};
const getAllTemplateIDs = (): string[] => {
return layoutData ? Array.from(layoutData.templateSettings.keys()) : [];
};
const getAllLayouts = (): LayoutInfo[] => {
return layoutData?.layoutSchema || [];
};
const getFullDataByTemplateID = (templateID: string): FullDataInfo[] => {
return layoutData?.fullDataByTemplateID.get(templateID) || [];
};
const getCustomTemplateFonts = (presentationId: string): string[] | null => {
return customTemplateFonts.get(presentationId) || null;
};
// Load layouts on mount
useEffect(() => {
loadLayouts();
}, []); // Add presentationId to dependency array
const contextValue: LayoutContextType = {
getLayoutById,
getLayoutsByTemplateID,
getTemplateSetting,
getAllTemplateIDs,
getAllLayouts,
getFullDataByTemplateID,
getCustomTemplateFonts,
loading,
error,
getLayout,
isPreloading,
cacheSize: layoutCache.size,
refetch: loadLayouts,
};
return (
<LayoutContext.Provider value={contextValue}>
{children}
</LayoutContext.Provider>
);
};
export const useLayout = (): LayoutContextType => {
const context = useContext(LayoutContext);
if (context === undefined) {
throw new Error("useLayout must be used within a LayoutProvider");
}
return context;
};

View file

@ -8,7 +8,7 @@ export const useLayoutSaving = (
slides: ProcessedSlide[],
UploadedFonts: UploadedFont[],
fontsData: FontData | null,
refetch: () => void,
// refetch: () => void,
setSlides: React.Dispatch<React.SetStateAction<ProcessedSlide[]>>
) => {
const [isSavingLayout, setIsSavingLayout] = useState(false);
@ -61,15 +61,15 @@ export const useLayoutSaving = (
} catch (error) {
retryCount++;
console.error(`Error converting slide ${slide.slide_number} (attempt ${retryCount}):`, error);
if (retryCount < maxRetries) {
toast.error(`Failed to convert slide ${slide.slide_number}. Retrying in 2 minutes...`, {
description: `Attempt ${retryCount}/${maxRetries}. Error: ${error instanceof Error ? error.message : "An unexpected error occurred"}`,
});
// Wait for 2 minutes before retrying
await delay(2 * 60 * 1000);
toast.info(`Retrying conversion for slide ${slide.slide_number}...`);
} else {
throw new Error(`Failed to convert slide ${slide.slide_number} after ${maxRetries} attempts: ${error instanceof Error ? error.message : "An unexpected error occurred"}`);
@ -173,7 +173,7 @@ export const useLayoutSaving = (
});
toast.success(`Layout "${layoutName}" saved successfully`);
refetch();
// refetch();
closeSaveModal();
return presentationId;
} catch (error) {
@ -188,7 +188,7 @@ export const useLayoutSaving = (
} finally {
setIsSavingLayout(false);
}
}, [slides, UploadedFonts, fontsData, refetch, closeSaveModal, setSlides]);
}, [slides, UploadedFonts, fontsData, closeSaveModal, setSlides]);
return {
isSavingLayout,

View file

@ -3,7 +3,7 @@
import React, { useEffect } from "react";
import FontManager from "./components/FontManager";
import Header from "../dashboard/components/Header";
import { useLayout } from "../context/LayoutContext";
import { useCustomLayout } from "./hooks/useCustomLayout";
import { useFontManagement } from "./hooks/useFontManagement";
import { useFileUpload } from "./hooks/useFileUpload";
@ -22,14 +22,14 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
const CustomTemplatePage = () => {
const router = useRouter();
const pathname = usePathname();
const { refetch } = useLayout();
// Custom hooks for different concerns
const { hasRequiredKey, isRequiredKeyLoading } = useAPIKeyCheck();
const { selectedFile, handleFileSelect, removeFile } = useFileUpload();
const { slides, setSlides, completedSlides } = useCustomLayout();
const { fontsData, UploadedFonts, uploadFont, removeFont, getAllUnsupportedFonts, setFontsData } = useFontManagement();
const { isProcessingPptx, processFile, retrySlide,processSlideToHtml } = useSlideProcessing(
const { isProcessingPptx, processFile, retrySlide, processSlideToHtml } = useSlideProcessing(
selectedFile,
slides,
setSlides,
@ -39,7 +39,7 @@ const CustomTemplatePage = () => {
slides,
UploadedFonts,
fontsData,
refetch,
setSlides
);
@ -53,7 +53,7 @@ const CustomTemplatePage = () => {
};
const handleProcessSlideToHtml = (slide: any) => {
processSlideToHtml(slide,0)
processSlideToHtml(slide, 0)
}
// Handle slide updates
@ -62,15 +62,15 @@ const CustomTemplatePage = () => {
prevSlides.map((s, i) =>
i === index
? {
...s,
...updatedSlideData,
modified: true,
}
...s,
...updatedSlideData,
modified: true,
}
: s
)
);
};
useEffect(() => {
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
@ -90,7 +90,7 @@ const CustomTemplatePage = () => {
// Anthropic key warning
if (!hasRequiredKey) {
return <APIKeyWarning />;
}
return (
@ -112,7 +112,7 @@ const CustomTemplatePage = () => {
</div>
</div>
</div>
{/* File Upload Section */}
<FileUploadSection
@ -133,7 +133,7 @@ const CustomTemplatePage = () => {
uploadFont={uploadFont}
removeFont={removeFont}
getAllUnsupportedFonts={getAllUnsupportedFonts}
processSlideToHtml={()=>handleProcessSlideToHtml(slides[0])}
processSlideToHtml={() => handleProcessSlideToHtml(slides[0])}
/>
)}

View file

@ -10,7 +10,7 @@ import {
} from "@/components/ui/popover";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { useTemplateLayouts } from "@/app/(presentation-generator)/hooks/useTemplateLayouts";
import SlideScale from "../../components/PresentationRender";
export const PresentationCard = ({
id,
@ -26,7 +26,7 @@ export const PresentationCard = ({
onDeleted?: (presentationId: string) => void;
}) => {
const router = useRouter();
const { renderSlideContent } = useTemplateLayouts();
@ -92,7 +92,7 @@ export const PresentationCard = ({
>
<div className="absolute bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
{renderSlideContent(slide, false)}
<SlideScale slide={slide} />
</div>
</div>

View file

@ -1,98 +0,0 @@
"use client";
import React, { useMemo } from "react";
import { useDispatch } from "react-redux";
import { useLayout } from "../context/LayoutContext";
import EditableLayoutWrapper from "../components/EditableLayoutWrapper";
import SlideErrorBoundary from "../components/SlideErrorBoundary";
import TiptapTextReplacer from "../components/TiptapTextReplacer";
import { updateSlideContent } from "../../../store/slices/presentationGeneration";
import { Loader2 } from "lucide-react";
export const useTemplateLayouts = () => {
const dispatch = useDispatch();
const { getLayoutById, getLayout, loading } =
useLayout();
const getTemplateLayout = useMemo(() => {
return (layoutId: string, groupName: string) => {
const layout = getLayoutById(layoutId);
if (layout) {
return getLayout(layoutId);
}
return null;
};
}, [getLayoutById, getLayout]);
// Render slide content with group validation, automatic Tiptap text editing, and editable images/icons
const renderSlideContent = useMemo(() => {
return (slide: any, isEditMode: boolean) => {
const Layout = getTemplateLayout(slide.layout, slide.layout_group);
if (loading) {
return (
<div className="flex flex-col items-center justify-center aspect-video h-full bg-gray-100 rounded-lg">
<Loader2 className="w-8 h-8 animate-spin text-blue-800" />
</div>
);
}
if (!Layout) {
return (
<div className="flex flex-col items-center justify-center aspect-video h-full bg-gray-100 rounded-lg">
<p className="text-gray-600 text-center text-base">
Layout &quot;{slide.layout}&quot; not found in &quot;
{slide.layout_group}&quot; group
</p>
</div>
);
}
if (isEditMode) {
return (
<EditableLayoutWrapper
slideIndex={slide.index}
slideData={slide.content}
properties={slide.properties}
>
<TiptapTextReplacer
key={slide.id}
slideData={slide.content}
slideIndex={slide.index}
onContentChange={(
content: string,
dataPath: string,
slideIndex?: number
) => {
if (dataPath && slideIndex !== undefined) {
dispatch(
updateSlideContent({
slideIndex: slideIndex,
dataPath: dataPath,
content: content,
})
);
}
}}
>
<SlideErrorBoundary label={`Slide ${slide.index + 1}`}>
<Layout data={slide.content} />
</SlideErrorBoundary>
</TiptapTextReplacer>
</EditableLayoutWrapper>
);
}
return (
<SlideErrorBoundary label={`Slide ${slide.index + 1}`}>
<Layout data={slide.content} />
</SlideErrorBoundary>
);
};
}, [getTemplateLayout, dispatch]);
return {
getTemplateLayout,
renderSlideContent,
loading,
};
};

View file

@ -0,0 +1,98 @@
"use client";
import React, { memo } from "react";
import { Card } from "@/components/ui/card";
import { CustomTemplates, useCustomTemplatePreview } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { CompiledLayout } from "@/app/hooks/compileLayout";
// Memoized preview component to prevent re-renders during scroll
export const LayoutPreview = memo(({ layout, templateId, index }: { layout: CompiledLayout, templateId: string, index: number }) => {
const LayoutComponent = layout.component;
return (
<div
key={`${templateId}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
style={{ contain: 'layout style paint', willChange: 'auto' }}
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]"
style={{ transform: 'scale(0.2) translateZ(0)', backfaceVisibility: 'hidden' }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
});
LayoutPreview.displayName = 'LayoutPreview';
export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => {
const { previewLayouts, loading: customLoading } = useCustomTemplatePreview(template.id);
const isSelected = selectedTemplate === template.id;
return (
<Card
className={`${isSelected ? 'border-2 border-blue-500' : ''} cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden relative`}
style={{ contain: 'layout style paint' }}
onClick={() => {
onSelectTemplate(template.id);
}}
>
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900">
{template.name}
</h3>
</div>
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
{customLoading ? (
// Loading placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-loading-${index}`}
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts && previewLayouts?.length > 0 ? (
// Actual layout previews - using memoized component
previewLayouts?.slice(0, 4).map((layout: CompiledLayout, index: number) => (
<LayoutPreview
key={`${template.id}-preview-${index}`}
layout={layout}
templateId={template.id}
index={index}
/>
))
) : (
// Empty state placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-empty-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<span className="text-xs text-gray-400">No preview</span>
</div>
))
)}
</div>
</div>
{isSelected && (
<div className="absolute top-0 right-0 bg-blue-500 text-white px-2 py-1 rounded-bl-lg">
Selected
</div>
)}
</Card>
);
});
CustomTemplateCard.displayName = 'CustomTemplateCard';

View file

@ -3,11 +3,12 @@ import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { Button } from "@/components/ui/button";
import { LoadingState, Template } from "../types/index";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates";
interface GenerateButtonProps {
loadingState: LoadingState;
streamState: { isStreaming: boolean; isLoading: boolean };
selectedTemplate: Template | null;
selectedTemplate: TemplateLayoutsWithSettings | string | null;
onSubmit: () => void;
outlineCount: number;
}

View file

@ -15,6 +15,7 @@ import { useOutlineStreaming } from "../hooks/useOutlineStreaming";
import { useOutlineManagement } from "../hooks/useOutlineManagement";
import { usePresentationGeneration } from "../hooks/usePresentationGeneration";
import TemplateSelection from "./TemplateSelection";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates";
const OutlinePage: React.FC = () => {
const { presentation_id, outlines } = useSelector(
@ -22,7 +23,7 @@ const OutlinePage: React.FC = () => {
);
const [activeTab, setActiveTab] = useState<string>(TABS.OUTLINE);
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<TemplateLayoutsWithSettings | string | null>(null);
// Custom hooks
const streamState = useOutlineStreaming(presentation_id);
const { handleDragEnd, handleAddSlide } = useOutlineManagement(outlines);

View file

@ -1,87 +0,0 @@
import { CheckCircle } from "lucide-react";
import React from "react";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { Template } from "../types/index";
import { useLayout } from "../../context/LayoutContext";
import { useFontLoader } from "../../hooks/useFontLoader";
interface TemplateLayoutsProps {
template: Template;
onSelectTemplate: (template: Template) => void;
selectedTemplate: Template | null;
}
const TemplateLayouts: React.FC<TemplateLayoutsProps> = ({
template,
onSelectTemplate,
selectedTemplate,
}) => {
const { getFullDataByTemplateID, getCustomTemplateFonts } = useLayout();
const layoutTemplate = getFullDataByTemplateID(template.id);
const fonts = getCustomTemplateFonts(template.id.split("custom-")[1]);
useFontLoader(fonts || []);
const pathname = usePathname();
return (
<div
onClick={() => {
trackEvent(MixpanelEvent.Group_Layout_Selected_Clicked, { pathname });
onSelectTemplate(template);
}}
className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${selectedTemplate?.id === template.id
? "border-blue-500 bg-blue-50 shadow-md"
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
}`}
>
{selectedTemplate?.id === template.id && (
<div className="absolute top-3 right-3">
<CheckCircle className="w-5 h-5 text-blue-500" />
</div>
)}
<div className="mb-3 ">
<h6 className="text-base capitalize font-medium text-gray-900 mb-1">
{template.name}
</h6>
<p className="text-sm text-gray-600">{template.description}</p>
</div>
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2 mb-3 min-h-[300px]">
{layoutTemplate &&
layoutTemplate?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
templateID,
} = layout;
return (
<div
key={`${templateID}-${index}`}
className=" relative cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{layoutTemplate?.length} layouts</span>
<span
className={`px-2 py-1 rounded text-xs ${template.ordered
? "bg-gray-100 text-gray-700"
: "bg-blue-100 text-blue-700"
}`}
>
{template.ordered ? "Structured" : "Flexible"}
</span>
</div>
</div>
);
};
export default TemplateLayouts;

View file

@ -1,111 +1,25 @@
"use client";
import React, { useEffect } from "react";
import { useLayout } from "../../context/LayoutContext";
import TemplateLayouts from "./TemplateLayouts";
import { Template } from "../types/index";
import { getHeader } from "../../services/api/header";
import { templates, TemplateLayoutsWithSettings } from "@/app/presentation-templates";
import { Card } from "@/components/ui/card";
import { TemplateWithData } from "@/app/presentation-templates/utils";
import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { CustomTemplateCard } from "./CustomTemplateCard";
interface TemplateSelectionProps {
selectedTemplate: Template | null;
onSelectTemplate: (template: Template) => void;
selectedTemplate: (TemplateLayoutsWithSettings | string) | null;
onSelectTemplate: (template: TemplateLayoutsWithSettings | string) => void;
}
const TemplateSelection: React.FC<TemplateSelectionProps> = ({
selectedTemplate,
onSelectTemplate
}) => {
const {
getLayoutsByTemplateID,
getTemplateSetting,
getAllTemplateIDs,
getFullDataByTemplateID,
loading
} = useLayout();
const [summaryMap, setSummaryMap] = React.useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
useEffect(() => {
// Fetch custom templates summary to get last_updated_at and template meta for sorting and display
fetch(`/api/v1/ppt/template-management/summary`, {
headers: getHeader(),
})
.then(res => res.json())
.then(data => {
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation_id}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
name: p.template?.name,
description: p.template?.description,
};
}
}
setSummaryMap(map);
})
.catch(() => setSummaryMap({}));
}, []);
const templates: Template[] = React.useMemo(() => {
const templates = getAllTemplateIDs();
if (templates.length === 0) return [];
const Templates: Template[] = templates
.filter((templateID: string) => {
// Filter out template that contain any errored layouts (from custom templates compile/parse errors)
const fullData = getFullDataByTemplateID(templateID);
const hasErroredLayouts = fullData.some((fd: any) => (fd as any)?.component?.displayName === "CustomTemplateErrorSlide");
return !hasErroredLayouts;
})
.map(templateID => {
const settings = getTemplateSetting(templateID);
const customMeta = summaryMap[templateID];
const isCustom = templateID.toLowerCase().startsWith("custom-");
return {
id: templateID,
name: isCustom && customMeta?.name ? customMeta.name : templateID,
description: (isCustom && customMeta?.description) ? customMeta.description : (settings?.description || `${templateID} presentation templates`),
ordered: settings?.ordered || false,
default: settings?.default || false,
};
});
// Sort templates to put default first, then by name
return Templates.sort((a, b) => {
if (a.default && !b.default) return -1;
if (!a.default && b.default) return 1;
return a.name.localeCompare(b.name);
});
}, [getAllTemplateIDs, getLayoutsByTemplateID, getTemplateSetting, getFullDataByTemplateID, summaryMap]);
const inBuiltTemplates = React.useMemo(
() => templates.filter(g => !g.id.toLowerCase().startsWith("custom-")),
[templates]
);
const customTemplates = React.useMemo(() => {
const unsorted = templates.filter(g => g.id.toLowerCase().startsWith("custom-"));
// Sort by last_updated_at desc using summaryMap keyed by template id
return unsorted.sort((a, b) => (summaryMap[b.id]?.lastUpdatedAt || 0) - (summaryMap[a.id]?.lastUpdatedAt || 0));
}, [templates, summaryMap]);
// Auto-select first template when templates are loaded
useEffect(() => {
if (templates.length > 0 && !selectedTemplate) {
const defaultTemplate = templates.find(g => g.default) || templates[0];
const slides = getLayoutsByTemplateID(defaultTemplate.id);
onSelectTemplate({
...defaultTemplate,
slides: slides,
});
}
}, [templates, selectedTemplate, onSelectTemplate]);
useEffect(() => {
if (loading) {
return;
}
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
@ -118,49 +32,11 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = ({
}, []);
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
if (loading) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="p-4 rounded-lg border border-gray-200 bg-gray-50 animate-pulse">
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-3 bg-gray-200 rounded mb-3"></div>
<div className="grid grid-cols-3 gap-2 mb-3">
{[1, 2, 3].map((j) => (
<div key={j} className="aspect-video bg-gray-200 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
);
}
if (templates.length === 0) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<h5 className="text-lg font-medium mb-2 text-gray-700">
No Templates Available
</h5>
<p className="text-gray-600 text-sm">
No presentation templates could be loaded. Please try refreshing the page.
</p>
</div>
</div>
);
}
const handleTemplateSelection = (template: Template) => {
const slides = getLayoutsByTemplateID(template.id);
onSelectTemplate({
...template,
slides: slides,
});
}
return (
<div className="space-y-8 mb-4">
@ -168,14 +44,56 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = ({
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">In Built Templates</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{inBuiltTemplates.map((template) => (
<TemplateLayouts
key={template.id}
template={template}
onSelectTemplate={handleTemplateSelection}
selectedTemplate={selectedTemplate}
/>
))}
{templates.map((template: TemplateLayoutsWithSettings) => {
const previewLayouts = template.layouts.slice(0, 4);
return (
<Card
key={template.id}
className={`${typeof selectedTemplate !== 'string' && selectedTemplate?.id === template.id ? 'border-2 border-blue-500' : ''} cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden relative`}
onClick={() => onSelectTemplate(template)}
>
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900 capitalize">
{template.name}
</h3>
</div>
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
style={{ contain: 'layout style paint' }}
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]"
style={{ transform: 'scale(0.2) translateZ(0)', backfaceVisibility: 'hidden' }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})}
</div>
</div>
{typeof selectedTemplate !== 'string' && selectedTemplate?.id === template.id && (
<div className="absolute top-0 right-0 bg-blue-500 text-white px-2 py-1 rounded-bl-lg">
Selected
</div>
)}
</Card>
);
})}
</div>
</div>
@ -184,18 +102,27 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = ({
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">Custom AI Templates</h3>
</div>
{customTemplates.length === 0 ? (
<div className="text-sm text-gray-600 py-2">
No custom templates. Create one from "All Templates" menu.
{customLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
) : customTemplates.length === 0 ? (
<Card className="p-8 text-center">
<p className="text-gray-500">No custom templates yet.</p>
<p className="text-sm text-gray-400 mt-2">
Custom templates you create will appear here.
</p>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{customTemplates.map((template) => (
<TemplateLayouts
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{customTemplates.map((template: CustomTemplates) => (
<CustomTemplateCard
key={template.id}
template={template}
onSelectTemplate={handleTemplateSelection}
selectedTemplate={selectedTemplate}
onSelectTemplate={onSelectTemplate}
selectedTemplate={typeof selectedTemplate === 'string' ? selectedTemplate : null}
/>
))}
</div>
@ -205,4 +132,4 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = ({
);
};
export default TemplateSelection;
export default TemplateSelection;

View file

@ -6,6 +6,8 @@ import { clearPresentationData } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { Template, LoadingState, TABS } from "../types/index";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates";
import { getCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
const DEFAULT_LOADING_STATE: LoadingState = {
message: "",
@ -17,7 +19,7 @@ const DEFAULT_LOADING_STATE: LoadingState = {
export const usePresentationGeneration = (
presentationId: string | null,
outlines: { content: string }[] | null,
selectedTemplate: Template | null,
selectedTemplate: TemplateLayoutsWithSettings | string | null,
setActiveTab: (tab: string) => void
) => {
const dispatch = useDispatch();
@ -38,24 +40,34 @@ export const usePresentationGeneration = (
});
return false;
}
if (!selectedTemplate.slides.length) {
toast.error("No Slide Schema found", {
description: "Please select a Group before generating presentation",
});
return false;
}
return true;
}, [outlines, selectedTemplate]);
const prepareLayoutData = useCallback(() => {
if (!selectedTemplate) return null;
return {
name: selectedTemplate.name,
ordered: selectedTemplate.ordered,
slides: selectedTemplate.slides
};
}, [selectedTemplate]);
const clearTheme = () => {
const element = document.getElementById('presentation-page')
if (!element) return;
element.style.removeProperty('--primary-color');
element.style.removeProperty('--background-color');
element.style.removeProperty('--card-color');
element.style.removeProperty('--stroke');
element.style.removeProperty('--primary-text');
element.style.removeProperty('--background-text');
element.style.removeProperty('--graph-0');
element.style.removeProperty('--graph-1');
element.style.removeProperty('--graph-2');
element.style.removeProperty('--graph-3');
element.style.removeProperty('--graph-4');
element.style.removeProperty('--graph-5');
element.style.removeProperty('--graph-6');
element.style.removeProperty('--graph-7');
element.style.removeProperty('--graph-8');
element.style.removeProperty('--graph-9');
}
const handleSubmit = useCallback(async () => {
if (!selectedTemplate) {
@ -64,8 +76,6 @@ export const usePresentationGeneration = (
}
if (!validateInputs()) return;
setLoadingState({
message: "Generating presentation data...",
isLoading: true,
@ -74,19 +84,72 @@ export const usePresentationGeneration = (
});
try {
const layoutData = prepareLayoutData();
let layout;
// Check if it's a custom template (string = presentationId)
if (typeof selectedTemplate === 'string') {
setLoadingState({
message: "Loading custom template...",
isLoading: true,
showProgress: true,
duration: 30,
});
// Fetch custom template details using the shared function
const customTemplateDetail = await getCustomTemplateDetails(selectedTemplate);
if (!customTemplateDetail || customTemplateDetail.layouts.length === 0) {
toast.error("Template Error", {
description: "Failed to load custom template layouts",
});
return;
}
setLoadingState({
message: "Generating presentation data...",
isLoading: true,
showProgress: true,
duration: 30,
});
layout = {
name: customTemplateDetail.id,
ordered: false,
slides: customTemplateDetail.layouts.map((compiledLayout) => ({
id: customTemplateDetail.id.startsWith('custom-') ? `${customTemplateDetail.id}:${compiledLayout.layoutId}` : `custom-${customTemplateDetail.id}:${compiledLayout.layoutId}`,
name: compiledLayout.layoutName,
description: compiledLayout.layoutDescription,
templateID: customTemplateDetail.id,
templateName: customTemplateDetail.name,
json_schema: compiledLayout.schemaJSON,
}))
};
} else {
// Built-in template
layout = {
name: selectedTemplate.id,
ordered: false,
slides: selectedTemplate.layouts.map((layoutItem) => ({
id: layoutItem.layoutId,
name: layoutItem.layoutName,
description: layoutItem.layoutDescription,
templateID: selectedTemplate.id,
templateName: selectedTemplate.name,
json_schema: layoutItem.schemaJSON,
}))
};
}
if (!layoutData) return;
trackEvent(MixpanelEvent.Presentation_Prepare_API_Call);
const response = await PresentationGenerationApi.presentationPrepare({
presentation_id: presentationId,
outlines: outlines,
layout: layoutData,
layout: layout,
});
if (response) {
dispatch(clearPresentationData());
router.replace(`/presentation?id=${presentationId}&stream=true`);
clearTheme();
router.replace(`/presentation?id=${presentationId}&stream=true&type=standard`);
}
} catch (error: any) {
console.error('Error In Presentation Generation(prepare).', error);
@ -96,7 +159,7 @@ export const usePresentationGeneration = (
} finally {
setLoadingState(DEFAULT_LOADING_STATE);
}
}, [validateInputs, prepareLayoutData, presentationId, outlines, dispatch, router, selectedTemplate]);
}, [validateInputs, presentationId, outlines, dispatch, router, selectedTemplate]);
return { loadingState, handleSubmit };
};

View file

@ -10,32 +10,24 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { AlertCircle } from "lucide-react";
import { setPresentationData } from "@/store/slices/presentationGeneration";
import { DashboardApi } from "../services/api/dashboard";
import { useLayout } from "../context/LayoutContext";
import { useFontLoader } from "../hooks/useFontLoader";
import { useTemplateLayouts } from "../hooks/useTemplateLayouts";
import { V1ContentRender } from "../components/V1ContentRender";
const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const { renderSlideContent, loading } = useTemplateLayouts();
const pathname = usePathname();
const [contentLoading, setContentLoading] = useState(true);
const { getCustomTemplateFonts } = useLayout()
const dispatch = useDispatch();
const { presentationData } = useSelector(
(state: RootState) => state.presentationGeneration
);
const [error, setError] = useState(false);
useEffect(() => {
if (!loading && presentationData?.slides && presentationData?.slides.length > 0) {
const presentation_id = presentationData?.slides[0].layout.split(":")[0].split("custom-")[1];
const fonts = getCustomTemplateFonts(presentation_id);
useFontLoader(fonts || []);
}
}, [presentationData, loading]);
useEffect(() => {
if (presentationData?.slides[0].layout.includes("custom")) {
const existingScript = document.querySelector(
@ -103,7 +95,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
className="mx-auto flex flex-col items-center overflow-hidden justify-center "
>
{!presentationData ||
loading ||
contentLoading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
@ -125,7 +117,8 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
presentationData.slides.map((slide: any, index: number) => (
// [data-speaker-note] is used to extract the speaker note from the slide for export to pptx
<div key={index} className="w-full" data-speaker-note={slide.speaker_note}>
{renderSlideContent(slide, true)}
<V1ContentRender slide={slide} isEditMode={true} theme={null}
/>
</div>
))}
</>

View file

@ -1,5 +1,5 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { Skeleton } from "@/components/ui/skeleton";
@ -20,7 +20,7 @@ import {
} from "../hooks";
import { PresentationPageProps } from "../types";
import LoadingState from "./LoadingState";
import { useLayout } from "../../context/LayoutContext";
import { useFontLoader } from "../../hooks/useFontLoader";
import { usePresentationUndoRedo } from "../hooks/PresentationUndoRedo";
const PresentationPage: React.FC<PresentationPageProps> = ({
@ -33,8 +33,8 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
const [isFullscreen, setIsFullscreen] = useState(false);
const [error, setError] = useState(false);
const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false);
const {getCustomTemplateFonts} = useLayout();
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
@ -82,14 +82,14 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
};
useEffect(() => {
if(!loading && !isStreaming && presentationData?.slides && presentationData?.slides.length > 0){
const presentation_id = presentationData?.slides[0].layout.split(":")[0].split("custom-")[1];
const fonts = getCustomTemplateFonts(presentation_id);
useFontLoader(fonts || []);
}
}, [presentationData,loading,isStreaming]);
// useEffect(() => {
// if(!loading && !isStreaming && presentationData?.slides && presentationData?.slides.length > 0){
// const presentation_id = presentationData?.slides[0].layout.split(":")[0].split("custom-")[1];
// const fonts = getCustomTemplateFonts(presentation_id);
// useFontLoader(fonts || []);
// }
// }, [presentationData,loading,isStreaming]);
// Presentation Mode View
if (isPresentMode) {
return (
@ -144,16 +144,16 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
isMobilePanelOpen={isMobilePanelOpen}
setIsMobilePanelOpen={setIsMobilePanelOpen}
/>
<div className="flex-1 h-[calc(100vh-100px)] overflow-y-auto">
<div
id="presentation-slides-wrapper"
className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0"
>
{!presentationData ||
loading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
loading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
<div className="relative w-full h-[calc(100vh-120px)] mx-auto">
<div className="">
{Array.from({ length: 2 }).map((_, index) => (

View file

@ -22,7 +22,7 @@ import {
import { setPresentationData } from "@/store/slices/presentationGeneration";
import { SortableSlide } from "./SortableSlide";
import { SortableListItem } from "./SortableListItem";
import { useTemplateLayouts } from "../../hooks/useTemplateLayouts";
import SlideScale from "../../components/PresentationRender";
interface SidePanelProps {
selectedSlide: number;
@ -48,8 +48,7 @@ const SidePanel = ({
const dispatch = useDispatch();
// Use the centralized group layouts hook
const { renderSlideContent } = useTemplateLayouts();
useEffect(() => {
if (window.innerWidth < 768) {
@ -279,7 +278,7 @@ const SidePanel = ({
<div className=" bg-white pointer-events-none relative overflow-hidden aspect-video">
<div className="absolute bg-gray-100/5 z-50 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
{renderSlideContent(slide, false)}
<SlideScale slide={slide} />
</div>
</div>
</div>
@ -299,7 +298,7 @@ const SidePanel = ({
index={index}
selectedSlide={selectedSlide}
onSlideClick={onSlideClick}
renderSlideContent={(slide) => renderSlideContent(slide, false)}
/>
))}
</SortableContext>

View file

@ -16,11 +16,11 @@ import {
deletePresentationSlide,
updateSlide,
} from "@/store/slices/presentationGeneration";
import { useTemplateLayouts } from "../../hooks/useTemplateLayouts";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import NewSlide from "../../components/NewSlide";
import { addToHistory } from "@/store/slices/undoRedoSlice";
import { V1ContentRender } from "../../components/V1ContentRender";
interface SlideContentProps {
slide: any;
@ -37,7 +37,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
);
// Use the centralized group layouts hook
const { renderSlideContent, loading } = useTemplateLayouts();
const pathname = usePathname();
const handleSubmit = async () => {
@ -110,15 +110,10 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
}
}, [presentationData?.slides?.length, isStreaming]);
// Memoized slide content rendering to prevent unnecessary re-renders
const slideContent = useMemo(() => {
return renderSlideContent(slide, isStreaming ? false : true); // Enable edit mode for main content
}, [renderSlideContent, slide, isStreaming]);
useEffect(() => {
if (loading) {
return;
}
if (slide.layout.includes("custom")) {
const existingScript = document.querySelector(
@ -131,7 +126,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
document.head.appendChild(script);
}
}
}, [slide, isStreaming, loading]);
}, [slide, isStreaming]);
return (
<>
@ -147,19 +142,11 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
data-group={slide.layout_group}
className={` w-full group `}
>
{/* render slides */}
{loading ? (
<div className="flex flex-col bg-white aspect-video items-center justify-center h-full">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : (
slideContent
)}
<V1ContentRender slide={slide} isEditMode={true} theme={null} />
{!showNewSlideSelection && (
<div className="group-hover:opacity-100 hidden md:block opacity-0 transition-opacity my-4 duration-300">
<ToolTip content="Add new slide below">
{!isStreaming && !loading && (
{!isStreaming && (
<div
onClick={() => {
trackEvent(MixpanelEvent.Slide_Add_New_Slide_Button_Clicked, { pathname });
@ -173,7 +160,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
</ToolTip>
</div>
)}
{showNewSlideSelection && !loading && (
{showNewSlideSelection && (
<NewSlide
index={index}
templateID={`${slide.layout.split(":")[0]}`}
@ -182,7 +169,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
/>
)}
{!isStreaming && !loading && (
{!isStreaming && (
<ToolTip content="Delete slide">
<div
onClick={() => {

View file

@ -2,18 +2,20 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Slide } from '../../types/slide';
import { useRef } from 'react';
import { V1ContentRender } from '../../components/V1ContentRender';
import { useSearchParams } from 'next/navigation';
interface SortableSlideProps {
slide: Slide;
index: number;
selectedSlide: number;
onSlideClick: (index: any) => void;
renderSlideContent: (slide: any, isEditMode?: boolean) => React.ReactElement;
}
const SCALE = 0.2;
export function SortableSlide({ slide, index, selectedSlide, onSlideClick, renderSlideContent }: SortableSlideProps) {
export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: SortableSlideProps) {
const searchParams = useSearchParams();
const type = searchParams.get("type") as 'standard' | 'smart';
const lastClickTime = useRef(0);
const {
attributes,
listeners,
@ -26,7 +28,9 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick, rende
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1
opacity: isDragging ? 0.5 : 1,
backgroundColor: `var(--card-color, #ffffff)`,
borderColor: selectedSlide === index ? `#5141e5` : `var(--stroke, #e5e7eb)`
};
const handleClick = (e: React.MouseEvent) => {
@ -54,12 +58,31 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick, rende
className={` cursor-pointer border-[3px] relative p-1 shadow-lg rounded-md transition-all duration-200 ${selectedSlide === index ? ' border-[#5141e5]' : 'border-gray-300'
}`}
>
<div className=" slide-box relative z-50 overflow-hidden aspect-video">
<div className="absolute bg-transparent z-50 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex pointer-events-none justify-center items-center origin-top-left w-[500%] h-[500%]">
{renderSlideContent(slide, false)}
<div
className="relative"
style={{ height: `${720 * SCALE}px`, overflow: "hidden" }}
>
<div
className="absolute top-0 left-0 pointer-events-none"
style={{
width: 1280,
height: 720,
transformOrigin: "top left",
transform: `scale(${SCALE})`,
}}
>
<V1ContentRender slide={slide} isEditMode={true} />
</div>
</div>
{/* <div className=" slide-box relative z-50 overflow-hidden aspect-video">
<div className="absolute bg-transparent z-50 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex pointer-events-none justify-center items-center origin-top-left w-[500%] h-[500%]"
>
<ContentRender slide={slide} isEditMode={true} />
</div>
</div> */}
</div>
);
}

View file

@ -0,0 +1,36 @@
import { ApiResponseHandler } from "./api-error-handler";
class TemplateService {
static async getCustomTemplateSummaries() {
try {
const response = await fetch(`/api/v1/ppt/template-management/summary`,);
return await ApiResponseHandler.handleResponse(response, "Failed to get custom template summaries");
} catch (error) {
console.error("Failed to get custom template summaries", error);
throw error;
}
}
static async getCustomTemplateDetails(templateId: string) {
try {
const response = await fetch(`/api/v1/ppt/template-management/get-templates/${templateId}`,);
return await ApiResponseHandler.handleResponse(response, "Failed to get custom template details");
} catch (error) {
console.error("Failed to get custom template details", error);
throw error;
}
}
static async deleteCustomTemplate(presentationId: string) {
try {
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, { method: "DELETE" });
return await ApiResponseHandler.handleResponseWithResult(response, "Failed to delete custom template");
} catch (error) {
console.error("Failed to delete custom template", error);
throw error;
}
}
}
export default TemplateService;

View file

@ -0,0 +1,179 @@
'use client';
import { useEffect, useRef, useCallback, useState } from 'react';
import { FASTAPI_URL } from '@/constants';
import { getHeader } from '@/app/(presentation-generator)/services/api/header';
import { ApiResponseHandler } from '@/app/(presentation-generator)/services/api/api-error-handler';
import { ProcessedSlide } from '@/app/custom-template/types';
import { CustomTemplateLayout } from '@/app/hooks/useCustomTemplates';
interface LayoutPayload {
layout_id: string;
layout_code: string;
layout_name: string;
}
interface UseTemplateLayoutsAutoSaveOptions {
templateId: string | null;
layouts: CustomTemplateLayout[];
slideStates: ProcessedSlide[];
debounceMs?: number;
enabled?: boolean;
}
export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
export const useTemplateLayoutsAutoSave = ({
templateId,
layouts,
slideStates,
debounceMs = 2000,
enabled = true,
}: UseTemplateLayoutsAutoSaveOptions) => {
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastSavedDataRef = useRef<string>('');
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
const isSavingRef = useRef<boolean>(false);
const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);
// Build the payload for saving
const buildPayload = useCallback((): LayoutPayload[] => {
const payload: LayoutPayload[] = [];
layouts.forEach((layout, index) => {
const slideState = slideStates[index];
if (slideState?.react && layout.rawLayoutId) {
payload.push({
layout_id: layout.rawLayoutId,
layout_code: slideState.react,
layout_name: slideState.layout_name || `Slide${index + 1}`
});
}
});
return payload;
}, [layouts, slideStates]);
// Save function
const saveLayouts = useCallback(async (payload: LayoutPayload[]) => {
if (!templateId || payload.length === 0 || isSavingRef.current) {
return false;
}
const currentDataString = JSON.stringify(payload);
// Skip if data hasn't changed since last save
if (currentDataString === lastSavedDataRef.current) {
return false;
}
try {
isSavingRef.current = true;
setSaveStatus('saving');
console.log('🔄 Auto-saving template layouts...');
const response = await fetch(`${FASTAPI_URL}/api/v1/ppt/template/update`, {
method: 'PUT',
headers: getHeader(),
body: JSON.stringify({
id: templateId,
layouts: payload,
}),
});
await ApiResponseHandler.handleResponse(response, 'Failed to auto-save layouts');
// Update last saved data reference
lastSavedDataRef.current = currentDataString;
setLastSavedAt(new Date());
setSaveStatus('saved');
console.log('✅ Auto-save successful');
// Reset to idle after showing "saved" briefly
setTimeout(() => {
setSaveStatus('idle');
}, 2000);
return true;
} catch (error) {
console.error('❌ Auto-save failed:', error);
setSaveStatus('error');
// Reset to idle after showing error briefly
setTimeout(() => {
setSaveStatus('idle');
}, 3000);
return false;
} finally {
isSavingRef.current = false;
}
}, [templateId]);
// Debounced save trigger
const debouncedSave = useCallback(() => {
if (!enabled || !templateId) return;
// Clear existing timeout
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
// Set new timeout
saveTimeoutRef.current = setTimeout(() => {
const payload = buildPayload();
if (payload.length > 0) {
saveLayouts(payload);
}
}, debounceMs);
}, [enabled, templateId, buildPayload, saveLayouts, debounceMs]);
// Watch for changes in slideStates
useEffect(() => {
if (!enabled || !templateId || slideStates.length === 0) return;
// Check if any slide is still processing
const hasProcessingSlide = Array.from(slideStates.values()).some(
slide => slide.processing
);
if (hasProcessingSlide) return;
debouncedSave();
// Cleanup timeout on unmount or when dependencies change
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, [slideStates, enabled, templateId, debouncedSave]);
// Manual save function
const saveNow = useCallback(async () => {
// Clear any pending debounced save
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
const payload = buildPayload();
return saveLayouts(payload);
}, [buildPayload, saveLayouts]);
// Cleanup on unmount - save any pending changes
useEffect(() => {
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, []);
return {
saveStatus,
lastSavedAt,
saveNow,
};
};

View file

@ -1,362 +1,306 @@
"use client";
import React, { useEffect, useState } from "react";
import { useParams, useRouter, usePathname } from "next/navigation";
import LoadingStates from "../components/LoadingStates";
import React, { useCallback, useEffect, useState } from "react";
import { useParams, usePathname, useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Home, Trash2, Code, Save, X, Pencil } from "lucide-react";
import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
import Editor from "react-simple-code-editor";
import { highlight, languages } from "prismjs";
import "prismjs/components/prism-clike";
import "prismjs/components/prism-javascript";
import "prismjs/components/prism-markup";
import "prismjs/components/prism-jsx";
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { ArrowLeft, Home, Loader2, Trash2 } from "lucide-react";
import { useFontLoader } from "../../hooks/useFontLoader";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { getHeader } from "../../services/api/header";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
import TemplateService from "../../services/api/template";
import Header from "../../dashboard/components/Header";
import { toast } from "sonner";
import { CustomTemplateLayout, useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { templates as templateGroups, getTemplatesByTemplateName } from "@/app/presentation-templates";
const GroupLayoutPreview = () => {
const params = useParams();
const router = useRouter();
const rawSlug = ((): string => {
const value: any = (params as any)?.slug;
if (typeof value === "string") return value;
if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") return value[0];
return "";
})();
const pathname = usePathname();
const { getFullDataByTemplateID, loading, refetch } = useLayout();
const layoutGroup = getFullDataByTemplateID(rawSlug);
const templateParams = params.slug as string;
const isCustom = rawSlug.startsWith("custom-");
const presentationId = isCustom && rawSlug.length > 7 ? rawSlug.slice(7) : "";
// Check if this is a custom template
const isCustom = templateParams.startsWith("custom-");
const customTemplateId = isCustom ? templateParams.split("custom-")[1] : null;
const [editorOpen, setEditorOpen] = useState(false);
const [currentCode, setCurrentCode] = useState("");
const [currentLayoutName, setCurrentLayoutName] = useState("");
const [currentLayoutId, setCurrentLayoutId] = useState("");
const [currentFonts, setCurrentFonts] = useState<string[] | undefined>(undefined);
const [isSaving, setIsSaving] = useState(false);
const [layoutsMap, setLayoutsMap] = useState<Record<string, { layout_id: string; layout_name: string; layout_code: string; fonts?: string[] }>>({});
const [templateMeta, setTemplateMeta] = useState<{ name?: string; description?: string } | null>(null);
// Fetch static templates if not custom
const staticTemplates = !isCustom ? getTemplatesByTemplateName(templateParams) : [];
const staticGroup = !isCustom ? templateGroups.find((g: { id: string }) => g.id === templateParams) : null;
// Fetch custom template details if custom
const {
template: customTemplate,
loading: customLoading,
error: customError,
fonts: customFonts,
} = useCustomTemplateDetails({ id: templateParams?.split("custom-")[1] || "", name: "", description: "" });
useEffect(() => {
const loadCustomLayouts = async () => {
if (!isCustom || !presentationId) return;
try {
const res = await fetch(`/api/v1/ppt/template-management/get-templates/${presentationId}`, {
headers: getHeader(),
});
if (!res.ok) return;
const data = await res.json();
const map: Record<string, { layout_id: string; layout_name: string; layout_code: string; fonts?: string[] }> = {};
for (const l of data.layouts || []) {
map[l.layout_name] = {
layout_id: l.layout_id,
layout_name: l.layout_name,
layout_code: l.layout_code,
fonts: l.fonts,
};
}
setLayoutsMap(map);
// Set template meta and inject aggregated fonts if provided
if (data?.template) {
setTemplateMeta({ name: data.template.name, description: data.template.description });
}
if (Array.isArray(data?.fonts) && data.fonts.length) {
useFontLoader(data.fonts);
}
} catch (e) {
// noop
}
};
loadCustomLayouts();
}, [isCustom, presentationId]);
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
const existingScript = document.querySelector('script[src*="tailwindcss.com"]');
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
script.async = true;
document.head.appendChild(script);
}
}, [rawSlug]);
}, [templateParams]);
// Ensure fonts are injected if layoutsMap changes dynamically
useEffect(() => {
if (!isCustom) return;
const allFonts: string[] = [];
Object.values(layoutsMap).forEach((entry) => {
(entry.fonts || []).forEach((f) => allFonts.push(f));
});
if (allFonts.length) useFontLoader(allFonts);
}, [layoutsMap, isCustom]);
const handleDeleteCustomTemplate = async () => {
if (!customTemplateId) return;
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />;
}
const confirmed = window.confirm(
"Are you sure you want to delete this template? This action cannot be undone."
);
if (!confirmed) return;
// Handle empty state
if (!layoutGroup || layoutGroup.length === 0) {
return <LoadingStates type="empty" />;
}
const deleteLayouts = async () => {
refetch();
router.back();
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, {
method: "DELETE",
headers: getHeader(),
});
if (response.ok) {
const success = await TemplateService.deleteCustomTemplate(customTemplateId);
if (success.success) {
toast.success("Template deleted successfully");
router.push("/template-preview");
} else {
toast.error("Failed to delete template");
}
};
// Loading state for custom templates
if (isCustom && (customLoading)) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="flex items-center justify-center py-24">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Compiling templates...</span>
</div>
</div>
);
}
const openEditor = (layoutName: string) => {
const entry = layoutsMap[layoutName];
if (!entry) return;
setCurrentLayoutName(entry.layout_name);
setCurrentLayoutId(entry.layout_id);
setCurrentCode(entry.layout_code || "");
setCurrentFonts(entry.fonts);
// Make sure fonts for this layout are loaded before editing
useFontLoader(entry.fonts || []);
setEditorOpen(true);
};
// Error state
if (isCustom && customError) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="flex flex-col items-center justify-center py-24">
<h2 className="text-2xl font-bold text-red-600 mb-4">Error loading template</h2>
<p className="text-gray-600 mb-4">{customError}</p>
<Button onClick={() => router.push("/template-preview")}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Templates
</Button>
</div>
</div>
);
}
const handleCancel = () => {
// reset to original code
const entry = layoutsMap[currentLayoutName];
if (entry) setCurrentCode(entry.layout_code || "");
setEditorOpen(false);
};
// Empty state
if (
(!isCustom && (!staticGroup || staticTemplates.length === 0)) ||
(isCustom && (!customTemplate))
) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="flex flex-col items-center justify-center py-24">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Template not found
</h2>
<Button onClick={() => router.push("/template-preview")}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Templates
</Button>
</div>
</div>
);
}
const handleSave = async () => {
try {
setIsSaving(true);
const payload = {
layouts: [
{
presentation: presentationId,
layout_id: currentLayoutId,
layout_name: currentLayoutName,
layout_code: currentCode,
fonts: currentFonts,
},
],
};
const res = await fetch(`/api/v1/ppt/template-management/save-templates`, {
method: "POST",
headers: getHeader(),
body: JSON.stringify(payload),
});
if (!res.ok) return;
// update cache map
setLayoutsMap((prev) => ({
...prev,
[currentLayoutName]: {
layout_id: currentLayoutId,
layout_name: currentLayoutName,
layout_code: currentCode,
fonts: currentFonts,
},
}));
await refetch();
setEditorOpen(false);
} finally {
setIsSaving(false);
}
};
// Determine what to render
const templateName = isCustom ? customTemplate?.template.name || "Custom Template" : staticGroup?.name || "";
const templateDescription = isCustom
? customTemplate?.template.description || ""
: staticGroup?.description || "";
const layoutCount = isCustom
? customTemplate?.layouts.length || 0
: staticTemplates.length;
console.log('compileLayout', customTemplate)
return (
<div className="min-h-screen bg-gray-50">
<Header />
{/* Header */}
<header className="bg-white shadow-sm border-b sticky top-0 z-30">
<div className="max-w-7xl mx-auto px-6 py-6">
{/* Navigation */}
<div className="flex items-center gap-4 mb-4">
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Back_Button_Clicked, { pathname });
router.back();
}}
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname });
router.push("/template-preview");
}}
className="flex items-center gap-2"
>
<Home className="w-4 h-4" />
All Templates
</Button>
{isCustom && <button className=" border border-red-200 flex justify-center items-center gap-2 text-red-700 px-4 py-1 rounded-md" onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_API_Call);
deleteLayouts();
}}><Trash2 className="w-4 h-4" />Delete</button>}
<div className=" mx-auto px-6 py-6">
<div className="flex items-center justify-between mb-4 max-w-[1440px] mx-auto">
<div className="flex items-center gap-4">
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Back_Button_Clicked, { pathname });
router.back();
}}
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname });
router.push("/template-preview");
}}
className="flex items-center gap-2"
>
<Home className="w-4 h-4" />
All Templates
</Button>
</div>
{isCustom && (
<div className="flex items-center gap-4">
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_API_Call);
handleDeleteCustomTemplate();
}}
className="flex items-center gap-2 border-red-200 text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
Delete Template
</Button>
</div>
)}
</div>
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 capitalize">
{templateMeta?.name || layoutGroup[0].templateID} Layouts
</h1>
<p className="text-gray-600 mt-2">
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} {templateMeta?.description || layoutGroup[0].templateID}
<div className="flex items-center justify-center gap-2 mb-2">
<h1 className="text-3xl font-bold text-gray-900">{templateName}</h1>
{isCustom && (
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-sm">
Custom
</span>
)}
</div>
<p className="text-gray-600">
{layoutCount} layout{layoutCount !== 1 ? "s" : ""} {" "}
{templateDescription}
</p>
</div>
</div>
</header>
{/* Layout Grid */}
<main className="max-w-7xl mx-auto px-6 py-8">
<div className="space-y-8">
{layoutGroup.map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
name,
fileName,
} = layout;
{/* Layout Grid - Wrapped in SchemaHighlightProvider for custom templates */}
<main className="mx-auto px-2 py-8" id="presentation-page">
{/* Static Templates */}
{!isCustom && (
<div className="space-y-12 w-[1440px] h-[720px] aspect-video mx-auto">
{staticTemplates.map((template: any, index: number) => {
const LayoutComponent = template.component;
return (
<Card
key={`${layoutGroup[0].templateID}-${index}`}
className="overflow-hidden shadow-md hover:shadow-lg transition-shadow"
>
{/* Layout Header */}
<div className="bg-white px-6 py-4 border-b">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900">
{name}
</h3>
<div className="flex items-center gap-4 mt-1">
<span className="text-sm text-gray-500 font-mono">
{fileName}
return (
<Card
key={`${templateParams}-${template.layoutId}-${index}`}
id={template.layoutId}
className="overflow-hidden shadow-md"
>
<div className="bg-white px-6 py-4 border-b">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900">
{template.layoutName}
</h3>
<p className="text-sm text-gray-500 mt-1 max-w-2xl">
{template.layoutDescription}
</p>
</div>
<div className="flex items-center gap-3">
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm font-mono">
{template.layoutId}
</span>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{layoutGroup[0].templateID}
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
#{index + 1}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<div className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-700">
Layout #{index + 1}
</div>
{isCustom && (
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 bg-blue-50 border border-blue-400 text-blue-700"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Open_Editor_Button_Clicked, { pathname });
openEditor(fileName);
}}
disabled={!layoutsMap[fileName]}
title={!layoutsMap[fileName] ? "Loading layout code..." : "Edit layout code"}
>
<Pencil className="w-4 h-4" /> Edit
</Button>
)}
</div>
</div>
</div>
{/* Layout Content */}
<div className="bg-gray-50 aspect-video max-w-[1280px] w-full">
<LayoutComponent data={sampleData} />
</div>
</Card>
);
})}
</div>
</main>
{/* Footer */}
<footer className="bg-white border-t mt-16">
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="text-center text-gray-600">
<p>
{layoutGroup[0].templateID} {layoutGroup.length} components
</p>
<div className="bg-gray-100 p-6 flex justify-center overflow-x-auto">
<div
className="flex-shrink-0"
style={{ width: "1280px", height: "720px" }}
>
<LayoutComponent data={template.sampleData} />
</div>
</div>
</Card>
);
})}
</div>
</div>
</footer>
)}
{/* Right-side Sheet Editor */}
{isCustom && (
<Sheet open={editorOpen} onOpenChange={(open) => { if (!open) handleCancel(); }}>
<SheetContent side="right" className="w-full sm:max-w-[860px] p-0">
<SheetHeader className="px-6 py-4 border-b">
<SheetTitle className="flex items-center justify-between w-full">
<span className="flex items-center gap-2 text-purple-800">
<Code className="w-5 h-5 text-purple-600" />
HTML Editor
</span>
</SheetTitle>
</SheetHeader>
<div className="space-y-4 px-2 overflow-y-auto h-[85%]">
<div className="container__content_area">
<Editor
value={currentCode}
onValueChange={(code) => setCurrentCode(code)}
highlight={(code) => highlight(code, languages.jsx!, "jsx")}
padding={10}
id="layout-code-editor"
name="layout-code-editor"
className="container__editor"
/>
</div>
</div>
<SheetFooter className="px-6 py-4 border-b">
<SheetTitle className="flex items-center justify-between w-full">
<div></div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleCancel}
className="flex items-center gap-1"
disabled={isSaving}
>
<X size={14} />
Cancel
</Button>
<Button
onClick={handleSave}
className="flex items-center gap-1 bg-purple-600 hover:bg-purple-700"
size="sm"
disabled={isSaving}
>
<Save size={14} />
Save HTML
</Button>
</div>
</SheetTitle>
</SheetFooter>
</SheetContent>
</Sheet>
)}
{/* Custom Templates - with page-level schema editor */}
{isCustom && (
<div className="flex flex-col items-center justify-center w-full gap-10 aspect-video mx-auto">
{/* Slides List */}
{customTemplate && customTemplate.layouts.map((layout: CustomTemplateLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<Card
key={`${templateParams}-${layout.layoutId}-${index}`}
id={layout.layoutId}
className="overflow-hidden shadow-md"
>
<div className="bg-white px-6 py-4 border-b">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900">
{layout.rawLayoutName}
</h3>
<p className="text-sm text-gray-500 mt-1 max-w-2xl">
{layout.layoutDescription}
</p>
</div>
</div>
<div className="flex items-end justify-end ">
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm font-mono">
{templateParams}:{layout.layoutId}
</span>
</div>
</div>
<div className="bg-gray-100 p-6 flex justify-center overflow-x-auto">
<div
className="flex-shrink-0"
style={{ width: "1280px", height: "720px" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
</Card>
);
})}
</div>
)}
</main>
</div>
);
};

View file

@ -86,10 +86,10 @@ const LoadingStates: React.FC<LoadingStatesProps> = ({ type, message }) => {
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-700">
No Template Found
No Layouts Found
</h3>
<p className="text-gray-500 text-sm leading-relaxed">
No valid Template files were discovered. Make sure your layout
No valid layout files were discovered. Make sure your layout
components export both a default component and a Schema.
</p>
</div>

View file

@ -1,33 +1,108 @@
"use client";
import React, { useEffect, useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import LoadingStates from "./components/LoadingStates";
import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { Copy, ExternalLink } from "lucide-react";
import Header from "@/app/(presentation-generator)/dashboard/components/Header";
import { useLayout } from "../context/LayoutContext";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { getHeader } from "../services/api/header";
import { toast } from "sonner";
import { ExternalLink, Loader2, Plus } from "lucide-react";
import { templates } from "@/app/presentation-templates";
import type { TemplateLayoutsWithSettings } from "@/app/presentation-templates";
import { TemplateWithData } from "@/app/presentation-templates/utils";
import {
useCustomTemplateSummaries,
useCustomTemplatePreview,
CustomTemplates,
} from "@/app/hooks/useCustomTemplates";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import Header from "../dashboard/components/Header";
// Component for rendering custom template card with lazy-loaded previews
const CustomTemplateCard = ({ template }: { template: CustomTemplates }) => {
const router = useRouter();
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(template.id);
const handleNavigate = () => {
if (template.id.startsWith('custom-')) {
router.push(`/template-preview/${template.id}`);
} else {
router.push(`/template-preview/custom-${template.id}`);
}
}
return (
<Card
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
onClick={handleNavigate}
>
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900">
{template.name}
</h3>
<div className="flex items-center gap-2">
<span className="px-2.5 py-0.5 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
{totalLayouts}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
</div>
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
{loading ? (
// Loading placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-loading-${index}`}
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts.length > 0 ? (
// Actual layout previews
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})
) : (
// Empty state placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-empty-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<span className="text-xs text-gray-400">No preview</span>
</div>
))
)}
</div>
</div>
</Card>
);
};
const LayoutPreview = () => {
const {
getAllTemplateIDs,
getLayoutsByTemplateID,
getTemplateSetting,
getFullDataByTemplateID,
loading,
error,
} = useLayout();
const router = useRouter();
const pathname = usePathname();
const [summaryMap, setSummaryMap] = useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
const existingScript = document.querySelector('script[src*="tailwindcss.com"]');
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
@ -36,245 +111,109 @@ const LayoutPreview = () => {
}
}, []);
useEffect(() => {
// Fetch summary to map custom template slug to template meta and last updated time
fetch(`/api/v1/ppt/template-management/summary`, {
headers: getHeader(),
})
.then((res) => res.json())
.then((data) => {
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation_id}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
name: p.template?.name,
description: p.template?.description,
};
}
}
setSummaryMap(map);
})
.catch(() => setSummaryMap({}));
}, []);
// Transform context data to match expected format
const layoutTemplates = getAllTemplateIDs().map((templateID) => ({
templateID,
layouts: getLayoutsByTemplateID(templateID),
settings: getTemplateSetting(templateID) || { description: "", ordered: false },
}));
const inBuiltTemplates = layoutTemplates.filter(
(g) => !g.templateID.toLowerCase().startsWith("custom-")
);
const customTemplates = layoutTemplates.filter((g) =>
g.templateID.toLowerCase().startsWith("custom-")
);
// Sort custom templates by last_updated_at desc using summaryMap
const customTemplatesSorted = [...customTemplates].sort(
(a, b) => (summaryMap[b.templateID]?.lastUpdatedAt || 0) - (summaryMap[a.templateID]?.lastUpdatedAt || 0)
);
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />;
}
// Handle error state
if (error) {
return <LoadingStates type="error" message={error} />;
}
// Handle empty state
if (!loading && layoutTemplates.length === 0) {
return <LoadingStates type="empty" />;
}
const totalStaticLayouts = templates.reduce((acc: number, g: TemplateLayoutsWithSettings) => acc + g.layouts.length, 0);
const totalCustomLayouts = customTemplates.reduce((acc: number, t: CustomTemplates) => acc + t.layoutCount, 0);
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className=" sticky top-0 z-30">
<div className="max-w-7xl mx-auto border-b px-6 py-6">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">All Templates</h1>
<p className="text-gray-600 mt-2">
{layoutTemplates.length} templates
</p>
</div>
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">All Templates</h1>
<p className="text-gray-600 mt-2">
{totalStaticLayouts + totalCustomLayouts} layouts across{" "}
{templates.length + customTemplates.length} templates
</p>
</div>
{/* Custom Templates */}
<section className="h-full pt-8 pb-8 flex justify-center items-center">
<div className="max-w-7xl mx-auto px-6 py-6 w-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900">Custom AI Templates</h2>
<button className="text-sm text-gray-800 hover:text-blue-600 transition-colors flex items-center gap-2 group" onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/custom-template` });
router.push(`/custom-template`)
}}>
Create Custom Template
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{customTemplatesSorted.length > 0 ? (
customTemplatesSorted.map((template) => {
const meta = summaryMap[template.templateID];
const displayName = meta?.name ? meta.name : template.templateID;
const displayDescription = meta?.description ? meta.description : template.settings.description;
const layoutTemplate = getFullDataByTemplateID(template.templateID);
return (
<Card
key={template.templateID}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${template.templateID}` });
router.push(`/template-preview/${template.templateID}`)
}}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{displayName}
</h3>
{/* Inbuilt Templates Section */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-800 mb-6">Inbuilt Templates</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{templates.map((template: TemplateLayoutsWithSettings) => {
const previewLayouts = template.layouts.slice(0, 4);
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{template.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-600 ">ID: {template.templateID}</p>
<Copy className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" onClick={() => {
navigator.clipboard.writeText(template.templateID);
toast.success("Copied to clipboard");
}} />
</div>
<p className="text-sm text-gray-600 my-4">
{displayDescription}
</p>
<div className="grid grid-cols-2 gap-2 mb-3 min-h-[300px]">
{layoutTemplate &&
layoutTemplate?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
templateID,
} = layout;
return (
<div
key={`${templateID}-${index}`}
className=" relative border border-gray-200 cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
</div>
</div>
</Card>
);
})
) : (
return (
<Card
className="cursor-pointer hover:shadow-md transition-all border-blue-500 duration-200 group"
onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/custom-template` });
router.push(`/custom-template`)
}}
key={template.id}
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
onClick={() => router.push(`/template-preview/${template.id}`)}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
Create Custom Template
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900 capitalize">
{template.name}
</h3>
<div className="flex items-center gap-2">
<span className="px-2.5 py-0.5 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
{template.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
Create your first custom template
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})}
</div>
</div>
</Card>
)}
</div>
);
})}
</div>
</section>
{/* In Built Templates */}
<section className="h-full pt-8 flex justify-center items-center">
<div className="max-w-7xl mx-auto px-6 py-6 w-full">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Inbuilt Templates</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{inBuiltTemplates.map((template) => {
const isCustom = template.templateID.toLowerCase().startsWith("custom-");
const meta = summaryMap[template.templateID];
const displayName = isCustom && meta?.name ? meta.name : template.templateID;
const displayDescription = isCustom && meta?.description ? meta.description : template.settings.description;
const layoutTemplate = getFullDataByTemplateID(template.templateID);
return (
<Card
key={template.templateID}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${template.templateID}` });
router.push(`/template-preview/${template.templateID}`)
}}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{displayName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{template.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
{displayDescription}
</p>
<div className="grid grid-cols-2 gap-2 mb-3 min-h-[300px]">
{layoutTemplate &&
layoutTemplate?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
templateID,
} = layout;
return (
<div
key={`${templateID}-${index}`}
className=" relative border border-gray-200 cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
</div>
</div>
</Card>
);
})}
</div>
{/* Custom Templates Section */}
<section>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-800 ">
My Custom Templates
</h2>
<a href="/custom-template" className="text-sm flex font-bold font-inter items-center justify-center gap-2 bg-[#5146E5] text-white px-4 py-2 rounded-md">
<Plus className="w-4 h-4" /> Create new template
</a>
</div>
{customLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
) : customTemplates.length === 0 ? (
<Card className="p-8 text-center">
<p className="text-gray-500">No custom templates yet.</p>
<p className="text-sm text-gray-400 mt-2">
Custom templates you create will appear here.
</p>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{customTemplates.map((template: CustomTemplates) => (
<CustomTemplateCard key={template.id} template={template} />
))}
</div>
)}
</section>
</div>
</div>
);

View file

@ -0,0 +1,132 @@
"use client";
import React from "react";
import * as z from "zod";
import * as Recharts from "recharts";
import * as Babel from "@babel/standalone";
import * as d3 from "d3";
// import * as d3Cloud from "d3-cloud";
export interface CompiledLayout {
component: React.ComponentType<{ data: any }>;
layoutId: string;
layoutName: string;
layoutDescription: string;
schema: any;
sampleData: Record<string, any>;
schemaJSON: any;
}
/**
* Compiles a layout code string into a usable React component
*/
export function compileCustomLayout(layoutCode: string): CompiledLayout | null {
console.log('compileCustomLayout called');
try {
// Clean up imports that we'll provide ourselves
const cleanCode = layoutCode
// Remove React imports
.replace(/import\s+React\s*,?\s*\{?[^}]*\}?\s*from\s+['"]react['"];?/g, "")
.replace(/import\s+\*\s+as\s+React\s+from\s+['"]react['"];?/g, "")
.replace(/import\s+{\s*[^}]*\s*}\s*from\s+['"]react['"];?/g, "")
// Remove zod imports
.replace(/import\s+\*\s+as\s+z\s+from\s+['"]zod['"];?/g, "")
.replace(/import\s+{\s*z\s*}\s*from\s+['"]zod['"];?/g, "")
.replace(/import\s+.*\s+from\s+['"]zod['"];?/g, "")
// Remove recharts imports
.replace(/import\s+.*\s+from\s+['"]recharts['"];?/g, "")
// Remove other common imports we'll provide
.replace(/import\s+.*\s+from\s+['"]@\/[^'"]+['"];?/g, "")
// Remove export default at the end (we'll handle it differently)
.replace(/export\s+default\s+\w+;?\s*$/g, "");
const compiled = Babel.transform(cleanCode, {
presets: [
["react", { runtime: "classic" }],
["typescript", { isTSX: true, allExtensions: true }],
],
sourceType: "script",
}).code;
// Create a factory function that executes the compiled code
const factory = new Function(
"React",
"_z",
"Recharts",
"_d3",
// "_d3Cloud",
`
const z = _z;
// const d3Cloud= _d3Cloud;
const d3 = _d3;
// Expose React hooks
const { useState, useEffect, useRef, useMemo, useCallback, Fragment } = React;
// Expose Recharts components
const {
ResponsiveContainer, LineChart, Line, BarChart, Bar,
XAxis, YAxis, CartesianGrid, Tooltip, Legend,
PieChart, Pie, Cell, AreaChart, Area,
RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis,
ComposedChart, ScatterChart, Scatter,
RadialBarChart, RadialBar,
ReferenceLine, ReferenceDot, ReferenceArea,
Brush, LabelList, Label,Text
} = Recharts || {};
// Execute the compiled code
${compiled}
// Return the exports
return {
__esModule: true,
component: typeof dynamicSlideLayout !== 'undefined'
? dynamicSlideLayout
: (typeof DefaultLayout !== 'undefined' ? DefaultLayout : undefined),
layoutId: typeof layoutId !== 'undefined' ? layoutId : 'custom-layout',
layoutName: typeof layoutName !== 'undefined' ? layoutName : 'Custom Layout',
layoutDescription: typeof layoutDescription !== 'undefined' ? layoutDescription : '',
Schema: typeof Schema !== 'undefined' ? Schema : null,
};
`
);
// Execute the factory
const result = factory(React, z, Recharts, d3);
if (!result.component) {
console.error("No component found in compiled code");
return null;
}
// Parse schema to get sample data
let sampleData: Record<string, any> = {};
if (result.Schema) {
try {
sampleData = result.Schema.parse({});
} catch (e) {
console.warn("Could not parse schema defaults:", e);
}
}
const schemaJSON = z.toJSONSchema(result.Schema);
return {
component: result.component,
layoutId: result.layoutId,
layoutName: result.layoutName,
layoutDescription: result.layoutDescription,
schema: result.Schema,
sampleData,
schemaJSON,
};
} catch (error) {
console.error("Error compiling layout:", error);
return null;
}
}

View file

@ -0,0 +1,469 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { compileCustomLayout, CompiledLayout } from "./compileLayout";
import TemplateService from "../(presentation-generator)/services/api/template";
export interface TemplateSummary {
id: string;
name: string;
total_layouts: number;
}
export interface RawLayoutResponse {
template: string;
layout_id: string;
layout_name: string;
layout_code: string;
fonts?: string[];
}
export interface CustomTemplateDetailResponse {
layouts: RawLayoutResponse[];
template: any;
fonts?: string[];
}
// Compiled layout with all metadata
export interface CustomTemplateLayout extends CompiledLayout {
templateId: string;
rawLayoutId: string;
rawLayoutName: string;
layoutCode: string;
fonts?: string[];
}
export interface CustomTemplateDetail {
layouts: CustomTemplateLayout[];
name: string;
description: string;
id: string;
template: any;
fonts?: string[];
}
// Custom templates for the main page
export interface CustomTemplates {
id: string;
name: string;
layoutCount: number;
isCustom: true;
}
// GLOBAL CACHE
const customTemplateDetailsCache = new Map<string, CustomTemplateDetail>();
// GLOBAL IN-FLIGHT PROMISE TRACKER - prevents duplicate API calls for same ID
const inFlightRequests = new Map<string, Promise<CustomTemplateDetail | null>>();
// GLOBAL CACHE: compiled first-slide previews (we only compile the first layout)
const customTemplateFirstSlideCache = new Map<string, CompiledLayout | null>();
// GLOBAL IN-FLIGHT PROMISE TRACKER - prevents duplicate preview calls for same ID
const inFlightFirstSlideRequests = new Map<string, Promise<CompiledLayout | null>>();
function normalizeCustomTemplateId(id: string): string {
if (!id) return id;
return id.startsWith("custom-") ? id.slice("custom-".length) : id;
}
/**
* Fetch + compile ONLY the first layout for a custom template.
* Accepts either a raw presentationId or a "custom-..." id.
* Uses global cache + in-flight request deduplication.
*/
export async function getCustomTemplateFirstSlidePreview(
presentationIdOrCustomId: string
): Promise<CompiledLayout | null> {
const presentationId = normalizeCustomTemplateId(presentationIdOrCustomId);
if (!presentationId) return null;
// Cache first
if (customTemplateFirstSlideCache.has(presentationId)) {
return customTemplateFirstSlideCache.get(presentationId) ?? null;
}
// In-flight dedupe
const existing = inFlightFirstSlideRequests.get(presentationId);
if (existing) return existing;
const fetchPromise = (async (): Promise<CompiledLayout | null> => {
try {
const data: CustomTemplateDetailResponse = await TemplateService.getCustomTemplateDetails(presentationId);
const firstLayout = data?.layouts?.[0];
if (!firstLayout?.layout_code) {
customTemplateFirstSlideCache.set(presentationId, null);
return null;
}
const compiled = compileCustomLayout(firstLayout.layout_code);
customTemplateFirstSlideCache.set(presentationId, compiled);
return compiled;
} catch (err) {
console.error("Error fetching first-slide preview:", err);
// Don't cache errors; allow retry next time.
return null;
} finally {
inFlightFirstSlideRequests.delete(presentationId);
}
})();
inFlightFirstSlideRequests.set(presentationId, fetchPromise);
return fetchPromise;
}
/**
* Standalone async function to fetch and compile custom template details
* Can be called from hooks or regular async functions (like handleSubmit)
* Uses global cache and in-flight request deduplication
*/
export async function getCustomTemplateDetails(
templateId: string,
name: string = "Custom Template",
description: string = "User-created template"
): Promise<CustomTemplateDetail | null> {
if (!templateId) {
return null;
}
// Check cache first
const cachedTemplate = customTemplateDetailsCache.get(templateId);
if (cachedTemplate) {
return cachedTemplate;
}
// Check if there's already an in-flight request for this ID
const existingRequest = inFlightRequests.get(templateId);
if (existingRequest) {
return existingRequest;
}
// Create new request and track it
const fetchPromise = (async (): Promise<CustomTemplateDetail | null> => {
try {
const data: CustomTemplateDetailResponse = await TemplateService.getCustomTemplateDetails(templateId);
// Compile each layout
const compiledLayouts: CustomTemplateLayout[] = [];
for (const layout of data.layouts) {
try {
const compiled = compileCustomLayout(layout.layout_code);
if (compiled) {
compiledLayouts.push({
...compiled,
templateId: layout.template,
rawLayoutId: layout.layout_id,
rawLayoutName: layout.layout_name,
layoutCode: layout.layout_code,
fonts: layout.fonts,
});
} else {
console.warn(`Failed to compile layout: ${layout.layout_name}`);
}
} catch (compileError) {
console.error(`Error compiling ${layout.layout_name}:`, compileError);
}
}
const result: CustomTemplateDetail = {
layouts: compiledLayouts,
name,
description,
id: templateId,
template: data.template ? data.template : null,
fonts: data.fonts
};
// Cache the result
customTemplateDetailsCache.set(templateId, result);
return result;
} catch (err) {
console.error("Error fetching template details:", err);
throw err;
} finally {
// Clean up in-flight tracker
inFlightRequests.delete(templateId);
}
})();
// Track this request
inFlightRequests.set(templateId, fetchPromise);
return fetchPromise;
}
/**
* Hook to fetch custom template summaries
*/
export function useCustomTemplateSummaries() {
const [templates, setTemplates] = useState<CustomTemplates[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchTemplates = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await TemplateService.getCustomTemplateSummaries();
// const mappedTemplates: CustomTemplates[] = data.filter(item => item.total_layouts && item.total_layouts > 0).map((item) => {
// return {
// id: item.id,
// name: item.name || "Custom Template",
// layoutCount: item.total_layouts,
// isCustom: true as const,
// }
// });
const mappedTemplates: CustomTemplates[] = data.presentations.map((item: any) => {
return {
id: item.template.id,
name: item.template.name || "Custom Template",
layoutCount: 0,
isCustom: true as const,
}
});
setTemplates(mappedTemplates);
} catch (err) {
console.error("Error fetching custom templates:", err);
setError(err instanceof Error ? err.message : "Unknown error");
setTemplates([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchTemplates();
}, [fetchTemplates]);
return { templates, loading, error, refetch: fetchTemplates };
}
/**
* Hook to fetch and compile custom template layouts
* Uses global cache and in-flight request deduplication to prevent duplicate API calls
*/
export function useCustomTemplateDetails(templateDetail: { id: string, name: string, description: string }) {
const [template, setTemplate] = useState<CustomTemplateDetail | null>(() => {
return templateDetail.id ? customTemplateDetailsCache.get(templateDetail.id) ?? null : null;
});
const [fonts, setFonts] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(() => {
return templateDetail.id ? !customTemplateDetailsCache.has(templateDetail.id) : false;
});
const [error, setError] = useState<string | null>(null);
const fetchTemplateDetails = useCallback(async () => {
if (!templateDetail.id) {
return;
}
// Check cache first - instant return if cached
const cachedTemplate = customTemplateDetailsCache.get(templateDetail.id);
if (cachedTemplate) {
setTemplate(cachedTemplate);
setFonts(cachedTemplate?.fonts ?? []);
setLoading(false);
return;
}
// Check if there's already an in-flight request for this ID
const existingRequest = inFlightRequests.get(templateDetail.id);
if (existingRequest) {
// Wait for the existing request instead of making a new one
setLoading(true);
try {
const result = await existingRequest;
if (result) {
setTemplate(result);
setFonts(result?.fonts ?? []);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
return;
}
// Create new request and track it
setLoading(true);
setError(null);
const fetchPromise = (async (): Promise<CustomTemplateDetail | null> => {
try {
const data: CustomTemplateDetailResponse = await TemplateService.getCustomTemplateDetails(templateDetail.id);
// Compile each layout
const compiledLayouts: CustomTemplateLayout[] = [];
for (const layout of data.layouts) {
try {
const compiled = compileCustomLayout(layout.layout_code);
if (compiled) {
compiledLayouts.push({
...compiled,
templateId: layout.template,
rawLayoutId: layout.layout_id,
rawLayoutName: layout.layout_name,
layoutCode: layout.layout_code,
fonts: layout.fonts,
layoutId: compiled?.layoutId ?? "",
});
} else {
console.warn(`Failed to compile layout: ${layout.layout_name}`);
}
} catch (compileError) {
console.error(`Error compiling ${layout.layout_name}:`, compileError);
}
}
const result: CustomTemplateDetail = {
layouts: compiledLayouts,
name: templateDetail.name,
description: templateDetail.description,
id: templateDetail.id,
template: data.template,
fonts: data.fonts
};
// Cache the result
customTemplateDetailsCache.set(templateDetail.id, result);
return result;
} catch (err) {
console.error("Error fetching template details:", err);
throw err;
} finally {
// Clean up in-flight tracker
inFlightRequests.delete(templateDetail.id);
}
})();
// Track this request
inFlightRequests.set(templateDetail.id, fetchPromise);
try {
const result = await fetchPromise;
if (result) {
setTemplate(result);
setFonts(result?.fonts ?? []);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
setTemplate(null);
setFonts([]);
} finally {
setLoading(false);
}
}, [templateDetail.id, templateDetail.name, templateDetail.description]);
useEffect(() => {
if (templateDetail.id) {
fetchTemplateDetails();
}
}, [templateDetail.id, fetchTemplateDetails]);
return { template, loading, error, refetch: fetchTemplateDetails, fonts };
}
/**
* Hook to fetch and compile preview layouts for a single template (first 4 layouts)
*/
export function useCustomTemplatePreview(presentationId: string) {
const [previewLayouts, setPreviewLayouts] = useState<CompiledLayout[]>([]);
const [loading, setLoading] = useState(true);
const [totalLayouts, setTotalLayouts] = useState(0);
useEffect(() => {
if (!presentationId) return;
const fetchPreviews = async () => {
try {
setLoading(true);
const data = await TemplateService.getCustomTemplateDetails(presentationId);
setTotalLayouts(data.layouts.length);
// Compile first 4 layouts for preview
const compiled: CompiledLayout[] = [];
const layoutsToPreview = data.layouts.slice(0, 4);
for (const layout of layoutsToPreview) {
try {
const result = compileCustomLayout(layout.layout_code);
if (result) {
compiled.push(result);
}
} catch (e) {
console.warn(`Failed to compile preview: ${layout.layout_name}`);
}
}
setPreviewLayouts(compiled);
} catch (err) {
console.error("Error fetching preview layouts:", err);
} finally {
setLoading(false);
}
};
fetchPreviews();
}, [presentationId]);
return { previewLayouts, loading: loading, totalLayouts: totalLayouts };
}
/**
* Hook to fetch and compile preview for ONLY the first layout of a custom template.
* Accepts either a raw presentationId or a "custom-..." id.
*/
export function useCustomTemplateFirstSlidePreview(presentationIdOrCustomId: string) {
const [previewLayout, setPreviewLayout] = useState<CompiledLayout | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!presentationIdOrCustomId) return;
let cancelled = false;
const run = async () => {
try {
setLoading(true);
setError(null);
const compiled = await getCustomTemplateFirstSlidePreview(presentationIdOrCustomId);
if (cancelled) return;
setPreviewLayout(compiled);
} catch (e) {
if (cancelled) return;
setError(e instanceof Error ? e.message : "Unknown error");
setPreviewLayout(null);
} finally {
if (!cancelled) setLoading(false);
}
};
run();
return () => {
cancelled = true;
};
}, [presentationIdOrCustomId]);
return { previewLayout, loading, error };
}

View file

@ -4,7 +4,6 @@ import { Roboto, Instrument_Sans } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";
import MixpanelInitializer from "./MixpanelInitializer";
import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext";
import { Toaster } from "@/components/ui/sonner";
const inter = localFont({
src: [
@ -87,9 +86,9 @@ export default function RootLayout({
>
<Providers>
<MixpanelInitializer>
<LayoutProvider>
{children}
</LayoutProvider>
{children}
</MixpanelInitializer>
</Providers>
<Toaster position="top-center" />

View file

@ -1,5 +1,5 @@
import * as z from "zod";
import { ImageSchema, IconSchema } from "@/presentation-templates/defaultSchemes";
import { ImageSchema, IconSchema } from "@/app/presentation-templates/defaultSchemes";
export const Schema = z.object({
title: z.string().min(5).max(50).default("Quarterly Business Review").meta({

View file

@ -2,7 +2,7 @@ import * as z from "zod";
// Note:
// If you want to use images and icons, you must use ImageSchema and IconSchema
// Images and icons are the only media types supported for PDF and PPTX exports
import { ImageSchema, IconSchema } from "@/presentation-templates/defaultSchemes";
import { ImageSchema, IconSchema } from "@/app/presentation-templates/defaultSchemes";
// Schema definition

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'basic-info-slide'
export const layoutName = 'Basic Info'
@ -21,6 +21,7 @@ const basicInfoSlideSchema = z.object({
})
})
export const Schema = basicInfoSlideSchema
export type BasicInfoSlideData = z.infer<typeof basicInfoSlideSchema>
@ -32,6 +33,7 @@ interface BasicInfoSlideLayoutProps {
const BasicInfoSlideLayout: React.FC<BasicInfoSlideLayoutProps> = ({ data: slideData }) => {
return (
<>
{/* Import Google Fonts */}
@ -43,10 +45,23 @@ const BasicInfoSlideLayout: React.FC<BasicInfoSlideLayoutProps> = ({ data: slide
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
@ -65,15 +80,15 @@ const BasicInfoSlideLayout: React.FC<BasicInfoSlideLayoutProps> = ({ data: slide
{/* Right Section - Content */}
<div className="flex-1 flex flex-col justify-center pl-8 space-y-6">
{/* Title */}
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
<h1 style={{ color: "var(--background-text, #111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Product Overview'}
</h1>
{/* Purple accent line */}
<div style={{ background: "var(--text-heading-color,#9333ea)" }} className="w-20 h-1 bg-purple-600"></div>
<div style={{ background: "var(--primary-color, #9333ea)" }} className="w-20 h-1 bg-purple-600"></div>
{/* Description */}
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
<p style={{ color: "var(--background-text, #4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
{slideData?.description || 'Our product offers customizable dashboards for real-time reporting and data-driven decisions. It integrates with third-party tools to enhance operations and scales with business growth for improved efficiency.'}
</p>

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema, IconSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema, IconSchema } from '../defaultSchemes';
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
export const layoutId = 'bullet-icons-only-slide'
@ -97,22 +97,34 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Decorative Wave Patterns */}
<div className="absolute top-0 left-0 w-32 h-full opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 100 400" fill="none">
<path d="M0 100C25 150 50 50 75 100C87.5 125 100 100 100 100V0H0V100Z" fill="#8b5cf6" opacity="0.4" />
<path d="M0 200C37.5 250 62.5 150 100 200V150C75 175 50 150 25 175L0 200Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 100C25 150 50 50 75 100C87.5 125 100 100 100 100V0H0V100Z" fill="var(--primary-color, #9333ea)" opacity="0.4" />
<path d="M0 200C37.5 250 62.5 150 100 200V150C75 175 50 150 25 175L0 200Z" fill="var(--primary-color, #9333ea)" opacity="0.3" />
</svg>
</div>
<div className="absolute bottom-0 left-0 w-48 h-32 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 100" fill="none">
<path d="M0 50C50 25 100 75 150 50C175 37.5 200 50 200 50V100H0V50Z" fill="#8b5cf6" opacity="0.2" />
<path d="M0 50C50 25 100 75 150 50C175 37.5 200 50 200 50V100H0V50Z" fill="var(--primary-color, #9333ea)" opacity="0.2" />
</svg>
</div>
@ -121,7 +133,7 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
{/* Left Section - Title and Bullet Points */}
<div className="flex-1 flex flex-col pr-8">
{/* Title */}
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-5xl sm:text-6xl lg:text-7xl font-bold text-gray-900 mb-8">
<h1 style={{ color: "var(--background-text, #111827)" }} className="text-5xl sm:text-6xl lg:text-7xl font-bold text-gray-900 mb-8">
{slideData?.title || 'Solutions'}
</h1>
@ -133,23 +145,23 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
className={`flex items-start space-x-4 p-4 rounded-lg`}
>
{/* Icon */}
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center">
<div style={{ background: "var(--primary-color,#9333ea)" }} className="flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center">
<RemoteSvgIcon
url={bullet.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-6 h-6"
color="var(--text-heading-color,#ffffff)"
color="var(--primary-text, #ffffff)"
title={bullet.icon.__icon_query__}
/>
</div>
{/* Content */}
<div className="flex-1">
<h3 style={{ color: "var(--text-heading-color,#111827)" }} className="text-lg sm:text-xl font-semibold text-gray-900 mb-1">
<h3 style={{ color: "var(--background-text, #111827)" }} className="text-lg sm:text-xl font-semibold text-gray-900 mb-1">
{bullet.title}
</h3>
{bullet.subtitle && (
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-sm text-gray-700 leading-relaxed">
<p style={{ color: "var(--background-text, #4b5563)" }} className="text-sm text-gray-700 leading-relaxed">
{bullet.subtitle}
</p>
)}
@ -162,14 +174,14 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
{/* Right Section - Image */}
<div className="flex-shrink-0 w-96 flex items-center justify-center relative">
{/* Decorative Elements */}
<div style={{ color: "var(--primary-accent-color,#9333ea)" }} className="absolute top-8 right-8 text-purple-600 opacity-60">
<div style={{ color: "var(--primary-color,#9333ea)" }} className="absolute top-8 right-8 text-purple-600 opacity-60">
<svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor">
<path d="M16 0l4.12 8.38L28 12l-7.88 3.62L16 24l-4.12-8.38L4 12l7.88-3.62L16 0z" />
</svg>
</div>
<div className="absolute top-16 left-8 opacity-20">
<svg width="80" height="20" viewBox="0 0 80 20" className="text-purple-600" style={{ color: "var(--primary-accent-color,#9333ea)" }}>
<svg width="80" height="20" viewBox="0 0 80 20" className="text-purple-600" style={{ color: "var(--primary-color,#9333ea)" }}>
<path
d="M0 10 Q20 0 40 10 T80 10"
stroke="currentColor"

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema, IconSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema, IconSchema } from '../defaultSchemes';
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
export const layoutId = 'bullet-with-icons-slide'
@ -71,18 +71,30 @@ const BulletWithIconsSlideLayout: React.FC<BulletWithIconsSlideLayoutProps> = ({
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-gradient-to-br from-gray-50 to-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="flex flex-col h-full px-8 sm:px-12 lg:px-20 pt-12 pb-8">
{/* Title Section - Full Width */}
<div className="mb-8">
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900">
<h1 style={{ color: "var(--background-text, #111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900">
{slideData?.title || 'Problem'}
</h1>
</div>
@ -96,7 +108,7 @@ const BulletWithIconsSlideLayout: React.FC<BulletWithIconsSlideLayoutProps> = ({
<svg className="w-full h-full opacity-30" viewBox="0 0 200 200">
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="var(--primary-accent-color,#9333ea)" strokeWidth="0.5" />
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="var(--primary-color, #9333ea)" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
@ -115,7 +127,7 @@ const BulletWithIconsSlideLayout: React.FC<BulletWithIconsSlideLayoutProps> = ({
</div>
{/* Decorative Sparkle */}
<div style={{ color: "var(--primary-accent-color,#9333ea)" }} className="absolute top-20 right-8 text-purple-600">
<div style={{ color: "var(--primary-color,#9333ea)" }} className="absolute top-20 right-8 text-purple-600">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0l3.09 6.26L22 9l-6.91 2.74L12 18l-3.09-6.26L2 9l6.91-2.74L12 0z" />
</svg>
@ -125,7 +137,7 @@ const BulletWithIconsSlideLayout: React.FC<BulletWithIconsSlideLayoutProps> = ({
{/* Right Section - Content */}
<div className="flex-1 flex flex-col justify-center pl-8 lg:pl-16">
{/* Description */}
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-lg text-gray-700 leading-relaxed mb-8">
<p style={{ color: "var(--background-text, #4b5563)" }} className="text-lg text-gray-700 leading-relaxed mb-8">
{slideData?.description || 'Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.'}
</p>
@ -134,23 +146,23 @@ const BulletWithIconsSlideLayout: React.FC<BulletWithIconsSlideLayoutProps> = ({
{bulletPoints.map((bullet, index) => (
<div key={index} className="flex items-start space-x-4">
{/* Icon */}
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="flex-shrink-0 w-12 h-12 rounded-lg shadow-md flex items-center justify-center">
<div style={{ background: "var(--primary-color,#9333ea)" }} className="flex-shrink-0 w-12 h-12 rounded-lg shadow-md flex items-center justify-center">
<RemoteSvgIcon
url={bullet.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-6 h-6"
color="var(--text-heading-color,#ffffff)"
color="var(--primary-text, #ffffff)"
title={bullet.icon.__icon_query__}
/>
</div>
{/* Content */}
<div className="flex-1">
<h3 style={{ color: "var(--text-heading-color,#111827)" }} className="text-xl font-semibold text-gray-900 mb-2">
<h3 style={{ color: "var(--background-text, #111827)" }} className="text-xl font-semibold text-gray-900 mb-2">
{bullet.title}
</h3>
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-12 h-0.5 bg-purple-600 mb-3"></div>
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-base text-gray-700 leading-relaxed">
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-12 h-0.5 bg-purple-600 mb-3"></div>
<p style={{ color: "var(--background-text, #4b5563)" }} className="text-base text-gray-700 leading-relaxed">
{bullet.description}
</p>
</div>

View file

@ -1,9 +1,8 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from '@/presentation-templates/defaultSchemes';
import { IconSchema } from '../defaultSchemes';
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
import { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent } from "@/components/ui/chart";
import { BarChart, Bar, LineChart, Line, PieChart, Pie, AreaChart, Area, ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Cell, ResponsiveContainer } from "recharts";
import { BarChart, Bar, LineChart, Line, PieChart, Pie, AreaChart, Area, ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts";
export const layoutId = 'chart-with-bullets-slide'
export const layoutName = 'Chart with Bullet Boxes'
@ -33,19 +32,15 @@ const chartWithBulletsSlideSchema = z.object({
description: "Description text below the title",
}),
chartData: z.union([barPieLineAreaChartDataSchema, scatterChartDataSchema]).default({
type: 'scatter',
type: 'bar',
data: [
{ x: 5, y: 5 },
{ x: 10, y: 12 },
{ x: 15, y: 18 },
{ x: 20, y: 23 },
{ x: 25, y: 26 },
{ name: 'Q1', value: 5 },
{ name: 'Q1', value: 5 },
{ name: 'Q1', value: 5 },
]
}
),
color: z.string().default('#3b82f6').meta({
description: "Primary color for chart elements",
}),
showLegend: z.boolean().default(false).meta({
description: "Whether to show chart legend",
}),
@ -99,13 +94,26 @@ interface ChartWithBulletsSlideLayoutProps {
data?: Partial<ChartWithBulletsSlideData>
}
const chartConfig = {
value: {
label: "Value",
},
name: {
label: "Name",
},
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-xs font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }} >{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[10px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
const CHART_COLORS = [
@ -113,15 +121,12 @@ const CHART_COLORS = [
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
];
const BULLET_COLORS = [
'#7F31E9', '#2C78DA', '#F58AAB', '#10b981', '#f59e0b',
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
];
const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> = ({ data: slideData }) => {
const chartData = slideData?.chartData?.data || [];
const chartType = slideData?.chartData?.type;
const color = slideData?.color || 'var(--primary-accent-color,#9333ea)';
const color = 'var(--background-text, #9333ea)';
const xAxis = chartType === 'scatter' ? 'x' : 'name';
const yAxis = chartType === 'scatter' ? 'y' : 'value';
const showLegend = slideData?.showLegend || false;
@ -132,81 +137,94 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
const renderPieLabel = (props: any) => {
const { name, percent, x, y, textAnchor } = props;
return (
<text x={x} y={y} textAnchor={textAnchor} fill="var(--text-body-color,#4b5563)" fontSize={12}>
<text x={x} y={y} textAnchor={textAnchor} fill="var(--background-text, #4b5563)" fontSize={12}>
{`${name} ${(percent * 100).toFixed(0)}%`}
</text>
);
};
const commonProps = {
data: chartData,
margin: { top: 20, right: 30, left: 40, bottom: 60 },
margin: { top: 20, right: 30, left: 0, bottom: 0 },
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 12, fontWeight: 600 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
switch (chartType) {
case 'bar':
return (
<BarChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" stroke={color} />
<XAxis dataKey={xAxis} tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
<YAxis tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<Bar dataKey={yAxis} fill={color} radius={[4, 4, 0, 0]} />
<BarChart {...commonProps} >
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, ${color})`} />
<XAxis dataKey={xAxis} {...axisProps} />
<YAxis {...axisProps} />
{showTooltip && <Tooltip content={<CustomTooltip />} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Bar dataKey={yAxis} barSize={70} radius={[8, 8, 0, 0]} >
{chartData.map((_, index) => (
<Cell key={`cell-${index}`} fill={`var(--graph-${index}, ${CHART_COLORS[index % CHART_COLORS.length]})`} />
))}
</Bar>
</BarChart>
);
case 'line':
return (
<LineChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" stroke={color} />
<XAxis dataKey={xAxis} tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
<YAxis tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, ${color})`} />
<XAxis dataKey={xAxis} {...axisProps} />
<YAxis {...axisProps} />
{showTooltip && <Tooltip content={<CustomTooltip />} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Line
type="monotone"
dataKey={yAxis}
stroke={color}
strokeWidth={3}
dot={{ fill: color, strokeWidth: 2, r: 4 }}
/>
dot={{ fill: `var(--graph-0, ${CHART_COLORS[0]})`, strokeWidth: 2, r: 4 }}
>
{chartData.map((_, index) => (
<Cell key={`cell-${index}`} fill={`var(--graph-${index}, ${CHART_COLORS[index % CHART_COLORS.length]})`} />
))}
</Line>
</LineChart>
);
case 'area':
return (
<AreaChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" stroke={color} />
<XAxis dataKey={xAxis} tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
<YAxis tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, ${color})`} />
<XAxis dataKey={xAxis} {...axisProps} />
<YAxis {...axisProps} />
{showTooltip && <Tooltip content={<CustomTooltip />} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Area
type="monotone"
dataKey={yAxis}
stroke={color}
fill={color}
fillOpacity={0.6}
/>
>
{chartData.map((_, index) => (
<Cell key={`cell-${index}`} fill={`var(--graph-${index}, ${CHART_COLORS[index % CHART_COLORS.length]})`} />
))}
</Area>
</AreaChart>
);
case 'pie':
return (
<PieChart margin={{ top: 20, right: 30, left: 40, bottom: 60 }}>
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<PieChart margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
{showTooltip && <Tooltip content={<CustomTooltip />} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Pie
data={chartData}
cx="50%"
cy="40%"
outerRadius={70}
fill={color}
fill={`var(--background-text, ${color})`}
dataKey={yAxis}
label={renderPieLabel}
>
{chartData.map((_, index) => (
<Cell key={`cell-${index}`} fill={color} />
<Cell key={`cell-${index}`} fill={`var(--graph-${index}, ${CHART_COLORS[index % CHART_COLORS.length]})`} />
))}
</Pie>
</PieChart>
@ -215,12 +233,16 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
case 'scatter':
return (
<ScatterChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" stroke={color} />
<XAxis dataKey={xAxis} type="number" tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
<YAxis dataKey={yAxis} type="number" tick={{ fill: 'var(--text-body-color,#4b5563)', fontWeight: 600 }} />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<Scatter dataKey="value" fill={color} />
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, ${color})`} />
<XAxis dataKey={xAxis} type="number" {...axisProps} />
<YAxis dataKey={yAxis} type="number" {...axisProps} />
{showTooltip && <Tooltip content={<CustomTooltip />} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Scatter dataKey="value" >
{chartData.map((_, index) => (
<Cell key={`cell-${index}`} fill={`var(--graph-${index}, ${CHART_COLORS[index % CHART_COLORS.length]})`} />
))}
</Scatter>
</ScatterChart>
);
@ -232,38 +254,57 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
return (
<>
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="flex h-full px-8 sm:px-12 lg:px-20 pt-8 pb-8">
{/* Left Section - Title, Description, Chart */}
<div className="flex-1 flex flex-col pr-8">
{/* Title */}
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 mb-4">
<h1 style={{ color: "var(--background-text, #111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 mb-4">
{slideData?.title || 'Market Size'}
</h1>
{/* Description */}
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-base text-gray-700 leading-relaxed mb-8">
<p style={{ color: "var(--background-text, #4b5563)" }} className="text-base text-gray-700 leading-relaxed mb-8">
{slideData?.description || 'Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.'}
</p>
{/* Chart Container */}
<div className="flex-1 rounded-lg shadow-sm border border-gray-100 p-4" style={{ background: 'var(--primary-accent-color,#F5F8FE)' }}>
<ChartContainer config={chartConfig} className="h-full w-full">
<div className="flex-1 rounded-lg shadow-sm border border-gray-100 p-4"
style={{
borderColor: 'var(--stroke, #F8F9FA)',
}}
>
{/* <ChartContainer config={chartConfig} className="h-full w-full"> */}
<ResponsiveContainer maxHeight={460} height='100%' className="">
{renderChart()}
</ChartContainer>
</ResponsiveContainer>
{/* </ChartContainer> */}
</div>
</div>
@ -274,27 +315,27 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
key={index}
className="rounded-2xl p-6 text-white"
style={{
backgroundColor: 'var(--primary-accent-color,#9333ea)'
backgroundColor: 'var(--primary-color,#9333ea)'
}}
>
{/* Icon and Title */}
<div className="flex items-center space-x-3 mb-3">
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-8 h-8 rounded-lg flex items-center justify-center">
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-8 h-8 rounded-lg flex items-center justify-center">
<RemoteSvgIcon
url={bullet.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-5 h-5"
color="var(--text-heading-color,#ffffff)"
color="var(--primary-text, #ffffff)"
title={bullet.icon.__icon_query__}
/>
</div>
<h3 style={{ color: "var(--text-heading-color,#ffffff)" }} className="text-lg font-semibold">
<h3 style={{ color: "var(--primary-text, #ffffff)" }} className="text-lg font-semibold">
{bullet.title}
</h3>
</div>
{/* Description */}
<p style={{ color: "var(--text-body-color,#ffffff)" }} className="text-sm leading-relaxed opacity-90">
<p style={{ color: "var(--primary-text, #ffffff)" }} className="text-sm leading-relaxed opacity-90">
{bullet.description}
</p>
</div>

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'general-intro-slide'
export const layoutName = 'Intro Slide'
@ -44,19 +44,31 @@ const IntroSlideLayout: React.FC<IntroSlideLayoutProps> = ({ data: slideData })
const presenterInitials = getInitials(slideData?.presenterName || 'John Doe');
return (
<>
<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={{
background: "var(--card-background-color,#ffffff)"
, fontFamily: "var(--heading-font-family,Inter)"
background: "var(--background-color,#ffffff)"
, fontFamily: "var(--heading-font-family,Poppins)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
@ -75,34 +87,39 @@ const IntroSlideLayout: React.FC<IntroSlideLayoutProps> = ({ data: slideData })
{/* Right Section - Content */}
<div className="flex-1 flex flex-col justify-center pl-8 space-y-6">
{/* Title */}
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Product Overview'}
</h1>
{/* Purple accent line */}
<div style={{ background: "var(--text-heading-color,#9333ea)" }} className="w-20 h-1 bg-purple-600"></div>
<div style={{ background: "var(--background-text,#9333ea)" }} className="w-20 h-1 bg-purple-600"></div>
{/* Description */}
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
{slideData?.description || 'Our product offers customizable dashboards for real-time reporting and data-driven decisions. It integrates with third-party tools to enhance operations and scales with business growth for improved efficiency.'}
</p>
{/* Presenter Section */}
<div style={{ background: "var(--card-background-color,rgb(255 255 255 / 0.5))" }} className="bg-white/50 backdrop-blur-sm rounded-lg p-4 lg:p-6 border border-gray-200 shadow-sm">
<div className="bg-white/50 backdrop-blur-sm rounded-lg p-4 lg:p-6 border border-gray-200 shadow-sm"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<div className="flex items-center gap-4">
{/* Custom Initials Icon */}
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-10 h-10 lg:w-12 lg:h-12 bg-purple-600 rounded-full flex items-center justify-center">
<span className="font-bold text-sm lg:text-base" style={{ color: "var(--text-heading-color,#FFFFFF)" }}>
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-10 h-10 lg:w-12 lg:h-12 bg-purple-600 rounded-full flex items-center justify-center">
<span className="font-bold text-sm lg:text-base" style={{ color: "var(--primary-text,#FFFFFF)" }}>
{presenterInitials}
</span>
</div>
{/* Presenter Info */}
<div className="flex flex-col">
<span style={{ color: "var(--text-heading-color,#111827)" }} className="text-lg lg:text-xl font-bold text-gray-900">
<span style={{ color: "var(--background-text,#111827)" }} className="text-lg lg:text-xl font-bold text-gray-900">
{slideData?.presenterName || 'John Doe'}
</span>
<span style={{ color: "var(--text-body-color,#4b5563)" }} className="text-sm lg:text-base text-gray-600 font-medium">
<span style={{ color: "var(--background-text,#4b5563)" }} className="text-sm lg:text-base text-gray-600 font-medium">
{slideData?.presentationDate || 'December 2024'}
</span>
</div>

View file

@ -85,25 +85,37 @@ const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* 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" />
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="var(--primary-color,#9333ea)" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="var(--primary-color,#9333ea)" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="var(--primary-color,#9333ea)" 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" />
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="var(--primary-color,#9333ea)" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="var(--primary-color,#9333ea)" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="var(--primary-color,#9333ea)" opacity="0.1" />
</svg>
</div>
@ -114,7 +126,7 @@ const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData
<div className="space-y-12">
{/* Title */}
<div className="text-center">
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900">
{slideData?.title || 'Company Traction'}
</h1>
</div>
@ -126,22 +138,22 @@ const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData
{metrics.map((metric, index) => (
<div key={index} className={`text-center space-y-4 ${getItemClasses(metrics.length)}`}>
{/* Label */}
<div className="text-sm text-gray-600 font-medium" style={{ color: "var(--text-body-color,#ffffff)" }}>
<div className="text-sm text-gray-600 font-medium" style={{ color: "var(--background-text,#ffffff)" }}>
{metric.label}
</div>
{/* Large Metric Value */}
<div style={{ color: "var(--text-heading-color,#9333ea)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-purple-600">
<div style={{ color: "var(--primary-color,#9333ea)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-purple-600">
{metric.value}
</div>
{/* Description Box */}
<div
className="bg-purple-50 rounded-lg p-4 lg:p-5 text-center mt-4"
style={{ background: "var(--primary-accent-color,#9333ea)" }}
style={{ background: "var(--primary-color,#9333ea)" }}
>
<p style={{ color: "var(--text-body-color,#ffffff)" }} className="text-xs sm:text-sm text-gray-700 leading-relaxed">
<p style={{ color: "var(--primary-text,#ffffff)" }} className="text-xs sm:text-sm text-gray-700 leading-relaxed">
{metric.description}
</p>
</div>

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'metrics-with-image-slide'
export const layoutName = 'Metrics with Image'
@ -62,22 +62,34 @@ const MetricsWithImageSlideLayout: React.FC<MetricsWithImageSlideLayoutProps> =
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Decorative Wave Patterns */}
<div className="absolute bottom-0 left-0 w-48 h-48 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 200" fill="none">
<path d="M0 100C50 75 100 125 150 100C175 87.5 200 100 200 100V200H0V100Z" fill="#8b5cf6" opacity="0.4" />
<path d="M0 150C75 175 125 125 200 150V175C150 162.5 100 175 50 162.5L0 150Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 100C50 75 100 125 150 100C175 87.5 200 100 200 100V200H0V100Z" fill="var(--primary-color,#9333ea)" opacity="0.4" />
<path d="M0 150C75 175 125 125 200 150V175C150 162.5 100 175 50 162.5L0 150Z" fill="var(--primary-color,#9333ea)" opacity="0.3" />
</svg>
</div>
<div className="absolute top-0 right-0 w-64 h-64 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 200" fill="none">
<path d="M100 0C150 50 200 0 200 50C200 100 150 150 100 150C50 150 0 100 0 50C0 0 50 50 100 0Z" fill="#8b5cf6" opacity="0.2" />
<path d="M100 0C150 50 200 0 200 50C200 100 150 150 100 150C50 150 0 100 0 50C0 0 50 50 100 0Z" fill="var(--primary-color,#9333ea)" opacity="0.2" />
</svg>
</div>
@ -97,12 +109,12 @@ const MetricsWithImageSlideLayout: React.FC<MetricsWithImageSlideLayoutProps> =
{/* Right Section - Content and Metrics */}
<div className="flex-1 flex flex-col justify-center pl-8 space-y-6">
{/* Title */}
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Competitive Advantage'}
</h1>
{/* Description */}
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
{slideData?.description || 'Ginyard International Co. stands out by offering custom digital solutions tailored to client needs, alongside long-term support to ensure lasting relationships and continuous adaptation.'}
</p>
@ -110,10 +122,10 @@ const MetricsWithImageSlideLayout: React.FC<MetricsWithImageSlideLayoutProps> =
<div className="grid grid-cols-2 gap-6">
{metrics.map((metric, index) => (
<div key={index} className="text-center space-y-2">
<div style={{ color: "var(--text-body-color,#4b5563)" }} className="text-sm text-gray-600 font-medium">
<div style={{ color: "var(--background-text,#4b5563)" }} className="text-sm text-gray-600 font-medium">
{metric.label}
</div>
<div style={{ color: "var(--text-heading-color,#9333ea)" }} className="text-3xl sm:text-4xl lg:text-5xl font-bold text-purple-600">
<div style={{ color: "var(--primary-color,#9333ea)" }} className="text-3xl sm:text-4xl lg:text-5xl font-bold text-purple-600">
{metric.value}
</div>
</div>

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'numbered-bullets-slide'
export const layoutName = 'Numbered Bullets'
@ -66,11 +66,23 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content Container */}
<div className="px-8 sm:px-12 lg:px-20 pt-12 pb-8 h-full">
@ -78,11 +90,11 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
<div className="flex items-start justify-between mb-8">
{/* Title Section */}
<div className="flex-1 pr-8">
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight mb-4">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight mb-4">
{slideData?.title || 'Market Validation'}
</h1>
{/* Purple accent line */}
<div style={{ background: "var(--text-heading-color,#9333ea)" }} className="w-24 h-1 bg-purple-600 mb-6"></div>
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-24 h-1 bg-purple-600 mb-6"></div>
</div>
{/* Image Section */}
@ -90,7 +102,7 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
<img
src={slideData?.image?.__image_url__ || ''}
alt={slideData?.image?.__image_prompt__ || slideData?.title || ''}
className="w-full h-full object-cover rounded-lg shadow-md" style={{ background: "var(--tertiary-accent-color,#e5e7eb)" }}
className="w-full h-full object-cover rounded-lg shadow-md" style={{ background: "var(--stroke, #e5e7eb)" }}
/>
</div>
</div>
@ -101,17 +113,17 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
<div key={index} className="flex items-start space-x-4">
{/* Number */}
<div className="flex-shrink-0">
<div style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl font-bold text-gray-900">
<div style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl font-bold text-gray-900">
{String(index + 1).padStart(2, '0')}
</div>
</div>
{/* Content */}
<div className="flex-1 pt-2">
<h3 style={{ color: "var(--text-heading-color,#111827)" }} className="text-xl sm:text-2xl font-bold text-gray-900 mb-3">
<h3 style={{ color: "var(--background-text,#111827)" }} className="text-xl sm:text-2xl font-bold text-gray-900 mb-3">
{bullet.title}
</h3>
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-base text-gray-700 leading-relaxed">
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base text-gray-700 leading-relaxed">
{bullet.description}
</p>
</div>
@ -133,9 +145,9 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
/>
<defs>
<linearGradient id="wave-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="var(--primary-accent-color,#9333ea)" />
<stop offset="50%" stopColor="var(--primary-accent-color,#9333ea)" />
<stop offset="100%" stopColor="var(--primary-accent-color,#9333ea)" />
<stop offset="0%" stopColor="var(--primary-color,#9333ea)" />
<stop offset="50%" stopColor="var(--primary-color,#9333ea)" />
<stop offset="100%" stopColor="var(--primary-color,#9333ea)" />
</linearGradient>
</defs>
</svg>

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'quote-slide'
export const layoutName = 'Quote'
@ -44,11 +44,23 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Background Image */}
<div
className="absolute inset-0 w-full h-full bg-cover bg-center bg-no-repeat"
@ -60,7 +72,7 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
{/* Background Overlay - low opacity primary accent */}
<div
className="absolute inset-0"
style={{ backgroundColor: 'var(--primary-accent-color, #9333ea)', opacity: 0.3 }}
style={{ backgroundColor: 'var(--background-color, #000000)', opacity: 0.5 }}
></div>
{/* Decorative Elements */}
@ -74,11 +86,11 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
{/* Heading */}
<div className="space-y-4">
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white leading-tight">
<h1 style={{ color: "var(--background-text,#ffffff)" }} 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 style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-20 h-1 bg-purple-400 mx-auto"></div>
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-20 h-1 bg-purple-400 mx-auto"></div>
</div>
{/* Quote Section */}
@ -86,7 +98,7 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
{/* Quote Icon */}
<div className="flex justify-center">
<svg
className="w-12 h-12 text-purple-300 opacity-80" style={{ color: "var(--primary-accent-color,#9333ea)" }}
className="w-12 h-12 text-purple-300 opacity-80" style={{ color: "var(--primary-color,#9333ea)" }}
fill="currentColor"
viewBox="0 0 24 24"
>
@ -95,24 +107,26 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
</div>
{/* Quote Text */}
<blockquote style={{ color: "var(--text-body-color,#ffffff)" }} className="text-xl sm:text-2xl lg:text-3xl font-medium text-white leading-relaxed italic">
<blockquote style={{ color: "var(--background-text,#ffffff)" }} 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 style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-16 h-px bg-purple-300"></div>
<cite className="text-base sm:text-lg text-purple-200 font-semibold not-italic">
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-16 h-px bg-purple-300"></div>
<cite className="text-base sm:text-lg text-purple-200 font-semibold not-italic"
style={{ color: "var(--background-text,#ffffff)" }}
>
{slideData?.author || 'Winston Churchill'}
</cite>
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-16 h-px bg-purple-300"></div>
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-16 h-px bg-purple-300"></div>
</div>
</div>
</div>
</div>
{/* Bottom Decorative Border uses heading color */}
<div className="absolute bottom-0 left-0 right-0 h-2" style={{ backgroundColor: 'var(--text-heading-color,#111827)' }}></div>
<div className="absolute bottom-0 left-0 right-0 h-2" style={{ backgroundColor: 'var(--background-text,#111827)' }}></div>
</div>
</>
)

View file

@ -60,25 +60,37 @@ const TableInfoSlideLayout: React.FC<TableInfoSlideLayoutProps> = ({ data: slide
<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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* 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" />
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="var(--primary-color,#9333ea)" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="var(--primary-color,#9333ea)" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="var(--primary-color,#9333ea)" 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" />
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="var(--primary-color,#9333ea)" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="var(--primary-color,#9333ea)" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="var(--primary-color,#9333ea)" opacity="0.1" />
</svg>
</div>
@ -87,22 +99,22 @@ const TableInfoSlideLayout: React.FC<TableInfoSlideLayoutProps> = ({ data: slide
{/* Title Section */}
<div className="text-center space-y-4">
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900">
{slideData?.title || 'Market Comparison'}
</h1>
{/* Purple accent line */}
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-20 h-1 bg-purple-600 mx-auto"></div>
<div style={{ background: "var(--primary-color,#9333ea)" }} 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 style={{ background: "var(--tertiary-accent-color,#e5e7eb)", borderColor: "var(--secondary-accent-color,#e5e7eb)" }} className="bg-white rounded-lg shadow-lg border overflow-hidden">
<div style={{ background: "var(--card-color, #e5e7eb)", borderColor: "var(--stroke, #e5e7eb)" }} className="bg-white rounded-lg shadow-lg border overflow-hidden">
{/* Table Header */}
<div style={{ backgroundColor: "var(--primary-accent-color,#9333ea)" }}>
<div style={{ backgroundColor: "var(--primary-color,#9333ea)" }}>
<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" style={{ color: "var(--text-heading-color,#111827)" }}>
<div key={index} className="px-6 py-4 font-semibold text-center text-sm sm:text-base" style={{ color: "var(--primary-text,#111827)" }}>
{header}
</div>
))}
@ -110,22 +122,24 @@ const TableInfoSlideLayout: React.FC<TableInfoSlideLayoutProps> = ({ data: slide
</div>
{/* Table Body */}
<div className="divide-y divide-gray-200">
<div className="divide-y divide-gray-200 "
// style={{ borderColor: "var(--stroke, #e5e7eb)" }}
>
{tableRows.map((row, rowIndex) => (
<div
key={rowIndex}
className={`grid gap-px ${rowIndex % 2 === 0 ? 'bg-gray-50' : 'bg-white'} transition-colors duration-200`}
style={{ gridTemplateColumns: `repeat(${tableHeaders.length}, 1fr)` }}
className={`grid gap-px border-r transition-colors duration-200`}
style={{ gridTemplateColumns: `repeat(${tableHeaders.length}, 1fr)`, borderColor: "var(--stroke, #e5e7eb)", backgroundColor: "var(--card-color, #e5e7eb)" }}
>
{row.slice(0, tableHeaders.length).map((cell, cellIndex) => (
<div
key={cellIndex}
className="px-6 py-4 text-center text-sm sm:text-base"
style={{
color: "var(--text-body-color,#4b5563)",
color: "var(--background-text,#4b5563)",
background: cellIndex % 2 === 0
? "var(--secondary-accent-color,#e5e7eb)"
: "var(--tertiary-accent-color,#f3f4f6)",
? "var(--card-color, #e5e7eb)"
: "var(--card-color, #f3f4f6)",
}}
>
{cell}
@ -142,7 +156,7 @@ const TableInfoSlideLayout: React.FC<TableInfoSlideLayoutProps> = ({ data: slide
{/* Description Section */}
<div className="text-center space-y-4">
<div className="max-w-4xl mx-auto">
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-sm sm:text-base text-gray-700 leading-relaxed">
<p style={{ color: "var(--background-text,#4b5563)" }} 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>

View file

@ -56,20 +56,32 @@ const TableOfContentsSlideLayout: React.FC<TableOfContentsSlideLayoutProps> = ({
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg px-8 sm:px-12 lg:px-20 py-8 sm:py-12 lg:py-16 max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
style={{
fontFamily: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Title Section */}
<div className="text-center mb-8 sm:mb-12 mt-6">
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 mb-4">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 mb-4">
Table of Contents
</h1>
{/* Decorative Wave */}
<div className="flex justify-center">
<svg width="80" height="20" viewBox="0 0 80 20" className="text-purple-600" style={{ color: "var(--primary-accent-color,#9333ea)" }}>
<svg width="80" height="20" viewBox="0 0 80 20" className="text-purple-600" style={{ color: "var(--primary-color,#9333ea)" }}>
<path
d="M0 10 Q20 0 40 10 T80 10"
stroke="currentColor"
@ -88,21 +100,21 @@ const TableOfContentsSlideLayout: React.FC<TableOfContentsSlideLayoutProps> = ({
<div key={section.number} className="flex items-center justify-between group">
<div className="flex items-center space-x-4">
{/* Number Box */}
<div style={{ background: "var(--primary-accent-color,#9333ea)", color: "var(--text-heading-color,#ffffff)" }} className="w-12 h-12 sm:w-14 sm:h-14 bg-purple-600 rounded-xl flex items-center justify-center text-white font-bold text-lg sm:text-xl group-hover:bg-purple-700 transition-colors">
<div style={{ background: "var(--primary-color,#9333ea)", color: "var(--primary-text,#ffffff)" }} className="w-12 h-12 sm:w-14 sm:h-14 bg-purple-600 rounded-xl flex items-center justify-center text-white font-bold text-lg sm:text-xl group-hover:bg-purple-700 transition-colors">
{section.number}
</div>
{/* Title */}
<span style={{ color: "var(--text-heading-color,#111827)" }} className="text-lg sm:text-xl font-medium text-gray-800 group-hover:text-purple-600 transition-colors">
<span style={{ color: "var(--background-text,#111827)" }} className="text-lg sm:text-xl font-medium text-gray-800 group-hover:text-purple-600 transition-colors">
{section.title}
</span>
</div>
{/* Page Number */}
<div className="text-right">
<span style={{ color: "var(--text-body-color,#4b5563)" }} className="text-lg sm:text-xl text-gray-600">
<span style={{ color: "var(--background-text,#4b5563)" }} className="text-lg sm:text-xl text-gray-600">
{section.pageNumber}
</span>
{/* Dotted line effect */}
<div style={{ color: "var(--text-body-color,#4b5563)" }} className="text-gray-300 text-sm mt-1">
<div style={{ color: "var(--background-text,#4b5563)" }} className="text-gray-300 text-sm mt-1">
.....
</div>
</div>
@ -116,21 +128,21 @@ const TableOfContentsSlideLayout: React.FC<TableOfContentsSlideLayoutProps> = ({
<div key={section.number} className="flex items-center justify-between group">
<div className="flex items-center space-x-4">
{/* Number Box */}
<div style={{ background: "var(--primary-accent-color,#9333ea)", color: "var(--text-heading-color,#ffffff)" }} className="w-12 h-12 sm:w-14 sm:h-14 bg-purple-600 rounded-xl flex items-center justify-center text-white font-bold text-lg sm:text-xl group-hover:bg-purple-700 transition-colors">
<div style={{ background: "var(--primary-color,#9333ea)", color: "var(--primary-text,#ffffff)" }} className="w-12 h-12 sm:w-14 sm:h-14 bg-purple-600 rounded-xl flex items-center justify-center text-white font-bold text-lg sm:text-xl group-hover:bg-purple-700 transition-colors">
{section.number}
</div>
{/* Title */}
<span style={{ color: "var(--text-heading-color,#111827)" }} className="text-lg sm:text-xl font-medium text-gray-800 group-hover:text-purple-600 transition-colors">
<span style={{ color: "var(--background-text,#111827)" }} className="text-lg sm:text-xl font-medium text-gray-800 group-hover:text-purple-600 transition-colors">
{section.title}
</span>
</div>
{/* Page Number */}
<div className="text-right">
<span style={{ color: "var(--text-body-color,#4b5563)" }} className="text-lg sm:text-xl text-gray-600">
<span style={{ color: "var(--background-text,#4b5563)" }} className="text-lg sm:text-xl text-gray-600">
{section.pageNumber}
</span>
{/* Dotted line effect */}
<div style={{ color: "var(--text-body-color,#4b5563)" }} className="text-gray-300 text-sm mt-1">
<div style={{ color: "var(--background-text,#4b5563)" }} className="text-gray-300 text-sm mt-1">
.....
</div>
</div>

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '@/presentation-templates/defaultSchemes';
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'team-slide'
export const layoutName = 'Team Slide'
@ -91,25 +91,37 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
}
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<> <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: 'var(--heading-font-family,Inter)',
background: "var(--card-background-color,#ffffff)"
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Decorative Wave Pattern */}
<div className="absolute bottom-0 left-0 w-80 h-40 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 300 150" fill="none">
<path d="M0 75C75 50 150 100 225 75C262.5 62.5 300 75 300 75V150H0V75Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 100C100 125 200 75 300 100V125C225 112.5 150 125 75 112.5L0 100Z" fill="#8b5cf6" opacity="0.2" />
<path d="M0 75C75 50 150 100 225 75C262.5 62.5 300 75 300 75V150H0V75Z" fill="var(--primary-color,#9333ea)" opacity="0.3" />
<path d="M0 100C100 125 200 75 300 100V125C225 112.5 150 125 75 112.5L0 100Z" fill="var(--primary-color,#9333ea)" opacity="0.2" />
</svg>
</div>
@ -118,15 +130,15 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
{/* Left Section - Title and Company Description */}
<div className="flex-1 flex flex-col justify-center pr-8 space-y-6">
{/* Title */}
<h1 style={{ color: "var(--text-heading-color,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Our Team Members'}
</h1>
{/* Purple accent line */}
<div style={{ background: "var(--primary-accent-color,#9333ea)" }} className="w-20 h-1 bg-purple-600"></div>
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-20 h-1 bg-purple-600"></div>
{/* Company Description */}
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
{slideData?.companyDescription || 'Ginyard International Co. is a leading provider of innovative digital solutions tailored for businesses. Our mission is to empower organizations to achieve their goals through cutting-edge technology and strategic partnerships.'}
</p>
</div>
@ -137,7 +149,7 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
{teamMembers.map((member, index) => (
<div key={index} className="text-center space-y-3">
{/* Member Photo */}
<div className="w-32 h-32 mx-auto rounded-lg overflow-hidden shadow-md" style={{ background: "var(--tertiary-accent-color,#e5e7eb)" }}>
<div className="w-32 h-32 mx-auto rounded-lg overflow-hidden shadow-md" style={{ background: "var(--card-color,#e5e7eb)" }}>
<img
src={member.image.__image_url__ || ''}
alt={member.image.__image_prompt__ || member.name}
@ -147,13 +159,13 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
{/* Member Info */}
<div>
<h3 style={{ color: "var(--text-heading-color,#111827)" }} className="text-lg font-semibold text-gray-900">
<h3 style={{ color: "var(--background-text,#111827)" }} className="text-lg font-semibold text-gray-900">
{member.name}
</h3>
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-sm font-medium text-gray-600 italic mb-2">
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-sm font-medium text-gray-600 italic mb-2">
{member.position}
</p>
<p style={{ color: "var(--text-body-color,#4b5563)" }} className="text-xs text-gray-600 leading-relaxed px-2">
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-xs text-gray-600 leading-relaxed px-2">
{member.description}
</p>
</div>

View file

@ -0,0 +1,453 @@
import { TemplateWithData, TemplateGroupSettings, createTemplateEntry, TemplateLayoutsWithSettings } from "./utils";
// TODO: Step 1: Import All templates Layouts Here (like the ones below)
// General templates
import GeneralIntroSlideLayout, { Schema as GeneralIntroSchema, layoutId as GeneralIntroId, layoutName as GeneralIntroName, layoutDescription as GeneralIntroDesc } from "./general/IntroSlideLayout";
import BasicInfoSlideLayout, { Schema as BasicInfoSchema, layoutId as BasicInfoId, layoutName as BasicInfoName, layoutDescription as BasicInfoDesc } from "./general/BasicInfoSlideLayout";
import BulletIconsOnlySlideLayout, { Schema as BulletIconsOnlySchema, layoutId as BulletIconsOnlyId, layoutName as BulletIconsOnlyName, layoutDescription as BulletIconsOnlyDesc } from "./general/BulletIconsOnlySlideLayout";
import BulletWithIconsSlideLayout, { Schema as BulletWithIconsSchema, layoutId as BulletWithIconsId, layoutName as BulletWithIconsName, layoutDescription as BulletWithIconsDesc } from "./general/BulletWithIconsSlideLayout";
import ChartWithBulletsSlideLayout, { Schema as ChartWithBulletsSchema, layoutId as ChartWithBulletsId, layoutName as ChartWithBulletsName, layoutDescription as ChartWithBulletsDesc } from "./general/ChartWithBulletsSlideLayout";
import MetricsSlideLayout, { Schema as MetricsSchema, layoutId as MetricsId, layoutName as MetricsName, layoutDescription as MetricsDesc } from "./general/MetricsSlideLayout";
import MetricsWithImageSlideLayout, { Schema as MetricsWithImageSchema, layoutId as MetricsWithImageId, layoutName as MetricsWithImageName, layoutDescription as MetricsWithImageDesc } from "./general/MetricsWithImageSlideLayout";
import NumberedBulletsSlideLayout, { Schema as NumberedBulletsSchema, layoutId as NumberedBulletsId, layoutName as NumberedBulletsName, layoutDescription as NumberedBulletsDesc } from "./general/NumberedBulletsSlideLayout";
import QuoteSlideLayout, { Schema as QuoteSchema, layoutId as QuoteId, layoutName as QuoteName, layoutDescription as QuoteDesc } from "./general/QuoteSlideLayout";
import TableInfoSlideLayout, { Schema as TableInfoSchema, layoutId as TableInfoId, layoutName as TableInfoName, layoutDescription as TableInfoDesc } from "./general/TableInfoSlideLayout";
import TableOfContentsSlideLayout, { Schema as TableOfContentsSchema, layoutId as TableOfContentsId, layoutName as TableOfContentsName, layoutDescription as TableOfContentsDesc } from "./general/TableOfContentsSlideLayout";
import TeamSlideLayout, { Schema as TeamSchema, layoutId as TeamId, layoutName as TeamName, layoutDescription as TeamDesc } from "./general/TeamSlideLayout";
// Neo general templates
import HeadlineTextWithBulletsAndStatsLayout, { Schema as HeadlineTextWithBulletsAndStatsSchema, layoutId as HeadlineTextWithBulletsAndStatsId, layoutName as HeadlineTextWithBulletsAndStatsName, layoutDescription as HeadlineTextWithBulletsAndStatsDesc } from "./neo-general/HeadlineTextWithBulletsAndStats";
import HeadlineDescriptionWithImageLayout, { Schema as HeadlineDescriptionWithImageSchema, layoutId as HeadlineDescriptionWithImageId, layoutName as HeadlineDescriptionWithImageName, layoutDescription as HeadlineDescriptionWithImageDesc } from "./neo-general/HeadlineDescriptionWithImage";
import HeadlineDescriptionWithDoubleImageLayout, { Schema as HeadlineDescriptionWithDoubleImageSchema, layoutId as HeadlineDescriptionWithDoubleImageId, layoutName as HeadlineDescriptionWithDoubleImageName, layoutDescription as HeadlineDescriptionWithDoubleImageDesc } from "./neo-general/HeadlineDescriptionWithDoubleImage";
import IndexedThreeColumnListLayout, { Schema as IndexedThreeColumnListSchema, layoutId as IndexedThreeColumnListId, layoutName as IndexedThreeColumnListName, layoutDescription as IndexedThreeColumnListDesc } from "./neo-general/IndexedThreeColumnList";
import LayoutTextBlockWithMetricCardsLayout, { Schema as LayoutTextBlockWithMetricCardsSchema, layoutId as LayoutTextBlockWithMetricCardsId, layoutName as LayoutTextBlockWithMetricCardsName, layoutDescription as LayoutTextBlockWithMetricCardsDesc } from "./neo-general/LayoutTextBlockWithMetricCards";
import LeftAlignQuotesLayout, { Schema as LeftAlignQuotesSchema, layoutId as LeftAlignQuotesId, layoutName as LeftAlignQuotesName, layoutDescription as LeftAlignQuotesDesc } from "./neo-general/LeftAlignQuote";
import TitleDescriptionWithTableLayout, { Schema as TitleDescriptionWithTableSchema, layoutId as TitleDescriptionWithTableId, layoutName as TitleDescriptionWithTableName, layoutDescription as TitleDescriptionWithTableDesc } from "./neo-general/TitleDescriptionWithTable";
import ChallengeAndOutcomeWithOneStatLayout, { Schema as ChallengeAndOutcomeWithOneStatSchema, layoutId as ChallengeAndOutcomeWithOneStatId, layoutName as ChallengeAndOutcomeWithOneStatName, layoutDescription as ChallengeAndOutcomeWithOneStatDesc } from "./neo-general/ChallengeAndOutcomeWithOneStat";
import GridBasedEightMetricsSnapshotsLayout, { Schema as GridBasedEightMetricsSnapshotsSchema, layoutId as GridBasedEightMetricsSnapshotsId, layoutName as GridBasedEightMetricsSnapshotsName, layoutDescription as GridBasedEightMetricsSnapshotsDesc } from "./neo-general/GridBasedEightMetricsSnapshots";
import TitleTopDescriptionFourTeamMembersGridLayout, { Schema as TitleTopDescriptionFourTeamMembersGridSchema, layoutId as TitleTopDescriptionFourTeamMembersGridId, layoutName as TitleTopDescriptionFourTeamMembersGridName, layoutDescription as TitleTopDescriptionFourTeamMembersGridDesc } from "./neo-general/TitleTopDescriptionFourTeamMembersGrid";
import TitleThreeColumnRiskConstraintsLayout, { Schema as TitleThreeColumnRiskConstraintsSchema, layoutId as TitleThreeColumnRiskConstraintsId, layoutName as TitleThreeColumnRiskConstraintsName, layoutDescription as TitleThreeColumnRiskConstraintsDesc } from "./neo-general/TitleThreeColumnRiskConstraints";
import ThankYouContactInfoFooterImageSlideLayout, { Schema as ThankYouContactInfoFooterImageSlideSchema, layoutId as ThankYouContactInfoFooterImageSlideId, layoutName as ThankYouContactInfoFooterImageSlideName, layoutDescription as ThankYouContactInfoFooterImageSlideDesc } from "./neo-general/ThankYouContactInfoFooterImageSlide";
import TimelineLayout, { Schema as TimelineLayoutSchema, layoutId as TimelineLayoutId, layoutName as TimelineLayoutName, layoutDescription as TimelineLayoutDesc } from "./neo-general/Timeline";
import TitleWithFullWidthChartLayout, { Schema as TitleWithFullWidthChartSchema, layoutId as TitleWithFullWidthChartId, layoutName as TitleWithFullWidthChartName, layoutDescription as TitleWithFullWidthChartDesc } from "./neo-general/TitleWithFullWidthChart";
import TitleMetricsWithChartLayout, { Schema as TitleMetricsWithChartSchema, layoutId as TitleMetricsWithChartId, layoutName as TitleMetricsWithChartName, layoutDescription as TitleMetricsWithChartDesc } from "./neo-general/TitleMetricsWithChart";
import TitleWithGridBasedHeadingAndDescriptionLayout, { Schema as TitleWithGridBasedHeadingAndDescriptionSchema, layoutId as TitleWithGridBasedHeadingAndDescriptionId, layoutName as TitleWithGridBasedHeadingAndDescriptionName, layoutDescription as TitleWithGridBasedHeadingAndDescriptionDesc } from './neo-general/TitleWithGridBasedHeadingAndDescription'
import TextSplitWithEmphasisBlockLayout, { Schema as TextSplitWithEmphasisBlockSchema, layoutId as TextSplitWithEmphasisBlockId, layoutName as TextSplitWithEmphasisBlockName, layoutDescription as TextSplitWithEmphasisBlockDesc } from './neo-general/TextSplitWithEmphasisBlock'
import BulletIconsOnlySlideNeoGeneralLayout, { Schema as BulletIconsOnlyNeoGeneralSchema, layoutId as BulletIconsOnlyNeoGeneralId, layoutName as BulletIconsOnlyNeoGeneralName, layoutDescription as BulletIconsOnlyNeoGeneralDesc } from "./neo-general/BulletIconsOnlySlideLayout";
import BulletWithIconsSlideNeoGeneralLayout, { Schema as BulletWithIconsNeoGeneralSchema, layoutId as BulletWithIconsNeoGeneralId, layoutName as BulletWithIconsNeoGeneralName, layoutDescription as BulletWithIconsNeoGeneralDesc } from "./neo-general/BulletWithIconsSlideLayout";
import ChartWithBulletsSlideNeoGeneralLayout, { Schema as ChartWithBulletsNeoGeneralSchema, layoutId as ChartWithBulletsNeoGeneralId, layoutName as ChartWithBulletsNeoGeneralName, layoutDescription as ChartWithBulletsNeoGeneralDesc } from "./neo-general/ChartWithBulletsSlideLayout";
import MetricsWithImageSlideNeoGeneralLayout, { Schema as MetricsWithImageNeoGeneralSchema, layoutId as MetricsWithImageNeoGeneralId, layoutName as MetricsWithImageNeoGeneralName, layoutDescription as MetricsWithImageNeoGeneralDesc } from "./neo-general/MetricsWithImageSlideLayout";
import NumberedBulletsSlideNeoGeneralLayout, { Schema as NumberedBulletsNeoGeneralSchema, layoutId as NumberedBulletsNeoGeneralId, layoutName as NumberedBulletsNeoGeneralName, layoutDescription as NumberedBulletsNeoGeneralDesc } from "./neo-general/NumberedBulletsSlideLayout";
import QuoteSlideNeoGeneralLayout, { Schema as QuoteNeoGeneralSchema, layoutId as QuoteNeoGeneralId, layoutName as QuoteNeoGeneralName, layoutDescription as QuoteNeoGeneralDesc } from "./neo-general/QuoteSlideLayout";
import TeamSlideNeoGeneralLayout, { Schema as TeamNeoGeneralSchema, layoutId as TeamNeoGeneralId, layoutName as TeamNeoGeneralName, layoutDescription as TeamNeoGeneralDesc } from "./neo-general/TeamSlideLayout";
import TableOfContentWithoutPageNumberLayout, { Schema as TableOfContentWithoutPageNumberSchema, layoutId as TableOfContentWithoutPageNumberId, layoutName as TableOfContentWithoutPageNumberName, layoutDescription as TableOfContentWithoutPageNumberDesc } from "./neo-general/TableOfContentWithoutPageNumber";
import TitleMetricValueMetricLabelFunnelStagesLayout, { Schema as TitleMetricValueMetricLabelFunnelStagesSchema, layoutId as TitleMetricValueMetricLabelFunnelStagesId, layoutName as TitleMetricValueMetricLabelFunnelStagesName, layoutDescription as TitleMetricValueMetricLabelFunnelStagesDesc } from "./neo-general/TitleMetricValueMetricLabelFunnelStages";
import MultiChartGridSlideLayout, { Schema as MultiChartGridSlideSchema, layoutId as MultiChartGridSlideId, layoutName as MultiChartGridSlideName, layoutDescription as MultiChartGridSlideDesc } from "./neo-general/MultiChartGridSlideLayout";
import TitleDescriptionMultiChartGridWithMetricsLayout, { Schema as TitleDescriptionMultiChartGridWithMetricsSchema, layoutId as TitleDescriptionMultiChartGridWithMetricsId, layoutName as TitleDescriptionMultiChartGridWithMetricsName, layoutDescription as TitleDescriptionMultiChartGridWithMetricsDesc } from "./neo-general/TitleDescriptionMultiChartGridWithMetrics";
import TitleDescriptionMultiChartGridWithBulletsLayout, { Schema as TitleDescriptionMultiChartGridWithBulletsSchema, layoutId as TitleDescriptionMultiChartGridWithBulletsId, layoutName as TitleDescriptionMultiChartGridWithBulletsName, layoutDescription as TitleDescriptionMultiChartGridWithBulletsDesc } from "./neo-general/TitleDescriptionMultiChartGridWithBullets";
// Modern templates
import ModernIntroSlideLayout, { Schema as ModernIntroSchema, layoutId as ModernIntroId, layoutName as ModernIntroName, layoutDescription as ModernIntroDesc } from "./modern/IntroSlideLayout";
import BulletsWithIconsDescriptionGrid, { Schema as BulletsIconsGridSchema, layoutId as BulletsIconsGridId, layoutName as BulletsIconsGridName, layoutDescription as BulletsIconsGridDesc } from "./modern/BulletsWithIconsDescriptionGrid";
import ModernBulletWithIconsSlideLayout, { Schema as ModernBulletIconsSchema, layoutId as ModernBulletIconsId, layoutName as ModernBulletIconsName, layoutDescription as ModernBulletIconsDesc } from "./modern/BulletWithIconsSlideLayout";
import ChartOrTableWithDescription, { Schema as ChartTableDescSchema, layoutId as ChartTableDescId, layoutName as ChartTableDescName, layoutDescription as ChartTableDescDesc } from "./modern/ChartOrTableWithDescription";
import ChartOrTableWithMetricsDescription, { Schema as ChartMetricsSchema, layoutId as ChartMetricsId, layoutName as ChartMetricsName, layoutDescription as ChartMetricsDesc } from "./modern/ChartOrTableWithMetricsDescription";
import ImageAndDescriptionLayout, { Schema as ImageDescSchema, layoutId as ImageDescId, layoutName as ImageDescName, layoutDescription as ImageDescDesc } from "./modern/ImageAndDescriptionLayout";
import ImageListWithDescriptionSlideLayout, { Schema as ImageListDescSchema, layoutId as ImageListDescId, layoutName as ImageListDescName, layoutDescription as ImageListDescDesc } from "./modern/ImageListWithDescriptionSlideLayout";
import ImagesWithDescriptionLayout, { Schema as ImagesDescSchema, layoutId as ImagesDescId, layoutName as ImagesDescName, layoutDescription as ImagesDescDesc } from "./modern/ImagesWithDescriptionLayout";
import MetricsWithDescription, { Schema as MetricsDescSchema, layoutId as MetricsDescId, layoutName as MetricsDescName, layoutDescription as MetricsDescDesc } from "./modern/MetricsWithDescription";
import ModernTableOfContentsLayout, { Schema as ModernTocSchema, layoutId as ModernTocId, layoutName as ModernTocName, layoutDescription as ModernTocDesc } from "./modern/TableOfContentsLayout";
// Neo modern templates
import TitleDescriptionBulletListModernLayout, { Schema as TitleDescriptionBulletListModernSchema, layoutId as TitleDescriptionBulletListModernId, layoutName as TitleDescriptionBulletListModernName, layoutDescription as TitleDescriptionBulletListModernDesc } from './neo-modern/TitleDescriptionBulletList';
import TitleDescriptionContactListLayout, { Schema as TitleDescriptionContactListSchema, layoutId as TitleDescriptionContactListId, layoutName as TitleDescriptionContactListName, layoutDescription as TitleDescriptionContactListDesc } from './neo-modern/TitleDescriptionContactList';
import TitleDescriptionDualMetricsGridLayout, { Schema as TitleDescriptionDualMetricsGridSchema, layoutId as TitleDescriptionDualMetricsGridId, layoutName as TitleDescriptionDualMetricsGridName, layoutDescription as TitleDescriptionDualMetricsGridDesc } from './neo-modern/TitleDescriptionDualMetricsGrid';
import TitleDescriptionIconTimelineLayout, { Schema as TitleDescriptionIconTimelineSchema, layoutId as TitleDescriptionIconTimelineId, layoutName as TitleDescriptionIconTimelineName, layoutDescription as TitleDescriptionIconTimelineDesc } from './neo-modern/TitleDescriptionIconTimeline';
import TitleDescriptionImageRightModernLayout, { Schema as TitleDescriptionImageRightModernSchema, layoutId as TitleDescriptionImageRightModernId, layoutName as TitleDescriptionImageRightModernName, layoutDescription as TitleDescriptionImageRightModernDesc } from './neo-modern/TitleDescriptionImageRight';
import TitleDescriptionMetricsChartLayout, { Schema as TitleDescriptionMetricsChartSchema, layoutId as TitleDescriptionMetricsChartId, layoutName as TitleDescriptionMetricsChartName, layoutDescription as TitleDescriptionMetricsChartDesc } from './neo-modern/TitleDescriptionMetricsChart';
import TitleDescriptionMetricsImageLayout, { Schema as TitleDescriptionMetricsImageSchema, layoutId as TitleDescriptionMetricsImageId, layoutName as TitleDescriptionMetricsImageName, layoutDescription as TitleDescriptionMetricsImageDesc } from './neo-modern/TitleDescriptionMetricsImage';
import TitleDescriptionMetricsTableLayout, { Schema as TitleDescriptionMetricsTableSchema, layoutId as TitleDescriptionMetricsTableId, layoutName as TitleDescriptionMetricsTableName, layoutDescription as TitleDescriptionMetricsTableDesc } from './neo-modern/TitleDescriptionTable';
import TitleDualComparisonChartsLayout, { Schema as TitleDualComparisonChartsSchema, layoutId as TitleDualComparisonChartsId, layoutName as TitleDualComparisonChartsName, layoutDescription as TitleDualComparisonChartsDesc } from './neo-modern/TitleDualComparisonCharts';
import TitleDualComparisonCardsModernLayout, { Schema as TitleDualComparisonCardsModernSchema, layoutId as TitleDualComparisonCardsModernId, layoutName as TitleDualComparisonCardsModernName, layoutDescription as TitleDualComparisonCardsModernDesc } from './neo-modern/TitleDualComparisonCards';
import TitleHorizontalAltenenatingTimelineLayout, { Schema as TitleHorizontalAltenenatingTimelineSchema, layoutId as TitleHorizontalAltenenatingTimelineId, layoutName as TitleHorizontalAltenenatingTimelineName, layoutDescription as TitleHorizontalAltenenatingTimelineDesc } from './neo-modern/TitleHorizontalAlternatingTimeline';
import TitleKpiSnapshotGridLayout, { Schema as TitleKpiSnapshotGridSchema, layoutId as TitleKpiSnapshotGridId, layoutName as TitleKpiSnapshotGridName, layoutDescription as TitleKpiSnapshotGridDesc } from './neo-modern/TitleKpiSnapshotGrid';
import TitleSubtitlesChartLayout, { Schema as TitleSubtitlesChartSchema, layoutId as TitleSubtitlesChartId, layoutName as TitleSubtitlesChartName, layoutDescription as TitleSubtitlesChartDesc } from './neo-modern/TitleSubtitlesChart';
import TitleTwoColumnNumberListLayout, { Schema as TitleTwoColumnNumberListSchema, layoutId as TitleTwoColumnNumberListId, layoutName as TitleTwoColumnNumberListName, layoutDescription as TitleTwoColumnNumberListDesc } from './neo-modern/TitleTwoColumnNumberedList';
import TitleDescriptionMultiChartGridLayout, { Schema as TitleDescriptionMultiChartGridSchema, layoutId as TitleDescriptionMultiChartGridId, layoutName as TitleDescriptionMultiChartGridName, layoutDescription as TitleDescriptionMultiChartGridDesc } from './neo-modern/TitleDescriptionMultiChartGrid';
import TitleDescriptionMultiChartGridWithMetricsModernLayout, { Schema as TitleDescriptionMultiChartGridWithMetricsModernSchema, layoutId as TitleDescriptionMultiChartGridWithMetricsModernId, layoutName as TitleDescriptionMultiChartGridWithMetricsModernName, layoutDescription as TitleDescriptionMultiChartGridWithMetricsModernDesc } from './neo-modern/TitleDescriptionMultiChartGridWithMetrics';
import TitleDescriptionMultiChartGridWithBulletsModernLayout, { Schema as TitleDescriptionMultiChartGridWithBulletsModernSchema, layoutId as TitleDescriptionMultiChartGridWithBulletsModernId, layoutName as TitleDescriptionMultiChartGridWithBulletsModernName, layoutDescription as TitleDescriptionMultiChartGridWithBulletsModernDesc } from './neo-modern/TitleDescriptionMultiChartGridWithBullets';
// Standard templates
import StandardIntroSlideLayout, { Schema as StandardIntroSchema, layoutId as StandardIntroId, layoutName as StandardIntroName, layoutDescription as StandardIntroDesc } from "./standard/IntroSlideLayout";
import ChartLeftTextRightLayout, { Schema as ChartLeftSchema, layoutId as ChartLeftId, layoutName as ChartLeftName, layoutDescription as ChartLeftDesc } from "./standard/ChartLeftTextRightLayout";
import ContactLayout, { Schema as ContactSchema, layoutId as ContactId, layoutName as ContactName, layoutDescription as ContactDesc } from "./standard/ContactLayout";
import HeadingBulletImageDescriptionLayout, { Schema as HeadingBulletSchema, layoutId as HeadingBulletId, layoutName as HeadingBulletName, layoutDescription as HeadingBulletDesc } from "./standard/HeadingBulletImageDescriptionLayout";
import IconBulletDescriptionLayout, { Schema as IconBulletSchema, layoutId as IconBulletId, layoutName as IconBulletName, layoutDescription as IconBulletDesc } from "./standard/IconBulletDescriptionLayout";
import IconImageDescriptionLayout, { Schema as IconImageSchema, layoutId as IconImageId, layoutName as IconImageName, layoutDescription as IconImageDesc } from "./standard/IconImageDescriptionLayout";
import StandardImageListWithDescriptionLayout, { Schema as StdImageListSchema, layoutId as StdImageListId, layoutName as StdImageListName, layoutDescription as StdImageListDesc } from "./standard/ImageListWithDescriptionLayout";
import MetricsDescriptionLayout, { Schema as MetricsDescLayoutSchema, layoutId as MetricsDescLayoutId, layoutName as MetricsDescLayoutName, layoutDescription as MetricsDescLayoutDesc } from "./standard/MetricsDescriptionLayout";
import NumberedBulletSingleImageLayout, { Schema as NumBulletImgSchema, layoutId as NumBulletImgId, layoutName as NumBulletImgName, layoutDescription as NumBulletImgDesc } from "./standard/NumberedBulletSingleImageLayout";
import StandardTableOfContentsLayout, { Schema as StdTocSchema, layoutId as StdTocId, layoutName as StdTocName, layoutDescription as StdTocDesc } from "./standard/TableOfContentsLayout";
import VisualMetricsSlideLayout, { Schema as VisualMetricsSchema, layoutId as VisualMetricsId, layoutName as VisualMetricsName, layoutDescription as VisualMetricsDesc } from "./standard/VisualMetricsSlideLayout";
// Neo standard templates
import TitleBadgeChartLayout, { Schema as TitleBadgeChartSchema, layoutId as TitleBadgeChartId, layoutName as TitleBadgeChartName, layoutDescription as TitleBadgeChartDesc } from './neo-standard/TitleBadgeChart';
import TitleDescriptionBulletListStandardLayout, { Schema as TitleDescriptionBulletListStandardSchema, layoutId as TitleDescriptionBulletListStandardId, layoutName as TitleDescriptionBulletListStandardName, layoutDescription as TitleDescriptionBulletListStandardDesc } from './neo-standard/TitleDescriptionBulletList';
import TitleDescriptionContactCardsLayout, { Schema as TitleDescriptionContactCardsSchema, layoutId as TitleDescriptionContactCardsId, layoutName as TitleDescriptionContactCardsName, layoutDescription as TitleDescriptionContactCardsDesc } from './neo-standard/TitleDescriptionContactCards';
import TitleDescriptionIconListLayout, { Schema as TitleDescriptionIconListSchema, layoutId as TitleDescriptionIconListId, layoutName as TitleDescriptionIconListName, layoutDescription as TitleDescriptionIconListDesc } from './neo-standard/TitleDescriptionIconList';
import TitleDescriptionImageRightLayout, { Schema as TitleDescriptionImageRightSchema, layoutId as TitleDescriptionImageRightId, layoutName as TitleDescriptionImageRightName, layoutDescription as TitleDescriptionImageRightDesc } from './neo-standard/TitleDescriptionImageRight';
import TitleDescriptionRadialCardsLayout, { Schema as TitleDescriptionRadialCardsSchema, layoutId as TitleDescriptionRadialCardsId, layoutName as TitleDescriptionRadialCardsName, layoutDescription as TitleDescriptionRadialCardsDesc } from './neo-standard/TitleDescriptionRadialCards';
import TitleDescriptionTableLayout, { Schema as TitleDescriptionTableSchema, layoutId as TitleDescriptionTableId, layoutName as TitleDescriptionTableName, layoutDescription as TitleDescriptionTableDesc } from './neo-standard/TitleDescriptionTable';
import TitleDescriptionTimelineLayout, { Schema as TitleDescriptionTimelineSchema, layoutId as TitleDescriptionTimelineId, layoutName as TitleDescriptionTimelineName, layoutDescription as TitleDescriptionTimelineDesc } from './neo-standard/TitleDescriptionTimeline';
import TitleDualChartsComparisonLayout, { Schema as TitleDualChartsComparisonSchema, layoutId as TitleDualChartsComparisonId, layoutName as TitleDualChartsComparisonName, layoutDescription as TitleDualChartsComparisonDesc } from './neo-standard/TitleDualChartsComparison';
import TitleDualComparisonCardsLayout, { Schema as TitleDualComparisonCardsSchema, layoutId as TitleDualComparisonCardsId, layoutName as TitleDualComparisonCardsName, layoutDescription as TitleDualComparisonCardsDesc } from './neo-standard/TitleDualComparisonCards';
import TitleKpiGridLayout, { Schema as TitleKpiGridSchema, layoutId as TitleKpiGridId, layoutName as TitleKpiGridName, layoutDescription as TitleKpiGridDesc } from './neo-standard/TitleKpiGrid';
import TitleMetricsChartLayout, { Schema as TitleMetricsChartSchema, layoutId as TitleMetricsChartId, layoutName as TitleMetricsChartName, layoutDescription as TitleMetricsChartDesc } from './neo-standard/TitleMetricsChart';
import TitleMetricsImageLayout, { Schema as TitleMetricsImageSchema, layoutId as TitleMetricsImageId, layoutName as TitleMetricsImageName, layoutDescription as TitleMetricsImageDesc } from './neo-standard/TitleMetricsImage';
import TitlePointsDonutGridLayout, { Schema as TitlePointsDonutGridSchema, layoutId as TitlePointsDonutGridId, layoutName as TitlePointsDonutGridName, layoutDescription as TitlePointsDonutGridDesc } from './neo-standard/TitlePointsDonutGrid';
import TitleDescriptionMultiChartGridStandardLayout, { Schema as TitleDescriptionMultiChartGridStandardSchema, layoutId as TitleDescriptionMultiChartGridStandardId, layoutName as TitleDescriptionMultiChartGridStandardName, layoutDescription as TitleDescriptionMultiChartGridStandardDesc } from './neo-standard/TitleDescriptionMultiChartGrid';
import TitleDescriptionMultiChartGridWithMetricsStandardLayout, { Schema as TitleDescriptionMultiChartGridWithMetricsStandardSchema, layoutId as TitleDescriptionMultiChartGridWithMetricsStandardId, layoutName as TitleDescriptionMultiChartGridWithMetricsStandardName, layoutDescription as TitleDescriptionMultiChartGridWithMetricsStandardDesc } from './neo-standard/TitleDescriptionMultiChartGridWithMetrics';
import TitleDescriptionMultiChartGridWithBulletsStandardLayout, { Schema as TitleDescriptionMultiChartGridWithBulletsStandardSchema, layoutId as TitleDescriptionMultiChartGridWithBulletsStandardId, layoutName as TitleDescriptionMultiChartGridWithBulletsStandardName, layoutDescription as TitleDescriptionMultiChartGridWithBulletsStandardDesc } from './neo-standard/TitleDescriptionMultiChartGridWithBullets';
// Swift templates
import SwiftIntroSlideLayout, { Schema as SwiftIntroSchema, layoutId as SwiftIntroId, layoutName as SwiftIntroName, layoutDescription as SwiftIntroDesc } from "./swift/IntroSlideLayout";
import BulletsWithIconsTitleDescription, { Schema as BulletsIconsTitleSchema, layoutId as BulletsIconsTitleId, layoutName as BulletsIconsTitleName, layoutDescription as BulletsIconsTitleDesc } from "./swift/BulletsWithIconsTitleDescription";
import IconBulletListDescription, { Schema as IconBulletListSchema, layoutId as IconBulletListId, layoutName as IconBulletListName, layoutDescription as IconBulletListDesc } from "./swift/IconBulletListDescription";
import ImageListDescription, { Schema as ImageListSchema, layoutId as ImageListId, layoutName as ImageListName, layoutDescription as ImageListDesc } from "./swift/ImageListDescription";
import MetricsNumbers, { Schema as MetricsNumbersSchema, layoutId as MetricsNumbersId, layoutName as MetricsNumbersName, layoutDescription as MetricsNumbersDesc } from "./swift/MetricsNumbers";
import SimpleBulletPointsLayout, { Schema as SimpleBulletSchema, layoutId as SimpleBulletId, layoutName as SimpleBulletName, layoutDescription as SimpleBulletDesc } from "./swift/SimpleBulletPointsLayout";
import SwiftTableOfContents, { Schema as SwiftTocSchema, layoutId as SwiftTocId, layoutName as SwiftTocName, layoutDescription as SwiftTocDesc } from "./swift/TableOfContents";
import TableorChart, { Schema as TableChartSchema, layoutId as TableChartId, layoutName as TableChartName, layoutDescription as TableChartDesc } from "./swift/TableorChart";
import Timeline, { Schema as TimelineSchema, layoutId as TimelineId, layoutName as TimelineName, layoutDescription as TimelineDesc } from "./swift/Timeline";
// neo swift templates
import TitleCenteredChartLayout, { Schema as TitleCenteredChartSchema, layoutId as TitleCenteredChartId, layoutName as TitleCenteredChartName, layoutDescription as TitleCenteredChartDesc } from './neo-swift/TitleCenteredChart';
import TitleChartMetricsSidebarLayout, { Schema as TitleChartMetricsSidebarSchema, layoutId as TitleChartMetricsSidebarId, layoutName as TitleChartMetricsSidebarName, layoutDescription as TitleChartMetricsSidebarDesc } from './neo-swift/TitleChartMetricsSidebar';
import TitleDescriptionBulletListLayout, { Schema as TitleDescriptionBulletListSchema, layoutId as TitleDescriptionBulletListId, layoutName as TitleDescriptionBulletListName, layoutDescription as TitleDescriptionBulletListDesc } from './neo-swift/TitleDescriptionBulletList';
import TitleDescriptionDataTableLayout, { Schema as TitleDescriptionDataTableSchema, layoutId as TitleDescriptionDataTableId, layoutName as TitleDescriptionDataTableName, layoutDescription as TitleDescriptionDataTableDesc } from './neo-swift/TitleDescriptionDataTable';
import TitleDescriptionImageRightSwiftLayout, { Schema as TitleDescriptionImageRightSwiftSchema, layoutId as TitleDescriptionImageRightSwiftId, layoutName as TitleDescriptionImageRightSwiftName, layoutDescription as TitleDescriptionImageRightSwiftDesc } from './neo-swift/TitleDescriptionImageRight';
import TitleDescriptionMetricsGridLayout, { Schema as TitleDescriptionMetricsGridSchema, layoutId as TitleDescriptionMetricsGridId, layoutName as TitleDescriptionMetricsGridName, layoutDescription as TitleDescriptionMetricsGridDesc } from './neo-swift/TitleDescriptionMetricsGrid';
import TitleDescriptionMetricsGridImageLayout, { Schema as TitleDescriptionMetricsGridImageSchema, layoutId as TitleDescriptionMetricsGridImageId, layoutName as TitleDescriptionMetricsGridImageName, layoutDescription as TitleDescriptionMetricsGridImageDesc } from './neo-swift/TitleDescriptionMetricsGridImage';
import TitleDualComparisionBlockLayout, { Schema as TitleDualComparisionBlockSchema, layoutId as TitleDualComparisionBlockId, layoutName as TitleDualComparisionBlockName, layoutDescription as TitleDualComparisionBlockDesc } from './neo-swift/TitleDualComparisonBlocks';
import TitleLabelDescriptionStatCardsLayout, { Schema as TitleLabelDescriptionStatCardsSchema, layoutId as TitleLabelDescriptionStatCardsId, layoutName as TitleLabelDescriptionStatCardsName, layoutDescription as TitleLabelDescriptionStatCardsDesc } from './neo-swift/TitleLabelDescriptionStatCards';
import TitleSubtitleTeamMemberCardsLayout, { Schema as TitleSubtitleTeamMemberCardsSchema, layoutId as TitleSubtitleTeamMemberCardsId, layoutName as TitleSubtitleTeamMemberCardsName, layoutDescription as TitleSubtitleTeamMemberCardsDesc } from './neo-swift/TitleSubtitleTeamMemberCards';
import TitleTaglineDescriptionNumberedStepsLayout, { Schema as TitleTaglineDescriptionNumberedStepsSchema, layoutId as TitleTaglineDescriptionNumberedStepsId, layoutName as TitleTaglineDescriptionNumberedStepsName, layoutDescription as TitleTaglineDescriptionNumberedStepsDesc } from './neo-swift/TitleTaglineDescriptionNumberedSteps';
import TitleThreeByThreeMetricsGridLayout, { Schema as TitleThreeByThreeMetricsGridSchema, layoutId as TitleThreeByThreeMetricsGridId, layoutName as TitleThreeByThreeMetricsGridName, layoutDescription as TitleThreeByThreeMetricsGridDesc } from './neo-swift/TitleThreeByThreeMetricsGrid';
import TitleDescriptionSixChartsGridLayout, { Schema as TitleDescriptionSixChartsGridSchema, layoutId as TitleDescriptionSixChartsGridId, layoutName as TitleDescriptionSixChartsGridName, layoutDescription as TitleDescriptionSixChartsGridDesc } from './neo-swift/TitleDescriptionSixChartsGrid';
import TitleDescriptionSixChartsFourMetricsLayout, { Schema as TitleDescriptionSixChartsFourMetricsSchema, layoutId as TitleDescriptionSixChartsFourMetricsId, layoutName as TitleDescriptionSixChartsFourMetricsName, layoutDescription as TitleDescriptionSixChartsFourMetricsDesc } from './neo-swift/TitleDescriptionSixChartsFourMetrics';
import TitleDescriptionFourChartsSixBulletsLayout, { Schema as TitleDescriptionFourChartsSixBulletsSchema, layoutId as TitleDescriptionFourChartsSixBulletsId, layoutName as TitleDescriptionFourChartsSixBulletsName, layoutDescription as TitleDescriptionFourChartsSixBulletsDesc } from './neo-swift/TitleDescriptionFourChartsSixBullets';
// TODO: Step 2: Import template settings Here (like the ones below)
// Template template settings
import generalSettings from "./general/settings.json";
import modernSettings from "./modern/settings.json";
import standardSettings from "./standard/settings.json";
import swiftSettings from "./swift/settings.json";
import neoGeneralSettings from "./neo-general/settings.json";
import neoStandardSettings from "./neo-standard/settings.json";
import neoModernSettings from "./neo-modern/settings.json";
import neoSwiftSettings from "./neo-swift/settings.json";
// Helper to create template entry
// TODO: Step 3: Create template entries for each template (like the ones below)
export const neoGeneralTemplates: TemplateWithData[] = [
createTemplateEntry(TextSplitWithEmphasisBlockLayout, TextSplitWithEmphasisBlockSchema, TextSplitWithEmphasisBlockId, TextSplitWithEmphasisBlockName, TextSplitWithEmphasisBlockDesc, 'neo-general', 'TextSplitWithEmphasisBlock'),
createTemplateEntry(TitleWithGridBasedHeadingAndDescriptionLayout, TitleWithGridBasedHeadingAndDescriptionSchema, TitleWithGridBasedHeadingAndDescriptionId, TitleWithGridBasedHeadingAndDescriptionName, TitleWithGridBasedHeadingAndDescriptionDesc, "neo-general", "TitleWithGridBasedHeadingAndDescriptionLayout"),
createTemplateEntry(TitleWithFullWidthChartLayout, TitleWithFullWidthChartSchema, TitleWithFullWidthChartId, TitleWithFullWidthChartName, TitleWithFullWidthChartDesc, "neo-general", "TitleWithFullWidthChartLayout"),
createTemplateEntry(TitleMetricsWithChartLayout, TitleMetricsWithChartSchema, TitleMetricsWithChartId, TitleMetricsWithChartName, TitleMetricsWithChartDesc, "neo-general", "TitleMetricsWithChartLayout"),
createTemplateEntry(TitleTopDescriptionFourTeamMembersGridLayout, TitleTopDescriptionFourTeamMembersGridSchema, TitleTopDescriptionFourTeamMembersGridId, TitleTopDescriptionFourTeamMembersGridName, TitleTopDescriptionFourTeamMembersGridDesc, "neo-general", "TitleTopDescriptionFourTeamMembersGridLayout"),
createTemplateEntry(TitleThreeColumnRiskConstraintsLayout, TitleThreeColumnRiskConstraintsSchema, TitleThreeColumnRiskConstraintsId, TitleThreeColumnRiskConstraintsName, TitleThreeColumnRiskConstraintsDesc, "neo-general", "TitleThreeColumnRiskConstraintsLayout"),
createTemplateEntry(TitleMetricValueMetricLabelFunnelStagesLayout, TitleMetricValueMetricLabelFunnelStagesSchema, TitleMetricValueMetricLabelFunnelStagesId, TitleMetricValueMetricLabelFunnelStagesName, TitleMetricValueMetricLabelFunnelStagesDesc, "neo-general", "TitleMetricValueMetricLabelFunnelStages"),
createTemplateEntry(ThankYouContactInfoFooterImageSlideLayout, ThankYouContactInfoFooterImageSlideSchema, ThankYouContactInfoFooterImageSlideId, ThankYouContactInfoFooterImageSlideName, ThankYouContactInfoFooterImageSlideDesc, "neo-general", "ThankYouContactInfoFooterImageSlideLayout"),
createTemplateEntry(TimelineLayout, TimelineLayoutSchema, TimelineLayoutId, TimelineLayoutName, TimelineLayoutDesc, "neo-general", "TimelineLayoutLayout"),
createTemplateEntry(IndexedThreeColumnListLayout, IndexedThreeColumnListSchema, IndexedThreeColumnListId, IndexedThreeColumnListName, IndexedThreeColumnListDesc, "neo-general", "IndexedThreeColumnListLayout"),
createTemplateEntry(LayoutTextBlockWithMetricCardsLayout, LayoutTextBlockWithMetricCardsSchema, LayoutTextBlockWithMetricCardsId, LayoutTextBlockWithMetricCardsName, LayoutTextBlockWithMetricCardsDesc, "neo-general", "LayoutTextBlockWithMetricCardsLayout"),
createTemplateEntry(LeftAlignQuotesLayout, LeftAlignQuotesSchema, LeftAlignQuotesId, LeftAlignQuotesName, LeftAlignQuotesDesc, "neo-general", "LeftAlignQuotesLayout"),
createTemplateEntry(TitleDescriptionWithTableLayout, TitleDescriptionWithTableSchema, TitleDescriptionWithTableId, TitleDescriptionWithTableName, TitleDescriptionWithTableDesc, "neo-general", "TitleDescriptionWithTableLayout"),
createTemplateEntry(ChallengeAndOutcomeWithOneStatLayout, ChallengeAndOutcomeWithOneStatSchema, ChallengeAndOutcomeWithOneStatId, ChallengeAndOutcomeWithOneStatName, ChallengeAndOutcomeWithOneStatDesc, "neo-general", "ChallengeAndOutcomeWithOneStatLayout"),
createTemplateEntry(GridBasedEightMetricsSnapshotsLayout, GridBasedEightMetricsSnapshotsSchema, GridBasedEightMetricsSnapshotsId, GridBasedEightMetricsSnapshotsName, GridBasedEightMetricsSnapshotsDesc, "neo-general", "GridBasedEightMetricsSnapshotsLayout"),
createTemplateEntry(HeadlineTextWithBulletsAndStatsLayout, HeadlineTextWithBulletsAndStatsSchema, HeadlineTextWithBulletsAndStatsId, HeadlineTextWithBulletsAndStatsName, HeadlineTextWithBulletsAndStatsDesc, "neo-general", "HeadlineTextWithBulletsAndStatsLayout"),
createTemplateEntry(HeadlineDescriptionWithImageLayout, HeadlineDescriptionWithImageSchema, HeadlineDescriptionWithImageId, HeadlineDescriptionWithImageName, HeadlineDescriptionWithImageDesc, "neo-general", "HeadlineDescriptionWithImageLayout"),
createTemplateEntry(HeadlineDescriptionWithDoubleImageLayout, HeadlineDescriptionWithDoubleImageSchema, HeadlineDescriptionWithDoubleImageId, HeadlineDescriptionWithDoubleImageName, HeadlineDescriptionWithDoubleImageDesc, "neo-general", "HeadlineDescriptionWithDoubleImageLayout"),
createTemplateEntry(BulletIconsOnlySlideNeoGeneralLayout, BulletIconsOnlyNeoGeneralSchema, BulletIconsOnlyNeoGeneralId, BulletIconsOnlyNeoGeneralName, BulletIconsOnlyNeoGeneralDesc, "neo-general", "BulletIconsOnlySlideLayout"),
createTemplateEntry(BulletWithIconsSlideNeoGeneralLayout, BulletWithIconsNeoGeneralSchema, BulletWithIconsNeoGeneralId, BulletWithIconsNeoGeneralName, BulletWithIconsNeoGeneralDesc, "neo-general", "BulletWithIconsSlideLayout"),
createTemplateEntry(ChartWithBulletsSlideNeoGeneralLayout, ChartWithBulletsNeoGeneralSchema, ChartWithBulletsNeoGeneralId, ChartWithBulletsNeoGeneralName, ChartWithBulletsNeoGeneralDesc, "neo-general", "ChartWithBulletsSlideLayout"),
createTemplateEntry(MetricsWithImageSlideNeoGeneralLayout, MetricsWithImageNeoGeneralSchema, MetricsWithImageNeoGeneralId, MetricsWithImageNeoGeneralName, MetricsWithImageNeoGeneralDesc, "neo-general", "MetricsWithImageSlideLayout"),
createTemplateEntry(NumberedBulletsSlideNeoGeneralLayout, NumberedBulletsNeoGeneralSchema, NumberedBulletsNeoGeneralId, NumberedBulletsNeoGeneralName, NumberedBulletsNeoGeneralDesc, "neo-general", "NumberedBulletsSlideLayout"),
createTemplateEntry(QuoteSlideNeoGeneralLayout, QuoteNeoGeneralSchema, QuoteNeoGeneralId, QuoteNeoGeneralName, QuoteNeoGeneralDesc, "neo-general", "QuoteSlideLayout"),
createTemplateEntry(TableOfContentWithoutPageNumberLayout, TableOfContentWithoutPageNumberSchema, TableOfContentWithoutPageNumberId, TableOfContentWithoutPageNumberName, TableOfContentWithoutPageNumberDesc, "neo-general", "TableOfContentWithoutPageNumber"),
createTemplateEntry(TeamSlideNeoGeneralLayout, TeamNeoGeneralSchema, TeamNeoGeneralId, TeamNeoGeneralName, TeamNeoGeneralDesc, "neo-general", "TeamSlideLayout"),
createTemplateEntry(MultiChartGridSlideLayout, MultiChartGridSlideSchema, MultiChartGridSlideId, MultiChartGridSlideName, MultiChartGridSlideDesc, "neo-general", "MultiChartGridSlideLayout"),
createTemplateEntry(TitleDescriptionMultiChartGridWithMetricsLayout, TitleDescriptionMultiChartGridWithMetricsSchema, TitleDescriptionMultiChartGridWithMetricsId, TitleDescriptionMultiChartGridWithMetricsName, TitleDescriptionMultiChartGridWithMetricsDesc, "neo-general", "TitleDescriptionMultiChartGridWithMetrics"),
createTemplateEntry(TitleDescriptionMultiChartGridWithBulletsLayout, TitleDescriptionMultiChartGridWithBulletsSchema, TitleDescriptionMultiChartGridWithBulletsId, TitleDescriptionMultiChartGridWithBulletsName, TitleDescriptionMultiChartGridWithBulletsDesc, "neo-general", "TitleDescriptionMultiChartGridWithBullets"),
]
export const neoStandardTemplates: TemplateWithData[] = [
createTemplateEntry(TitleBadgeChartLayout, TitleBadgeChartSchema, TitleBadgeChartId, TitleBadgeChartName, TitleBadgeChartDesc, "neo-standard", "TitleBadgeChartLayout"),
createTemplateEntry(TitleDescriptionBulletListStandardLayout, TitleDescriptionBulletListStandardSchema, TitleDescriptionBulletListStandardId, TitleDescriptionBulletListStandardName, TitleDescriptionBulletListStandardDesc, "neo-standard", "TitleDescriptionBulletList"),
createTemplateEntry(TitleDescriptionContactCardsLayout, TitleDescriptionContactCardsSchema, TitleDescriptionContactCardsId, TitleDescriptionContactCardsName, TitleDescriptionContactCardsDesc, "neo-standard", "TitleDescriptionContactCardsLayout"),
createTemplateEntry(TitleDescriptionIconListLayout, TitleDescriptionIconListSchema, TitleDescriptionIconListId, TitleDescriptionIconListName, TitleDescriptionIconListDesc, "neo-standard", "TitleDescriptionIconListLayout"),
createTemplateEntry(TitleDescriptionImageRightLayout, TitleDescriptionImageRightSchema, TitleDescriptionImageRightId, TitleDescriptionImageRightName, TitleDescriptionImageRightDesc, "neo-standard", "TitleDescriptionImageRightLayout"),
createTemplateEntry(TitleDescriptionRadialCardsLayout, TitleDescriptionRadialCardsSchema, TitleDescriptionRadialCardsId, TitleDescriptionRadialCardsName, TitleDescriptionRadialCardsDesc, "neo-standard", "TitleDescriptionRadialCardsLayout"),
createTemplateEntry(TitleDescriptionTableLayout, TitleDescriptionTableSchema, TitleDescriptionTableId, TitleDescriptionTableName, TitleDescriptionTableDesc, "neo-standard", "TitleDescriptionTableLayout"),
createTemplateEntry(TitleDescriptionTimelineLayout, TitleDescriptionTimelineSchema, TitleDescriptionTimelineId, TitleDescriptionTimelineName, TitleDescriptionTimelineDesc, "neo-standard", "TitleDescriptionTimelineLayout"),
createTemplateEntry(TitleDualChartsComparisonLayout, TitleDualChartsComparisonSchema, TitleDualChartsComparisonId, TitleDualChartsComparisonName, TitleDualChartsComparisonDesc, "neo-standard", "TitleDualChartsComparisonLayout"),
createTemplateEntry(TitleDualComparisonCardsLayout, TitleDualComparisonCardsSchema, TitleDualComparisonCardsId, TitleDualComparisonCardsName, TitleDualComparisonCardsDesc, "neo-standard", "TitleDualComparisonCardsLayout"),
createTemplateEntry(TitleKpiGridLayout, TitleKpiGridSchema, TitleKpiGridId, TitleKpiGridName, TitleKpiGridDesc, "neo-standard", "TitleKpiGridLayout"),
createTemplateEntry(TitleMetricsChartLayout, TitleMetricsChartSchema, TitleMetricsChartId, TitleMetricsChartName, TitleMetricsChartDesc, "neo-standard", "TitleMetricsChartLayout"),
createTemplateEntry(TitleMetricsImageLayout, TitleMetricsImageSchema, TitleMetricsImageId, TitleMetricsImageName, TitleMetricsImageDesc, "neo-standard", "TitleMetricsImageLayout"),
createTemplateEntry(TitlePointsDonutGridLayout, TitlePointsDonutGridSchema, TitlePointsDonutGridId, TitlePointsDonutGridName, TitlePointsDonutGridDesc, "neo-standard", "TitlePointsDonutGridLayout"),
createTemplateEntry(TitleDescriptionMultiChartGridStandardLayout, TitleDescriptionMultiChartGridStandardSchema, TitleDescriptionMultiChartGridStandardId, TitleDescriptionMultiChartGridStandardName, TitleDescriptionMultiChartGridStandardDesc, "neo-standard", "TitleDescriptionMultiChartGrid"),
createTemplateEntry(TitleDescriptionMultiChartGridWithMetricsStandardLayout, TitleDescriptionMultiChartGridWithMetricsStandardSchema, TitleDescriptionMultiChartGridWithMetricsStandardId, TitleDescriptionMultiChartGridWithMetricsStandardName, TitleDescriptionMultiChartGridWithMetricsStandardDesc, "neo-standard", "TitleDescriptionMultiChartGridWithMetrics"),
createTemplateEntry(TitleDescriptionMultiChartGridWithBulletsStandardLayout, TitleDescriptionMultiChartGridWithBulletsStandardSchema, TitleDescriptionMultiChartGridWithBulletsStandardId, TitleDescriptionMultiChartGridWithBulletsStandardName, TitleDescriptionMultiChartGridWithBulletsStandardDesc, "neo-standard", "TitleDescriptionMultiChartGridWithBullets"),
]
export const neoModernTemplates: TemplateWithData[] = [
createTemplateEntry(TitleDescriptionBulletListModernLayout, TitleDescriptionBulletListModernSchema, TitleDescriptionBulletListModernId, TitleDescriptionBulletListModernName, TitleDescriptionBulletListModernDesc, "neo-modern", "TitleDescriptionBulletList"),
createTemplateEntry(TitleDescriptionContactListLayout, TitleDescriptionContactListSchema, TitleDescriptionContactListId, TitleDescriptionContactListName, TitleDescriptionContactListDesc, "neo-modern", "TitleDescriptionContactListLayout"),
createTemplateEntry(TitleDescriptionDualMetricsGridLayout, TitleDescriptionDualMetricsGridSchema, TitleDescriptionDualMetricsGridId, TitleDescriptionDualMetricsGridName, TitleDescriptionDualMetricsGridDesc, "neo-modern", "TitleDescriptionDualMetricsGridLayout"),
createTemplateEntry(TitleDescriptionIconTimelineLayout, TitleDescriptionIconTimelineSchema, TitleDescriptionIconTimelineId, TitleDescriptionIconTimelineName, TitleDescriptionIconTimelineDesc, "neo-modern", "TitleDescriptionIconTimelineLayout"),
createTemplateEntry(TitleDescriptionImageRightModernLayout, TitleDescriptionImageRightModernSchema, TitleDescriptionImageRightModernId, TitleDescriptionImageRightModernName, TitleDescriptionImageRightModernDesc, "neo-modern", "TitleDescriptionImageRightModernLayout"),
createTemplateEntry(TitleDescriptionMetricsChartLayout, TitleDescriptionMetricsChartSchema, TitleDescriptionMetricsChartId, TitleDescriptionMetricsChartName, TitleDescriptionMetricsChartDesc, "neo-modern", "TitleDescriptionMetricsChartLayout"),
createTemplateEntry(TitleDescriptionMetricsImageLayout, TitleDescriptionMetricsImageSchema, TitleDescriptionMetricsImageId, TitleDescriptionMetricsImageName, TitleDescriptionMetricsImageDesc, "neo-modern", "TitleDescriptionMetricsImageLayout"),
createTemplateEntry(TitleDescriptionMetricsTableLayout, TitleDescriptionMetricsTableSchema, TitleDescriptionMetricsTableId, TitleDescriptionMetricsTableName, TitleDescriptionMetricsTableDesc, "neo-modern", "TitleDescriptionMetricsTableLayout"),
createTemplateEntry(TitleDualComparisonChartsLayout, TitleDualComparisonChartsSchema, TitleDualComparisonChartsId, TitleDualComparisonChartsName, TitleDualComparisonChartsDesc, "neo-modern", "TitleDualComparisonChartsLayout"),
createTemplateEntry(TitleDualComparisonCardsModernLayout, TitleDualComparisonCardsModernSchema, TitleDualComparisonCardsModernId, TitleDualComparisonCardsModernName, TitleDualComparisonCardsModernDesc, "neo-modern", "TitleDualComparisonCardsModernLayout"),
createTemplateEntry(TitleHorizontalAltenenatingTimelineLayout, TitleHorizontalAltenenatingTimelineSchema, TitleHorizontalAltenenatingTimelineId, TitleHorizontalAltenenatingTimelineName, TitleHorizontalAltenenatingTimelineDesc, "neo-modern", "TitleHorizontalAltenenatingTimelineLayout"),
createTemplateEntry(TitleKpiSnapshotGridLayout, TitleKpiSnapshotGridSchema, TitleKpiSnapshotGridId, TitleKpiSnapshotGridName, TitleKpiSnapshotGridDesc, "neo-modern", "TitleKpiSnapshotGridLayout"),
createTemplateEntry(TitleSubtitlesChartLayout, TitleSubtitlesChartSchema, TitleSubtitlesChartId, TitleSubtitlesChartName, TitleSubtitlesChartDesc, "neo-modern", "TitleSubtitlesChartLayout"),
createTemplateEntry(TitleTwoColumnNumberListLayout, TitleTwoColumnNumberListSchema, TitleTwoColumnNumberListId, TitleTwoColumnNumberListName, TitleTwoColumnNumberListDesc, "neo-modern", "TitleTwoColumnNumberListLayout"),
createTemplateEntry(TitleDescriptionMultiChartGridLayout, TitleDescriptionMultiChartGridSchema, TitleDescriptionMultiChartGridId, TitleDescriptionMultiChartGridName, TitleDescriptionMultiChartGridDesc, "neo-modern", "TitleDescriptionMultiChartGrid"),
createTemplateEntry(TitleDescriptionMultiChartGridWithMetricsModernLayout, TitleDescriptionMultiChartGridWithMetricsModernSchema, TitleDescriptionMultiChartGridWithMetricsModernId, TitleDescriptionMultiChartGridWithMetricsModernName, TitleDescriptionMultiChartGridWithMetricsModernDesc, "neo-modern", "TitleDescriptionMultiChartGridWithMetrics"),
createTemplateEntry(TitleDescriptionMultiChartGridWithBulletsModernLayout, TitleDescriptionMultiChartGridWithBulletsModernSchema, TitleDescriptionMultiChartGridWithBulletsModernId, TitleDescriptionMultiChartGridWithBulletsModernName, TitleDescriptionMultiChartGridWithBulletsModernDesc, "neo-modern", "TitleDescriptionMultiChartGridWithBullets"),
]
export const neoSwiftTemplates: TemplateWithData[] = [
createTemplateEntry(TitleCenteredChartLayout, TitleCenteredChartSchema, TitleCenteredChartId, TitleCenteredChartName, TitleCenteredChartDesc, "neo-swift", "TitleCenteredChartLayout"),
createTemplateEntry(TitleChartMetricsSidebarLayout, TitleChartMetricsSidebarSchema, TitleChartMetricsSidebarId, TitleChartMetricsSidebarName, TitleChartMetricsSidebarDesc, "neo-swift", "TitleChartMetricsSidebarLayout"),
createTemplateEntry(TitleDescriptionBulletListLayout, TitleDescriptionBulletListSchema, TitleDescriptionBulletListId, TitleDescriptionBulletListName, TitleDescriptionBulletListDesc, "neo-swift", "TitleDescriptionBulletListLayout"),
createTemplateEntry(TitleDescriptionDataTableLayout, TitleDescriptionDataTableSchema, TitleDescriptionDataTableId, TitleDescriptionDataTableName, TitleDescriptionDataTableDesc, "neo-swift", "TitleDescriptionDataTableLayout"),
createTemplateEntry(TitleDescriptionImageRightSwiftLayout, TitleDescriptionImageRightSwiftSchema, TitleDescriptionImageRightSwiftId, TitleDescriptionImageRightSwiftName, TitleDescriptionImageRightSwiftDesc, "neo-swift", "TitleDescriptionImageRightSwiftLayout"),
createTemplateEntry(TitleDescriptionMetricsGridLayout, TitleDescriptionMetricsGridSchema, TitleDescriptionMetricsGridId, TitleDescriptionMetricsGridName, TitleDescriptionMetricsGridDesc, "neo-swift", "TitleDescriptionMetricsGridLayout"),
createTemplateEntry(TitleDescriptionMetricsGridImageLayout, TitleDescriptionMetricsGridImageSchema, TitleDescriptionMetricsGridImageId, TitleDescriptionMetricsGridImageName, TitleDescriptionMetricsGridImageDesc, "neo-swift", "TitleDescriptionMetricsGridImageLayout"),
createTemplateEntry(TitleDualComparisionBlockLayout, TitleDualComparisionBlockSchema, TitleDualComparisionBlockId, TitleDualComparisionBlockName, TitleDualComparisionBlockDesc, "neo-swift", "TitleDualComparisionBlockLayout"),
createTemplateEntry(TitleLabelDescriptionStatCardsLayout, TitleLabelDescriptionStatCardsSchema, TitleLabelDescriptionStatCardsId, TitleLabelDescriptionStatCardsName, TitleLabelDescriptionStatCardsDesc, "neo-swift", "TitleLabelDescriptionStatCardsLayout"),
createTemplateEntry(TitleSubtitleTeamMemberCardsLayout, TitleSubtitleTeamMemberCardsSchema, TitleSubtitleTeamMemberCardsId, TitleSubtitleTeamMemberCardsName, TitleSubtitleTeamMemberCardsDesc, "neo-swift", "TitleSubtitleTeamMemberCardsLayout"),
createTemplateEntry(TitleTaglineDescriptionNumberedStepsLayout, TitleTaglineDescriptionNumberedStepsSchema, TitleTaglineDescriptionNumberedStepsId, TitleTaglineDescriptionNumberedStepsName, TitleTaglineDescriptionNumberedStepsDesc, "neo-swift", "TitleTaglineDescriptionNumberedStepsLayout"),
createTemplateEntry(TitleThreeByThreeMetricsGridLayout, TitleThreeByThreeMetricsGridSchema, TitleThreeByThreeMetricsGridId, TitleThreeByThreeMetricsGridName, TitleThreeByThreeMetricsGridDesc, "neo-swift", "TitleThreeByThreeMetricsGridLayout"),
createTemplateEntry(TitleDescriptionSixChartsGridLayout, TitleDescriptionSixChartsGridSchema, TitleDescriptionSixChartsGridId, TitleDescriptionSixChartsGridName, TitleDescriptionSixChartsGridDesc, "neo-swift", "TitleDescriptionSixChartsGridLayout"),
createTemplateEntry(TitleDescriptionSixChartsFourMetricsLayout, TitleDescriptionSixChartsFourMetricsSchema, TitleDescriptionSixChartsFourMetricsId, TitleDescriptionSixChartsFourMetricsName, TitleDescriptionSixChartsFourMetricsDesc, "neo-swift", "TitleDescriptionSixChartsFourMetricsLayout"),
createTemplateEntry(TitleDescriptionFourChartsSixBulletsLayout, TitleDescriptionFourChartsSixBulletsSchema, TitleDescriptionFourChartsSixBulletsId, TitleDescriptionFourChartsSixBulletsName, TitleDescriptionFourChartsSixBulletsDesc, "neo-swift", "TitleDescriptionFourChartsSixBulletsLayout"),
]
// General templates array
export const generalTemplates: TemplateWithData[] = [
createTemplateEntry(GeneralIntroSlideLayout, GeneralIntroSchema, GeneralIntroId, GeneralIntroName, GeneralIntroDesc, "general", "IntroSlideLayout"),
createTemplateEntry(BasicInfoSlideLayout, BasicInfoSchema, BasicInfoId, BasicInfoName, BasicInfoDesc, "general", "BasicInfoSlideLayout"),
createTemplateEntry(BulletIconsOnlySlideLayout, BulletIconsOnlySchema, BulletIconsOnlyId, BulletIconsOnlyName, BulletIconsOnlyDesc, "general", "BulletIconsOnlySlideLayout"),
createTemplateEntry(BulletWithIconsSlideLayout, BulletWithIconsSchema, BulletWithIconsId, BulletWithIconsName, BulletWithIconsDesc, "general", "BulletWithIconsSlideLayout"),
createTemplateEntry(ChartWithBulletsSlideLayout, ChartWithBulletsSchema, ChartWithBulletsId, ChartWithBulletsName, ChartWithBulletsDesc, "general", "ChartWithBulletsSlideLayout"),
createTemplateEntry(MetricsSlideLayout, MetricsSchema, MetricsId, MetricsName, MetricsDesc, "general", "MetricsSlideLayout"),
createTemplateEntry(MetricsWithImageSlideLayout, MetricsWithImageSchema, MetricsWithImageId, MetricsWithImageName, MetricsWithImageDesc, "general", "MetricsWithImageSlideLayout"),
createTemplateEntry(NumberedBulletsSlideLayout, NumberedBulletsSchema, NumberedBulletsId, NumberedBulletsName, NumberedBulletsDesc, "general", "NumberedBulletsSlideLayout"),
createTemplateEntry(QuoteSlideLayout, QuoteSchema, QuoteId, QuoteName, QuoteDesc, "general", "QuoteSlideLayout"),
createTemplateEntry(TableInfoSlideLayout, TableInfoSchema, TableInfoId, TableInfoName, TableInfoDesc, "general", "TableInfoSlideLayout"),
createTemplateEntry(TableOfContentsSlideLayout, TableOfContentsSchema, TableOfContentsId, TableOfContentsName, TableOfContentsDesc, "general", "TableOfContentsSlideLayout"),
createTemplateEntry(TeamSlideLayout, TeamSchema, TeamId, TeamName, TeamDesc, "general", "TeamSlideLayout"),
];
// Modern templates array
export const modernTemplates: TemplateWithData[] = [
createTemplateEntry(ModernIntroSlideLayout, ModernIntroSchema, ModernIntroId, ModernIntroName, ModernIntroDesc, "modern", "IntroSlideLayout"),
createTemplateEntry(BulletsWithIconsDescriptionGrid, BulletsIconsGridSchema, BulletsIconsGridId, BulletsIconsGridName, BulletsIconsGridDesc, "modern", "BulletsWithIconsDescriptionGrid"),
createTemplateEntry(ModernBulletWithIconsSlideLayout, ModernBulletIconsSchema, ModernBulletIconsId, ModernBulletIconsName, ModernBulletIconsDesc, "modern", "BulletWithIconsSlideLayout"),
createTemplateEntry(ChartOrTableWithDescription, ChartTableDescSchema, ChartTableDescId, ChartTableDescName, ChartTableDescDesc, "modern", "ChartOrTableWithDescription"),
createTemplateEntry(ChartOrTableWithMetricsDescription, ChartMetricsSchema, ChartMetricsId, ChartMetricsName, ChartMetricsDesc, "modern", "ChartOrTableWithMetricsDescription"),
createTemplateEntry(ImageAndDescriptionLayout, ImageDescSchema, ImageDescId, ImageDescName, ImageDescDesc, "modern", "ImageAndDescriptionLayout"),
createTemplateEntry(ImageListWithDescriptionSlideLayout, ImageListDescSchema, ImageListDescId, ImageListDescName, ImageListDescDesc, "modern", "ImageListWithDescriptionSlideLayout"),
createTemplateEntry(ImagesWithDescriptionLayout, ImagesDescSchema, ImagesDescId, ImagesDescName, ImagesDescDesc, "modern", "ImagesWithDescriptionLayout"),
createTemplateEntry(MetricsWithDescription, MetricsDescSchema, MetricsDescId, MetricsDescName, MetricsDescDesc, "modern", "MetricsWithDescription"),
createTemplateEntry(ModernTableOfContentsLayout, ModernTocSchema, ModernTocId, ModernTocName, ModernTocDesc, "modern", "TableOfContentsLayout"),
];
// Standard templates array
export const standardTemplates: TemplateWithData[] = [
createTemplateEntry(StandardIntroSlideLayout, StandardIntroSchema, StandardIntroId, StandardIntroName, StandardIntroDesc, "standard", "IntroSlideLayout"),
createTemplateEntry(ChartLeftTextRightLayout, ChartLeftSchema, ChartLeftId, ChartLeftName, ChartLeftDesc, "standard", "ChartLeftTextRightLayout"),
createTemplateEntry(ContactLayout, ContactSchema, ContactId, ContactName, ContactDesc, "standard", "ContactLayout"),
createTemplateEntry(HeadingBulletImageDescriptionLayout, HeadingBulletSchema, HeadingBulletId, HeadingBulletName, HeadingBulletDesc, "standard", "HeadingBulletImageDescriptionLayout"),
createTemplateEntry(IconBulletDescriptionLayout, IconBulletSchema, IconBulletId, IconBulletName, IconBulletDesc, "standard", "IconBulletDescriptionLayout"),
createTemplateEntry(IconImageDescriptionLayout, IconImageSchema, IconImageId, IconImageName, IconImageDesc, "standard", "IconImageDescriptionLayout"),
createTemplateEntry(StandardImageListWithDescriptionLayout, StdImageListSchema, StdImageListId, StdImageListName, StdImageListDesc, "standard", "ImageListWithDescriptionLayout"),
createTemplateEntry(MetricsDescriptionLayout, MetricsDescLayoutSchema, MetricsDescLayoutId, MetricsDescLayoutName, MetricsDescLayoutDesc, "standard", "MetricsDescriptionLayout"),
createTemplateEntry(NumberedBulletSingleImageLayout, NumBulletImgSchema, NumBulletImgId, NumBulletImgName, NumBulletImgDesc, "standard", "NumberedBulletSingleImageLayout"),
createTemplateEntry(StandardTableOfContentsLayout, StdTocSchema, StdTocId, StdTocName, StdTocDesc, "standard", "TableOfContentsLayout"),
createTemplateEntry(VisualMetricsSlideLayout, VisualMetricsSchema, VisualMetricsId, VisualMetricsName, VisualMetricsDesc, "standard", "VisualMetricsSlideLayout"),
];
// Swift templates array
export const swiftTemplates: TemplateWithData[] = [
createTemplateEntry(SwiftIntroSlideLayout, SwiftIntroSchema, SwiftIntroId, SwiftIntroName, SwiftIntroDesc, "swift", "IntroSlideLayout"),
createTemplateEntry(BulletsWithIconsTitleDescription, BulletsIconsTitleSchema, BulletsIconsTitleId, BulletsIconsTitleName, BulletsIconsTitleDesc, "swift", "BulletsWithIconsTitleDescription"),
createTemplateEntry(IconBulletListDescription, IconBulletListSchema, IconBulletListId, IconBulletListName, IconBulletListDesc, "swift", "IconBulletListDescription"),
createTemplateEntry(ImageListDescription, ImageListSchema, ImageListId, ImageListName, ImageListDesc, "swift", "ImageListDescription"),
createTemplateEntry(MetricsNumbers, MetricsNumbersSchema, MetricsNumbersId, MetricsNumbersName, MetricsNumbersDesc, "swift", "MetricsNumbers"),
createTemplateEntry(SimpleBulletPointsLayout, SimpleBulletSchema, SimpleBulletId, SimpleBulletName, SimpleBulletDesc, "swift", "SimpleBulletPointsLayout"),
createTemplateEntry(SwiftTableOfContents, SwiftTocSchema, SwiftTocId, SwiftTocName, SwiftTocDesc, "swift", "TableOfContents"),
createTemplateEntry(TableorChart, TableChartSchema, TableChartId, TableChartName, TableChartDesc, "swift", "TableorChart"),
createTemplateEntry(Timeline, TimelineSchema, TimelineId, TimelineName, TimelineDesc, "swift", "Timeline"),
];
// TODO: Step 4: Combine all templates into a single array For UseCases (like the ones below)
// All templates combined
export const allLayouts: TemplateWithData[] = [
...neoGeneralTemplates,
...neoModernTemplates,
...neoStandardTemplates,
...neoSwiftTemplates,
...generalTemplates,
...modernTemplates,
...standardTemplates,
...swiftTemplates,
];
// TODO: Step 5: Combine all templates into a single array For UseCases (like the ones below)
// For UseCases we need to combine all templates into a single array with settings
export const templates: TemplateLayoutsWithSettings[] = [
{
id: "neo-general",
name: "Neo General",
description: neoGeneralSettings.description,
settings: neoGeneralSettings as TemplateGroupSettings,
layouts: neoGeneralTemplates,
},
{
id: "neo-standard",
name: "Neo Standard",
description: neoStandardSettings.description,
settings: neoStandardSettings as TemplateGroupSettings,
layouts: neoStandardTemplates,
},
{
id: "neo-modern",
name: "Neo Modern",
description: neoModernSettings.description,
settings: neoModernSettings as TemplateGroupSettings,
layouts: neoModernTemplates,
},
{
id: "neo-swift",
name: "Neo Swift",
description: neoSwiftSettings.description,
settings: neoSwiftSettings as TemplateGroupSettings,
layouts: neoSwiftTemplates,
},
{
id: "general",
name: "General",
description: generalSettings.description,
settings: generalSettings as TemplateGroupSettings,
layouts: generalTemplates,
},
{
id: "modern",
name: "Modern",
description: modernSettings.description,
settings: modernSettings as TemplateGroupSettings,
layouts: modernTemplates,
},
{
id: "standard",
name: "Standard",
description: standardSettings.description,
settings: standardSettings as TemplateGroupSettings,
layouts: standardTemplates,
},
{
id: "swift",
name: "Swift",
description: swiftSettings.description,
settings: swiftSettings as TemplateGroupSettings,
layouts: swiftTemplates,
},
];
// Helper to get templates by group ID
export function getTemplatesByTemplateName(templateId: string): TemplateWithData[] {
const template = templates.find((t) => t.id === templateId);
return template?.layouts || [];
}
export function getSchemaByTemplateId(templateId: string): any {
const template = templates.find((t) => t.id === templateId);
return template?.layouts.map(t => {
return {
id: t.layoutId,
name: t.layoutName,
description: t.layoutDescription,
json_schema: t.schemaJSON,
}
}) || {};
}
export function getSettingsByTemplateId(templateId: string): TemplateGroupSettings | undefined {
const template = templates.find((t) => t.id === templateId);
return template?.settings || undefined;
}
// Helper to get template by layout ID
export function getTemplateByLayoutId(layoutId: string): TemplateWithData | undefined {
return allLayouts.find((t) => t.layoutId === layoutId);
}
export function getLayoutByLayoutId(layout: string): TemplateWithData | undefined {
const templateName = layout.split(':')[0]
const template = templates.find((t) => t.id === templateName)
if (template) {
return template.layouts.find((t) => t.layoutId === layout);
}
return undefined;
}

View file

@ -1,13 +1,12 @@
import React from "react";
import * as z from "zod";
import { ImageSchema, IconSchema } from "@/presentation-templates/defaultSchemes";
import { ImageSchema, IconSchema } from "../defaultSchemes";
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
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({
export const layoutId = "bullet-with-icons";
export const layoutName = "Bullet With Icons Slide Layout";
export const layoutDescription = "Bullets with icons slide layout";
const bulletWithIconsSlideSchema = z.object({
title: z.string().min(3).max(20).default("Problem").meta({
description: "Main title of the problem statement slide",
}),
@ -44,7 +43,7 @@ const problemStatementSlideSchema = z.object({
"Businesses struggle to find digital tools that meet their needs, causing operational slowdowns.",
icon: {
__icon_url__:
"/static/icons/placeholder.svg",
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg",
__icon_query__: "warning alert inefficiency",
},
},
@ -54,7 +53,7 @@ const problemStatementSlideSchema = z.object({
"Outdated systems increase expenses, while small businesses struggle to expand their market reach.",
icon: {
__icon_url__:
"/static/icons/placeholder.svg",
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/fediverse-logo-bold.svg",
__icon_query__: "trending up costs chart",
},
},
@ -64,7 +63,7 @@ const problemStatementSlideSchema = z.object({
"Businesses struggle to find digital tools that meet their needs, causing operational slowdowns.",
icon: {
__icon_url__:
"/static/icons/placeholder.svg",
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/video-bold.svg",
__icon_query__: "warning alert inefficiency",
},
},
@ -74,7 +73,7 @@ const problemStatementSlideSchema = z.object({
"Businesses struggle to find digital tools that meet their needs, causing operational slowdowns.",
icon: {
__icon_url__:
"/static/icons/placeholder.svg",
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/users-four-bold.svg",
__icon_query__: "warning alert inefficiency",
},
},
@ -83,27 +82,23 @@ const problemStatementSlideSchema = z.object({
description:
"List of problem categories with titles, descriptions, and optional icons",
}),
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: "Today Date displayed in header",
}),
});
export const Schema = problemStatementSlideSchema;
export const Schema = bulletWithIconsSlideSchema;
export type ProblemStatementSlideData = z.infer<
typeof problemStatementSlideSchema
export type BulletWithIconsSlideData = z.infer<
typeof bulletWithIconsSlideSchema
>;
interface ProblemStatementSlideLayoutProps {
data?: Partial<ProblemStatementSlideData>;
interface BulletWithIconsSlideLayoutProps {
data?: Partial<BulletWithIconsSlideData>;
}
const ProblemStatementSlideLayout: React.FC<
ProblemStatementSlideLayoutProps
> = ({ data: slideData }) => {
const BulletWithIconsSlideLayout = ({
data: slideData,
}: BulletWithIconsSlideLayoutProps) => {
const problemCategories = slideData?.problemCategories || [];
return (
@ -115,27 +110,37 @@ const ProblemStatementSlideLayout: React.FC<
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-blue-600 relative z-20 mx-auto overflow-hidden text-white"
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "Montserrat, sans-serif",
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* 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>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</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-5xl font-bold text-white mb-8 leading-tight text-left">
<h2 className="text-5xl font-bold mb-8 leading-tight text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{slideData?.title}
</h2>
<div className="text-lg text-white leading-relaxed font-normal mb-12 max-w-lg text-left">
<div className="text-lg leading-relaxed font-normal mb-12 max-w-lg text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{slideData?.description}
</div>
</div>
@ -147,23 +152,27 @@ const ProblemStatementSlideLayout: React.FC<
{problemCategories.map((category, index) => (
<div
key={index}
className="flex items-start gap-5 bg-white bg-opacity-5 rounded-lg p-5"
className="flex items-start gap-5 rounded-lg p-5"
style={{ backgroundColor: 'var(--card-color, #F5F8FE)' }}
>
<div className="flex-shrink-0">
{category.icon?.__icon_url__ && (
<img
src={category.icon?.__icon_url__}
alt={category.icon?.__icon_query__}
<div className="flex-shrink-0 w-12 h-12 flex items-center justify-center">
{category.icon?.__icon_url__ ? (
<RemoteSvgIcon
url={category.icon.__icon_url__}
strokeColor={"var(--background-text, #234CD9)"}
className="w-12 h-12"
style={{ filter: "invert(1)" }}
color="var(--primary-color, #1E4CD9)"
title={category.icon.__icon_query__}
/>
) : (
<div className="w-12 h-12 rounded-full opacity-40" style={{ backgroundColor: 'var(--background-text, #111827)' }} />
)}
</div>
<div>
<h3 className="text-xl font-semibold text-white mb-1">
<h3 className="text-xl font-semibold mb-1" style={{ color: 'var(--background-text, #234CD9)' }}>
{category.title}
</h3>
<p className="text-sm text-blue-100 leading-relaxed max-w-md">
<p className="text-sm leading-relaxed max-w-md" style={{ color: 'var(--background-text, #234CD9)' }}>
{category.description}
</p>
</div>
@ -174,10 +183,10 @@ const ProblemStatementSlideLayout: React.FC<
</div>
{/* Bottom border line */}
<div className="absolute bottom-0 left-0 w-full h-1 bg-white"></div>
<div className="absolute bottom-0 left-0 w-full h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }}></div>
</div>
</>
);
};
export default ProblemStatementSlideLayout;
export default BulletWithIconsSlideLayout;

View file

@ -0,0 +1,196 @@
import React from "react";
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
import * as z from "zod";
import { IconSchema } from "../defaultSchemes";
export const layoutId = "bullet-with-icons-description-grid";
export const layoutName = "Bullet With Icons Description Grid";
export const layoutDescription =
"A bullet with icons description grid slide layout";
const bulletWithIconsDescriptionGridSlideSchema = z.object({
title: z.string().min(3).max(25).default("Businesses struggle").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 is here and what is hsd sdksdf klfdslkf lkflkfsldkf.",
)
.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(70).meta({
description: "Section description",
}),
icon: IconSchema.optional().meta({
description: "Icon for the section",
}),
}),
)
.min(2)
.max(6)
.default([
{
title: "Market",
description:
"Innovative and widely accepted. Innovative and widely accepted. Innovative and widely accepted.",
icon: {
__icon_query__: "market innovation",
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg",
},
},
{
title: "Industry",
description: "Based on sound market decisions.",
icon: {
__icon_query__: "industry building",
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/fediverse-logo-bold.svg",
},
},
{
title: "SEM",
description: "Driven by precise data and analysis.",
icon: {
__icon_query__: "SEM data analysis",
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/video-bold.svg",
},
},
{
title: "End User",
description: "Focused on real user impact.",
icon: {
__icon_query__: "end user impact",
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/users-four-bold.svg",
},
},
{
title: "Industry",
description: "Based on sound market decisions.",
icon: {
__icon_query__: "industry building",
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/fediverse-logo-bold.svg",
},
},
{
title: "SEM",
description: "Driven by precise data and analysis.",
icon: {
__icon_query__: "SEM data analysis",
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/video-bold.svg",
},
},
])
.meta({
description:
"List of solution sections with titles, descriptions, and optional icons",
}),
});
export const Schema = bulletWithIconsDescriptionGridSlideSchema;
export type BulletWithIconsDescriptionGridSlideData = z.infer<typeof bulletWithIconsDescriptionGridSlideSchema>;
interface BulletWithIconsDescriptionGridSlideLayoutProps {
data?: Partial<BulletWithIconsDescriptionGridSlideData>;
}
const BulletWithIconsDescriptionGridSlideLayout = ({
data: slideData,
}: BulletWithIconsDescriptionGridSlideLayoutProps) => {
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 aspect-video relative z-20 mx-auto overflow-hidden border-2 border-gray-800"
style={{
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* Header */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</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-5xl font-bold mb-8 leading-tight text-left" style={{ color: 'var(--background-text, #1E4CD9)' }}>
{slideData?.title}
</h1>
<p className="text-lg leading-relaxed font-normal mb-12 max-w-lg text-left" style={{ color: 'var(--background-text, #334155)' }}>
{slideData?.mainDescription}
</p>
</div>
<div className="grid grid-cols-2 gap-4 w-full">
{sections.map((section, idx) => (
<div
key={idx}
className="flex flex-col items-center text-center rounded-lg shadow px-3 py-4 "
style={{ backgroundColor: 'var(--card-color, #F5F8FE)' }}
>
<div className="mb-2">
{section?.icon?.__icon_url__ && (
<RemoteSvgIcon
url={section.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-12 h-12 mb-2"
color="var(--primary-color, #1E4CD9)"
title={section.icon.__icon_query__}
/>
)}
</div>
<h2 className="text-lg font-semibold mb-1" style={{ color: 'var(--background-text, #234CD9)' }}>
{section.title}
</h2>
<div className="w-8 h-1 mb-2" style={{ backgroundColor: 'var(--primary-color, #234CD9)' }}></div>
<p className="text-xs leading-snug" style={{ color: 'var(--background-text, #234CD9)' }}>
{section.description}
</p>
</div>
))}
</div>
</div>
{/* Bottom Border */}
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }}></div>
</div>
</>
);
};
export default BulletWithIconsDescriptionGridSlideLayout;

View file

@ -0,0 +1,286 @@
import React from "react";
import * as z from "zod";
import {
BarChart,
Bar,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
Legend,
LineChart,
Line,
PieChart,
Pie,
Cell,
} from "recharts";
export const layoutId = "chart-or-table-with-description";
export const layoutName = "Chart or Table With Description";
export const layoutDescription =
"Chart with description slide layout";
const businessModelSchema = z
.object({
title: z.string().min(3).max(60).default("Data Table or Chart"),
description: z
.string()
.default(
"Present structured information in a flexible table or visualize it with a chart.",
)
.meta({
description: "Supporting description for the table/chart",
}),
mode: z.enum(["table", "chart"]).default("chart"),
// Table configuration (generic)
columns: z
.array(z.string().min(1).max(40))
.min(2)
.max(10)
.default(["Column 1", "Column 2", "Column 3"]),
rows: z
.array(
z.object({
cells: z
.array(z.string().min(0).max(200))
.min(2)
.max(10)
.default(["Row 1", "Value", "Value"]),
}),
)
.min(1)
.max(30)
.default([
{ cells: ["Row A", "✓", "-"] },
{ cells: ["Row B", "Text", "123"] },
{ cells: ["Row C", "More text", "456"] },
]),
// Chart configuration (parity with Swift TableorChart)
chart: z
.object({
type: z.enum(["bar", "horizontalBar", "line", "pie"]).default("line"),
data: z
.array(z.object({ label: z.string().min(1).max(12), value: z.number() }))
.min(3)
.max(12)
.default([
{ label: "A", value: 60 },
{ label: "B", value: 42 },
{ label: "C", value: 75 },
{ label: "D", value: 30 },
]),
showLabels: z.boolean().default(true),
})
.default({
type: "line",
data: [
{ label: "A", value: 60 },
{ label: "B", value: 42 },
{ label: "C", value: 75 },
{ label: "D", value: 30 },
],
showLabels: true,
}),
})
.default({
title: "Data Table or Chart",
description:
"Present structured information in a flexible table or visualize it with a chart.",
mode: "table",
columns: ["Column 1", "Column 2", "Column 3"],
rows: [
{ cells: ["Row A", "✓", "-"] },
{ cells: ["Row B", "Text", "123"] },
{ cells: ["Row C", "More text", "456"] },
],
chart: {
type: "line",
data: [
{ label: "A", value: 60 },
{ label: "B", value: 42 },
{ label: "C", value: 75 },
{ label: "D", value: 30 },
],
showLabels: true,
},
});
const CHART_COLORS = [
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
];
export const Schema = businessModelSchema;
export type BusinessModelData = z.infer<typeof businessModelSchema>;
interface Props {
data?: Partial<BusinessModelData>;
}
const BusinessModelSlide: React.FC<Props> = ({ data }) => {
const mode = data?.mode || "table";
const columns = data?.columns || [];
const rows = data?.rows || [];
const cData = data?.chart?.data || [];
const type = data?.chart?.type || "bar";
const showLabels = data?.chart?.showLabels !== false;
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 12, fontWeight: 600 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
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 mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* Header */}
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(data as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="px-16 py-16 flex h-full gap-8">
{/* Left Column - Title and description */}
<div className="flex-1 pr-12 flex flex-col justify-center">
<h1 className="text-5xl font-bold mb-4 leading-tight text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{data?.title}
</h1>
<p className="text-base leading-relaxed font-normal max-w-xl text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{data?.description}
</p>
</div>
{/* Right Column - Table or Chart (based on mode) */}
<div className="flex flex-col items-start justify-center w-[52%] gap-8">
{mode === "table" ? (
<div className="w-full">
<div className="rounded-lg border" style={{ borderColor: 'var(--stroke, rgba(0,0,0,0.08))' }}>
<table className="w-full border-separate border-spacing-0">
<thead>
<tr>
{columns.map((col, idx) => (
<th key={idx} className="text-left text-sm font-semibold px-4 py-3 border-b" style={{ borderColor: 'var(--stroke, rgba(0,0,0,0.12))', color: 'var(--primary-color, #1E4CD9)' }}>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rIdx) => (
<tr key={rIdx} className="align-top">
{columns.map((_, cIdx) => (
<td key={cIdx} className="text-sm px-4 py-3 border-t" style={{ borderColor: 'var(--stroke, rgba(0,0,0,0.08))', color: 'var(--background-text, #334155)' }}>
{row.cells[cIdx] || ''}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="w-full">
<div className="bg-white rounded-lg shadow p-4"
style={{ backgroundColor: 'var(--card-color, #F5F8FE)' }}
>
<div className="w-full h-72">
<ResponsiveContainer width="100%" height="100%">
{type === "bar" ? (
<BarChart data={cData} margin={{ top: 20, right: 20, left: 0, bottom: 10 }} barCategoryGap="30%">
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, #E5E7EB)`} />
<XAxis dataKey="label" {...axisProps} />
<YAxis {...axisProps} />
<Tooltip />
<Legend />
<Bar dataKey="value" fill={CHART_COLORS[0]} radius={[8, 8, 0, 0]} label={showLabels ? { position: 'top', fill: 'var(--background-text, #111827)', fontSize: 12 } : false} >
{cData.map((_, i) => (
<Cell key={i} fill={`var(--graph-${i}, ${CHART_COLORS[i % CHART_COLORS.length]})`} />
))}
</Bar>
</BarChart>
) : type === "horizontalBar" ? (
<BarChart data={cData} layout="vertical" margin={{ top: 10, right: 20, bottom: 10, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, #E5E7EB)`} />
<XAxis type="number" {...axisProps} />
<YAxis type="category" dataKey="label" {...axisProps} />
<Tooltip />
<Legend />
<Bar dataKey="value" fill={CHART_COLORS[0]} radius={[0, 6, 6, 0]} label={showLabels ? { position: 'right', fill: 'var(--background-text, #111827)', fontSize: 12 } : false} >
{cData.map((_, i) => (
<Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
))}
</Bar>
</BarChart>
) : type === "line" ? (
<LineChart data={cData} margin={{ top: 10, right: 20, bottom: 10, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, #E5E7EB)`} />
<XAxis dataKey="label" {...axisProps} />
<YAxis {...axisProps} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="value" strokeWidth={3} dot={{ r: 4, color: CHART_COLORS[0] }} label={showLabels ? { position: 'top', fill: 'var(--background-text, #111827)', fontSize: 12 } : false} >
{cData.map((_, i) => (
<Cell key={i} fill={`var(--graph-${i}, ${CHART_COLORS[i % CHART_COLORS.length]})`} />
))}
</Line>
</LineChart>
) : (
<PieChart >
<Tooltip />
<Legend />
<Pie data={cData} dataKey="value" nameKey="label" cx="50%" cy="50%" outerRadius={100} label={showLabels}>
{cData.map((_, i) => (
<Cell key={i} fill={`var(--graph-${i}, ${CHART_COLORS[i % CHART_COLORS.length]})`} />
))}
</Pie>
</PieChart>
)}
</ResponsiveContainer>
</div>
</div>
</div>
)}
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }} />
</div>
</>
);
};
export default BusinessModelSlide;

View file

@ -11,10 +11,10 @@ import {
} from "recharts";
import * as z from "zod";
export const layoutId = "company-traction-slide";
export const layoutName = "Company Traction Slide";
export const layoutId = "chart-with-metrics";
export const layoutName = "Chart With Metrics 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.";
"A chart or table with metrics slide layout";
const growthStatsSchema = z
.object({
@ -28,12 +28,8 @@ const growthStatsSchema = z
// growthStats: list of dicts, each dict is { year: string, <metric1>: number, <metric2>: number, ... }
const tractionSchema = 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: "Today Date displayed in header",
}),
title: z.string().default("Company Traction").meta({
description: "Main title of the slide",
}),
@ -48,6 +44,9 @@ const tractionSchema = z.object({
description:
"Main content text describing the company's traction and growth momentum.",
}),
tableMode: z.boolean().default(false),
tableColumns: z.array(z.string().min(1).max(40)).min(2).max(10).default(["Metric", "Value"]),
tableRows: z.array(z.array(z.string().min(0).max(200)).min(2).max(10)).min(1).max(30).default([["Users", "10K+"], ["Revenue", "$1.2M"], ["Satisfaction", "95%"]]),
// growthStats is a list of objects, each with a 'year' and any number of metric keys (all numbers)
growthStats: z
.array(growthStatsSchema)
@ -181,49 +180,60 @@ const CompanyTractionSlideLayout: React.FC<Props> = ({ data }) => {
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"
className="w-full max-w-[1280px] max-h-[720px] aspect-video mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "Montserrat, sans-serif",
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* 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>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(data as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</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-6xl font-bold text-blue-600 mb-4 leading-tight text-left">
<h1 className="text-5xl font-bold mb-4 leading-tight text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{data?.title}
</h1>
<div className="bg-white rounded-lg shadow p-4 mb-8">
<div className=" rounded-lg shadow p-4 mb-8"
style={{ backgroundColor: 'var(--card-color, #ffffff)' }}
>
<div className="w-full h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={growthStats}>
<CartesianGrid stroke="#e5eafe" />
<LineChart data={growthStats} margin={{ top: 10, right: 20, left: 0, bottom: 10 }}>
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, #E5E7EB)`} />
<XAxis
dataKey="year"
stroke="#1E4CD9"
tick={{ fill: "#1E4CD9", fontWeight: 600 }}
stroke="var(--background-text, #234CD9)"
tick={{ fill: "var(--background-text, #234CD9)", fontSize: 12, fontWeight: 600 }}
/>
<YAxis
stroke="#1E4CD9"
tick={{ fill: "#1E4CD9", fontWeight: 600 }}
stroke="var(--background-text, #234CD9)"
tick={{ fill: "var(--background-text, #234CD9)", fontSize: 12, fontWeight: 600 }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1E4CD9",
backgroundColor: "var(--card-color, #234CD9)",
border: "none",
color: "#fff",
color: "var(--background-text, #ffffff)",
}}
labelStyle={{ color: "#fff" }}
itemStyle={{ color: "#fff" }}
/>
<Legend
wrapperStyle={{ color: "#1E4CD9", fontWeight: 600 }}
wrapperStyle={{ color: "var(--background-text, #234CD9)", fontSize: 12, fontWeight: 600 }}
iconType="circle"
/>
{seriesKeys.map((key, idx) => (
@ -231,18 +241,18 @@ const CompanyTractionSlideLayout: React.FC<Props> = ({ data }) => {
key={key}
type="monotone"
dataKey={key}
stroke={defaultColors[idx % defaultColors.length]}
stroke={`var(--graph-${idx}, ${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],
fill: `var(--graph-${idx}, ${defaultColors[idx % defaultColors.length]})`,
}}
activeDot={{
r: 6,
fill: defaultColors[idx % defaultColors.length],
fill: `var(--graph-${idx}, ${defaultColors[idx % defaultColors.length]})`,
}}
/>
))}
@ -252,33 +262,63 @@ const CompanyTractionSlideLayout: React.FC<Props> = ({ data }) => {
</div>
</div>
{/* Right Column - Description and Stats */}
{/* Right Column - Description and Stats or Table */}
<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">
<p className="text-base leading-relaxed font-normal mb-6 max-w-xl text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{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>
{data?.tableMode ? (
<div className="w-full">
<div className="rounded-lg ring-1" style={{ borderColor: 'var(--secondary-accent-color, rgba(0,0,0,0.08))' }}>
<table className="w-full border-separate border-spacing-0">
<thead>
<tr>
{data.tableColumns?.map((col, idx) => (
<th key={idx} className="text-left text-sm font-semibold px-4 py-3 border-b" style={{ borderColor: 'var(--stroke, rgba(0,0,0,0.12))', color: 'var(--primary-color, #1E4CD9)' }}>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{data.tableRows?.map((row, rIdx) => (
<tr key={rIdx} className="align-top">
{row.map((cell, cIdx) => (
<td key={cIdx} className="text-sm px-4 py-3 border-t" style={{ borderColor: 'var(--stroke, rgba(0,0,0,0.08))', color: 'var(--background-text, #334155)' }}>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
</div>
) : (
<div className="flex flex-row w-full gap-6">
{stats.map((stat, index) => (
<div
key={index}
className="flex-1 rounded-lg shadow-sm px-5 py-4 flex flex-col items-start"
style={{ backgroundColor: 'var(--primary-color, #F5F8FE)' }}
>
<div className="text-white text-xs font-semibold px-3 py-1 rounded-sm mb-2" style={{ backgroundColor: 'var(--card-color, #234CD9)', color: 'var(--background-text, #ffffff)' }}>
{stat.label}
</div>
<div className="text-2xl font-bold mb-1" style={{ color: 'var(--primary-text, #234CD9)' }}>
{stat.value}
</div>
<p className="text-sm leading-snug" style={{ color: 'var(--primary-text, #234CD9)' }}>
{stat.description}
</p>
</div>
))}
</div>
)}
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600" />
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }} />
</div>
</>
);

View file

@ -1,47 +1,46 @@
import React from "react";
import * as z from "zod";
import { ImageSchema, IconSchema } from "@/presentation-templates/defaultSchemes";
import { ImageSchema } from "../defaultSchemes";
export const layoutId = "about-company-slide";
export const layoutName = "About Our Company Slide";
export const layoutId = "image-and-description";
export const layoutName = "Image And Description";
export const layoutDescription =
"A slide layout providing an overview of the company, its background, and key information.";
"A slide layout with a title, a description, and an image.";
const aboutCompanySlideSchema = z.object({
title: z.string().min(3).max(30).default("About Our Company").meta({
const imageWithDescriptionSlideSchema = z.object({
title: z.string().min(3).max(30).default("Image With Description").meta({
description: "Main title of the slide",
}),
content: z
.string()
.min(25)
.max(400)
.max(300)
.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: "Today Date displayed in header",
}),
image: ImageSchema.optional().meta({
image: ImageSchema.default({
__image_url__:
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600&auto=format&fit=crop",
__image_prompt__: "Abstract business background",
}).meta({
description:
"Optional supporting image for the slide (building, office, etc.)",
}),
});
export const Schema = aboutCompanySlideSchema;
export const Schema = imageWithDescriptionSlideSchema;
export type AboutCompanySlideData = z.infer<typeof aboutCompanySlideSchema>;
export type ImageWithDescriptionSlideData = z.infer<typeof imageWithDescriptionSlideSchema>;
interface AboutCompanySlideLayoutProps {
data?: Partial<AboutCompanySlideData>;
interface ImageWithDescriptionSlideLayoutProps {
data?: Partial<ImageWithDescriptionSlideData>;
}
const AboutCompanySlideLayout: React.FC<AboutCompanySlideLayoutProps> = ({
const ImageWithDescriptionSlideLayout: React.FC<ImageWithDescriptionSlideLayoutProps> = ({
data: slideData,
}) => {
return (
@ -53,16 +52,26 @@ const AboutCompanySlideLayout: React.FC<AboutCompanySlideLayoutProps> = ({
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg aspect-video bg-white relative z-20 mx-auto overflow-hidden"
className="w-full rounded-sm max-w-[1280px] shadow-lg aspect-video relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "Montserrat, sans-serif",
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* 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>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main content area */}
<div className="flex h-full px-16 pb-16">
@ -123,13 +132,13 @@ const AboutCompanySlideLayout: React.FC<AboutCompanySlideLayoutProps> = ({
{/* Right side - Content */}
<div className="flex-1 pl-16 flex flex-col justify-center">
{slideData?.title && (
<h2 className="text-6xl font-bold text-blue-600 mb-12 leading-tight">
<h2 className="text-5xl font-bold mb-12 leading-tight" style={{ color: 'var(--background-text, #1E4CD9)' }}>
{slideData?.title}
</h2>
)}
{slideData?.content && (
<div className="text-lg text-blue-600 leading-relaxed font-normal max-w-lg">
<div className="text-lg leading-relaxed font-normal max-w-lg" style={{ color: 'var(--background-text, #334155)' }}>
{slideData?.content}
</div>
)}
@ -140,4 +149,4 @@ const AboutCompanySlideLayout: React.FC<AboutCompanySlideLayoutProps> = ({
);
};
export default AboutCompanySlideLayout;
export default ImageWithDescriptionSlideLayout;

View file

@ -0,0 +1,193 @@
import React from "react";
import * as z from "zod";
import { ImageSchema } from "../defaultSchemes";
export const layoutId = "image-list-with-description";
export const layoutName = "Image List with Description";
export const layoutDescription =
"An image list with description slide layout";
const imageListWithDescriptionSlideSchema = z.object({
title: z.string().min(3).max(40).default("Product Overview").meta({
description: "Main title of the slide. Max 4 words",
}),
// removed mainDescription
products: z
.array(
z.object({
title: z.string().min(3).max(50).meta({
description: "Product title",
}),
description: z.string().min(30).max(100).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(1)
.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: "Analytics Dashboard",
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-1556157382-97eda2d62296?w=300&h=200&fit=crop",
__image_prompt__: "Analytics dashboard on laptop screen",
},
isBlueBackground: true,
},
{
title: "Mobile App Suite",
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-1511707171634-5f897ff02aa9?w=300&h=200&fit=crop",
__image_prompt__: "Mobile apps on smartphone in hand",
},
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 = imageListWithDescriptionSlideSchema;
export type ImageListWithDescriptionSlideData = z.infer<
typeof imageListWithDescriptionSlideSchema
>;
interface ImageListWithDescriptionSlideLayoutProps {
data?: Partial<ImageListWithDescriptionSlideData>;
}
const ImageListWithDescriptionSlideLayout: React.FC<ImageListWithDescriptionSlideLayoutProps> = ({
data: slideData,
}) => {
const products = slideData?.products || [];
// Make the product boxes smaller
const PRODUCT_BOX_HEIGHT = 340; // px (reduced height)
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 relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* Header */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="flex h-full px-16 pb-16">
{/* Title at the top */}
<div className="absolute top-20 left-16 right-16">
<h1 className="text-5xl font-bold leading-tight text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{slideData?.title}
</h1>
</div>
{/* Product Row centered (up to 4 items) */}
<div className="flex flex-row gap-8 justify-center w-full mt-56">
{products.slice(0, 4).map((prod, idx) => (
<div
key={idx}
className="flex flex-col items-stretch"
style={{ width: `${PRODUCT_BOX_WIDTH + 40}px`, height: `${PRODUCT_BOX_HEIGHT + 60}px` }}
>
{/* Alternate layout per column: even -> text first; odd -> image first */}
{idx % 2 === 0 ? (
<>
<div
className={`p-5 flex flex-col justify-center text-center rounded-t-md`}
style={{ height: `${TEXT_SECTION_HEIGHT + 32}px`, backgroundColor: 'var(--card-color, #F5F8FE)', color: 'var(--background-text, #234CD9)' }}
>
<h2 className={`text-xl font-semibold mb-3`}>{prod.title}</h2>
<p className={`text-sm leading-relaxed`}>{prod.description}</p>
</div>
<div className="rounded-b-md overflow-hidden" style={{ height: `${IMAGE_SECTION_HEIGHT + 28}px` }}>
<img src={prod.image.__image_url__} alt={prod.image.__image_prompt__ || prod.title} className="w-full h-full object-cover" />
</div>
</>
) : (
<>
<div className="rounded-t-md overflow-hidden" style={{ height: `${IMAGE_SECTION_HEIGHT + 28}px` }}>
<img src={prod.image.__image_url__} alt={prod.image.__image_prompt__ || prod.title} className="w-full h-full object-cover" />
</div>
<div
className={`p-5 flex flex-col justify-center text-center rounded-b-md`}
style={{ height: `${TEXT_SECTION_HEIGHT + 32}px`, backgroundColor: 'var(--card-color, #F5F8FE)', color: 'var(--background-text, #234CD9)' }}
>
<h2 className={`text-xl font-semibold mb-3`}>{prod.title}</h2>
<p className={`text-sm leading-relaxed`}>{prod.description}</p>
</div>
</>
)}
</div>
))}
</div>
</div>
{/* Bottom Border */}
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }}></div>
</div>
</>
);
};
export default ImageListWithDescriptionSlideLayout;

View file

@ -1,21 +1,18 @@
import React from "react";
import * as z from "zod";
import { ImageSchema } from "@/presentation-templates/defaultSchemes";
import { ImageSchema } from "../defaultSchemes";
export const layoutId = "modern-team-slide";
export const layoutName = "Modern Team Slide";
export const layoutId = "images-with-description";
export const layoutName = "Images With Description";
export const layoutDescription =
"A clean modern team slide showcasing team members with professional profiles and blue-white design.";
"Images with description slide layout";
const teamMemberSchema = z.object({
const imagesWithDescriptionSlideSchema = 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: "Card title",
}),
description: z.string().min(20).max(120).meta({
description: "Brief professional description of the team member",
description: "Short description for the card",
}),
image: ImageSchema,
linkedIn: z.string().optional().meta({
@ -23,21 +20,20 @@ const teamMemberSchema = z.object({
}),
});
const modernTeamSlideSchema = z.object({
title: z.string().min(3).max(15).default("Our Team").meta({
const imagesWithDescriptionSlideSchema2 = 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)
.array(imagesWithDescriptionSlideSchema)
.min(2)
.max(3)
.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: {
@ -48,7 +44,6 @@ const modernTeamSlideSchema = z.object({
},
{
name: "Michael Chen",
position: "CTO",
description:
"Technology expert specializing in scalable architecture and AI solutions. PhD in Computer Science from MIT.",
image: {
@ -59,7 +54,6 @@ const modernTeamSlideSchema = z.object({
},
{
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: {
@ -70,7 +64,6 @@ const modernTeamSlideSchema = z.object({
},
{
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: {
@ -83,23 +76,19 @@ const modernTeamSlideSchema = z.object({
.meta({
description: "List of team members with their information",
}),
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: "Today Date displayed in header",
}),
});
export const Schema = modernTeamSlideSchema;
export const Schema = imagesWithDescriptionSlideSchema2;
export type ModernTeamSlideData = z.infer<typeof modernTeamSlideSchema>;
export type ImagesWithDescriptionSlideData = z.infer<typeof imagesWithDescriptionSlideSchema2>;
interface ModernTeamSlideLayoutProps {
data?: Partial<ModernTeamSlideData>;
interface ImagesWithDescriptionSlideLayoutProps {
data?: Partial<ImagesWithDescriptionSlideData>;
}
const ModernTeamSlideLayout: React.FC<ModernTeamSlideLayoutProps> = ({
const ImagesWithDescriptionSlideLayout: React.FC<ImagesWithDescriptionSlideLayoutProps> = ({
data: slideData,
}) => {
return (
@ -111,40 +100,54 @@ const ModernTeamSlideLayout: React.FC<ModernTeamSlideLayoutProps> = ({
/>
<div
className="w-full max-w-[1280px] max-h-[720px] aspect-video bg-white mx-auto rounded shadow-lg overflow-hidden relative z-20"
className="w-full max-w-[1280px] max-h-[720px] aspect-video mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "Montserrat, sans-serif",
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* 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>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="relative z-10 flex flex-col items-start justify-center h-full px-16 pt-24 pb-10">
<div className="relative z-10 flex flex-col items-start justify-center h-full px-16 pt-16 pb-8">
{/* Title */}
<h1
className="text-7xl font-bold text-blue-600 mb-4 leading-tight text-left"
style={{ letterSpacing: "-0.03em" }}
className="text-5xl font-bold mb-4 leading-tight text-left"
style={{ letterSpacing: "-0.03em", color: 'var(--background-text, #234CD9)' }}
>
{slideData?.title}
</h1>
{/* Subtitle */}
<p className="text-blue-600 text-lg leading-relaxed font-normal mb-12 max-w-lg text-left">
<p className="text-lg leading-relaxed font-normal mb-12 max-w-lg text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{slideData?.subtitle}
</p>
{/* Team Members Row */}
<div className="flex flex-row w-full justify-between items-start gap-6 mt-2">
{/* Items Row */}
<div className="flex flex-row w-full justify-between items-start gap-6 mt-1">
{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 }}
className="flex flex-col items-center rounded-lg shadow-md px-6 pt-6 pb-4 w-1/4 min-w-[260px] max-w-[280px] mx-auto"
style={{ minHeight: 380, backgroundColor: 'var(--card-color, #F5F8FE)' }}
>
{/* 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">
{/* Image full width */}
<div className="relative w-full h-40 mb-4 rounded-md overflow-hidden bg-white">
{member.image.__image_url__ && (
<img
src={member.image.__image_url__}
@ -154,15 +157,11 @@ const ModernTeamSlideLayout: React.FC<ModernTeamSlideLayoutProps> = ({
)}
</div>
{/* Name */}
<div className="text-lg font-bold text-blue-700 mb-1">
<div className="text-lg font-bold mb-1" style={{ color: 'var(--background-text, #234CD9)' }}>
{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]">
<div className="text-sm text-center mb-2 min-h-[48px]" style={{ color: 'var(--background-text, #234CD9)' }}>
{member.description}
</div>
{/* LinkedIn Link (if provided) */}
@ -171,7 +170,8 @@ const ModernTeamSlideLayout: React.FC<ModernTeamSlideLayoutProps> = ({
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"
className="inline-flex items-center text-xs transition-colors duration-200 mt-1"
style={{ color: 'var(--background-text, #234CD9)' }}
>
<svg
className="w-4 h-4 mr-1"
@ -192,10 +192,10 @@ const ModernTeamSlideLayout: React.FC<ModernTeamSlideLayoutProps> = ({
</div>
</div>
{/* Bottom Divider */}
<div className="absolute bottom-0 left-0 right-0 h-1 bg-blue-600" />
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }} />
</div>
</>
);
};
export default ModernTeamSlideLayout;
export default ImagesWithDescriptionSlideLayout;

View file

@ -0,0 +1,141 @@
import React from "react";
import * as z from "zod";
import { ImageSchema } from "../defaultSchemes";
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. This Slide is always the first slide in a pitch deck, setting the tone for the presentation with a clean and professional look.";
const introPitchDeckSchema = z.object({
title: z.string().min(2).max(15).default("Pitch Deck").meta({
description: "Main title of the slide",
}),
description: z
.string()
.min(1)
.max(200)
.default("Add a short subtitle or description here. Add a short subtitle or description here. Add a short subtitle or description here. Add a short subtitle or description here.")
.meta({
description: "Description shown below the title",
}),
introCard: z
.object({
enabled: z.boolean().default(true),
name: z.string().min(1).max(60).default("John Doe"),
date: z.string().min(1).max(60).default("December 2025"),
})
.default({ enabled: true, name: "John Doe", date: "December 2025" })
.meta({ description: "Optional intro card shown below description" }),
image: ImageSchema.default({
__image_url__:
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600&auto=format&fit=crop",
__image_prompt__: "Abstract business background",
}),
});
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: "var(--heading-font-family,Montserrat)",
backgroundColor: 'var(--background-color, #FFFFFF)',
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
{/* Top Header */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Title and Description (shifted slightly up) */}
<div
className="absolute left-10 right-[42%]"
style={{
top: "50%",
transform: "translateY(-50%)",
}}
>
{slideData?.title && (
<div className="relative inline-block">
<h1
className="text-5xl font-bold leading-none"
style={{ color: 'var(--background-text, #1E4CD9)' }}
id="pitchdeck-title"
>
{slideData?.title}
</h1>
<span
className="block h-[4px] absolute left-0"
style={{
width: "50%",
bottom: "-0.5em",
transition: "width 0.3s",
backgroundColor: 'var(--primary-color, #1E4CD9)'
}}
/>
</div>
)}
<p className="text-lg leading-relaxed font-normal mt-6 max-w-xl" style={{ color: 'var(--background-text, #234CD9)' }}>
{slideData?.description}
</p>
{slideData?.introCard?.enabled && (
<div className="mt-6 inline-flex items-center gap-4 rounded-lg px-5 py-4 shadow-sm min-w-[400px]" style={{ backgroundColor: 'var(--card-color, #FFFFFF)', border: '1px solid var(--stroke, #E5E7EB)' }}>
<div className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold" style={{ backgroundColor: 'var(--primary-color, #F5F8FE)', color: 'var(--primary-text, #234CD9)' }}>
{(slideData?.introCard?.name || "").split(" ").map(p => p.charAt(0)).join("").slice(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
<div className="text-[16px] font-semibold" style={{ color: 'var(--background-text, #234CD9)' }}>{slideData?.introCard?.name}</div>
<div className="text-[14px]" style={{ color: 'var(--background-text, #234CD9)' }}>{slideData?.introCard?.date}</div>
</div>
</div>
)}
</div>
{/* Right Image */}
{slideData?.image && slideData?.image?.__image_url__ && (
<div className="absolute top-16 bottom-16 right-10 w-[42%] rounded-md overflow-hidden">
<img
src={slideData?.image?.__image_url__}
alt={slideData?.image?.__image_prompt__ || slideData?.title || "intro-image"}
className="w-full h-full object-cover"
crossOrigin="anonymous"
referrerPolicy="no-referrer"
/>
</div>
)}
</div>
</>
);
};
export default IntroPitchDeckSlide;

View file

@ -1,22 +1,18 @@
import React from "react";
import * as z from "zod";
import { ImageSchema } from "@/presentation-templates/defaultSchemes";
import { ImageSchema } from "../defaultSchemes";
export const layoutId = "market-size-pitchdeck-slide";
export const layoutName = "Market Size Pitch Deck Slide";
export const layoutId = "metrics-with-description-image";
export const layoutName = "Metrics With Description and Image Slide Layout";
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.";
"Metrics with description slide layout with an image as whole for the slide";
const marketSizeSlideSchema = z.object({
title: z.string().min(3).max(15).default("Market Size").meta({
description: "Main slide title",
}),
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: "Today Date displayed in header",
}),
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
@ -31,7 +27,7 @@ const marketSizeSlideSchema = z.object({
}),
)
.min(1)
.max(3)
.max(4)
.default([
{
label: "Total Available Market (TAM)",
@ -46,11 +42,17 @@ const marketSizeSlideSchema = z.object({
"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",
label: "Total Available Market (TAM)",
value: "1.4 Billion",
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.",
"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.",
}
])
.meta({
description:
@ -87,16 +89,26 @@ const MarketSizeSlideLayout: React.FC<MarketSizeSlideProps> = ({
/>
<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"
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "Montserrat, sans-serif",
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: 'var(--background-color, #FFFFFF)'
}}
>
{/* 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>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="flex h-full px-16 pb-16">
@ -105,9 +117,8 @@ const MarketSizeSlideLayout: React.FC<MarketSizeSlideProps> = ({
<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-6xl font-bold text-blue-600 mb-8 leading-tight text-left"
style={{ marginTop: "112px" }} // 112px matches top-36 (9rem) of stats
>
className="text-5xl font-bold mb-8 leading-tight text-left"
style={{ color: 'var(--background-text, #1E4CD9)' }}>
{slideData?.title || "Market Size"}
</h1>
<div className="w-full bg-[#CBE3CC] rounded-md mb-8 flex items-center justify-center">
@ -121,27 +132,27 @@ const MarketSizeSlideLayout: React.FC<MarketSizeSlideProps> = ({
)}
</div>
{slideData?.description && (
<p className="text-blue-600 text-sm leading-relaxed font-normal mb-12 max-w-lg text-left">
<p className="text-sm leading-relaxed font-normal mb-12 max-w-lg text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{slideData?.description}
</p>
)}
</div>
</div>
{/* Market Stats on the right */}
{/* Market Stats on the right - vertically centered */}
<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">
<div className="w-full 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">
<div className="text-white text-sm font-semibold px-3 py-1 inline-block rounded-sm" style={{ backgroundColor: 'var(--primary-color, #234CD9)', color: 'var(--primary-text, #ffffff)' }}>
<span className="text-sm">{stat.label}</span>
</div>
<div className="text-2xl font-bold text-[#1E4CD9]">
<div className="text-2xl font-bold" style={{ color: 'var(--primary-color, #1E4CD9)' }}>
{stat.value}
</div>
</div>
<p className="text-sm text-gray-700 leading-snug">
<p className="text-sm leading-snug" style={{ color: 'var(--background-text, #334155)' }}>
{stat.description}
</p>
</div>

View file

@ -0,0 +1,112 @@
import React from "react";
import * as z from "zod";
export const layoutId = "table-of-contents";
export const layoutName = "Table Of Contents";
export const layoutDescription =
"A clean table of contents layout with up to 10 items, each with a short description, styled to match the modern template.";
const TocItemSchema = z.object({
title: z.string().min(3).max(40).meta({
description: "Item title (short)",
}),
description: z.string().min(10).max(80).meta({
description: "Short item description",
}),
});
const tableOfContentsSchema = z.object({
title: z.string().min(3).max(40).default("Table Of Contents").meta({
description: "Main title displayed at the top",
}),
items: z
.array(TocItemSchema)
.min(2)
.max(10)
.default(
Array.from({ length: 10 }).map((_, i) => ({
title: `Section ${i + 1}`,
description: "Brief description for this section.",
}))
)
.meta({ description: "List of up to 10 TOC items" }),
});
export const Schema = tableOfContentsSchema;
export type TableOfContentsData = z.infer<typeof tableOfContentsSchema>;
interface TableOfContentsLayoutProps {
data?: Partial<TableOfContentsData>;
}
const TableOfContentsLayout: React.FC<TableOfContentsLayoutProps> = ({
data: slideData,
}) => {
const items = slideData?.items?.slice(0, 10) || [];
return (
<>
{/* Import 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] max-h-[720px] aspect-video mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{ fontFamily: "var(--heading-font-family,Montserrat)", backgroundColor: 'var(--background-color, #FFFFFF)' }}
>
{/* Header */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(slideData as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Title */}
<div className="absolute top-20 left-16 right-16">
<h1 className="text-5xl font-bold leading-tight text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{slideData?.title}
</h1>
</div>
{/* TOC Grid */}
<div className="absolute left-16 right-16" style={{ top: "28%" }}>
<div className="grid grid-cols-2 gap-x-10 gap-y-5">
{items.map((item, idx) => (
<div key={idx} className="flex items-start gap-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full text-white flex items-center justify-center text-base font-bold" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)', color: 'var(--primary-text, #ffffff )' }}>
{idx + 1}
</div>
<div className="flex-1 rounded-md p-3" style={{ backgroundColor: 'var(--card-color, #F5F8FE)' }}>
<div className="text-lg font-semibold mb-1" style={{ color: 'var(--background-text, #1E4CD9)' }}>
{item.title}
</div>
<div className="text-sm leading-relaxed" style={{ color: 'var(--background-text, #234CD9)' }}>
{item.description}
</div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Divider */}
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }} />
</div>
</>
);
};
export default TableOfContentsLayout;

View file

@ -0,0 +1,212 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema, IconSchema } from '../defaultSchemes';
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
export const layoutId = 'bullet-icons-only-slide'
export const layoutName = 'Icon Bullet Grid With Image'
export const layoutDescription = 'A layout featuring a large left-aligned title with a 2-4 icon bullet point grid, each with circular icon badge, title, and optional subtitle. A rounded supporting image sits on the right.'
const bulletIconsOnlySlideSchema = z.object({
title: z.string().min(3).max(40).default('Solutions').meta({
description: "Heading text of the slide",
}),
image: ImageSchema.default({
__image_url__: 'https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80',
__image_prompt__: 'Business professionals collaborating and discussing solutions'
}).meta({
description: "URL of the supporting image",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(80).meta({
description: "Title text for the bullet point",
}),
subtitle: z.string().min(5).max(150).optional().meta({
description: "Subtitle text for the bullet point",
}),
icon: IconSchema,
})).min(2).max(3).default([
{
title: 'Custom Software',
subtitle: 'We create tailored software to optimize processes and boost efficiency.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/code-bold.svg',
__icon_query__: 'code software development'
}
},
{
title: 'Digital Consulting',
subtitle: 'Our consultants guide organizations in leveraging the latest technologies.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/users-four-bold.svg',
__icon_query__: 'users consulting team'
}
},
{
title: 'Support Services',
subtitle: 'We provide ongoing support to help businesses adapt and maintain performance.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/headphones-bold.svg',
__icon_query__: 'headphones support service'
}
},
{
title: 'Scalable Marketing',
subtitle: 'Our data-driven strategies help businesses expand their reach and engagement.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/code-bold.svg',
__icon_query__: 'trending up marketing growth'
}
}
]).meta({
description: "List of icon bullet points",
})
})
export const Schema = bulletIconsOnlySlideSchema
export type BulletIconsOnlySlideData = z.infer<typeof bulletIconsOnlySlideSchema>
interface BulletIconsOnlySlideLayoutProps {
data?: Partial<BulletIconsOnlySlideData>
}
const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({ data: slideData }) => {
const bulletPoints = slideData?.bulletPoints || []
// Function to determine grid classes based on number of bullets
const getGridClasses = (count: number) => {
if (count <= 2) {
return 'grid-cols-1 gap-6'
} else if (count <= 4) {
return 'grid-cols-2 gap-6'
} else {
return 'grid-cols-2 lg:grid-cols-3 gap-6'
}
}
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: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Decorative Wave Patterns */}
<div className="absolute top-0 left-0 w-32 h-full opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 100 400" fill="none">
<path d="M0 100C25 150 50 50 75 100C87.5 125 100 100 100 100V0H0V100Z" fill="#8b5cf6" opacity="0.4" />
<path d="M0 200C37.5 250 62.5 150 100 200V150C75 175 50 150 25 175L0 200Z" fill="#8b5cf6" opacity="0.3" />
</svg>
</div>
<div className="absolute bottom-0 left-0 w-48 h-32 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 100" fill="none">
<path d="M0 50C50 25 100 75 150 50C175 37.5 200 50 200 50V100H0V50Z" fill="#8b5cf6" opacity="0.2" />
</svg>
</div>
{/* Main Content */}
<div className="relative z-10 flex h-full px-8 sm:px-12 lg:px-20 pt-12 pb-8">
{/* Left Section - Title and Bullet Points */}
<div className="flex-1 flex flex-col pr-8">
{/* Title */}
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-[42.7px] font-bold text-gray-900 mb-8">
{slideData?.title || 'Solutions'}
</h1>
{/* Bullet Points Grid */}
<div className={`grid ${getGridClasses(bulletPoints.length)} flex-1 content-center`}>
{bulletPoints.map((bullet, index) => (
<div
key={index}
className={`flex items-start space-x-4 p-4 rounded-lg`}
>
{/* Icon */}
<div style={{ background: "var(--primary-color,#9333ea)" }} className="flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center">
<RemoteSvgIcon
url={bullet.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-6 h-6"
color="var(--primary-text,#ffffff)"
title={bullet.icon.__icon_query__}
/>
</div>
{/* Content */}
<div className="flex-1">
<h3 style={{ color: "var(--background-text,#111827)" }} className="text-lg sm:text-xl font-semibold text-gray-900 mb-1">
{bullet.title}
</h3>
{bullet.subtitle && (
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-sm text-gray-700 leading-relaxed">
{bullet.subtitle}
</p>
)}
</div>
</div>
))}
</div>
</div>
{/* Right Section - Image */}
<div className="flex-shrink-0 w-96 flex items-center justify-center relative">
{/* Decorative Elements */}
<div style={{ color: "var(--primary-color,#9333ea)" }} className="absolute top-8 right-8 text-purple-600 opacity-60">
<svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor">
<path d="M16 0l4.12 8.38L28 12l-7.88 3.62L16 24l-4.12-8.38L4 12l7.88-3.62L16 0z" />
</svg>
</div>
<div className="absolute top-16 left-8 opacity-20">
<svg width="80" height="20" viewBox="0 0 80 20" className="text-purple-600" style={{ color: "var(--primary-color,#9333ea)" }}>
<path
d="M0 10 Q20 0 40 10 T80 10"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
</div>
{/* Main Image */}
<div className="w-full h-80 rounded-2xl overflow-hidden shadow-lg">
<img
src={slideData?.image?.__image_url__ || ''}
alt={slideData?.image?.__image_prompt__ || slideData?.title || ''}
className="w-full h-full object-cover"
/>
</div>
</div>
</div>
</div>
</>
)
}
export default BulletIconsOnlySlideLayout

View file

@ -0,0 +1,182 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema, IconSchema } from '../defaultSchemes';
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
export const layoutId = 'bullet-with-icons-slide'
export const layoutName = 'Image With Icon Bullets'
export const layoutDescription = 'A two-section layout with a full-width title, left-side image with decorative grid pattern, and right-side content featuring description text and 1-3 icon-enhanced bullet points. Each bullet has an icon badge, title, accent line, and description.'
const bulletWithIconsSlideSchema = z.object({
title: z.string().min(3).max(40).default('Problem').meta({
description: "Heading text of the slide",
}),
description: z.string().max(150).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
description: "Supporting description text",
}),
image: ImageSchema.default({
__image_url__: 'https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80',
__image_prompt__: 'Business people analyzing documents and charts in office'
}).meta({
description: "URL of the supporting image",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(60).meta({
description: "Title text for the bullet point",
}),
description: z.string().min(10).max(100).meta({
description: "Description text for the bullet point",
}),
icon: IconSchema,
})).min(1).max(3).default([
{
title: 'Inefficiency',
description: 'Businesses struggle to find digital tools that meet their needs, causing operational slowdowns.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg',
__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://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/fediverse-logo-bold.svg',
__icon_query__: 'trending up costs chart'
}
}
]).meta({
description: "List of icon bullet points",
})
})
export const Schema = bulletWithIconsSlideSchema
export type BulletWithIconsSlideData = z.infer<typeof bulletWithIconsSlideSchema>
interface BulletWithIconsSlideLayoutProps {
data?: Partial<BulletWithIconsSlideData>
}
const BulletWithIconsSlideLayout: React.FC<BulletWithIconsSlideLayoutProps> = ({ data: slideData }) => {
const bulletPoints = slideData?.bulletPoints || []
return (
<>
<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-gradient-to-br from-gray-50 to-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="flex flex-col h-full px-8 sm:px-12 lg:px-20 pt-12 pb-8">
{/* Title Section - Full Width */}
<div className="mb-8">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-[42.7px] font-bold text-gray-900">
{slideData?.title || 'Problem'}
</h1>
</div>
{/* Content Container */}
<div className="flex flex-1">
{/* Left Section - Image with Grid Pattern */}
<div className="flex-1 relative">
{/* Grid Pattern Background */}
<div className="absolute top-0 left-0 w-full h-full">
<svg className="w-full h-full opacity-30" viewBox="0 0 200 200">
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="var(--primary-color,#9333ea)" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
{/* Image Container */}
<div className="relative z-10 h-full flex items-center justify-center p-4">
<div className="w-full max-w-md h-80 rounded-2xl overflow-hidden shadow-lg">
<img
src={slideData?.image?.__image_url__ || ''}
alt={slideData?.image?.__image_prompt__ || slideData?.title || ''}
className="w-full h-full object-cover"
/>
</div>
</div>
{/* Decorative Sparkle */}
<div style={{ color: "var(--primary-color,#9333ea)" }} className="absolute top-20 right-8 text-purple-600">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0l3.09 6.26L22 9l-6.91 2.74L12 18l-3.09-6.26L2 9l6.91-2.74L12 0z" />
</svg>
</div>
</div>
{/* Right Section - Content */}
<div className="flex-1 flex flex-col justify-center pl-8 lg:pl-16">
{/* Description */}
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-lg text-gray-700 leading-relaxed mb-8">
{slideData?.description || 'Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.'}
</p>
{/* Bullet Points */}
<div className="space-y-6">
{bulletPoints.map((bullet, index) => (
<div key={index} className="flex items-start space-x-4">
{/* Icon */}
<div style={{ background: "var(--primary-color,#9333ea)" }} className="flex-shrink-0 w-12 h-12 rounded-lg shadow-md flex items-center justify-center">
<RemoteSvgIcon
url={bullet.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-6 h-6"
color="var(--primary-text,#ffffff)"
title={bullet.icon.__icon_query__}
/>
</div>
{/* Content */}
<div className="flex-1">
<h3 style={{ color: "var(--background-text,#111827)" }} className="text-xl font-semibold text-gray-900 mb-2">
{bullet.title}
</h3>
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-12 h-0.5 bg-purple-600 mb-3"></div>
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base text-gray-700 leading-relaxed">
{bullet.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default BulletWithIconsSlideLayout

View file

@ -0,0 +1,202 @@
/**
* Zod Schema for Table of Content Slide
* Defined based on the visual elements observed in the reference.
*/
import * as z from 'zod';
import React from 'react'
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
export const Schema = z.object({
title: z.string().max(30).describe('The main heading of the slide').default('Customer Proof / Case Snapshot'),
challengeSectionTitle: z.string().max(12).describe('Heading for the first content section').default('CHALLENGE'),
challengeContent: z.string().max(140).describe('Descriptive text for the first section').default('Fragmented marketing operations across 12 regions leading to inefficient spend allocation and inconsistent messaging. CAC increased 43% YoY.'),
outcomeSectionTitle: z.string().max(12).describe('Heading for the second content section').default('OUTCOME'),
outcomePoints: z.array(z.string().max(40)).min(1).max(5).describe('List of bullet points for the second section').default([
'34% reduction in CAC within 6 months',
'Unified operations across all regions',
'$4.2M additional pipeline generated'
]),
customerName: z.string().max(15).describe('Primary name or title in the card').default('TechCorp Global'),
customerSubTitle: z.string().max(26).describe('Subtitle or secondary text in the card').default('Fortune 500 Technology Company'),
metricValue: z.string().max(6).describe('The primary metric or statistic value').default('$4.2M'),
metricLabel: z.string().max(26).describe('Label describing the metric').default('incremental pipeline in Q4'),
metricIcon: z.object({
__icon_url__: z.string(),
__icon_query__: z.string().max(30),
}).describe('Icon displayed with the metric').default({
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg',
__icon_query__: 'circle',
}),
});
type FormData = z.infer<typeof Schema>;
export const layoutId = 'title-challenge-outcome-customer-card';
export const layoutName = 'Two Section Text With Highlight Card';
export const layoutDescription = 'A two-section layout featuring title with accent bar, first section with heading and description, numbered list in the second section on the left, and a highlight card on the right with name, subtitle, icon badge, and prominent metric.';
const dynamicSlideLayout: React.FC<{ data: Partial<FormData> }> = ({ data }) => {
const {
title,
challengeSectionTitle,
challengeContent,
outcomeSectionTitle,
outcomePoints,
customerName,
customerSubTitle,
metricValue,
metricLabel,
metricIcon,
} = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full h-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex flex-col p-[60px]"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Header */}
<div className="mb-8">
<h1 className="text-[42.7px] text-[#101828] font-bold leading-[56px] tracking-[-2px] mb-4"
style={{ color: 'var(--background-text, #111827)' }}
>
{title}
</h1>
<div className="w-[116.6px] h-[5.7px] "
style={{ background: 'var(--primary-color, #9234EB)' }}
/>
</div>
{/* Main Content Area */}
<div className="flex w-full h-full gap-10">
{/* Left Column */}
<div className="flex-[1.2] flex flex-col">
{/* Challenge Section */}
<div className=" mb-5">
<h2 className="text-[21.3px] font-normal mb-2 uppercase tracking-wide"
style={{
color: 'var(--background-text,#737373)'
}}
>
{challengeSectionTitle}
</h2>
<p className="text-[23.1px] font-normal leading-[32.3px]"
style={{
color: 'var(--background-text,#000000)'
}}
>
{challengeContent}
</p>
</div>
{/* Outcome Section */}
<div>
<h2 className="text-[21.3px] font-normal mb-2 uppercase tracking-wide"
style={{
color: 'var(--background-text,#737373)'
}}
>
{outcomeSectionTitle}
</h2>
<div className="flex flex-col gap-1">
{outcomePoints?.map((point, index) => (
<div key={index} className="flex text-[23.1px] font-normal leading-[32.3px]"
style={{
color: 'var(--background-text,#000000)'
}}
>
<span className="mr-2">{index + 1}.</span>
<span>{point}</span>
</div>
))}
</div>
</div>
</div>
{/* Right Column / Customer Card */}
<div className="flex-1 flex items-center justify-center">
<div className=" p-10 rounded-xl flex flex-col items-start min-w-[420px]"
style={{
backgroundColor: 'var(--card-color,#FFFFFF)',
}}
>
{/* Customer Info */}
<div className="mb-10">
<h3 className="text-[28.7px] font-bold leading-[40.2px]"
style={{
color: 'var(--background-text,#000000)'
}}
>
{customerName}
</h3>
<p className="text-[14.9px] font-normal"
style={{
color: 'var(--background-text,#000000)'
}}
>
{customerSubTitle}
</p>
</div>
{/* Metric Row */}
<div className="flex items-start gap-4">
{metricIcon?.__icon_url__ && <div
className="w-[56.7px] h-[56.7px] rounded-full flex items-center justify-center mt-4"
style={{ backgroundColor: 'var(--primary-color, #9134EB )' }}
>
<RemoteSvgIcon
url={metricIcon?.__icon_url__}
className="w-8 h-8 "
color="var(--primary-text,#ffffff)"
title={metricIcon?.__icon_query__}
/>
</div>}
<div className="flex flex-col">
<span className="text-[70.1px] text-[#4D5463] font-normal leading-[78.7px]"
style={{
color: 'var(--background-text,#4D5463)'
}}
>
{metricValue}
</span>
<span className="text-[17.4px] text-[#4D5463] font-normal leading-[22px] max-w-[180px]"
style={{
color: 'var(--background-text,#4D5463)'
}}
>
{metricLabel}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,582 @@
/**
* Chart with Bullets Slide Layout - Enhanced with multiple chart variations
*/
import React from 'react'
import * as z from "zod";
import { IconSchema } from '../defaultSchemes';
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
import {
BarChart, Bar, LineChart, Line, PieChart, Pie, AreaChart, Area, ScatterChart, Scatter,
XAxis, YAxis, CartesianGrid, Cell, ResponsiveContainer, Tooltip, Legend, LabelList, ReferenceLine
} from "recharts";
export const layoutId = 'chart-with-bullets-slide'
export const layoutName = 'Chart With Bullet Cards'
export const layoutDescription = 'A split layout with title, description, and a versatile chart on the left, paired with 1-3 colored icon bullet cards on the right. Supports bar, grouped, stacked, clustered, diverging, line, area, pie, and scatter charts.'
// Color palettes
const DEFAULT_CHART_COLORS = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899']
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
// Simple data for single series charts
const simpleDataSchema = z.object({
name: z.string().meta({ description: "Data point name" }),
value: z.number().meta({ description: "Data point value" }),
});
// Multi-series data
const multiSeriesDataSchema = z.object({
name: z.string().meta({ description: "Category name" }),
values: z.any().meta({ description: "Key-value pairs for each series (object with series names as keys and numbers as values)" }),
});
// Diverging data
const divergingDataSchema = z.object({
name: z.string().meta({ description: "Category name" }),
positive: z.number().meta({ description: "Positive value" }),
negative: z.number().meta({ description: "Negative value" }),
});
// Scatter data
const scatterDataSchema = z.object({
x: z.number().meta({ description: "X coordinate" }),
y: z.number().meta({ description: "Y coordinate" }),
});
const chartWithBulletsSlideSchema = z.object({
title: z.string().min(3).max(40).default('Market Size').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(150).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
description: "Description text below the title",
}),
chartData: z.object({
type: z.enum([
'bar',
'bar-horizontal',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter'
]).default('bar'),
data: z.union([
z.array(simpleDataSchema),
z.array(multiSeriesDataSchema),
z.array(divergingDataSchema),
z.array(scatterDataSchema),
]).default([
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
]),
series: z.array(z.string()).optional().meta({ description: "Series names for grouped/stacked charts" }),
divergingLabels: z.tuple([z.string(), z.string()]).optional(),
colorPalette: z.enum(['vibrant', 'ocean', 'professional']).default('vibrant'),
}).default({
type: 'bar',
data: [
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
],
colorPalette: 'vibrant',
}),
showLegend: z.boolean().default(false).meta({
description: "Whether to show chart legend",
}),
showTooltip: z.boolean().default(true).meta({
description: "Whether to show chart tooltip",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
description: z.string().min(10).max(150).meta({
description: "Bullet point description",
}),
icon: IconSchema,
})).min(1).max(3).default([
{
title: 'Total Addressable Market',
description: 'Companies can use TAM to plan future expansion and investment.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/chart-line-up-bold.svg',
__icon_query__: 'target market scope'
}
},
{
title: 'Serviceable Available Market',
description: 'Indicates more measurable market segments for sales efforts.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/chart-line-up-bold.svg',
__icon_query__: 'pie chart analysis'
}
},
{
title: 'Serviceable Obtainable Market',
description: 'Help companies plan development strategies according to the market.',
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/chart-line-up-bold.svg',
__icon_query__: 'trending up growth'
}
}
]).meta({
description: "List of bullet points with colored boxes and icons",
})
})
export const Schema = chartWithBulletsSlideSchema
export type ChartWithBulletsSlideData = z.infer<typeof chartWithBulletsSlideSchema>
interface ChartWithBulletsSlideLayoutProps {
data?: Partial<ChartWithBulletsSlideData>
}
// Transform multi-series data
const transformMultiSeriesData = (data: any[], series: string[]) => {
return data.map(item => {
const result: Record<string, any> = { name: item.name };
series.forEach(s => {
result[s] = item.values?.[s] ?? 0;
});
return result;
});
};
// Transform diverging data
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-xs font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }} >{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[10px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> = ({ data: slideData }) => {
const chartData = slideData?.chartData?.data || [];
const chartType = slideData?.chartData?.type || 'bar';
const series = slideData?.chartData?.series || [];
const showLegend = slideData?.showLegend || false;
const showTooltip = slideData?.showTooltip !== false;
const bulletPoints = slideData?.bulletPoints || [];
const divergingLabels = slideData?.chartData?.divergingLabels || ['Positive', 'Negative'];
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 10, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.7,
};
const graphColors = (index: number, serieColor?: string) => {
const fallback = serieColor || DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
const renderChart = () => {
const renderPieLabel = (props: any) => {
const { name, percent, x, y, textAnchor } = props;
if (percent < 0.08) return null;
return (
<text x={x} y={y} textAnchor={textAnchor} fill="var(--text-body-color,#4b5563)" fontSize={10}>
{`${name} ${(percent * 100).toFixed(0)}%`}
</text>
);
};
const commonProps = {
margin: { top: 15, right: 20, left: 0, bottom: 5 },
};
switch (chartType) {
case 'bar':
return (
<BarChart data={chartData as any[]} {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Bar dataKey="value" radius={[6, 6, 0, 0]}>
{(chartData as any[]).map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
);
case 'bar-horizontal':
return (
<BarChart data={chartData as any[]} layout="vertical" {...commonProps} height={400}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={60} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Bar dataKey="value" radius={[0, 6, 6, 0]}>
{(chartData as any[]).map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
);
case 'bar-grouped-vertical': {
const transformedData = transformMultiSeriesData(chartData as any[], series);
return (
<BarChart data={transformedData} {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[4, 4, 0, 0]} />
))}
</BarChart>
);
}
case 'bar-grouped-horizontal': {
const transformedData = transformMultiSeriesData(chartData as any[], series);
return (
<BarChart data={transformedData} layout="vertical" {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={60} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[0, 4, 4, 0]} />
))}
</BarChart>
);
}
case 'bar-stacked-vertical': {
const transformedData = transformMultiSeriesData(chartData as any[], series);
return (
<BarChart data={transformedData} {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
);
}
case 'bar-stacked-horizontal': {
const transformedData = transformMultiSeriesData(chartData as any[], series);
return (
<BarChart data={transformedData} layout="vertical" {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={60} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [0, 4, 4, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
);
}
case 'bar-clustered': {
const transformedData = transformMultiSeriesData(chartData as any[], series);
return (
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} barSize={Math.max(15, 50 / series.length)} />
))}
</BarChart>
);
}
case 'bar-diverging': {
const transformedData = transformDivergingData(chartData as any[]);
return (
<BarChart data={transformedData} layout="vertical" stackOffset="sign" {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={60} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Bar dataKey="positive" name={divergingLabels[0]} fill={graphColors(0)} stackId="stack" radius={[0, 4, 4, 0]} />
<Bar dataKey="negative" name={divergingLabels[1]} fill={graphColors(3)} stackId="stack" radius={[4, 0, 0, 4]} />
</BarChart>
);
}
case 'line':
return (
<LineChart data={chartData as any[]} {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Line
type="monotone"
dataKey="value"
stroke={graphColors(0)}
strokeWidth={3}
dot={{ fill: graphColors(0), strokeWidth: 2, r: 4 }}
/>
</LineChart>
);
case 'area':
return (
<AreaChart data={chartData as any[]} {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<defs>
<linearGradient id="bullets-area-gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="value"
stroke={graphColors(0)}
strokeWidth={2}
fill="url(#bullets-area-gradient)"
/>
</AreaChart>
);
case 'area-stacked': {
const transformedData = transformMultiSeriesData(chartData as any[], series);
return (
<AreaChart data={transformedData} {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
{series.map((s: string, index: number) => (
<Area key={s} type="monotone" dataKey={s} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.4} />
))}
</AreaChart>
);
}
case 'pie':
return (
<PieChart margin={{ top: 0, right: 0, left: 0, bottom: 0 }} height={460}>
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Pie
data={chartData as any[]}
outerRadius={80}
dataKey="value"
label={renderPieLabel}
>
{(chartData as any[]).map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
);
case 'donut':
return (
<PieChart margin={{ top: 0, right: 0, left: 0, bottom: 0 }} height={460}>
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Pie
data={chartData as any[]}
innerRadius={40}
outerRadius={80}
dataKey="value"
label={renderPieLabel}
paddingAngle={2}
>
{(chartData as any[]).map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
);
case 'scatter':
return (
<ScatterChart {...commonProps} height={460}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="x" type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis dataKey="y" type="number" {...axisProps} tickFormatter={formatComma} />
{showTooltip && <Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />}
{showLegend && <Legend wrapperStyle={{ fontSize: '10px' }} />}
<Scatter data={chartData as any[]}>
{(chartData as any[]).map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Scatter>
</ScatterChart>
);
default:
return <div className="flex items-center justify-center h-full text-gray-500">Unsupported chart type</div>;
}
};
return (
<>
<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: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="flex h-full px-8 sm:px-12 lg:px-20 pt-8 pb-8">
{/* Left Section - Title, Description, Chart */}
<div className="flex-1 flex flex-col pr-8">
{/* Title */}
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-[42.7px] font-bold text-gray-900 mb-4">
{slideData?.title || 'Market Size'}
</h1>
{/* Description */}
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base text-gray-700 leading-relaxed mb-4">
{slideData?.description || 'Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.'}
</p>
{/* Chart Container */}
<div className="flex-1 rounded-lg shadow-sm border p-2 max-h-[460px]"
style={{
borderColor: 'var(--stroke,#F8F9FA)',
backgroundColor: 'var(--card-color,#FFFFFF)'
}}
>
<ResponsiveContainer maxHeight={460} height='100%' className="">
{renderChart()}
</ResponsiveContainer>
</div>
</div>
{/* Right Section - Bullet Point Boxes */}
<div className="flex-shrink-0 w-80 flex flex-col justify-center space-y-4">
{bulletPoints.map((bullet, index) => (
<div
key={index}
className="rounded-2xl p-6 text-white"
style={{
backgroundColor: 'var(--card-color,#9333ea)'
}}
>
{/* Icon and Title */}
<div className="flex items-center space-x-3 mb-3">
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-8 h-8 rounded-lg flex items-center justify-center">
<RemoteSvgIcon
url={bullet.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-5 h-5"
color="var(--primary-text,#ffffff)"
title={bullet.icon.__icon_query__}
/>
</div>
<h3 style={{ color: "var(--background-text,#ffffff)" }} className="text-lg font-semibold">
{bullet.title}
</h3>
</div>
{/* Description */}
<p style={{ color: "var(--background-text,#ffffff)" }} className="text-sm leading-relaxed opacity-90">
{bullet.description}
</p>
</div>
))}
</div>
</div>
</div>
</>
)
}
export default ChartWithBulletsSlideLayout

View file

@ -0,0 +1,176 @@
import * as z from "zod";
import React from "react";
export const Schema = z.object({
title: z.string().max(30).describe('The main heading of the slide').default('Campaign Performance Snapshot'),
cards: z.array(z.object({
metric: z.string().describe('Primary value or statistic displayed').max(20),
label: z.string().describe('Title or name of the card item').max(30),
subtext: z.string().describe('Secondary text or additional data').max(30),
isHighlighted: z.boolean().describe('Whether the card uses highlighted styling').default(false),
})).max(8).min(3)
.describe('Array of metric cards for the grid').default([
{ metric: '342 SQLs', label: 'Enterprise ABM Launch', subtext: '28% CONVERSION RATE', isHighlighted: false },
{ metric: '$1.8M pipeline', label: 'Product Feature Release', subtext: '4.7X ROAS', isHighlighted: false },
{ metric: '156 Deals', label: 'Industry Summit Sponsorship', subtext: '42% MEETING RATE', isHighlighted: true },
{ metric: '156 Deals', label: 'Industry Summit Sponsorship', subtext: '42% MEETING RATE', isHighlighted: false },
{ metric: '342 SQLs', label: 'Enterprise ABM Launch', subtext: '28% CONVERSION RATE', isHighlighted: false },
{ metric: '$1.8M pipeline', label: 'Product Feature Release', subtext: '4.7X ROAS', isHighlighted: false },
// { metric: '156 Deals', label: 'Industry Summit Sponsorship', subtext: '42% MEETING RATE', isHighlighted: false },
// { metric: '156 Deals', label: 'Industry Summit Sponsorship', subtext: '42% MEETING RATE', isHighlighted: false },
]),
});
type CardProps = {
metric: string;
label: string;
subtext: string;
isHighlighted?: boolean;
};
const Card: React.FC<CardProps> = ({ metric, label, subtext, isHighlighted }) => {
const boxShadow = isHighlighted
? '0 0px 0px var(--background-text,#8600cd)'
: 'none';
const borderRadius = isHighlighted ? '16px' : '0px';
return (
<div
style={{
width: '282px',
backgroundColor: isHighlighted ? 'var(--card-color,#9810FA)' : 'transparent',
borderRadius,
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
padding: isHighlighted ? '24px' : '10px',
boxShadow,
boxSizing: 'border-box',
paddingBottom: "70px"
}}
>
<div
className="font-normal"
style={{
fontSize: '29.5px',
color: isHighlighted ? 'var(--background-text,#FFFFFF)' : 'var(--background-text,#101828)',
lineHeight: '29.5px',
marginBottom: '15px',
}}
>
{metric}
</div>
<div
className=" font-normal"
style={{
fontSize: '17.7px',
color: isHighlighted ? 'var(--background-text,#FFFFFF)' : 'var(--background-text,#101828)',
lineHeight: '20.4px',
marginBottom: '35px',
minHeight: '42px',
}}
>
{label}
</div>
<div
className=" font-normal"
style={{
fontSize: '13.3px',
color: isHighlighted ? 'var(--background-text,#FFFFFF)' : 'var(--background-text,#101828)',
lineHeight: '18.6px',
textTransform: 'uppercase',
}}
>
{subtext}
</div>
</div>
);
};
export const layoutId = 'performance-grid-snapshot-slide';
export const layoutName = 'Metric Cards Grid';
export const layoutDescription = 'A centered layout with bold title and accent bar, followed by a 4x2 grid of up to 8 metric cards. Each card displays a value, label, and subtext. Cards can optionally be highlighted with colored background.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video mx-auto overflow-hidden font-['Poppins'] flex flex-col items-center justify-center font-bold px-[64px] py-[40px]"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#fefefd)"
}}>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Title Section */}
<div
className=" text-center"
>
<h1 className="text-center" style={{
fontSize: '42.7px',
color: 'var(--background-text,#101828)',
lineHeight: '56.5px',
letterSpacing: '-2.0px',
}}>
{data.title}
</h1>
<div
className=" w-[116.6px] h-[5.7px] mx-auto mt-4"
style={{ backgroundColor: 'var(--primary-color,#9234EB)' }}
/>
</div>
<div className=" w-full mt-14 flex flex-wrap justify-center gap-x-2 gap-y-12">
{data && data.cards && data.cards.map((card, index) =>
<Card
key={index}
metric={card.metric || ''}
label={card.label || ''}
subtext={card.subtext || ''}
isHighlighted={card.isHighlighted || false}
/>
)
}
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,128 @@
import * as z from 'zod';
export const Schema = z.object({
slideNumber: z.string().max(2).describe('Slide number or index').default('1'),
title: z.string().max(30).describe('The main heading of the slide').default('Executive Summary'),
description: z.string().max(400).describe('Supporting description text').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
firstImage: z.object({
__image_url__: z.string(),
__image_prompt__: z.string().max(100)
}).default({
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'A close-up image of a professional team joining hands in a circle, symbolizing unity and partnership.'
}),
secondImage: z.object({
__image_url__: z.string(),
__image_prompt__: z.string().max(100)
}).default({
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'A close-up image of a professional team joining hands in a circle, symbolizing unity and partnership.'
})
});
/**
* Layout ID, Name, and Description
*/
export const layoutId = 'headline-description-with-double-image-layout';
export const layoutName = 'Title Description With Two Images';
export const layoutDescription = 'A clean layout with left-aligned bold title, accent bar, and description paragraph, paired with two overlapping rounded images on the right in a grid arrangement.';
/**
* React Component for the Slide Layout
*/
const HeadlineDescriptionWithDoubleImageLayout = ({ data }: { data: Partial<z.infer<typeof Schema>> }) => {
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex flex-col "
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Slide Content Wrapper */}
<div className="flex flex-1 w-full h-full px-[89.6px] py-[60px]">
{/* Left Section: Content */}
<div className="flex flex-col flex-[1.2] justify-center items-start">
{/* Title */}
<h1 className="text-[42.7px] text-[#101828] font-bold leading-[1.1] tracking-[-2px] mb-4"
style={{
color: 'var(--background-text,#101828)'
}}
>
{data.title}
</h1>
{/* Decorative Purple Line */}
<div className="w-[116.6px] h-[5.7px] bg-[#9234EB] mb-8"
style={{
backgroundColor: 'var(--primary-color,#9234EB)'
}}
/>
{/* Description */}
<p className="text-[16.0px] text-[#000000] font-['Poppins'] font-normal leading-[1.8] max-w-[510px]"
style={{
color: 'var(--background-text,#000000)'
}}
>
{data.description}
</p>
</div>
{/* Right Section: Overlapping Images */}
<div className="flex-1 flex items-center justify-end">
<div className="grid grid-cols-10 grid-rows-10 w-[550px] h-[550px]">
{/* Top-Left Image */}
<div
className="col-start-1 col-span-7 row-start-1 row-span-7 border-[2px] border-[#101828] rounded-[40px] overflow-hidden shadow-2xl"
style={{ zIndex: 5 }}
>
<img
src={data.firstImage?.__image_url__}
alt={data.firstImage?.__image_prompt__}
className="w-full h-full object-cover"
/>
</div>
{/* Bottom-Right Image */}
<div
className="col-start-4 col-span-7 row-start-4 row-span-7 border-[2px] border-[#101828] rounded-[40px] overflow-hidden shadow-2xl"
style={{ zIndex: 10 }}
>
<img
src={data.secondImage?.__image_url__}
alt={data.secondImage?.__image_prompt__}
className="w-full h-full object-cover"
/>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default HeadlineDescriptionWithDoubleImageLayout;

View file

@ -0,0 +1,104 @@
import * as z from 'zod';
export const Schema = z.object({
slideNumber: z.string().max(2).describe('Slide number or index').default('1'),
title: z.string().max(30).describe('The main heading of the slide').default('Executive Summary'),
description: z.string().max(400).describe('Supporting description text').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
image: z.object({
__image_url__: z.string(),
__image_prompt__: z.string().max(100)
}).default({
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'A close-up image of a professional team joining hands in a circle, symbolizing unity and partnership.'
})
});
/**
* Layout ID, Name, and Description
*/
export const layoutId = 'headline-description-with-image-layout';
export const layoutName = 'Title Description With Image';
export const layoutDescription = 'A minimal two-column layout featuring bold title, accent bar, and description on the left, with a single rounded image on the right.';
/**
* React Component for the Slide Layout
*/
const HeadlineDescriptionWithImageLayout = ({ data }: { data: Partial<z.infer<typeof Schema>> }) => {
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex items-center font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
<div className="flex w-full h-full px-[89.6px] items-center justify-between gap-[50px]">
{/* Left Content Column */}
<div className="flex flex-col flex-[1.2] max-w-[570px]">
<h1
className="text-[42.7px] font-bold leading-[1.05] tracking-[-2px]"
style={{
color: 'var(--background-text,#101828)'
}}
>
{data.title}
</h1>
{/* Decorative Purple Line */}
<div className="w-[116.6px] h-[5.7px]"
style={{
backgroundColor: 'var(--primary-color,#9234EB)'
}}
/>
<p
className="text-[16.0px] font-normal leading-[28.5px] mt-8"
style={{
color: 'var(--background-text,#000000)'
}}
>
{data.description}
</p>
</div>
{/* Right Image Column */}
<div className="flex flex-1 justify-end items-center ">
<div className="w-[380px] h-[350px] overflow-hidden rounded-[30px]">
<img
src={data.image?.__image_url__}
alt={data.image?.__image_prompt__}
className="w-full h-full object-cover"
style={{ objectPosition: '52.54% 44.07%' }}
/>
</div>
</div>
</div>
</div>
</>
);
};
export default HeadlineDescriptionWithImageLayout;

View file

@ -0,0 +1,144 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'headline-text-with-stats-layout'
export const layoutName = 'Numbered List With Side Metrics'
export const layoutDescription = 'A two-column layout with bold title, accent bar, and numbered bullet point list on the left, paired with 3 large vertical metrics on the right. Each metric shows value with label and accent dot.'
export const Schema = z.object({
title: z.string().max(30).describe('The main heading of the slide').default('Executive Summary'),
bulletPoints: z.array(z.string().max(160)).max(6).describe('List of bullet point text items').default([
'Exceeded revenue target by 12% ($2.4M vs $2.1M target), driven by strong performance in paid search and email campaigns',
'Marketing influenced 68% of total pipeline value, up from 52% last quarter',
'Paid Search ROI improved to 5.8x (from 4.1x), making it our most efficient channel',
'Marketing influenced 68% of total pipeline value, up from 52% last quarter',
'Paid Search ROI improved to 5.8x (from 4.1x), making it our most efficient channel',
'Marketing influenced 68% of total pipeline value, up from 52% last quarter',
'Paid Search ROI improved to 5.8x (from 4.1x), making it our most efficient channel',
]),
metrics: z.array(z.object({
value: z.string().max(8).describe('Value displayed for the metric'),
label: z.string().max(10).describe('Label text for the metric')
})).describe('Metric items displayed on the right side').default([
{ value: '8,450', label: 'Leads' },
{ value: '2,680', label: 'MQLS' },
{ value: '$2400', label: 'Revenue' }
])
});
export type HeadlineTextWithBulletsAndStatsData = z.infer<typeof Schema>
interface HeadlineTextWithBulletsAndStatsLayoutProps {
data: Partial<HeadlineTextWithBulletsAndStatsData>
}
const HeadlineTextWithBulletsAndStatsLayout: React.FC<HeadlineTextWithBulletsAndStatsLayoutProps> = ({ data }) => {
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden gap-10 flex flex-row items-center justify-between px-[90px] py-[80px] "
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Left Content Column */}
<div className="flex-[1.5] flex flex-col justify-center">
<div className="flex flex-col ">
<h1 className="text-[42.7px] font-bold leading-[1.1] tracking-[-2px]"
style={{
color: 'var(--background-text,#101828)'
}}
>
{data.title}
</h1>
<div className="w-[116.6px] h-[5.7px] mt-4"
style={{
backgroundColor: 'var(--primary-color,#9234EB)'
}}
/>
</div>
<div className="flex flex-col gap-[20px] mt-10">
{data.bulletPoints?.map((point, index) => (
<div key={index} className="flex items-start gap-[8px]">
<span className="text-[16px] font-normal leading-[1.8] shrink-0"
style={{
color: 'var(--background-text,#000000)'
}}
>
{index + 1}.
</span>
<p className="text-[16px] font-normal leading-[1.8]"
style={{
color: 'var(--background-text,#4D5463)'
}}
>
{point}
</p>
</div>
))}
</div>
</div>
{/* Right Metrics Column */}
<div className="w-[340px] space-y-[40px] pl-[60px]">
{data.metrics?.map((metric, index) => (
<div key={index} className="flex flex-col">
<span className="text-[70.1px] font-normal leading-none tracking-tight"
style={{
color: 'var(--background-text,#4D5463)'
}}
>
{metric.value}
</span>
<div className="flex items-center gap-[12px] mt-[10px]">
<div className="w-[15.8px] h-[15.8px] rounded-full shrink-0"
style={{
backgroundColor: 'var(--primary-color,#9134EB)'
}}
/>
<span className="text-[17.4px] font-normal uppercase tracking-wide"
style={{
color: 'var(--background-text,#4D5463)'
}}
>
{metric.label}
</span>
</div>
</div>
))}
</div>
</div>
</>
);
}
export default HeadlineTextWithBulletsAndStatsLayout;

View file

@ -0,0 +1,163 @@
import * as z from "zod";
import React from "react";
export const Schema = z.object({
title: z.string().max(30).describe('The main heading of the slide').default('Target Audience Breakdown'),
columns: z.array(z.object({
index: z.string().max(2).describe('Display number or index for the column').default('01'),
heading: z.string().max(20).describe('Primary heading of the column').default('C-Suite Executives'),
labelOne: z.string().max(12).describe('Label for the first content block').default('KEY NEED'),
contentOne: z.string().max(50).describe('Content for the first block').default('Strategic growth & competitive advantage'),
labelTwo: z.string().max(12).describe('Label for the second content block').default('PRIMARY CHANNEL'),
contentTwo: z.string().max(50).describe('Content for the second block').default('LinkedIn, executive events'),
})).max(3).describe('Array of columns with indexed content').default([
{
index: '01',
heading: 'C-Suite Executives',
labelOne: 'KEY NEED',
contentOne: 'Strategic growth & competitive advantage',
labelTwo: 'PRIMARY CHANNEL',
contentTwo: 'LinkedIn, executive events',
},
{
index: '02',
heading: 'VP of Operations',
labelOne: 'KEY NEED',
contentOne: 'Efficiency & cost optimization',
labelTwo: 'PRIMARY CHANNEL',
contentTwo: 'Industry publications, webinars',
},
{
index: '03',
heading: 'Technical Leaders',
labelOne: 'KEY NEED',
contentOne: 'Integration capabilities & security',
labelTwo: 'PRIMARY CHANNEL',
contentTwo: 'Technical content, product demos',
},
]),
});
type DataType = z.infer<typeof Schema>;
export const layoutId = 'title-three-columns-with-labels';
export const layoutName = 'Three Columns With Index Numbers';
export const layoutDescription = 'A layout featuring bold title with accent bar, followed by three indexed columns each containing large index number, heading, and two labeled content sections.';
const dynamicSlideLayout: React.FC<{ data: Partial<DataType> }> = ({ data }) => {
const { title, columns } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex flex-col p-[60px] pl-[72px] pr-[72px] "
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
<div className="flex flex-col mb-[50px]">
<h1 className="text-[42.7px] font-bold leading-[1.05] tracking-[-2.0px] mb-4"
style={{
color: 'var(--background-text,#101828)'
}}
>
{title}
</h1>
<div className="w-[116.6px] h-[5.7px]"
style={{
backgroundColor: 'var(--primary-color,#9234EB)'
}}
/>
</div>
<div className="flex flex-row justify-between items-start gap-[80px] flex-1">
{columns?.map((column, index) => (
<div key={index} className="flex-1 flex flex-col">
<div className="text-[85.3px] font-normal leading-none mb-[12px]"
style={{
color: 'var(--background-text,#9134EB)'
}}
>
{column?.index}
</div>
<div className="text-[28.4px] font-normal leading-tight mb-[5px] min-h-[70px]"
style={{
color: 'var(--background-text,#000000)'
}}
>
{column?.heading}
</div>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-[6px]">
<div className="text-[21.3px] font-normal uppercase tracking-[1px]"
style={{
color: 'var(--background-text,#737373)'
}}
>
{column?.labelOne}
</div>
<div className="text-[23.1px] font-normal leading-[1.4]"
style={{
color: 'var(--background-text,#000000)'
}}
>
{column?.contentOne}
</div>
</div>
<div className="flex flex-col gap-[6px]">
<div className="text-[21.3px] font-normal uppercase tracking-[1px]"
style={{
color: 'var(--background-text,#737373)'
}}
>
{column?.labelTwo}
</div>
<div className="text-[23.1px] font-normal leading-[1.4]"
style={{
color: 'var(--background-text,#000000)'
}}
>
{column?.contentTwo}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,324 @@
import * as z from "zod";
import React from "react";
export const Schema = z.object({
title: z
.string()
.max(30)
.describe("The main heading of the slide")
.default("Business Objective & KPIs"),
objectiveTitle: z
.string()
.max(80)
.describe("Subheading or objective statement")
.default(
"Accelerate enterprise customer acquisition across EMEA and North America"
),
description: z
.string()
.max(300)
.describe("Supporting description text")
.default(
"Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies."
),
kpis: z
.array(
z.object({
name: z.string().max(30).describe("Name of the metric card"),
value: z.string().max(10).describe("Current value displayed"),
targetValue: z.string().max(10).describe("Target value displayed"),
targetLabel: z.string().max(15).describe("Label text for target"),
progressPercentage: z
.number()
.min(0)
.max(100)
.describe("Progress percentage value"),
color: z.string().describe("Color hex code for progress bar"),
footerLabel: z.string().max(15).describe("Footer label text"),
})
)
.default([
{
name: "Pipeline Generated",
value: "$4.2M",
targetValue: "$3.5M",
targetLabel: "Target",
progressPercentage: 85,
color: "#9234EC",
footerLabel: "of total",
},
{
name: "Marketing Qualified Leads",
value: "8,420",
targetValue: "6,250",
targetLabel: "Target",
progressPercentage: 75,
color: "#9234EC",
footerLabel: "of total",
},
{
name: "Return on Ad Spend",
value: "4.8X",
targetValue: "4.0x",
targetLabel: "Target",
progressPercentage: 80,
color: "#FF5400",
footerLabel: "of total",
},
{
name: "Return on Ad Spend",
value: "4.8X",
targetValue: "4.0x",
targetLabel: "Target",
progressPercentage: 80,
color: "#FF5400",
footerLabel: "of total",
},
{
name: "Return on Ad Spend",
value: "4.8X",
targetValue: "4.0x",
targetLabel: "Target",
progressPercentage: 80,
color: "#FF5400",
footerLabel: "of total",
},
]),
});
export const layoutId = "layout-text-block-with-metric-cards";
export const layoutName = "Text Block With Progress Metric Cards";
export const layoutDescription =
"A split layout with title, subheading, and description on the left, paired with a gray panel containing up to 5 metric cards on the right. Each card shows name, value, target comparison, and semi-circular progress indicator.";
const SemiCircleProgress = ({
percentage,
color,
}: {
percentage: number;
color: string;
}) => {
const radius = 40;
const strokeWidth = 14;
const circumference = Math.PI * radius;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return (
<div className="relative w-[150px] h-[75px] overflow-hidden">
<svg
viewBox="0 0 100 50"
className="w-full h-full transform transition-all duration-500"
>
<path
d="M 10 50 A 40 40 0 0 1 90 50"
fill="none"
stroke="#E6EAF1"
strokeWidth={strokeWidth}
strokeLinecap="round"
/>
<path
d="M 10 50 A 40 40 0 0 1 90 50"
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
/>
</svg>
</div>
);
};
const KPICard = ({ kpi }: { kpi: z.infer<typeof Schema>["kpis"][0] }) => {
return (
<div className="relative min-w-[300px] ">
{/* Card Container */}
<div className=" rounded-xl shadow-sm border overflow-hidden"
style={{
backgroundColor: 'var(--card-color,#ffffff)',
borderColor: 'var(--stroke,#F0F0F2)'
}}
>
{/* Header Bar */}
<div
className=" w-full h-[65px] flex items-center justify-between px-5 text-white"
style={{
backgroundColor: 'var(--primary-color,#9234EC)',
color: 'var(--primary-text,#FFFFFF)'
}}
>
<span className="font-normal text-[17.8px] leading-tight w-1/2"
style={{
color: 'var(--primary-text,#ffffff)'
}}
>
{kpi.name}
</span>
<span className=" font-bold text-[31.9px]"
style={{
color: 'var(--primary-text,#ffffff)'
}}
>
{kpi.value}
</span>
</div>
{/* Content Area */}
<div className=" w-full h-[135px] flex items-center px-6">
<div className="flex flex-col flex-1">
<span className=" font-normal text-[#514E7D] text-[17.8px]"
style={{
color: 'var(--background-text,#514E7D)'
}}
>
{kpi.targetLabel}
</span>
<span className=" font-bold text-[#322C23] text-[24.9px]"
style={{
color: 'var(--background-text,#322C23)'
}}
>
{kpi.targetValue}
</span>
<span className=" font-normal text-[#322C23] opacity-70 text-[16px]"
style={{
color: 'var(--background-text,#322C23)'
}}
>
{kpi.footerLabel}
</span>
</div>
<SemiCircleProgress
percentage={kpi.progressPercentage}
color={kpi.color}
/>
</div>
</div>
</div>
);
};
const dynamicSlideLayout = ({ data }: { data: z.infer<typeof Schema> }) => {
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className=" w-full h-full rounded-sm max-w-[1280px] flex items-center gap-[20px] shadow-lg aspect-video bg-white relative z-20 mx-auto overflow-hidden "
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
<div className=" w-full p-8">
<h1 className="text-[42.7px] font-bold leading-[1.1] mb-4 tracking-tight"
style={{
color: 'var(--background-text,#101828)'
}}
>
{data.title}
</h1>
<div className="w-[116px] h-[6px]"
style={{
backgroundColor: 'var(--primary-color,#9234EB)'
}}
/>
<div className="space-y-6">
<h2 className="text-[21.3px] font-bold leading-snug"
style={{
color: 'var(--background-text,#000000)'
}}
>
{data.objectiveTitle}
</h2>
<p className="text-[16px] font-normal leading-relaxed opacity-80"
style={{
color: 'var(--background-text,#000000)'
}}
>
{data.description}
</p>
</div>
</div>
<div className="bg-[#EEF3F7] w-full h-full flex items-center justify-center p-8">
<div className="flex gap-[18px] w-full items-center justify-center"
>
{data.kpis.length > 2 && <div className="flex flex-col gap-[18px]">
<div className="">
{data.kpis[3] && <KPICard kpi={data.kpis[3]} />}
</div>
<div className=" ">
{data.kpis[4] && <KPICard kpi={data.kpis[4]} />}
</div>
</div>}
<div className="flex flex-col gap-[18px]">
<div className=" ">
{data.kpis[0] && <KPICard kpi={data.kpis[0]} />}
</div>
<div className=" ">
{data.kpis[1] && <KPICard kpi={data.kpis[1]} />}
</div>
<div className="">
{data.kpis[2] && <KPICard kpi={data.kpis[2]} />}
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,118 @@
import * as z from "zod";
export const Schema = z.object({
title: z.string().describe('The main heading of the slide').max(20).default("Word of Wisdom"),
quote: z.string().describe('Quotation text displayed').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.\""),
backgroundImage: z.object({
__image_url__: z.string().describe('URL of the background image').default('https://images.pexels.com/photos/33508509/pexels-photo-33508509.jpeg?auto=compress&cs=tinysrgb&h=650&w=940'),
__image_prompt__: z.string().describe('Prompt description for the image').default('Inspirational mountain landscape with dramatic sky and clouds'),
}).default({
__image_url__: 'https://images.pexels.com/photos/33508509/pexels-photo-33508509.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
__image_prompt__: 'Inspirational mountain landscape with dramatic sky and clouds',
}),
author: z.string().describe('Attribution name for the quote').max(30).default("-Winston Churchill"),
});
export const layoutId = 'left-align-quote';
export const layoutName = 'Left-Aligned Text On Background Image';
export const layoutDescription = 'A full-bleed background image layout featuring a left-aligned bold title with accent bar, a prominent quote in large text, and author attribution below.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, quote, author, backgroundImage } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Background Decorative Image */}
<div className="absolute inset-0">
<img
src={backgroundImage?.__image_url__ || 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/users/79e3281f-47a3-4e45-9c74-b495089583cb/xml-to-html/6cdcb19a975e4a02e2543f953b66a74c.jpeg'}
className="w-full h-full object-cover"
alt="background"
/>
</div>
{/* overlay */}
<div className="absolute inset-0 opacity-50"
style={{
backgroundColor: 'var(--background-color,#000000)'
}}
></div>
{/* Content Container */}
<div className="relative z-10 flex flex-col justify-center h-full px-[90px]">
{/* Title Section */}
<div className=" mb-4">
<h1
className="text-[42.7px] font-bold leading-[1.05]"
style={{
letterSpacing: '-2.0px',
color: 'var(--background-text,#ffffff)'
}}
>
{title}
</h1>
{/* Decorative Separator */}
<div className=" w-[116.6px] h-[5.7px] overflow-visible mt-4"
style={{ backgroundColor: 'var(--primary-color,#9234EB)' }}
></div>
</div>
{/* Quote/Description Section */}
<div className="max-w-[1080px]">
<p
className="text-[27.5px] font-normal leading-[1.78]"
style={{
color: 'var(--background-text,#ffffff)'
}}
>
{quote}
</p>
</div>
{/* Subheading/Author Section */}
<div className="mt-6">
<p
className="text-[27.5px] font-normal"
style={{
color: 'var(--background-text,#ffffff)'
}}
>
{author}
</p>
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,144 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'metrics-with-image-slide'
export const layoutName = 'Image With Title And Metrics'
export const layoutDescription = 'A two-column layout with a large supporting image on the left and content on the right including title, description, and a 2-column metrics grid displaying up to 3 statistics with labels and values.'
const metricsWithImageSlideSchema = z.object({
title: z.string().min(3).max(40).default('Competitive Advantage').meta({
description: "Heading text of the slide",
}),
description: z.string().min(10).max(150).default('Ginyard International Co. stands out by offering custom digital solutions tailored to client needs, alongside long-term support to ensure lasting relationships and continuous adaptation.').meta({
description: "Supporting description text",
}),
image: ImageSchema.default({
__image_url__: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80',
__image_prompt__: 'Person holding tablet with analytics dashboard and charts'
}).meta({
description: "URL of the supporting image",
}),
metrics: z.array(z.object({
label: z.string().min(2).max(100).meta({
description: "Label text for the metric"
}),
value: z.string().min(1).max(20).meta({
description: "Value displayed for the metric"
}),
})).min(1).max(3).default([
{
label: 'Satisfied Clients',
value: '200+'
},
{
label: 'Client Retention Rate',
value: '95%'
},
]).meta({
description: "List of metric items to display",
})
})
export const Schema = metricsWithImageSlideSchema
export type MetricsWithImageSlideData = z.infer<typeof metricsWithImageSlideSchema>
interface MetricsWithImageSlideLayoutProps {
data?: Partial<MetricsWithImageSlideData>
}
const MetricsWithImageSlideLayout: React.FC<MetricsWithImageSlideLayoutProps> = ({ data: slideData }) => {
const metrics = slideData?.metrics || []
return (
<>
<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 font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Decorative Wave Patterns */}
<div className="absolute bottom-0 left-0 w-48 h-48 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 200" fill="none">
<path d="M0 100C50 75 100 125 150 100C175 87.5 200 100 200 100V200H0V100Z" fill="#8b5cf6" opacity="0.4" />
<path d="M0 150C75 175 125 125 200 150V175C150 162.5 100 175 50 162.5L0 150Z" fill="#8b5cf6" opacity="0.3" />
</svg>
</div>
<div className="absolute top-0 right-0 w-64 h-64 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 200" fill="none">
<path d="M100 0C150 50 200 0 200 50C200 100 150 150 100 150C50 150 0 100 0 50C0 0 50 50 100 0Z" fill="#8b5cf6" opacity="0.2" />
</svg>
</div>
{/* Main Content */}
<div className="relative z-10 flex h-full px-8 sm:px-12 lg:px-20 pt-12 pb-8">
{/* Left Section - Image */}
<div className="flex-1 flex items-center justify-center pr-8">
<div className="w-full max-w-lg h-96 rounded-2xl overflow-hidden shadow-lg">
<img
src={slideData?.image?.__image_url__ || ''}
alt={slideData?.image?.__image_prompt__ || slideData?.title || ''}
className="w-full h-full object-cover"
/>
</div>
</div>
{/* Right Section - Content and Metrics */}
<div className="flex-1 flex flex-col justify-center pl-8 space-y-6">
{/* Title */}
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-[42.7px] font-bold text-gray-900 leading-tight">
{slideData?.title || 'Competitive Advantage'}
</h1>
{/* Description */}
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
{slideData?.description || 'Ginyard International Co. stands out by offering custom digital solutions tailored to client needs, alongside long-term support to ensure lasting relationships and continuous adaptation.'}
</p>
{/* Metrics Grid */}
<div className="grid grid-cols-2 gap-6">
{metrics.map((metric, index) => (
<div key={index} className="text-center space-y-2">
<div style={{ color: "var(--background-text,#4b5563)" }} className="text-sm text-gray-600 font-medium">
{metric.label}
</div>
<div style={{ color: "var(--primary-color,#9333ea)" }} className="text-3xl sm:text-4xl lg:text-5xl font-bold text-purple-600">
{metric.value}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</>
)
}
export default MetricsWithImageSlideLayout

View file

@ -0,0 +1,671 @@
import React from 'react';
import * as z from "zod";
import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
Cell,
LabelList,
ReferenceLine,
} from "recharts";
export const layoutId = 'multi-chart-grid-slide';
export const layoutName = 'Title Description With Multi-Chart Grid';
export const layoutDescription = 'A flexible dashboard layout featuring a title section with description and 1-6 auto-arranged charts in a responsive grid. Supports bar (vertical, horizontal, grouped, stacked, clustered, diverging), line, area, pie, donut, and scatter charts.';
// Color palettes
const CHART_COLOR_PALETTES = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899']
// Chart type enum
const ChartTypeEnum = z.enum([
'bar-vertical',
'bar-horizontal',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter',
]);
// Simple data point for single series
const SimpleDataPointSchema = z.object({
name: z.string(),
value: z.number(),
});
// Multi-series data point
const MultiSeriesDataPointSchema = z.object({
name: z.string(),
values: z.any().describe("Object with series names as keys and numbers as values (e.g., { 'Product A': 45, 'Product B': 62 })"),
});
// Diverging data point
const DivergingDataPointSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
// Scatter data point
const ScatterDataPointSchema = z.object({
x: z.number(),
y: z.number(),
name: z.string().optional(),
});
// Individual chart schema
const ChartItemSchema = z.object({
title: z.string().max(40).describe("Chart title").default("Chart Title"),
type: ChartTypeEnum.default('bar-vertical'),
data: z.union([
z.array(SimpleDataPointSchema),
z.array(MultiSeriesDataPointSchema),
z.array(DivergingDataPointSchema),
z.array(ScatterDataPointSchema),
]).default([
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
]),
series: z.array(z.string()).optional().describe("Series names for grouped/stacked charts"),
colorPalette: z.enum(['vibrant', 'ocean', 'forest', 'sunset', 'professional']).default('vibrant'),
});
// Main schema
export const Schema = z.object({
title: z.string().min(3).max(50).default('Data Analytics Dashboard').meta({
description: "Heading text of the slide",
}),
description: z.string().min(10).max(200).default('Comprehensive overview of key metrics and performance indicators across multiple data dimensions.').meta({
description: "Supporting description text",
}),
charts: z.array(ChartItemSchema).min(1).max(6).default([
{
title: 'Revenue by Quarter',
type: 'bar-vertical',
data: [
{ name: 'Q1', value: 125000 },
{ name: 'Q2', value: 158000 },
{ name: 'Q3', value: 142000 },
{ name: 'Q4', value: 189000 },
],
colorPalette: 'vibrant',
},
{
title: 'Market Distribution',
type: 'donut',
data: [
{ name: 'North America', value: 35 },
{ name: 'Europe', value: 28 },
{ name: 'Asia Pacific', value: 25 },
{ name: 'Others', value: 12 },
],
colorPalette: 'ocean',
},
{
title: 'Growth Trend',
type: 'line',
data: [
{ name: 'Jan', value: 30 },
{ name: 'Feb', value: 45 },
{ name: 'Mar', value: 52 },
{ name: 'Apr', value: 48 },
{ name: 'May', value: 67 },
{ name: 'Jun', value: 82 },
],
colorPalette: 'professional',
},
{
title: 'Department Performance',
type: 'bar-horizontal',
data: [
{ name: 'Sales', value: 87 },
{ name: 'Marketing', value: 72 },
{ name: 'Engineering', value: 95 },
{ name: 'Support', value: 68 },
],
colorPalette: 'sunset',
},
{
title: 'Product Comparison',
type: 'bar-clustered',
data: [
{ name: 'Q1', values: { 'Product A': 45, 'Product B': 62 } },
{ name: 'Q2', values: { 'Product A': 58, 'Product B': 71 } },
{ name: 'Q3', values: { 'Product A': 72, 'Product B': 65 } },
],
series: ['Product A', 'Product B'],
colorPalette: 'vibrant',
},
{
title: 'Customer Feedback',
type: 'bar-diverging',
data: [
{ name: 'Quality', positive: 78, negative: 22 },
{ name: 'Service', positive: 65, negative: 35 },
{ name: 'Price', positive: 42, negative: 58 },
],
series: ['Satisfied', 'Unsatisfied'],
colorPalette: 'professional',
},
]).meta({
description: "Array of chart configurations",
}),
showLegend: z.boolean().default(true).meta({
description: "Whether to display chart legends",
}),
showGrid: z.boolean().default(true).meta({
description: "Whether to display grid lines",
}),
});
export type MultiChartGridSlideData = z.infer<typeof Schema>;
interface MultiChartGridSlideLayoutProps {
data?: Partial<MultiChartGridSlideData>;
}
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-[10px] font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[9px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
// Mini chart renderer
const MiniChartRenderer: React.FC<{
chart: z.infer<typeof ChartItemSchema>;
showLegend: boolean;
showGrid: boolean;
}> = ({ chart, showLegend, showGrid }) => {
const data = chart.data as any[];
const series = chart.series || [];
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 9, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.5,
};
const transformMultiSeriesData = (data: any[], series: string[]) => {
return data.map(item => {
const result: Record<string, any> = { name: item.name };
series.forEach(s => {
result[s] = item.values?.[s] ?? 0;
});
return result;
});
};
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
const renderPieLabel = (props: any) => {
const { percent } = props;
if (percent < 0.08) return null;
return `${(percent * 100).toFixed(0)}%`;
};
const graphColors = (index: number, serieColor?: string) => {
const fallback = serieColor || CHART_COLOR_PALETTES[index % CHART_COLOR_PALETTES.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'bar-vertical':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data}
margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}
>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-horizontal':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-grouped-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[0, 3, 3, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [0, 3, 3, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-clustered': {
// If no series provided, fall back to simple bar chart behavior
if (series.length === 0) {
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} barSize={32}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} barSize={Math.max(12, 40 / series.length)} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-diverging': {
// Handle both formats: {positive, negative} or simple {value}
const hasDivergingFormat = data.length > 0 && ('positive' in data[0] || 'negative' in data[0]);
const transformedData = hasDivergingFormat
? transformDivergingData(data)
: data.map((item: any, idx: number) => ({
name: item.name,
positive: idx % 2 === 0 ? Math.abs(item.value) : 0,
negative: idx % 2 === 1 ? -Math.abs(item.value) : 0,
}));
const seriesLabels = chart.series || ['Positive', 'Negative'];
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" stackOffset="sign" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
<Bar dataKey="positive" name={seriesLabels[0]} fill={graphColors(0)} stackId="stack" radius={[0, 3, 3, 0]} />
<Bar dataKey="negative" name={seriesLabels[1]} fill={graphColors(3)} stackId="stack" radius={[3, 0, 0, 3]} />
</BarChart>
</ResponsiveContainer>
);
}
case 'line':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<LineChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Line type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} dot={{ fill: graphColors(0), r: 3 }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} fill="url(#areaGrad)" />
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Area key={s} type="monotone" dataKey={s} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.4} />
))}
</AreaChart>
</ResponsiveContainer>
);
}
case 'pie':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'donut':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" innerRadius="40%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false} paddingAngle={2}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<ScatterChart margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" dataKey="x" {...axisProps} tickFormatter={formatComma} />
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Scatter data={data}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
default:
return <div className="flex items-center justify-center h-full text-gray-400 text-sm">Unsupported chart type</div>;
}
};
// Grid layout calculator
const getGridLayout = (count: number): { cols: number; rows: number; className: string } => {
switch (count) {
case 1:
return { cols: 1, rows: 1, className: 'grid-cols-1' };
case 2:
return { cols: 2, rows: 1, className: 'grid-cols-2' };
case 3:
return { cols: 3, rows: 1, className: 'grid-cols-3' };
case 4:
return { cols: 2, rows: 2, className: 'grid-cols-2' };
case 5:
return { cols: 3, rows: 2, className: 'grid-cols-3' };
case 6:
return { cols: 3, rows: 2, className: 'grid-cols-3' };
default:
return { cols: 2, rows: 2, className: 'grid-cols-2' };
}
};
const MultiChartGridSlideLayout: React.FC<MultiChartGridSlideLayoutProps> = ({ data: slideData }) => {
const title = slideData?.title || 'Data Analytics Dashboard';
const description = slideData?.description || 'Comprehensive overview of key metrics and performance indicators.';
const charts = slideData?.charts || [];
const showLegend = slideData?.showLegend ?? true;
const showGrid = slideData?.showGrid ?? true;
const chartCount = charts.length;
const gridLayout = getGridLayout(chartCount);
// Calculate chart height based on count
const getChartHeight = () => {
if (chartCount <= 2) return 280;
if (chartCount <= 3) return 260;
return 180;
};
return (
<>
<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-gradient-to-br from-slate-50 via-white to-indigo-50/30 relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "var(--heading-font-family, 'Poppins')",
background: "var(--background-color, linear-gradient(135deg, #f8fafc 0%, #ffffff 50%, #eef2ff 100%))"
}}
>
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-96 h-96 bg-gradient-to-bl from-violet-100/40 to-transparent rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-72 h-72 bg-gradient-to-tr from-cyan-100/30 to-transparent rounded-full blur-3xl" />
{/* Company branding */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="relative h-full px-10 pt-10 pb-8 flex flex-col z-10">
{/* Header Section */}
<div className="mb-4">
<h1
className="text-[42.7px] font-bold mb-1 tracking-tight"
style={{ color: "var(--background-text, #111827)" }}
>
{title}
</h1>
<div
className="w-16 h-1 rounded-full"
style={{ backgroundColor: 'var(--primary-color, #8B5CF6)' }}
/>
<p
className="text-[16px] leading-relaxed mt-2"
style={{ color: "var(--background-text, #6B7280)" }}
>
{description}
</p>
</div>
{/* Charts Grid */}
<div className={`flex-1 grid ${gridLayout.className} gap-4 h-[525px]`}>
{charts.map((chart, index) => (
<div
key={index}
className=" backdrop-blur-sm rounded-xl border border-gray-100/80 shadow-sm flex flex-col overflow-hidden"
style={{ borderColor: 'var(--stroke,#F8F9FA)', backgroundColor: 'var(--card-color,#FFFFFF)' }}
>
{/* Chart Header */}
<div className="px-4 pt-3 pb-1">
<h3
className="text-sm font-semibold truncate"
style={{ color: "var(--background-text, #374151)" }}
>
{chart.title}
</h3>
</div>
{/* Chart Container */}
<div className="flex-1 px-2 pb-2" style={{ height: `${getChartHeight()}px` }}>
<MiniChartRenderer
chart={chart}
showLegend={showLegend && chartCount <= 4}
showGrid={showGrid}
/>
</div>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default MultiChartGridSlideLayout;

View file

@ -0,0 +1,163 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'numbered-bullets-slide'
export const layoutName = 'Title Image With Numbered Points'
export const layoutDescription = 'A layout featuring a large title with accent line, a supporting image in the upper right, and 1-3 numbered bullet points in a two-column grid below. Each point has a large number prefix, title, and description.'
const numberedBulletsSlideSchema = z.object({
title: z.string().min(3).max(40).default('Market Validation').meta({
description: "Heading text of the slide",
}),
image: ImageSchema.default({
__image_url__: 'https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80',
__image_prompt__: 'Business people analyzing charts and data on wall'
}).meta({
description: "URL of the supporting image",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(80).meta({
description: "Title text for the bullet point",
}),
description: z.string().min(10).max(150).meta({
description: "Description text for the bullet point",
}),
})).min(1).max(3).default([
{
title: 'Customer Insights',
description: 'Surveys reveal that 78% of businesses are planning to invest in digital solutions, with 85% preferring customized approaches.'
},
{
title: 'Pilot Program Success',
description: 'The survey revealed that 78% of businesses plan to invest in digital solutions, and 85% prefer a tailored approach.'
},
{
title: 'Pilot Program Success',
description: 'The survey revealed that 78% of businesses plan to invest in digital solutions, and 85% prefer a tailored approach.'
},
{
title: 'Pilot Program Success',
description: 'The survey revealed that 78% of businesses plan to invest in digital solutions, and 85% prefer a tailored approach.'
}
]).meta({
description: "List of numbered bullet points",
})
})
export const Schema = numberedBulletsSlideSchema
export type NumberedBulletsSlideData = z.infer<typeof numberedBulletsSlideSchema>
interface NumberedBulletsSlideLayoutProps {
data?: Partial<NumberedBulletsSlideData>
}
const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({ data: slideData }) => {
const bulletPoints = slideData?.bulletPoints || []
return (
<>
<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: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content Container */}
<div className="px-8 sm:px-12 lg:px-20 pt-12 pb-8 h-full">
{/* Top Section - Title and Image */}
<div className="flex items-start justify-between mb-8">
{/* Title Section */}
<div className="flex-1 pr-8">
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-[42.7px] font-bold text-gray-900 leading-tight mb-4">
{slideData?.title || 'Market Validation'}
</h1>
{/* Purple accent line */}
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-24 h-1 bg-purple-600 mb-6"></div>
</div>
{/* Image Section */}
<div className="flex-shrink-0 w-80 h-48">
<img
src={slideData?.image?.__image_url__ || ''}
alt={slideData?.image?.__image_prompt__ || slideData?.title || ''}
className="w-full h-full object-cover rounded-lg shadow-md" style={{ background: "var(--tertiary-accent-color,#e5e7eb)" }}
/>
</div>
</div>
{/* Numbered Bullet Points */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
{bulletPoints.map((bullet, index) => (
<div key={index} className="flex items-start space-x-4">
{/* Number */}
<div className="flex-shrink-0">
<div style={{ color: "var(--background-text,#111827)" }} className="text-4xl sm:text-5xl font-bold text-gray-900">
{String(index + 1).padStart(2, '0')}
</div>
</div>
{/* Content */}
<div className="flex-1 pt-2">
<h3 style={{ color: "var(--background-text,#111827)" }} className="text-xl sm:text-2xl font-bold text-gray-900 mb-3">
{bullet.title}
</h3>
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base text-gray-700 leading-relaxed">
{bullet.description}
</p>
</div>
</div>
))}
</div>
{/* Decorative Wave Pattern at Bottom */}
<div className="absolute bottom-0 left-0 right-0 h-20 overflow-hidden">
<svg
className="w-full h-full opacity-20"
viewBox="0 0 1200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 100C300 150 600 50 900 100C1050 125 1125 100 1200 100V200H0V100Z"
fill="url(#wave-gradient)"
/>
<defs>
<linearGradient id="wave-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="var(--primary-color,#9333ea)" />
<stop offset="50%" stopColor="var(--primary-color,#9333ea)" />
<stop offset="100%" stopColor="var(--primary-color,#9333ea)" />
</linearGradient>
</defs>
</svg>
</div>
</div>
</div>
</>
)
}
export default NumberedBulletsSlideLayout

View file

@ -0,0 +1,139 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'quote-slide'
export const layoutName = 'Centered Text On Image Overlay'
export const layoutDescription = 'A full-screen layout with background image, semi-transparent overlay, centered heading with accent line, large quote icon, quote text, and author attribution with decorative lines.'
const quoteSlideSchema = z.object({
heading: z.string().min(3).max(60).default('Words of Wisdom').meta({
description: "Heading text 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: "Quotation text displayed on the slide",
}),
author: z.string().min(2).max(50).default('Winston Churchill').meta({
description: "Attribution name for 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: "URL of the background image",
})
})
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: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* 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 - low opacity primary accent */}
<div
className="absolute inset-0"
style={{ backgroundColor: 'var(--background-color, #000000)', opacity: 0.5 }}
></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 pt-14 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 style={{ color: "var(--background-text,#ffffff)" }} 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 style={{ background: "var(--primary-color,#9333ea)" }} 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" style={{ color: "var(--primary-color,#9333ea)" }}
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 style={{ color: "var(--background-text,#ffffff)" }} 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 style={{ background: "var(--primary-color,#9333ea)" }} className="w-16 h-px bg-purple-300"></div>
<cite className="text-base sm:text-lg text-purple-200 font-semibold not-italic"
style={{
color: 'var(--background-text,#ffffff)'
}}
>
{slideData?.author || 'Winston Churchill'}
</cite>
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-16 h-px bg-purple-300"></div>
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default QuoteSlideLayout

View file

@ -0,0 +1,163 @@
/**
* Zod Schema for Table of Content Slide
* Defined based on the visual elements observed in the reference.
*/
import * as z from 'zod';
export const Schema = z.object({
title: z.string().max(30).describe('The main heading of the slide').default('Table of Content'),
items: z.array(z.object({
number: z.string().max(5).describe('Sequence number or index').default('1'),
label: z.string().max(40).describe('Label text for the item').default('Introduction'),
})).describe('List of items displayed in two columns').default([
{ number: '1', label: 'Introduction' },
{ number: '2', label: 'Key Findings' },
{ number: '3', label: 'Data Analysis' },
{ number: '4', label: 'Recommendations' },
{ number: '5', label: 'Conclusion' },
{ number: '6', label: 'Introduction' },
{ number: '7', label: 'Key Findings' },
{ number: '8', label: 'Data Analysis' },
{ number: '9', label: 'Recommendations' },
{ number: '10', label: 'Conclusion' },
]),
});
type DataProps = z.infer<typeof Schema>;
export const layoutId = 'title-two-column-numbered-list';
export const layoutName = 'Split Title With Two Column Numbered List';
export const layoutDescription = 'A split layout with large title on the left and two-column numbered list on the right. Each item displays a numbered circle badge and label.';
/**
* dynamicSlideLayout - A React component representing a Table of Content slide.
*/
const dynamicSlideLayout: React.FC<{ data: Partial<DataProps> }> = ({ data }) => {
const { title, items = [] } = data;
// Split items into two columns for the layout
const half = Math.ceil(items.length / 2);
const firstCol = items.slice(0, half);
const secondCol = items.slice(half);
// Helper to format title with a newline after the first word to match visual reference
const formattedTitle = title?.split(' ').map((word, i) => (i === 1 ? `\n${word}` : (i > 1 ? ` ${word}` : word))).join('');
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex items-center px-[90px] font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Left Section: Title and Decorative Bar */}
<div className="flex-[1.2] flex flex-col justify-center h-full">
<div className="max-w-[400px]">
<h1 className="text-[42.7px] font-bold leading-[1.05] tracking-[-2px] whitespace-pre-wrap"
style={{
color: 'var(--background-text,#101828)'
}}
>
{formattedTitle}
</h1>
<div className="w-[116.6px] h-[5.7px] mt-[12px]"
style={{
backgroundColor: 'var(--primary-color,#9234EB)'
}}
/>
</div>
</div>
{/* Right Section: Two Columns of Numbered Items */}
<div className="flex-[1.8] flex justify-between items-center h-full py-[100px] gap-8">
{/* First Column */}
<div className="flex flex-col gap-[55px] flex-1">
{firstCol.map((item, index) => (
<div key={`col1-${index}`} className="flex items-center gap-[24px]">
<div className="w-[50.8px] h-[50.3px] rounded-full flex items-center justify-center shrink-0"
style={{
backgroundColor: 'var(--primary-color,#703AC9)'
}}
>
<span className="text-[20.6px] font-normal leading-none"
style={{
color: 'var(--primary-text,#FFFFFF)'
}}
>
{item?.number}
</span>
</div>
<span className="text-[20.6px] font-normal truncate"
style={{
color: 'var(--background-text,#18181B)'
}}
>
{item?.label}
</span>
</div>
))}
</div>
{/* Second Column */}
<div className="flex flex-col gap-[55px] flex-1">
{secondCol.map((item, index) => (
<div key={`col2-${index}`} className="flex items-center gap-[24px]">
<div className="w-[50.8px] h-[50.3px] rounded-full flex items-center justify-center shrink-0"
style={{
backgroundColor: 'var(--primary-color,#703AC9)'
}}
>
<span className="text-[20.6px] font-normal leading-none"
style={{
color: 'var(--primary-text,#FFFFFF)'
}}
>
{item?.number}
</span>
</div>
<span className="text-[20.6px] font-normal truncate"
style={{
color: 'var(--background-text,#18181B)'
}}
>
{item?.label}
</span>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,184 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from '../defaultSchemes';
export const layoutId = 'team-slide'
export const layoutName = 'Description With Photo Cards Grid'
export const layoutDescription = 'A two-section layout with title, accent line, and description on the left, paired with a 2x2 or flexible grid of 2-4 person cards on the right. Each card displays photo, name, position, and bio.'
const teamMemberSchema = z.object({
name: z.string().min(2).max(50).meta({
description: "Name of the person"
}),
position: z.string().min(2).max(50).meta({
description: "Role or position title"
}),
description: z.string().max(150).meta({
description: "Brief description text"
}),
image: ImageSchema
});
const teamSlideSchema = z.object({
title: z.string().min(3).max(40).default('Our Team Members').meta({
description: "Heading text of the slide",
}),
companyDescription: z.string().min(10).max(150).default('Ginyard International Co. is a leading provider of innovative digital solutions tailored for businesses. Our mission is to empower organizations to achieve their goals through cutting-edge technology and strategic partnerships.').meta({
description: "Supporting description text",
}),
teamMembers: z.array(teamMemberSchema).min(2).max(4).default([
{
name: 'Juliana Silva',
position: 'CEO',
description: 'Strategic leader with 15+ years experience in digital transformation and business growth.',
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 businesswoman CEO headshot'
}
},
{
name: 'Daniel Gallego',
position: 'CTO',
description: 'Technology expert specializing in scalable solutions and innovative software architecture.',
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: 'Ketut Susilo',
position: 'COO',
description: 'Operations leader focused on efficiency, process optimization, and team development.',
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 COO headshot'
}
},
{
name: 'Anna Robertson',
position: 'CMO',
description: 'Marketing strategist with expertise in brand development and customer engagement.',
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 CMO headshot'
}
}
]).meta({
description: "List of person cards with their information",
})
})
export const Schema = teamSlideSchema
export type TeamSlideData = z.infer<typeof teamSlideSchema>
interface TeamSlideLayoutProps {
data?: Partial<TeamSlideData>
}
const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) => {
const teamMembers = slideData?.teamMembers || []
// Function to determine grid classes based on number of team members
const getGridClasses = (count: number) => {
if (count <= 2) {
return 'grid-cols-1 gap-6'
} else if (count <= 4) {
return 'grid-cols-2 gap-6'
} else {
return 'grid-cols-2 lg:grid-cols-3 gap-4'
}
}
return (
<> <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: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Decorative Wave Pattern */}
<div className="absolute bottom-0 left-0 w-80 h-40 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 300 150" fill="none">
<path d="M0 75C75 50 150 100 225 75C262.5 62.5 300 75 300 75V150H0V75Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 100C100 125 200 75 300 100V125C225 112.5 150 125 75 112.5L0 100Z" fill="#8b5cf6" opacity="0.2" />
</svg>
</div>
{/* Main Content */}
<div className="relative z-10 flex h-full px-8 sm:px-12 lg:px-20 pt-12 pb-8">
{/* Left Section - Title and Company Description */}
<div className="flex-1 flex flex-col justify-center pr-8 space-y-6">
{/* Title */}
<h1 style={{ color: "var(--background-text,#111827)" }} className="text-[42.7px] font-bold text-gray-900 leading-tight">
{slideData?.title || 'Our Team Members'}
</h1>
{/* Purple accent line */}
<div style={{ background: "var(--primary-color,#9333ea)" }} className="w-20 h-1 bg-purple-600"></div>
{/* Company Description */}
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-base sm:text-lg text-gray-700 leading-relaxed">
{slideData?.companyDescription || 'Ginyard International Co. is a leading provider of innovative digital solutions tailored for businesses. Our mission is to empower organizations to achieve their goals through cutting-edge technology and strategic partnerships.'}
</p>
</div>
{/* Right Section - Team Members Grid */}
<div className="flex-1 flex items-center justify-center pl-8">
<div className={`grid ${getGridClasses(teamMembers.length)} w-full max-w-2xl`}>
{teamMembers.map((member, index) => (
<div key={index} className="text-center space-y-3">
{/* Member Photo */}
<div className="w-32 h-32 mx-auto rounded-lg overflow-hidden shadow-md" style={{ background: "var(--card-color,#e5e7eb)" }}>
<img
src={member.image.__image_url__ || ''}
alt={member.image.__image_prompt__ || member.name}
className="w-full h-full object-cover"
/>
</div>
{/* Member Info */}
<div>
<h3 style={{ color: "var(--background-text,#111827)" }} className="text-lg font-semibold text-gray-900">
{member.name}
</h3>
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-sm font-medium text-gray-600 italic mb-2">
{member.position}
</p>
<p style={{ color: "var(--background-text,#4b5563)" }} className="text-xs text-gray-600 leading-relaxed px-2">
{member.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</>
)
}
export default TeamSlideLayout

View file

@ -0,0 +1,119 @@
import * as z from "zod";
import React from "react";
export const Schema = z.object({
title: z.string().max(25).describe('The main heading of the slide').default('Key Insights & Learnings'),
insightTitle: z.string().max(63).describe('Heading for the highlighted card').default('CONTENT + PAID SOCIAL COMBINATION DRIVES HIGHEST QUALITY LEADS'),
insightDescription: z.string().max(99).describe('Description text for the highlighted card').default('Leads from integrated campaigns had 47% faster time-to-close and 28% higher average contract value.')
});
export const layoutId = 'title-side-insight-slide';
export const layoutName = 'Split Title With Text Card';
export const layoutDescription = 'A balanced two-section layout with bold title and accent bar on the left, paired with a white card on the right containing accent-colored heading and description text.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, insightTitle, insightDescription } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex items-center px-16 "
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[40px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Title */}
<div className=" w-1/2 ">
<div
className="text-left min-h-[1.2em] max-w-[429.1px]"
style={{
lineHeight: '45.2px',
letterSpacing: '-1.6px',
fontSize: '42.7px',
color: 'var(--background-text,#101828)',
fontWeight: 700
}}
>
{title}
</div>
<div
className=" w-[116.6px] h-[5.7px] overflow-visible mt-4"
style={{ backgroundColor: 'var(--primary-color,#9234EB)' }}
></div>
</div>
<div className="w-1/2"
>
<div className=" p-12">
<div className="p-10 py-24 bg-white shadow-md rounded-lg"
style={{
background: 'var(--card-color,#ffffff)'
}}
>
{/* Insight Title */}
<div className=" overflow-visible">
<div
className="text-left min-h-[1.2em]"
style={{
lineHeight: '29.9px',
fontSize: '21.3px',
color: 'var(--background-text,#9234EC)',
fontWeight: 700
}}
>
{insightTitle}
</div>
</div>
{/* Insight Description */}
<div className="overflow-visible mt-6">
<div
className="text-left min-h-[1.2em]"
style={{
lineHeight: '32.3px',
fontSize: '23.1px',
color: 'var(--background-text,#000000)'
}}
>
{insightDescription}
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,120 @@
import * as z from "zod";
export const Schema = z.object({
title: z.string().max(10).describe('The main heading of the slide').default('Thank you'),
description: z.string().max(120).describe('Supporting description text').default('Thanks for supporting our small business! to show our love, please enjoy 20% off you next order with the code "CODE20"'),
contactTitle: z.string().max(15).describe('Heading for the contact section').default('Contact Us'),
phone: z.string().max(20).describe('Phone number text').default('+977-98000000'),
email: z.string().max(30).describe('Email address text').default('presenton@gmail.com'),
website: z.string().max(30).describe('Website URL text').default('www.presenton.com'),
footerImage: z.object({
__image_url__: z.string(),
__image_prompt__: z.string().max(100)
}).default({
__image_url__: "https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png",
__image_prompt__: "A professional aesthetic photo of business hands reviewing documents and charts"
})
});
export const layoutId = 'thank-you-contact-info-footer-image-slide-layout';
export const layoutName = 'Centered Title With Contact And Footer Image';
export const layoutDescription = 'A conclusion slide featuring centered title with accent bar, description text on the left, contact information (phone, email, website) aligned right, and a full-width footer image.';
const dynamicSlideLayout = ({ data }: { data: Partial<z.infer<typeof Schema>> }) => {
const { title, description, contactTitle, phone, email, website, footerImage } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Centered Header Section */}
<div className="absolute left-[370.0px] top-[71.5px] w-[540.0px] h-[100px] flex flex-col items-center">
<div className="text-center min-h-[1.2em]" style={{ lineHeight: '45.2px' }}>
<span className="text-[42.7px] font-bold" style={{ letterSpacing: '-1.6px', color: 'var(--background-text,#101828)' }}>
{title}
</span>
</div>
<div className="mt-[16px] w-[116.6px] h-[5.7px]" style={{ backgroundColor: 'var(--primary-color,#9234EB)' }}></div>
</div>
{/* Description / Appreciation Message */}
<div className="absolute left-[65.2px] top-[280.1px] w-[516.5px] h-[96.0px] overflow-visible">
<div className="text-left min-h-[1.2em]" style={{ lineHeight: '32.3px' }}>
<span className="text-[23.1px] font-normal" style={{ color: 'var(--background-text,#000000)' }}>
{description}
</span>
</div>
</div>
{/* Contact Section */}
<div className="absolute right-[82px] top-[231.2px] w-[397.4px] flex flex-col items-end">
<div className="text-right min-h-[1.2em] mb-[15px]" style={{ lineHeight: '28.4px' }}>
<span className="text-[28.4px] font-normal" style={{ color: 'var(--primary-color,#9234EB)' }}>
{contactTitle}
</span>
</div>
<div className="text-right min-h-[1.2em] mb-[6px]" style={{ lineHeight: '29.9px' }}>
<span className="text-[21.3px] font-normal" style={{ color: 'var(--background-text,#324712)' }}>
{phone}
</span>
</div>
<div className="text-right min-h-[1.2em] mb-[6px]" style={{ lineHeight: '29.9px' }}>
<span className="text-[21.3px] font-normal" style={{ color: 'var(--background-text,#324712)' }}>
{email}
</span>
</div>
<div className="text-right min-h-[1.2em]" style={{ lineHeight: '29.9px' }}>
<span className="text-[21.3px] font-normal" style={{ color: 'var(--background-text,#324712)' }}>
{website}
</span>
</div>
</div>
{/* Decorative Line */}
<div className="absolute left-[60.0px] top-[412.8px] w-[558.0px] h-[1.0px]">
<svg width="100%" height="100%" overflow="visible">
<line x1="0%" y1="50%" x2="100%" y2="50%" stroke="var(--background-text,#D3CFCF)" strokeWidth="0.7" />
</svg>
</div>
{/* Footer Image Section */}
{footerImage && <div className="absolute left-[65.2px] top-[412.8px] w-[1142.8px] h-[276.5px] overflow-hidden">
<img
src={footerImage.__image_url__}
alt={footerImage.__image_prompt__}
className="w-full h-full object-cover"
style={{ objectPosition: '68.32% 50.0%' }}
/>
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,179 @@
import * as z from "zod";
/**
* Zod Schema for the slide content.
*/
export const Schema = z.object({
title: z.string().max(20).describe('The main heading of the slide').default('Timeline'),
milestones: z.array(z.object({
year: z.string().max(4).describe('Time period or date label'),
description: z.string().max(100).describe('Description text for the milestone'),
})).min(2).max(6).describe('List of milestone items for the timeline').default([
{ year: '2017', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
{ year: '2018', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
{ year: '2019', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
{ year: '2020', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
{ year: '2021', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
{ year: '2022', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
{ year: '2023', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
{ year: '2024', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis lacinia dictum.' },
]),
});
/**
* Layout metadata.
*/
export const layoutId = 'timeline-alternating-cards-slide';
export const layoutName = 'Horizontal Timeline With Cards';
export const layoutDescription = 'A visual timeline layout featuring centered title, horizontal dashed axis line, and 2-6 milestone cards alternating above and below the axis. Each card shows a date label and description with colored accent dots.';
/**
* React Component for the slide.
*/
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, milestones } = data;
const accentColors = ['#FF751F', '#FFBD59', '#FF914D', '#FF751F', '#FFBD59', '#FF914D'];
// Dynamic positioning based on number of items
const itemCount = milestones?.length || 0;
const cardWidth = 216;
const spacing = itemCount <= 3 ? 220 : itemCount <= 4 ? 180 : itemCount <= 5 ? 150 : 130;
const totalWidth = (itemCount - 1) * spacing + cardWidth;
const slideWidth = 1280;
const startX = (slideWidth - totalWidth) / 2;
// Generate config dynamically
const config = milestones?.map((_, i) => {
const isTop = i % 2 === 0;
const boxX = startX + i * spacing;
const dotCenterX = boxX + cardWidth / 2;
return {
boxX,
boxY: isTop ? 235.6 : 452.9,
yearX: boxX + 50,
yearY: isTop ? 267.7 : 481.2,
descX: boxX + 15,
descY: isTop ? 292.6 : 506.1,
dotX: dotCenterX - (isTop ? 23.7 : 10.9),
dotY: isTop ? 389.4 : 403.1,
type: isTop ? 'primary' : 'secondary',
dotColor: accentColors[i % accentColors.length],
};
}) || [];
// Generate white dots between items
const whiteDots = config.slice(0, -1).map((item, i) => {
const nextItem = config[i + 1];
return (item.boxX + cardWidth / 2 + nextItem.boxX + cardWidth / 2) / 2 - 6.65;
});
// Calculate timeline line boundaries
const lineStartX = config.length > 0 ? config[0].boxX + cardWidth / 2 - 50 : 62;
const lineEndX = config.length > 0 ? config[config.length - 1].boxX + cardWidth / 2 + 50 : 1218;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Title Section */}
<div className="absolute left-[373.4px] top-[68.6px] w-[540px] h-[49.3px] text-center">
<h1 className="text-[42.7px] font-bold tracking-[-1.6px] leading-[45.2px]" style={{ color: 'var(--background-text,#101828)' }}>
{title}
</h1>
<div className=" w-[116.6px] mx-auto h-[5.7px] overflow-visible mt-4"
style={{ backgroundColor: 'var(--primary-color,#9234EB)' }}
></div>
</div>
{/* Central Axis Line */}
<div className="absolute top-[413.1px] h-[2px]"
style={{ left: `${lineStartX}px`, width: `${lineEndX - lineStartX}px` }}
>
<svg width="100%" height="100%" overflow="visible">
<line x1="0%" y1="50%" x2="100%" y2="50%" stroke="var(--background-text,#000000)" strokeWidth="1" strokeDasharray="3,3" />
</svg>
</div>
{/* Timeline Elements Loop */}
{milestones && milestones.map((m, i) => {
const item = config[i];
return (
<div key={i}>
{/* Card Background */}
<div
className="absolute rounded-[13.3px]"
style={{ left: item.boxX, top: item.boxY, width: '216px', height: '139.2px', backgroundColor: 'var(--card-color,#F0F0F2)' }}
/>
{/* Year */}
<div
className="absolute text-center"
style={{ left: item.yearX, top: item.yearY, width: '115.3px', height: '21.2px' }}
>
<span className="text-[19.5px] font-normal" style={{ color: 'var(--background-text,#000000)' }}>{m.year}</span>
</div>
{/* Description */}
<div
className="absolute text-center leading-[17.8px]"
style={{ left: item.descX, top: item.descY, width: '186.1px', height: '51.5px' }}
>
<span className="text-[11.2px] font-normal" style={{ color: 'var(--background-text,#000000)' }}>{m.description}</span>
</div>
{/* Dot/Marker */}
{item.type === 'primary' ? (
<div
className="absolute flex items-center justify-center rounded-full"
style={{ left: item.dotX, top: item.dotY, width: '47.4px', height: '47.4px', border: '2px solid var(--background-text,#707070)' }}
/>
) : (
<div className="absolute rounded-full"
style={{ left: item.dotX, top: item.dotY, width: '21.8px', height: '21.8px', backgroundColor: item.dotColor, borderColor: 'var(--background-text,#000000)' }}
/>
)}
</div>
);
})}
{/* Decorative White Dots */}
{whiteDots.map((x, i) => (
<div
key={`white-dot-${i}`}
className="absolute rounded-full border-[0.7px] bg-[#FFF7ED]"
style={{ left: x, top: '407.4px', width: '13.3px', height: '13.3px', borderColor: 'var(--background-text,#000000)' }}
/>
))}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,671 @@
import React from 'react';
import * as z from "zod";
import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
Cell,
ReferenceLine,
} from "recharts";
export const layoutId = 'title-description-multi-chart-grid-bullets';
export const layoutName = 'Title Description With Multi-Chart Grid + Bullets';
export const layoutDescription = 'A dashboard layout featuring a title and description, up to 4 bullet points, and 1-4 auto-arranged charts in a responsive grid. Supports bar (vertical, horizontal, grouped, stacked, clustered, diverging), line, area, pie, donut, and scatter charts.';
// Color palettes
const CHART_COLOR_PALETTES = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899']
// Chart type enum
const ChartTypeEnum = z.enum([
'bar-vertical',
'bar-horizontal',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter',
]);
// Simple data point for single series
const SimpleDataPointSchema = z.object({
name: z.string(),
value: z.number(),
});
// Multi-series data point
const MultiSeriesDataPointSchema = z.object({
name: z.string(),
values: z.any().describe("Object with series names as keys and numbers as values (e.g., { 'Product A': 45, 'Product B': 62 })"),
});
// Diverging data point
const DivergingDataPointSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
// Scatter data point
const ScatterDataPointSchema = z.object({
x: z.number(),
y: z.number(),
name: z.string().optional(),
});
// Individual chart schema
const ChartItemSchema = z.object({
title: z.string().max(40).describe("Chart title").default("Chart Title"),
type: ChartTypeEnum.default('bar-vertical'),
data: z.union([
z.array(SimpleDataPointSchema),
z.array(MultiSeriesDataPointSchema),
z.array(DivergingDataPointSchema),
z.array(ScatterDataPointSchema),
]).default([
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
]),
series: z.array(z.string()).optional().describe("Series names for grouped/stacked charts"),
colorPalette: z.enum(['vibrant', 'ocean', 'forest', 'sunset', 'professional']).default('vibrant'),
});
// Main schema
export const Schema = z.object({
title: z.string().min(3).max(50).default('Data Analytics Dashboard').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(200).default('Comprehensive overview of key metrics and performance indicators across multiple data dimensions.').meta({
description: "Description text below the title",
}),
bullets: z.array(z.string().max(80)).max(6).default([
'Pipeline coverage above 3x target lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.',
'CAC payback under 6 months',
'Enterprise conversion improved QoQ',
'Expansion revenue driving growth',
'Retention above 95% across cohorts',
'Forecast accuracy improved this quarter',
]).meta({
description: "Up to 6 bullet points to highlight key takeaways",
}),
charts: z.array(ChartItemSchema).min(1).max(4).default([
{
title: 'Revenue by Quarter',
type: 'bar-vertical',
data: [
{ name: 'Q1', value: 125000 },
{ name: 'Q2', value: 158000 },
{ name: 'Q3', value: 142000 },
{ name: 'Q4', value: 189000 },
],
colorPalette: 'vibrant',
},
{
title: 'Market Distribution',
type: 'donut',
data: [
{ name: 'North America', value: 35 },
{ name: 'Europe', value: 28 },
{ name: 'Asia Pacific', value: 25 },
{ name: 'Others', value: 12 },
],
colorPalette: 'ocean',
},
{
title: 'Growth Trend',
type: 'line',
data: [
{ name: 'Jan', value: 30 },
{ name: 'Feb', value: 45 },
{ name: 'Mar', value: 52 },
{ name: 'Apr', value: 48 },
{ name: 'May', value: 67 },
{ name: 'Jun', value: 82 },
],
colorPalette: 'professional',
},
{
title: 'Department Performance',
type: 'bar-horizontal',
data: [
{ name: 'Sales', value: 87 },
{ name: 'Marketing', value: 72 },
{ name: 'Engineering', value: 95 },
{ name: 'Support', value: 68 },
],
colorPalette: 'sunset',
},
]).meta({
description: "Array of charts to display (1-4 charts)",
}),
showLegend: z.boolean().default(true).meta({
description: "Whether to show chart legends",
}),
showGrid: z.boolean().default(true).meta({
description: "Whether to show chart grid lines",
}),
});
export type MultiChartGridWithBulletsSlideData = z.infer<typeof Schema>;
interface MultiChartGridWithBulletsSlideLayoutProps {
data?: Partial<MultiChartGridWithBulletsSlideData>;
}
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-[10px] font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[9px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
// Mini chart renderer
const MiniChartRenderer: React.FC<{
chart: z.infer<typeof ChartItemSchema>;
showLegend: boolean;
showGrid: boolean;
}> = ({ chart, showLegend, showGrid }) => {
const data = chart.data as any[];
const series = chart.series || [];
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 9, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.5,
};
const transformMultiSeriesData = (data: any[], series: string[]) => {
return data.map(item => {
const result: Record<string, any> = { name: item.name };
series.forEach(s => {
result[s] = item.values?.[s] ?? 0;
});
return result;
});
};
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
const renderPieLabel = (props: any) => {
const { percent } = props;
if (percent < 0.08) return null;
return `${(percent * 100).toFixed(0)}%`;
};
const graphColors = (index: number, serieColor?: string) => {
const fallback = serieColor || CHART_COLOR_PALETTES[index % CHART_COLOR_PALETTES.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'bar-vertical':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data}
margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-horizontal':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-grouped-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[0, 3, 3, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [0, 3, 3, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-clustered': {
// If no series provided, fall back to simple bar chart behavior
if (series.length === 0) {
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} barSize={32}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} barSize={Math.max(12, 40 / series.length)} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-diverging': {
// Handle both formats: {positive, negative} or simple {value}
const hasDivergingFormat = data.length > 0 && ('positive' in data[0] || 'negative' in data[0]);
const transformedData = hasDivergingFormat
? transformDivergingData(data)
: data.map((item: any, idx: number) => ({
name: item.name,
positive: idx % 2 === 0 ? Math.abs(item.value) : 0,
negative: idx % 2 === 1 ? -Math.abs(item.value) : 0,
}));
const seriesLabels = chart.series || ['Positive', 'Negative'];
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" stackOffset="sign" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
<Bar dataKey="positive" name={seriesLabels[0]} fill={graphColors(0)} stackId="stack" radius={[0, 3, 3, 0]} />
<Bar dataKey="negative" name={seriesLabels[1]} fill={graphColors(3)} stackId="stack" radius={[3, 0, 0, 3]} />
</BarChart>
</ResponsiveContainer>
);
}
case 'line':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<LineChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Line type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} dot={{ fill: graphColors(0), r: 3 }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} fill="url(#areaGrad)" />
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Area key={s} type="monotone" dataKey={s} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.4} />
))}
</AreaChart>
</ResponsiveContainer>
);
}
case 'pie':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'donut':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" innerRadius="40%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false} paddingAngle={2}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<ScatterChart margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" dataKey="x" {...axisProps} tickFormatter={formatComma} />
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Scatter data={data}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
default:
return <div className="flex items-center justify-center h-full text-gray-400 text-sm">Unsupported chart type</div>;
}
};
// Grid layout calculator
const getGridLayout = (count: number): { cols: number; rows: number; className: string } => {
switch (count) {
case 1:
return { cols: 1, rows: 1, className: 'grid-cols-1' };
case 2:
return { cols: 2, rows: 1, className: 'grid-cols-2' };
case 3:
return { cols: 2, rows: 2, className: 'grid-cols-2' };
case 4:
return { cols: 2, rows: 2, className: 'grid-cols-2' };
default:
return { cols: 2, rows: 2, className: 'grid-cols-2' };
}
};
const MultiChartGridWithBulletsSlideLayout: React.FC<MultiChartGridWithBulletsSlideLayoutProps> = ({ data: slideData }) => {
const title = slideData?.title || 'Data Analytics Dashboard';
const description = slideData?.description || 'Comprehensive overview of key metrics and performance indicators.';
const charts = slideData?.charts || [];
const bullets = (slideData?.bullets || []).slice(0, 6);
const showLegend = slideData?.showLegend ?? true;
const showGrid = slideData?.showGrid ?? true;
const chartCount = charts.length;
const gridLayout = getGridLayout(chartCount);
const hasBullets = bullets.length > 0;
// Calculate chart height based on count
const getChartHeight = () => {
if (chartCount <= 2) return 260;
return 200;
};
return (
<>
<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-gradient-to-br from-slate-50 via-white to-indigo-50/30 relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "var(--heading-font-family, 'Poppins')",
background: "var(--background-color, linear-gradient(135deg, #f8fafc 0%, #ffffff 50%, #eef2ff 100%))"
}}
>
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-96 h-96 bg-gradient-to-bl from-violet-100/40 to-transparent rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-72 h-72 bg-gradient-to-tr from-cyan-100/30 to-transparent rounded-full blur-3xl" />
{/* Company branding */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="relative h-full px-10 pt-10 pb-8 flex flex-col z-10">
{/* Header Section - full width */}
<div className="mb-4">
<div className="max-w-[760px]">
<h1
className="text-[42.7px] font-bold mb-1 tracking-tight"
style={{ color: "var(--background-text, #111827)" }}
>
{title}
</h1>
<div
className="w-16 h-1 rounded-full"
style={{ backgroundColor: 'var(--primary-color, #8B5CF6)' }}
/>
<p
className="text-[16px] leading-relaxed mt-2"
style={{ color: "var(--background-text, #6B7280)" }}
>
{description}
</p>
</div>
</div>
{/* Charts + Bullets row - aligned at top */}
<div className="flex-1 flex gap-8 min-h-0">
{/* Charts Grid */}
<div className={`flex-1 grid ${gridLayout.className} gap-4`} style={{ height: '525px' }}>
{charts.map((chart, index) => (
<div
key={index}
className="backdrop-blur-sm rounded-xl border border-gray-100/80 shadow-sm flex flex-col overflow-hidden"
style={{ borderColor: 'var(--stroke,#F8F9FA)', backgroundColor: 'var(--card-color,#FFFFFF)' }}
>
{/* Chart Header */}
<div className="px-4 pt-3 pb-1">
<h3
className="text-sm font-semibold truncate"
style={{ color: "var(--background-text, #374151)" }}
>
{chart.title}
</h3>
</div>
{/* Chart Container */}
<div className="flex-1 px-2 pb-2" style={{ height: `${getChartHeight()}px` }}>
<MiniChartRenderer
chart={chart}
showLegend={showLegend && chartCount <= 4}
showGrid={showGrid}
/>
</div>
</div>
))}
</div>
{hasBullets && (
<div
className="w-full max-w-[300px] rounded-xl border shadow-sm p-4 flex flex-col gap-3 self-start"
style={{ backgroundColor: 'var(--card-color, #ffffff)', borderColor: 'var(--stroke, #e5e7eb)' }}
>
{bullets.map((bullet, index) => (
<div key={index} className="flex items-start gap-2">
<span
className="mt-[6px] h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: 'var(--primary-color, #8B5CF6)' }}
/>
<span className="text-[14px] leading-relaxed" style={{ color: 'var(--background-text, #4B5563)' }}>
{bullet}
</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
</>
);
};
export default MultiChartGridWithBulletsSlideLayout;

View file

@ -0,0 +1,698 @@
import React from 'react';
import * as z from "zod";
import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
Cell,
ReferenceLine,
} from "recharts";
export const layoutId = 'title-description-multi-chart-grid-metrics';
export const layoutName = 'Title Description With Multi-Chart Grid + Metrics';
export const layoutDescription = 'A dashboard layout featuring a title and description, up to 4 KPI metrics, and 1-6 auto-arranged charts in a responsive grid. Supports bar (vertical, horizontal, grouped, stacked, clustered, diverging), line, area, pie, donut, and scatter charts. Ideal for analytics overviews, KPI summaries, and performance dashboards.';
// Color palettes
const CHART_COLOR_PALETTES = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899']
// Chart type enum
const ChartTypeEnum = z.enum([
'bar-vertical',
'bar-horizontal',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter',
]);
// Simple data point for single series
const SimpleDataPointSchema = z.object({
name: z.string(),
value: z.number(),
});
// Multi-series data point
const MultiSeriesDataPointSchema = z.object({
name: z.string(),
values: z.any().describe("Object with series names as keys and numbers as values (e.g., { 'Product A': 45, 'Product B': 62 })"),
});
// Diverging data point
const DivergingDataPointSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
// Scatter data point
const ScatterDataPointSchema = z.object({
x: z.number(),
y: z.number(),
name: z.string().optional(),
});
// Individual chart schema
const ChartItemSchema = z.object({
title: z.string().max(40).describe("Chart title").default("Chart Title"),
type: ChartTypeEnum.default('bar-vertical'),
data: z.union([
z.array(SimpleDataPointSchema),
z.array(MultiSeriesDataPointSchema),
z.array(DivergingDataPointSchema),
z.array(ScatterDataPointSchema),
]).default([
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
]),
series: z.array(z.string()).optional().describe("Series names for grouped/stacked charts"),
colorPalette: z.enum(['vibrant', 'ocean', 'forest', 'sunset', 'professional']).default('vibrant'),
});
// Main schema
export const Schema = z.object({
title: z.string().min(3).max(50).default('Data Analytics Dashboard').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(200).default('Comprehensive overview of key metrics and performance indicators across multiple data dimensions.').meta({
description: "Description text below the title",
}),
metrics: z.array(z.object({
value: z.string().max(12).describe('The metric value'),
label: z.string().max(24).describe('The metric label'),
})).max(4).default([
{ value: '$3.5M', label: 'Pipeline' },
{ value: '28%', label: 'Conversion' },
{ value: '1.9x', label: 'ROI' },
{ value: '42', label: 'Accounts' },
]).meta({
description: "Array of up to 4 KPI metrics",
}),
charts: z.array(ChartItemSchema).min(1).max(6).default([
{
title: 'Revenue by Quarter',
type: 'bar-vertical',
data: [
{ name: 'Q1', value: 125000 },
{ name: 'Q2', value: 158000 },
{ name: 'Q3', value: 142000 },
{ name: 'Q4', value: 189000 },
],
colorPalette: 'vibrant',
},
{
title: 'Market Distribution',
type: 'donut',
data: [
{ name: 'North America', value: 35 },
{ name: 'Europe', value: 28 },
{ name: 'Asia Pacific', value: 25 },
{ name: 'Others', value: 12 },
],
colorPalette: 'ocean',
},
{
title: 'Growth Trend',
type: 'line',
data: [
{ name: 'Jan', value: 30 },
{ name: 'Feb', value: 45 },
{ name: 'Mar', value: 52 },
{ name: 'Apr', value: 48 },
{ name: 'May', value: 67 },
{ name: 'Jun', value: 82 },
],
colorPalette: 'professional',
},
{
title: 'Department Performance',
type: 'bar-horizontal',
data: [
{ name: 'Sales', value: 87 },
{ name: 'Marketing', value: 72 },
{ name: 'Engineering', value: 95 },
{ name: 'Support', value: 68 },
],
colorPalette: 'sunset',
},
{
title: 'Product Comparison',
type: 'bar-clustered',
data: [
{ name: 'Q1', values: { 'Product A': 45, 'Product B': 62 } },
{ name: 'Q2', values: { 'Product A': 58, 'Product B': 71 } },
{ name: 'Q3', values: { 'Product A': 72, 'Product B': 65 } },
],
series: ['Product A', 'Product B'],
colorPalette: 'vibrant',
},
{
title: 'Customer Feedback',
type: 'bar-diverging',
data: [
{ name: 'Quality', positive: 78, negative: 22 },
{ name: 'Service', positive: 65, negative: 35 },
{ name: 'Price', positive: 42, negative: 58 },
],
series: ['Satisfied', 'Unsatisfied'],
colorPalette: 'professional',
},
]).meta({
description: "Array of charts to display (1-6 charts)",
}),
showLegend: z.boolean().default(true).meta({
description: "Whether to show chart legends",
}),
showGrid: z.boolean().default(true).meta({
description: "Whether to show chart grid lines",
}),
});
export type MultiChartGridWithMetricsSlideData = z.infer<typeof Schema>;
interface MultiChartGridWithMetricsSlideLayoutProps {
data?: Partial<MultiChartGridWithMetricsSlideData>;
}
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-[10px] font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[9px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
// Mini chart renderer
const MiniChartRenderer: React.FC<{
chart: z.infer<typeof ChartItemSchema>;
showLegend: boolean;
showGrid: boolean;
}> = ({ chart, showLegend, showGrid }) => {
const data = chart.data as any[];
const series = chart.series || [];
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 9, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.5,
};
const transformMultiSeriesData = (data: any[], series: string[]) => {
return data.map(item => {
const result: Record<string, any> = { name: item.name };
series.forEach(s => {
result[s] = item.values?.[s] ?? 0;
});
return result;
});
};
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
const renderPieLabel = (props: any) => {
const { percent } = props;
if (percent < 0.08) return null;
return `${(percent * 100).toFixed(0)}%`;
};
const graphColors = (index: number, serieColor?: string) => {
const fallback = serieColor || CHART_COLOR_PALETTES[index % CHART_COLOR_PALETTES.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'bar-vertical':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data}
margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-horizontal':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-grouped-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[0, 3, 3, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [0, 3, 3, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-clustered': {
// If no series provided, fall back to simple bar chart behavior
if (series.length === 0) {
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} barSize={32}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} barSize={Math.max(12, 40 / series.length)} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-diverging': {
// Handle both formats: {positive, negative} or simple {value}
const hasDivergingFormat = data.length > 0 && ('positive' in data[0] || 'negative' in data[0]);
const transformedData = hasDivergingFormat
? transformDivergingData(data)
: data.map((item: any, idx: number) => ({
name: item.name,
positive: idx % 2 === 0 ? Math.abs(item.value) : 0,
negative: idx % 2 === 1 ? -Math.abs(item.value) : 0,
}));
const seriesLabels = chart.series || ['Positive', 'Negative'];
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" stackOffset="sign" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
<Bar dataKey="positive" name={seriesLabels[0]} fill={graphColors(0)} stackId="stack" radius={[0, 3, 3, 0]} />
<Bar dataKey="negative" name={seriesLabels[1]} fill={graphColors(3)} stackId="stack" radius={[3, 0, 0, 3]} />
</BarChart>
</ResponsiveContainer>
);
}
case 'line':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<LineChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Line type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} dot={{ fill: graphColors(0), r: 3 }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} fill="url(#areaGrad)" />
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Area key={s} type="monotone" dataKey={s} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.4} />
))}
</AreaChart>
</ResponsiveContainer>
);
}
case 'pie':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'donut':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" innerRadius="40%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false} paddingAngle={2}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<ScatterChart margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" dataKey="x" {...axisProps} tickFormatter={formatComma} />
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Scatter data={data}>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
default:
return <div className="flex items-center justify-center h-full text-gray-400 text-sm">Unsupported chart type</div>;
}
};
// Grid layout calculator
const getGridLayout = (count: number): { cols: number; rows: number; className: string } => {
switch (count) {
case 1:
return { cols: 1, rows: 1, className: 'grid-cols-1' };
case 2:
return { cols: 2, rows: 1, className: 'grid-cols-2' };
case 3:
return { cols: 3, rows: 1, className: 'grid-cols-3' };
case 4:
return { cols: 2, rows: 2, className: 'grid-cols-2' };
case 5:
return { cols: 3, rows: 2, className: 'grid-cols-3' };
case 6:
return { cols: 3, rows: 2, className: 'grid-cols-3' };
default:
return { cols: 2, rows: 2, className: 'grid-cols-2' };
}
};
const MultiChartGridWithMetricsSlideLayout: React.FC<MultiChartGridWithMetricsSlideLayoutProps> = ({ data: slideData }) => {
const title = slideData?.title || 'Data Analytics Dashboard';
const description = slideData?.description || 'Comprehensive overview of key metrics and performance indicators.';
const charts = slideData?.charts || [];
const metrics = (slideData?.metrics || []).slice(0, 4);
const showLegend = slideData?.showLegend ?? true;
const showGrid = slideData?.showGrid ?? true;
const chartCount = charts.length;
const gridLayout = getGridLayout(chartCount);
const hasMetrics = metrics.length > 0;
// Calculate chart height based on count
const getChartHeight = () => {
if (chartCount <= 2) return hasMetrics ? 230 : 280;
if (chartCount <= 3) return hasMetrics ? 210 : 260;
return hasMetrics ? 160 : 180;
};
return (
<>
<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-gradient-to-br from-slate-50 via-white to-indigo-50/30 relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "var(--heading-font-family, 'Poppins')",
background: "var(--background-color, linear-gradient(135deg, #f8fafc 0%, #ffffff 50%, #eef2ff 100%))"
}}
>
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-96 h-96 bg-gradient-to-bl from-violet-100/40 to-transparent rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-72 h-72 bg-gradient-to-tr from-cyan-100/30 to-transparent rounded-full blur-3xl" />
{/* Company branding */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(slideData as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="relative h-full px-10 pt-10 pb-8 flex flex-col z-10">
{/* Header Section */}
<div className="mb-4">
<div className="max-w-[760px]">
<h1
className="text-[42.7px] font-bold mb-1 tracking-tight"
style={{ color: "var(--background-text, #111827)" }}
>
{title}
</h1>
<div
className="w-16 h-1 rounded-full"
style={{ backgroundColor: 'var(--primary-color, #8B5CF6)' }}
/>
<p
className="text-[16px] leading-relaxed mt-2"
style={{ color: "var(--background-text, #6B7280)" }}
>
{description}
</p>
</div>
{hasMetrics && (
<div className="mt-4 grid grid-cols-4 gap-3">
{metrics.map((metric, index) => (
<div
key={index}
className="rounded-lg border px-3 py-2 shadow-sm"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<div className="text-[18px] font-semibold leading-tight" style={{ color: 'var(--background-text, #111827)' }}>
{metric.value}
</div>
<div className="text-[10px] font-medium uppercase tracking-wide mt-1" style={{ color: 'var(--background-text, #6B7280)' }}>
{metric.label}
</div>
</div>
))}
</div>
)}
</div>
{/* Charts Grid */}
<div className={`flex-1 grid ${gridLayout.className} gap-4`} style={{ height: hasMetrics ? '450px' : '525px' }}>
{charts.map((chart, index) => (
<div
key={index}
className="backdrop-blur-sm rounded-xl border border-gray-100/80 shadow-sm flex flex-col overflow-hidden"
style={{ borderColor: 'var(--stroke,#F8F9FA)', backgroundColor: 'var(--card-color,#FFFFFF)' }}
>
{/* Chart Header */}
<div className="px-4 pt-3 pb-1">
<h3
className="text-sm font-semibold truncate"
style={{ color: "var(--background-text, #374151)" }}
>
{chart.title}
</h3>
</div>
{/* Chart Container */}
<div className="flex-1 px-2 pb-2" style={{ height: `${getChartHeight()}px` }}>
<MiniChartRenderer
chart={chart}
showLegend={showLegend && chartCount <= 4}
showGrid={showGrid}
/>
</div>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default MultiChartGridWithMetricsSlideLayout;

View file

@ -0,0 +1,150 @@
// Zod Schema for the content elements
import * as z from "zod";
import React from "react";
export const Schema = z.object({
title: z.string().max(50).describe('The main heading of the slide').default('Go-to-Market Strategy'),
description: z.string().max(400).describe('Supporting description text for the slide').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
sections: z.array(z.object({
heading: z.string().max(15).describe('Heading for the column section'),
bulletPoints: z.array(z.object({
title: z.string().max(10).describe('Title label for the bullet point'),
description: z.string().max(20).describe('Description text for the bullet point'),
})).max(5).describe('List of bullet points in the section')
})).max(3).describe('Column sections containing bullet points').default([
{
heading: 'Paid Channels',
bulletPoints: [
{ title: 'LinkedIn Ads:', description: ' ABM Retargeting' },
{ title: 'Google Ads:', description: ' Intent Capture' },
{ title: 'Display:', description: ' Brand Awareness' }
]
},
{
heading: 'Organic Channels',
bulletPoints: [
{ title: 'SEO:', description: ' Thought Leadership' },
{ title: 'Content:', description: ' Education' },
{ title: 'Social:', description: ' Community' }
]
},
{
heading: 'Partnerships',
bulletPoints: [
{ title: 'Events:', description: ' Network Building' },
{ title: 'Co-Marketing:', description: ' Reach Extension' },
{ title: 'Referrals:', description: ' Trust Building' },
{ title: 'Referrals:', description: ' Trust Building' },
{ title: 'Referrals:', description: ' Trust Building' },
{ title: 'Referrals:', description: ' Trust Building' },
]
}
])
});
export const layoutId = 'title-description-three-columns-table';
export const layoutName = 'Title Description With Three Column Table';
export const layoutDescription = 'A layout featuring split title and description at the top, followed by a three-column table with colored headers and vertical bullet point sections below each.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, sections } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex flex-col px-[60px] py-[60px] font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
<div className="flex justify-between items-start mb-[100px] w-full">
<div className="flex flex-col basis-1/2">
<h1 className="text-[42.7px] font-bold leading-[1.05] tracking-[-2.0px]" style={{ color: 'var(--background-text,#101828)' }}>
{title}
</h1>
<div className="w-[116.6px] h-[5.7px] mt-4" style={{ backgroundColor: 'var(--primary-color,#9234EB)' }} />
</div>
<div className="basis-1/2 flex justify-end">
<p className="max-w-[510px] text-[16px] font-normal leading-[1.6] text-left" style={{ color: 'var(--background-text,#000000)' }}>
{description}
</p>
</div>
</div>
<div className="w-full flex flex-col mx-auto"
style={{ width: sections?.length === 1 ? '60%' : '100%' }}
>
<div className="grid rounded-t-[28px]" style={{ backgroundColor: 'var(--primary-color,#9234EC)', gridTemplateColumns: `repeat(${sections?.length ?? 0}, minmax(0, 1fr))` }}>
{sections?.map((section, idx) => (
<div
key={`header-${idx}`}
className={`py-[20px] px-[15px] flex items-center justify-center ${idx !== sections.length - 1 ? 'border-r-[1.3px] ' : ''}`}
style={{ borderColor: 'var(--stroke,#9134EB)' }}
>
<h2 className="text-[21.3px] font-bold text-center" style={{ color: 'var(--primary-text,#FFFFFF)' }}>
{section?.heading}
</h2>
</div>
))}
</div>
<div className="grid w-full items-center "
style={{ gridTemplateColumns: `repeat(${sections?.length ?? 0}, minmax(0, 1fr))` }}
>
{sections?.map((section, colIdx) => (
<div
key={`col-${colIdx}`}
className={`flex flex-col pt-[20px] pb-[30px] px-[15px] gap-y-[25px] ${colIdx !== sections.length - 1 ? 'border-r-[1.3px]' : ''}`}
style={{ backgroundColor: 'var(--card-color,#FCFCFC)', borderColor: 'var(--stroke,#F8F9FA)' }}
>
{section?.bulletPoints?.map((point, pointIdx) => (
<div key={`point-${colIdx}-${pointIdx}`} className="text-center min-h-[1.2em]">
<span className="text-[18.7px] font-bold" style={{ color: 'var(--background-text,#000000)' }}>
{point?.title}
</span>
<span className="text-[18.7px] font-normal" style={{ color: 'var(--background-text,#000000)' }}>
{point?.description}
</span>
</div>
))}
</div>
))}
</div>
</div>
<div className="absolute right-[320px] bottom-[362px] w-[15.8px] h-[15.8px] rounded-full opacity-10" style={{ backgroundColor: 'var(--primary-color,#9134EB)' }} />
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,133 @@
import * as z from 'zod';
import React from 'react';
export const Schema = z.object({
title: z.string().max(30).describe('The main heading of the slide').default('Funnel Performance'),
metricValue: z.string().max(10).describe('Primary metric value displayed').default('0.24%'),
metricLabel: z.string().max(100).describe('Label describing the metric').default('Overall\nVisit → Customer'),
funnelStages: z.array(z.object({
label: z.string().max(30).describe('Label for the stage'),
value: z.string().max(15).describe('Value displayed for this stage'),
conversionRate: z.string().max(10).describe('Rate value shown for the stage').optional(),
})).max(5).describe('Data points for the funnel visualization').default([
{ label: 'Visitors', value: '124,500', conversionRate: '10%' },
{ label: 'Leads', value: '12,450', conversionRate: '10%' },
{ label: 'Marketing Qualified', value: '4,356', conversionRate: '35%' },
]),
});
export const layoutId = 'title-metricValue-metricLabel-funnelStages';
export const layoutName = 'Metric With Funnel Bars';
export const layoutDescription = 'A layout featuring title with accent bar, left-side key metric with label, and horizontal funnel visualization on the right. Each funnel stage shows labeled pill, connector line, and colored bar with value and rate.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, metricValue, metricLabel, funnelStages } = data;
const barWidths = ['100%', '89.7%', '77.2%', '68.0%', '60.8%'];
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden px-[64px] py-[40px] font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Left Content Column */}
<div>
<h1 className="text-[42.7px] font-bold leading-[1.1] tracking-[-2px] mb-4 whitespace-pre-line" style={{ color: 'var(--background-text,#101828)' }}>
{title || 'Funnel Performance'}
</h1>
<div className="w-[116px] h-[6px]" style={{ backgroundColor: 'var(--primary-color,#9234EB)' }} />
</div>
<div className="flex flex-row justify-between h-full items-center">
<div className="basis-[35%] flex flex-col justify-between ">
<div className="mb-16">
<div className="text-[70.1px] font-normal leading-none" style={{ color: 'var(--background-text,#4D5463)' }}>
{metricValue || '0.00%'}
</div>
<div className="text-[17.4px] font-normal mt-4 whitespace-pre-line leading-relaxed" style={{ color: 'var(--background-text,#4D5463)' }}>
{metricLabel || 'Overall\nMetric Description'}
</div>
</div>
</div>
{/* Right Content Column - Funnel Infographic */}
<div className="flex-1 flex flex-col justify-center gap-[12px] h-full">
{funnelStages?.map((stage, index) => (
<div key={index} className="flex items-center">
{/* Stage Title Pill */}
<div className="flex-shrink-0 w-[127px] h-[60px] border-[1.3px] rounded-full flex items-center justify-center px-4 z-10 shadow-sm" style={{ borderColor: 'var(--primary-color,#9134EB)', backgroundColor: 'var(--card-color,#FFFFFF)' }}>
<span className="text-[16px] font-normal text-center leading-[1.2]"
style={{ color: 'var(--background-text,#000000)' }}
>
{stage.label}
</span>
</div>
{/* Connector Line Shape */}
<div className="flex-shrink-0 w-[45px] h-[2.7px]" style={{ backgroundColor: 'var(--primary-color,#9134EB)' }} />
{/* Funnel Data Bar */}
<div
className="h-[94px] flex items-center px-8 justify-between text-white"
style={{
backgroundColor: `var(--primary-color,#9134EB)`,
width: barWidths[index % barWidths.length],
color: 'var(--primary-text,#FFFFFF)'
}}
>
<div className="text-[22.5px] font-bold" style={{ color: 'var(--primary-text,#FFFFFF)' }}>
{stage.value}
</div>
{stage.conversionRate && (
<div className="flex flex-col items-end">
<span className="text-[14.2px] font-bold opacity-90 leading-none"
style={{ color: 'var(--primary-text,#FFFFFF)' }}
>
Conversion
</span>
<span className="text-[22.5px] font-bold mt-1 leading-none"
style={{ color: 'var(--primary-text,#FFFFFF)' }}
>
{stage.conversionRate}
</span>
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,599 @@
/**
* Title Metrics With Chart - Enhanced with multiple bar chart variations
*/
import * as z from "zod";
import React from "react";
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
BarChart,
Bar,
LabelList,
AreaChart,
Area,
PieChart,
Pie,
Cell,
ReferenceLine,
} from "recharts";
// Color palettes
const DEFAULT_CHART_COLORS = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899']
const ChartLegend: React.FC<{ series: z.infer<typeof SeriesSchema>[], colors: string[] }> = ({ series, colors }) => (
<div className="my-2 flex flex-wrap justify-center gap-6">
{series.map((serie, index) => (
<div key={serie.name} className="flex items-center gap-2 font-normal text-sm text-[#101828]" style={{ color: 'var(--background-text, #111827)' }}>
<span className="h-3 w-3 rounded-full" style={{ backgroundColor: serie.color || colors[index % colors.length] }} />
{serie.name}
</div>
))}
</div>
);
const SeriesSchema = z.object({
name: z.string().max(32),
color: z.string().optional(),
values: z.array(z.number()).min(1),
});
// Diverging data
const DivergingDataSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
export const Schema = z.object({
title: z.string().max(21).describe('The main heading of the slide').default('Spend & ROI Overview'),
description: z.string().max(100).describe('Supporting description text for the slide').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
chart: z.object({
title: z.string().max(64).optional(),
type: z.enum([
'line',
'bar',
'horizontalBar',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'area',
'area-stacked',
'pie',
'donut',
]).default('bar-stacked-horizontal'),
categories: z.array(z.string().max(16)).min(1),
series: z.array(SeriesSchema).min(1),
divergingData: z.array(DivergingDataSchema).optional(),
divergingLabels: z.tuple([z.string(), z.string()]).optional(),
colorPalette: z.enum(['vibrant', 'ocean', 'professional']).default('vibrant'),
}).describe('Chart configuration to render on the slide').default({
title: 'Revenue vs Spend',
type: 'line',
categories: ['Jan', 'Feb', 'Mar'],
series: [
{ name: 'Revenue', color: '#8910FA', values: [520, 660, 985] },
{ name: 'Spend', color: '#457EE5', values: [140, 245, 400] },
],
colorPalette: 'vibrant',
}),
metrics: z.array(
z.object({
value: z.string().max(7).describe('The displayed metric value'),
label: z.string().max(13).describe('Label describing the metric'),
})
).max(6).default([
{ value: '$1,800K', label: 'Total Planned' },
{ value: '$1,800K', label: 'Total Actual' },
{ value: '$1,800K', label: 'Total Planned' },
{ value: '$1,800K', label: 'Total Actual' },
{ value: '$1,800K', label: 'Total Planned' },
{ value: '$1,800K', label: 'Total Actual' },
]),
});
type SlideData = z.infer<typeof Schema>;
export const layoutId = 'title-metrics-with-chart';
export const layoutName = 'Chart With Sidebar Metrics';
export const layoutDescription = 'A two-column layout featuring a bold title, a large chart container on the left, and up to 6 vertical metrics on the right sidebar. Supports line, bar, grouped, stacked, clustered, diverging, area, pie, and donut charts.';
const buildChartData = (
categories: string[],
series: any[]
) => categories.map((category, index) => {
const entry: Record<string, string | number> = { name: category };
series.forEach((serie) => {
entry[serie.name] = serie.values[index] ?? 0;
});
return entry;
});
const buildSimpleData = (categories: string[], series: any[]) => {
if (series.length === 0) return [];
return categories.map((name, index) => ({
name,
value: series[0].values[index] ?? 0,
}));
};
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-sm font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-xs" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
const ChartRenderer: React.FC<{ chart: { categories: string[]; series: any[], type: string, divergingData?: any[], divergingLabels?: [string, string] } }> = ({ chart }) => {
const data = buildChartData(chart.categories, chart.series);
const simpleData = buildSimpleData(chart.categories, chart.series);
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 11, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.6,
};
const graphColors = (index: number, serieColor?: string) => {
const fallback = serieColor || DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'line':
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{chart.series.map((serie, index) => (
<Line
key={serie.name}
type="monotone"
dataKey={serie.name}
stroke={graphColors(index, serie.color)}
strokeWidth={3}
dot={{ r: 5, strokeWidth: 0 }}
/>
))}
</LineChart>
</ResponsiveContainer>
);
case 'bar':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart
data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[6, 6, 0, 0]}
>
<LabelList dataKey={serie.name} position="top" fill="var(--background-text,#101828)" style={{ fontSize: '13px', fontFamily: 'Poppins' }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'horizontalBar':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} layout="vertical" margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[0, 6, 6, 0]}
>
<LabelList dataKey={serie.name} position="middle" fill="var(--background-text,#101828)" style={{ fontSize: '13px', fontFamily: 'Poppins' }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-horizontal':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} layout="vertical" margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={80} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[0, 4, 4, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-vertical':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
stackId="stack"
fill={graphColors(index, serie.color)}
radius={index === chart.series.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-horizontal':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} layout="vertical" margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={80} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
stackId="stack"
fill={graphColors(index, serie.color)}
radius={index === chart.series.length - 1 ? [0, 4, 4, 0] : [0, 0, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-clustered':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[4, 4, 0, 0]}
barSize={Math.max(20, 60 / chart.series.length)}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-diverging': {
const divergingData = chart.divergingData ? transformDivergingData(chart.divergingData) :
chart.series.length >= 2 ? chart.categories.map((name, index) => ({
name,
positive: chart.series[0].values[index] ?? 0,
negative: -(chart.series[1].values[index] ?? 0),
})) : [];
const labels = chart.divergingLabels || [chart.series[0]?.name || 'Positive', chart.series[1]?.name || 'Negative'];
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={divergingData} layout="vertical" stackOffset="sign" margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={80} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Bar
dataKey="positive"
name={labels[0]}
fill={graphColors(0)}
stackId="stack"
radius={[0, 4, 4, 0]}
/>
<Bar
dataKey="negative"
name={labels[1]}
fill={graphColors(3)}
stackId="stack"
radius={[4, 0, 0, 4]}
/>
</BarChart>
</ResponsiveContainer>
);
}
case 'area':
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
{chart.series.map((serie, index) => (
<linearGradient key={serie.name} id={`metrics-gradient-${serie.name}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(index, serie.color)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(index, serie.color)} stopOpacity={0.05} />
</linearGradient>
))}
</defs>
{chart.series.map((serie, index) => (
<Area
key={serie.name}
type="monotone"
dataKey={serie.name}
stroke={graphColors(index, serie.color)}
strokeWidth={2}
fill={`url(#metrics-gradient-${serie.name})`}
/>
))}
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked':
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Area
key={serie.name}
type="monotone"
dataKey={serie.name}
stackId="1"
stroke={graphColors(index, serie.color)}
fill={graphColors(index, serie.color)}
fillOpacity={0.5}
/>
))}
</AreaChart>
</ResponsiveContainer>
);
case 'pie': {
const pieData = simpleData.length > 0 ? simpleData :
chart.categories.map((name, index) => ({
name,
value: chart.series.reduce((sum, s) => sum + (s.values[index] || 0), 0),
}));
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart margin={{ top: 15, right: 15, left: 15, bottom: 15 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie
data={pieData}
cx="50%"
cy="50%"
outerRadius={120}
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{pieData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
case 'donut': {
const donutData = simpleData.length > 0 ? simpleData :
chart.categories.map((name, index) => ({
name,
value: chart.series.reduce((sum, s) => sum + (s.values[index] || 0), 0),
}));
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart margin={{ top: 15, right: 15, left: 15, bottom: 15 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie
data={donutData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={120}
dataKey="value"
paddingAngle={2}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{donutData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
default:
return (
<div className="flex items-center justify-center h-[400px] text-gray-500">
Unsupported chart type: {chart.type}
</div>
);
}
};
const dynamicSlideLayout: React.FC<{ data: Partial<SlideData> }> = ({ data }) => {
const { title, description, chart, metrics } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden font-['Poppins'] gap-6 font-normal px-16 py-10"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content Area */}
<div className="mb-8 flex justify-between">
<div className="max-w-[400px]">
<h1 className="text-[42.7px] font-bold leading-tight mb-4 tracking-[-2px]" style={{ color: 'var(--background-text,#101828)' }}>
{title}
</h1>
<div className="w-[116.6px] h-[5.7px]" style={{ backgroundColor: 'var(--primary-color,#9234EB)' }} />
</div>
<p className="text-[17.8px] max-w-[560px] leading-[1.6]" style={{ color: 'var(--background-text,#000000)' }}>
{description}
</p>
</div>
{/* Graph Section */}
<div className=" flex justify-between items-center gap-6 w-full">
<div className=" flex-1 border rounded-xl p-4 shadow-sm" style={{ backgroundColor: 'var(--card-color,#F0F0F2)', borderColor: 'var(--stroke,#F0F0F2)' }}>
<ChartLegend series={chart?.series ?? []} colors={DEFAULT_CHART_COLORS} />
<ChartRenderer chart={chart ?? { categories: [], series: [], type: 'bar' }} />
</div>
<div className=" flex-1 flex flex-wrap items-center justify-start gap-x-8 gap-y-12 pl-4">
{metrics?.map((metric, index) => (
<div key={index} className=" max-w-[245px] w-full ">
<div className="text-[45px] font-medium leading-none" style={{ color: 'var(--background-text,#4D5463)' }}>
{metric.value}
</div>
<div className="flex items-center gap-3 mt-2">
<div className="w-[10.5px] h-[10.5px] rounded-full" style={{ backgroundColor: 'var(--primary-color,#9134EB)' }} />
<div className="text-[14px] font-normal uppercase tracking-wide" style={{ color: 'var(--background-text,#4D5463)' }}>
{metric.label}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,124 @@
import * as z from "zod";
/**
* Zod Schema for the slide content.
*/
export const Schema = z.object({
title: z.string().max(20).describe('The main heading of the slide').default('Risks & Constraints'),
items: z.array(z.object({
bgTitle: z.string().max(10).describe('Large category label displayed prominently').default('MARKET'),
subtitle: z.string().max(20).describe('Secondary heading for the item').default('Market Saturation'),
description: z.string().max(70).describe('Detailed description text for the item').default('Increasing competition in key verticals may pressure conversion rates and CAC')
})).max(3).describe('List of category items with details').default([
{
bgTitle: 'MARKETbaba',
subtitle: 'Market Saturation',
description: 'Increasing competition in key verticals may pressure conversion rates and CAC'
},
{
bgTitle: 'BUDGET',
subtitle: 'Budget Constraints',
description: 'Q1 budget reduction of 15% may limit ability to scale successful campaigns'
},
{
bgTitle: 'CAPACITY',
subtitle: 'Resource Capacity',
description: 'Content production team at 110% capacity; may impact content velocity'
}
])
});
/**
* Layout ID, Name, and Description.
*/
export const layoutId = 'title-three-column-risk-constraints-slide-layout';
export const layoutName = 'Three Column Category Cards';
export const layoutDescription = 'A layout with bold title and accent bar at top, followed by three column cards each featuring large category label, subtitle with accent dot, and detailed description.';
/**
* React Component for the slide.
*/
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, items } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Title Section */}
<div className="absolute left-[62.9px] top-[97.9px] w-[429.1px] h-[49.6px]">
<h1 className="text-[42.7px] font-bold tracking-[-1.6px] leading-[45.2px]" style={{ color: 'var(--background-text,#101828)' }}>
{title}
</h1>
<div className=" w-[116.6px] h-[5.7px] overflow-visible mt-4"
style={{ backgroundColor: 'var(--primary-color,#9234EB)' }}
></div>
</div>
{/* Decorative Underline */}
{/* Columns Container */}
<div className="absolute top-[305px] left-[62.9px] right-[62.9px] flex justify-between">
{items && items.map((item, index) => (
<div key={index} className="relative w-[334.8px]">
{/* Background Title */}
<div
className="mb-[5.6px] overflow-visible"
style={{ lineHeight: '53.3px' }}
>
<span className="text-[53.3px] font-normal uppercase" style={{ color: 'var(--background-text,#4D5463)' }}>
{item.bgTitle}
</span>
</div>
{/* Subtitle with Icon */}
<div className="flex items-center gap-[10px] mt-[10px] mb-[15px]">
<div
className="w-[15.8px] h-[15.8px] rounded-full shrink-0"
style={{ backgroundColor: 'var(--primary-color,#9134EB)' }}
/>
<span className="text-[17.4px] font-normal leading-[27.8px]" style={{ color: 'var(--background-text,#4D5463)' }}>
{item.subtitle}
</span>
</div>
{/* Description */}
<div className="w-full">
<p className="text-[23.1px] font-normal leading-[32.3px]" style={{ color: 'var(--background-text,#000000)' }}>
{item.description}
</p>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,156 @@
/**
* Zod Schema for Slide Content
*/
import * as z from "zod";
export const Schema = z.object({
title: z.string().max(30).describe('The main heading of the slide').default('Our Team Members'),
description: z.string().max(400).describe('Supporting description text for the slide').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
teamMembers: z.array(z.object({
name: z.string().max(40).describe('Name of the person'),
designation: z.string().max(50).describe('Role or position title'),
image: z.object({
__image_url__: z.string(),
__image_prompt__: z.string().max(100),
}).describe('Profile image of the person'),
bio: z.string().max(100).describe('Brief biography or description text'),
})).max(4).describe('List of person cards, up to 4 items').default([
{
name: 'Hannah Morales',
designation: 'Founder & CEO',
image: {
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'Professional headshot of a female executive',
},
bio: 'Focus on companies with 500+ employees.',
},
{
name: 'James Wilson',
designation: 'Head of Sales',
image: {
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'Professional headshot of a male executive',
},
bio: 'Focus on companies with 500+ employees.',
},
{
name: 'Helene Paquet',
designation: 'Chief Tech Officer',
image: {
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'Professional headshot of a female technology leader',
},
bio: 'Focus on companies with 500+ employees.',
},
{
name: 'Marcus Chen',
designation: 'Creative Director',
image: {
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'Professional headshot of a male creative professional',
},
bio: 'Focus on companies with 500+ employees.',
},
]),
});
/**
* Layout ID, Name, and Description
*/
export const layoutId = 'title-description-team-grid';
export const layoutName = 'Title Description With Photo Row';
export const layoutDescription = 'A top-aligned layout featuring split title and description sections at the top, followed by a horizontal row of up to 4 person cards. Each card shows name, designation, square photo, and brief bio.';
/**
* React Component: dynamicSlideLayout
*/
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, teamMembers } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex flex-col p-[80px] font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Decorative Slide Number Placeholder (Based on HTML but kept subtle) */}
<div className="absolute left-[52px] top-[34px] opacity-0">
<span className="text-[20px] font-normal tracking-[5px]" style={{ color: 'var(--background-text,#000000)' }}>1</span>
</div>
{/* Header Section */}
<div className="flex justify-between items-start mb-[80px]">
<div className="flex flex-col basis-1/2">
<h1 className="text-[42.7px] font-bold leading-[1.1] tracking-[-1.6px] mb-[16px]" style={{ color: 'var(--background-text,#101828)' }}>
{title}
</h1>
<div className="w-[116px] h-[6px]" style={{ backgroundColor: 'var(--primary-color,#9234EB)' }} />
</div>
<div className="basis-1/2 pl-[40px]">
<p className="text-[16px] font-normal leading-[1.6]" style={{ color: 'var(--background-text,#000000)' }}>
{description}
</p>
</div>
</div>
{/* Team Grid Section */}
<div className="flex gap-[40px] items-start justify-center flex-grow">
{teamMembers?.map((member, index) => (
<div key={index} className="flex flex-col flex-1 max-w-[215px]">
{/* Name and Designation */}
<div className="mb-[30px] h-[50px] flex flex-col justify-end">
<h2 className="text-[18px] font-medium leading-[1.4] tracking-[-0.1px]" style={{ color: 'var(--background-text,#2B3A38)' }}>
{member?.name}
</h2>
<p className="text-[18px] font-normal leading-[1.4] tracking-[-0.1px]" style={{ color: 'var(--background-text,#A8ABA3)' }}>
{member?.designation}
</p>
</div>
{/* Photo */}
<div className="w-full aspect-square mb-[20px] overflow-hidden rounded-[8px]">
<img
src={member?.image?.__image_url__}
alt={member?.image?.__image_prompt__}
className="w-full h-full object-cover"
/>
</div>
{/* Bio */}
<div className="mt-auto">
<p className="text-[16px] font-normal leading-[1.3]" style={{ color: 'var(--background-text,#000000)' }}>
{member?.bio}
</p>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,588 @@
/**
* Enhanced Full Width Chart Slide
* Supports multiple bar chart types including grouped, stacked, clustered, and diverging
*/
import {
LabelList,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
CartesianGrid,
XAxis,
YAxis,
Bar,
BarChart,
AreaChart,
Area,
PieChart,
Pie,
Cell,
Legend,
ReferenceLine,
} from 'recharts';
import * as z from 'zod';
import React from 'react';
// Default color palettes for beautiful charts (fallback)
const DEFAULT_CHART_COLORS = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899']
// Series schema for multi-series charts
const SeriesSchema = z.object({
name: z.string().max(32),
color: z.string().optional(),
values: z.array(z.number()).min(1),
});
// Diverging data
const DivergingDataSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
export const Schema = z.object({
title: z.string().max(30).describe('The main title of the slide').default('Spend & ROI Dashbo ard'),
chart: z.object({
title: z.string().max(64).optional(),
type: z.enum([
'line',
'bar',
'horizontalBar',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'area',
'area-stacked',
'pie',
'donut',
]).default('bar'),
categories: z.array(z.string().max(16)).min(1),
series: z.array(SeriesSchema).min(1),
// For diverging charts
divergingData: z.array(DivergingDataSchema).optional(),
divergingLabels: z.tuple([z.string(), z.string()]).optional(),
}).describe('Chart configuration to render on the slide').default({
title: 'Revenue vs Spend',
type: 'bar',
categories: ['Jan', 'Feb', 'Mar'],
series: [
{ name: 'Revenue', color: '#8910FA', values: [520, 660, 185, 200, 250, 300] },
{ name: 'Spend', color: '#457EE5', values: [140, 245, 400] },
],
}),
});
type FormData = z.infer<typeof Schema>;
export const layoutId = 'title-with-full-width-chart';
export const layoutName = 'Title With Full-Width Chart';
export const layoutDescription = 'A centered layout with a bold title and underline accent, followed by a full-width chart container with legend. Supports line, bar, grouped, stacked, clustered, diverging, area, pie, and donut charts.';
const ChartLegend: React.FC<{ series: z.infer<typeof SeriesSchema>[], colors: string[] }> = ({ series, colors }) => {
const totalSeries = series.length;
const getSpreadIndex = (index: number) => {
if (totalSeries <= 1) return 0;
return Math.floor((index * 10) / totalSeries) % 10;
};
return (
<div className="my-2 flex flex-wrap justify-center gap-6">
{series.map((serie, index) => {
const spreadIndex = getSpreadIndex(index);
const fallback = serie.color || colors[index % colors.length];
return (
<div key={serie.name} className="flex items-center gap-2 font-normal text-sm" style={{ color: 'var(--background-text,#101828)' }}>
<span
className="h-3 w-3 rounded-full"
style={{ backgroundColor: `var(--graph-${spreadIndex}, ${fallback})` }}
/>
{serie.name}
</div>
);
})}
</div>
);
};
const buildChartData = (
categories: string[],
series: z.infer<typeof SeriesSchema>[]
) => categories.map((category, index) => {
const entry: Record<string, string | number> = { name: category };
series.forEach((serie) => {
entry[serie.name] = serie.values[index] ?? 0;
});
return entry;
});
// Build simple data for single series charts
const buildSimpleData = (categories: string[], series: z.infer<typeof SeriesSchema>[]) => {
if (series.length === 0) return [];
return categories.map((name, index) => ({
name,
value: series[0].values[index] ?? 0,
}));
};
// Transform diverging data
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-sm font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-xs" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
const ChartRenderer: React.FC<{ chart: z.infer<typeof Schema>['chart'] }> = ({ chart }) => {
const colors = DEFAULT_CHART_COLORS;
const data = buildChartData(chart.categories, chart.series);
const simpleData = buildSimpleData(chart.categories, chart.series);
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 11, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.7,
};
const graphColors = (index: number, serieColor?: string) => {
const fallback = serieColor || colors[index % colors.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'line':
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{chart.series.map((serie, index) => (
<Line
key={serie.name}
type="monotone"
dataKey={serie.name}
stroke={graphColors(index, serie.color)}
strokeWidth={3}
dot={{ r: 5, strokeWidth: 0 }}
activeDot={{ r: 7 }}
/>
))}
</LineChart>
</ResponsiveContainer>
);
case 'bar':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[6, 6, 0, 0]}
>
<LabelList dataKey={serie.name} position="top" fill="#101828" style={{ fontSize: '11px', fontWeight: 600 }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'horizontalBar':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} layout="vertical" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={80} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[0, 6, 6, 0]}
>
<LabelList dataKey={serie.name} position="right" fill="#101828" style={{ fontSize: '11px', fontWeight: 600 }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[4, 4, 0, 0]}
>
<LabelList dataKey={serie.name} position="top" fill="#4B5563" style={{ fontSize: '10px', fontWeight: 600 }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-horizontal':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} layout="vertical" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={80} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[0, 4, 4, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-vertical':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
stackId="stack"
fill={graphColors(index, serie.color)}
radius={index === chart.series.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-horizontal':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} layout="vertical" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={80} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
stackId="stack"
fill={graphColors(index, serie.color)}
radius={index === chart.series.length - 1 ? [0, 4, 4, 0] : [0, 0, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-clustered':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Bar
key={serie.name}
dataKey={serie.name}
fill={graphColors(index, serie.color)}
radius={[4, 4, 0, 0]}
barSize={Math.max(20, 60 / chart.series.length)}
>
<LabelList dataKey={serie.name} position="top" fill="#4B5563" style={{ fontSize: '9px', fontWeight: 600 }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-diverging': {
const divergingData = chart.divergingData ? transformDivergingData(chart.divergingData) :
chart.series.length >= 2 ? chart.categories.map((name, index) => ({
name,
positive: chart.series[0].values[index] ?? 0,
negative: -(chart.series[1].values[index] ?? 0),
})) : [];
const labels = chart.divergingLabels || [chart.series[0]?.name || 'Positive', chart.series[1]?.name || 'Negative'];
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={divergingData} layout="vertical" stackOffset="sign" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={80} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Bar
dataKey="positive"
name={labels[0]}
fill={graphColors(0)}
stackId="stack"
radius={[0, 4, 4, 0]}
/>
<Bar
dataKey="negative"
name={labels[1]}
fill={graphColors(3)}
stackId="stack"
radius={[4, 0, 0, 4]}
/>
</BarChart>
</ResponsiveContainer>
);
}
case 'area':
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
{chart.series.map((serie, index) => (
<linearGradient key={serie.name} id={`gradient-${serie.name}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(index, serie.color)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(index, serie.color)} stopOpacity={0.05} />
</linearGradient>
))}
</defs>
{chart.series.map((serie, index) => (
<Area
key={serie.name}
type="monotone"
dataKey={serie.name}
stroke={graphColors(index, serie.color)}
strokeWidth={2}
fill={`url(#gradient-${serie.name})`}
/>
))}
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked':
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={data} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chart.series.map((serie, index) => (
<Area
key={serie.name}
type="monotone"
dataKey={serie.name}
stackId="1"
stroke={graphColors(index, serie.color)}
fill={graphColors(index, serie.color)}
fillOpacity={0.5}
/>
))}
</AreaChart>
</ResponsiveContainer>
);
case 'pie': {
const pieData = simpleData.length > 0 ? simpleData :
chart.categories.map((name, index) => ({
name,
value: chart.series.reduce((sum, s) => sum + (s.values[index] || 0), 0),
}));
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart margin={{ top: 15, right: 15, left: 15, bottom: 15 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie
data={pieData}
cx="50%"
cy="50%"
outerRadius={140}
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{pieData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
case 'donut': {
const donutData = simpleData.length > 0 ? simpleData :
chart.categories.map((name, index) => ({
name,
value: chart.series.reduce((sum, s) => sum + (s.values[index] || 0), 0),
}));
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie
data={donutData}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={140}
dataKey="value"
paddingAngle={2}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{donutData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
default:
return (
<div className="flex items-center justify-center h-[400px] text-gray-500">
Unsupported chart type: {chart.type}
</div>
);
}
};
const dynamicSlideLayout: React.FC<{ data: Partial<FormData> }> = ({ data }) => {
const { title, chart } = data;
const chartConfig = chart;
const colors = DEFAULT_CHART_COLORS;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex flex-col items-center px-[48px] py-[40px]"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
<div className="flex flex-col items-center gap-[16px]">
<h1 className="text-[42.7px] font-bold tracking-[-2px]" style={{ color: 'var(--background-text,#101828)' }}>
{title || 'Spend & ROI Dashboard'}
</h1>
<div className="w-[116.6px] h-[5.7px]" style={{ backgroundColor: 'var(--primary-color,#9234EB)' }} />
</div>
<div className="mt-10 w-full">
<div className="rounded-[24px] px-6 py-6 border w-full" style={{ backgroundColor: 'var(--card-color,#F0F0F2)', borderColor: 'var(--stroke,#F0F0F2)' }}>
<ChartLegend series={chartConfig?.series ?? []} colors={colors} />
<ChartRenderer chart={chartConfig ?? { categories: [], series: [], type: 'bar' }} />
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,129 @@
import * as z from "zod";
import React from "react";
export const Schema = z.object({
title: z.string().max(25).describe('The main heading of the slide').default('Key Insights & Learnings'),
cards: z.array(z.object({
heading: z.string().max(56).describe('Heading text for the card'),
description: z.string().max(46).describe('Description text for the card'),
})).describe('Array of cards with heading and description').default([
{
heading: 'ENTERPRISE ABM DELIVERS 3.2X HIGHER CONVERSION RATES',
description: 'Account-based campaigns targeting enterprises.',
},
{
heading: 'CONTENT + PAID SOCIAL COMBINATION DRIVES HIGHEST',
description: 'Leads from integrated campaigns had 47% faster.',
},
{
heading: 'MOBILE OPTIMIZATION INCREASED MOBILE',
description: 'Landing page redesign focused on mobile.',
},
{
heading: 'ENTERPRISE ABM DELIVERS 3.2X HIGHER CONVERSION RATES',
description: 'Account-based campaigns targeting enterprises.',
},
{
heading: 'CONTENT + PAID SOCIAL COMBINATION DRIVES HIGHEST',
description: 'Leads from integrated campaigns had 47% faster.',
},
{
heading: 'MOBILE OPTIMIZATION INCREASED MOBILE',
description: 'Landing page redesign focused on mobile.',
},
]),
});
export const layoutId = 'title-six-card-grid-slide-layout';
export const layoutName = 'Title With Six Text Cards Grid';
export const layoutDescription = 'A layout featuring left-aligned bold title with accent bar, followed by a 3x2 grid of up to 6 cards. Each card contains an accent-colored heading and description text.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, cards } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden p-16 font-['Poppins']"
style={{
fontFamily: 'var(--heading-font-family,Poppins)',
background: "var(--background-color,#ffffff)"
}}
>
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Title */}
<div className=" w-[761.6px] overflow-visible mb-10">
<div className="text-left" style={{ lineHeight: '45.2px' }}>
<span className="text-[42.7px] font-bold" style={{ letterSpacing: '-1.6px', color: 'var(--background-text,#101828)' }}>
{title}
</span>
</div>
<div
className=" w-[116.6px] h-[5.7px] overflow-visible mt-4"
style={{ backgroundColor: 'var(--primary-color,#9234EB)' }}
></div>
</div>
{/* Decorative Underline */}
<div className="grid grid-cols-3 gap-2">
{/* Grid of Insight Cards */}
{cards?.slice(0, 6).map((card, index) => {
return (
<div key={index} className="px-4 py-6">
{/* Card Heading */}
<div
className=" overflow-visible"
>
<div className="text-left" >
<span className="text-[21.3px] font-bold" style={{ color: 'var(--background-text,#9234EC)' }}>
{card.heading}
</span>
</div>
</div>
{/* Card Description */}
<div
className=" overflow-visible mt-2"
>
<div className="text-left" >
<span className="text-[23.1px] font-normal" style={{ color: 'var(--background-text,#000000)' }}>
{card.description}
</span>
</div>
</div>
</div>
);
})}
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,5 @@
{
"description": "New general purpose layouts for common presentation elements",
"ordered": false,
"default": false
}

View file

@ -0,0 +1,92 @@
import * as z from 'zod';
import React from 'react';
export const Schema = z.object({
title: z.string().max(30).describe('The main title of the slide').default('Key Takeaways'),
description: z.string().max(300).describe('The main paragraph description on the slide').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
bullets: z.array(z.object({
heading: z.string().max(40).describe('The heading for this bullet point'),
description: z.string().max(120).describe('The description for this bullet point'),
})).max(5).describe('A list of up to 5 bullet points, each with a heading and description').default([
{ heading: 'Market expansion', description: 'Prioritize high-growth verticals and geographic regions with strong demand.' },
{ heading: 'Customer retention', description: 'Reduce churn through proactive support and tailored success programs.' },
{ heading: 'Product innovation', description: 'Ship features that align with top customer requests and usage data.' },
{ heading: 'Operational efficiency', description: 'Automate repetitive workflows to free capacity for strategic work.' },
{ heading: 'Team enablement', description: 'Invest in training and tools so teams can execute at scale.' },
]),
});
export const layoutId = 'title-description-bullet-list';
export const layoutName = 'Title Description Bullet List';
export const layoutDescription = 'A clean two-column layout with a main title and description on the left, and up to 5 bullet points on the right. Each bullet has a heading and a short description. Ideal for key takeaways, feature highlights, or structured lists with context.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, bullets } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
<div className="flex h-full w-full items-center justify-between px-[115px] gap-20">
{/* Left Section: Title + Description */}
<div className="flex flex-col flex-[1.2] justify-center">
{title && (
<h1
className="text-[42.7px] font-bold mb-6 leading-tight"
style={{ letterSpacing: '-1.6px', color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
)}
{description && (
<p
className="text-[16px] leading-[28.5px] max-w-[475px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
)}
</div>
{/* Right Section: Bullet list */}
<div className="flex flex-col flex-1 justify-center gap-5">
{bullets?.map((item, index) => (
<div
key={index}
className="flex flex-col justify-center px-5 py-4 rounded-[3.4px] border-l-4"
style={{
backgroundColor: 'var(--card-color,#F7F8FF)',
borderLeftColor: 'var(--stroke,#4C68DF)',
}}
>
<h3
className="text-[17.5px] font-bold leading-[21px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{item.heading}
</h3>
<p
className="text-[15.3px] leading-[18.4px] mt-1"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{item.description}
</p>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,153 @@
/**
* Zod Schema for the Thank You / Contact slide.
*/
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
import * as z from 'zod'
export const Schema = z.object({
title: z.string().max(20).describe('The title of the slide').default('Thank you'),
description: z.string().max(350).describe('The description of the slide').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
contactItems: z.array(z.object({
icon: z.object({
__icon_url__: z.string(),
__icon_query__: z.string().max(30),
}),
label: z.string().max(20).describe('The label of item'),
value: z.string().max(50).describe('The value of item'),
})).max(3).describe('A list of items').default([
{
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg',
__icon_query__: 'envelope',
},
label: 'Email',
value: 'presenton@gmail.com',
},
{
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg',
__icon_query__: 'phone',
},
label: 'Phone',
value: '+977-98000000',
},
{
icon: {
__icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg',
__icon_query__: 'globe',
},
label: 'Website',
value: 'www.presenton.com',
},
]),
});
export const layoutId = 'title-description-contact-list';
export const layoutName = 'Title Description Contact List';
export const layoutDescription = 'A slide featuring a title and description on the left with up to 3 icon-enhanced contact items on the right. Each item has an icon, label, and value.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, contactItems } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
<div className="flex h-full w-full items-center justify-between px-[115px] gap-20">
{/* Left Section: Content */}
<div className="flex flex-col flex-[1.2] justify-center">
{title && (
<h1
style={{ fontWeight: 700, letterSpacing: '-1.6px', color: 'var(--background-text,#002BB2)' }}
className="text-[42.7px] font-bold mb-6 leading-tight"
>
{title}
</h1>
)}
{description && (
<p
style={{ fontWeight: 400, color: 'var(--background-text,#002BB2)' }}
className="text-[16.0px] leading-[28.5px] max-w-[475px]"
>
{description}
</p>
)}
</div>
{/* Right Section: Contact List */}
<div className="flex flex-col flex-1 justify-center gap-10">
{contactItems?.map((item, index) => (
<div key={index} className="flex items-center gap-5">
{/* Icon Circle */}
<div
className="w-[66px] h-[66px] rounded-full border flex items-center justify-center flex-shrink-0"
style={{
borderColor: 'var(--stroke,#4C68DF)',
backgroundColor: 'var(--primary-color,#F7F8FF)',
}}
>
{item.icon?.__icon_url__ && (
<RemoteSvgIcon
url={item.icon.__icon_url__}
strokeColor={"currentColor"}
className="w-8 h-8 object-contain"
color="var(--primary-text, #4C68DF)"
title={item.icon.__icon_query__}
/>
)}
</div>
{/* Text Box */}
<div
className="flex flex-col justify-center px-6 py-4 rounded-[3.4px] w-full h-[92px]"
style={{
backgroundColor: 'var(--card-color,#F7F8FF)',
}}
>
{item.label && (
<div
style={{ fontWeight: 700, color: 'var(--background-text,#002BB2)' }}
className="text-[17.5px] font-bold leading-[21.0px]"
>
{item.label}
</div>
)}
{item.value && (
<div
style={{ fontWeight: 400, color: 'var(--background-text,#002BB2)' }}
className="text-[15.3px] leading-[18.4px] mt-1"
>
{item.value}
</div>
)}
</div>
</div>
))}
</div>
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,129 @@
/**
* Zod Schema for the slide content.
*/
import * as z from 'zod'
export const Schema = z.object({
title: z.string().describe('The main heading of the slide').max(30).default('Description and Metrix'),
description: z.string().describe('Supporting description text').max(250).default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
primaryMetrics: z.array(z.object({
label: z.string().max(25).describe('Label text for the metric'),
value: z.string().max(8).describe('Value displayed for the metric')
})).max(3).describe('List of primary metrics displayed').default([
{ label: 'Main Challenge: Delayed Client', value: '85%' },
{ label: 'Main Challenge: Delayed Client', value: '85%' },
{ label: 'Main Challenge: Delayed Client', value: '85%' }
]),
secondaryMetrics: z.array(z.object({
label: z.string().max(25).describe('Label text for the metric'),
value: z.string().max(8).describe('Value displayed for the metric')
})).max(3).describe('List of secondary metrics displayed').default([
{ label: 'Total Registered Users', value: '>500 M' },
{ label: 'Total Registered Users', value: '>500 M' },
{ label: 'Total Registered Users', value: '>500 M' }
])
});
export const layoutId = 'title-description-dual-metrics-grid';
export const layoutName = 'Title Description Dual Metrics Grid';
export const layoutDescription = 'A slide featuring a title and description on the left, with two columns of metric cards on the right - primary metrics in bold styling and secondary metrics in subtle styling. Supports up to 6 metrics total (3 per column).';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, primaryMetrics, secondaryMetrics } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex items-center px-[52px] justify-between"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{/* Left Content Section */}
<div className="flex flex-col max-w-[522px] gap-[30px]">
{title && (
<h1 className="text-[42.7px] font-bold leading-[1.05] tracking-[-1.6px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
)}
{description && (
<p className="text-[16px] font-normal leading-[1.5]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
)}
</div>
{/* Right Metrics Section */}
<div className="flex gap-[25px] items-center">
{/* Primary Metrics Column */}
<div className="flex flex-col gap-[20px]">
{primaryMetrics?.map((metric, index) => (
<div
key={index}
className="w-[259.3px] h-[152.8px] rounded-[3.5px] p-[28px] flex flex-col justify-between"
style={{
backgroundColor: 'var(--card-color,#6B89E6)',
}}
>
<div className="text-[17.8px] font-normal leading-[1.4]"
style={{ color: 'var(--background-text,#FFFFFF)' }}
>
{metric.label}
</div>
<div className="text-[39.3px] font-bold leading-none"
style={{ color: 'var(--background-text,#FFFFFF)' }}
>
{metric.value}
</div>
</div>
))}
</div>
{/* Secondary Metrics Column */}
<div className="flex flex-col gap-[20px]">
{secondaryMetrics?.map((metric, index) => (
<div
key={index}
className="w-[259px] h-[152.8px] rounded-[3.5px] p-[28px] flex flex-col justify-between"
style={{
backgroundColor: 'var(--card-color,#F7F8FF)',
}}
>
<div className="text-[17.8px] font-normal leading-[1.4]"
style={{ color: 'var(--background-text,#244CD9)' }}
>
{metric.label}
</div>
<div className="text-[39.3px] font-bold leading-none"
style={{ color: 'var(--background-text,#244CD9)' }}
>
{metric.value}
</div>
</div>
))}
</div>
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,134 @@
import { RemoteSvgIcon } from '@/app/hooks/useRemoteSvgIcon';
import * as z from 'zod'
export const Schema = z.object({
title: z.string().max(40).describe('The main heading of the slide').default('Process / Workflow Flow'),
description: z.string().max(300).describe('Supporting description text').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
processItems: z.array(z.object({
icon: z.object({
__icon_url__: z.string(),
__icon_query__: z.string().max(30)
}).describe('The icon representing the item'),
heading: z.string().max(10).describe('The heading of the item'),
subDescription: z.string().max(100).describe('The sub description of the item')
})).max(5).describe('A list of up to 5 items').default([
{
icon: { __icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg', __icon_query__: 'growth chart' },
heading: '2021',
subDescription: 'Briefly elaborate on what you want to discuss.Briefly elaborate on what you want to discuss.'
},
{
icon: { __icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg', __icon_query__: 'money bag' },
heading: '2020',
subDescription: 'Briefly elaborate on what you want to discuss.'
},
{
icon: { __icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg', __icon_query__: 'global' },
heading: '2019',
subDescription: 'Briefly elaborate on what you want to discuss.'
},
{
icon: { __icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg', __icon_query__: 'handshake' },
heading: '2018',
subDescription: 'Briefly elaborate on what you want to discuss.'
},
{
icon: { __icon_url__: 'https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/bold/checks-bold.svg', __icon_query__: 'lightbulb' },
heading: '2017',
subDescription: 'Briefly elaborate on what you want to discuss.'
}
])
});
export const layoutId = 'title-description-icon-timeline';
export const layoutName = 'Title Description Icon Timeline';
export const layoutDescription = 'A slide featuring a title and description on the left with a vertical list of icon-enhanced items on the right. Each item has a circular icon, heading, and description.';
const dynamicSlideLayout = ({ data }: { data: Partial<z.infer<typeof Schema>> }) => {
const { title, description, processItems } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex flex-row items-center justify-between gap-10 px-[115px]"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{/* Left Content */}
<div className="flex-[0.5] flex flex-col justify-center pr-10">
<h1 className="text-[42.7px] font-bold leading-[1.1] mb-[20px] tracking-[-1.6px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
<p className="text-[16.0px] font-normal leading-[1.7]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
</div>
{/* Right Content - Timeline List */}
<div className="flex-[0.5] flex flex-col justify-center gap-[24px]">
{processItems?.map((item, index) => (
<div key={index} className="flex flex-row items-center gap-[16px]">
{/* Icon inside circle */}
<div className="w-[66px] h-[66px] rounded-full border-[1.3px] border-[#4C68DF] flex items-center justify-center flex-shrink-0"
style={{
borderColor: 'var(--stroke,#4C68DF)',
backgroundColor: 'var(--primary-color,#F7F8FF)',
}}
>
<div className="w-[40px] h-[40px] flex items-center justify-center">
<RemoteSvgIcon
url={item.icon?.__icon_url__}
strokeColor={"currentColor"}
className="w-full h-full object-contain"
color="var(--primary-text, #4C68DF)"
title={item.icon.__icon_query__}
/>
</div>
</div>
{/* Content Box */}
<div className="bg-[#F7F8FF] rounded-[3px] w-full h-[92px] flex flex-col justify-center px-[18px]"
style={{
backgroundColor: 'var(--card-color,#F7F8FF)',
}}
>
<div className="text-[17.5px] font-bold leading-[1.2]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{item.heading}
</div>
<div className="text-[15.3px] font-normal leading-[1.2] mt-[6px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{item.subDescription}
</div>
</div>
</div>
))}
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,84 @@
import * as z from 'zod'
export const Schema = z.object({
title: z.string().max(50).describe('The main title of the slide').default('Image with Description'),
description: z.string().max(350).describe('The body text or description of the slide').default('Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies.'),
image: z.object({
__image_url__: z.string().describe('The URL of the featured image'),
__image_prompt__: z.string().max(100).describe('A description for generating a replacement image')
}).describe('The large image displayed on the right side of the slide').default({
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'Close up of diverse business hands joined together in a circle, representing teamwork and partnership.'
})
});
export const layoutId = 'title-description-image-right';
export const layoutName = 'Title Description Image Right';
export const layoutDescription = 'A two-column slide with a title and description on the left and a large featured image on the right. The balanced layout provides equal emphasis on textual content and visual representation.';
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, image } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full h-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden font-['Montserrat'] font-normal"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
<div className="flex w-full h-full items-center justify-between px-[50px] py-[40px]">
{/* Left Side: Title and Description */}
<div className="flex flex-col gap-[15px] w-full max-w-[525px]">
{title && (
<h1
className="font-bold text-[42.7px] leading-tight"
style={{ letterSpacing: '-1.6px', color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
)}
{description && (
<p
className="font-normal text-[16px] leading-[28.5px]"
style={{ color: 'var(--background-text,#244CD9)' }}
>
{description}
</p>
)}
</div>
{/* Right Side: Featured Image */}
<div className="flex-shrink-0 w-[531.3px] h-[559.0px] rounded-lg overflow-hidden">
{image?.__image_url__ && (
<img
src={image.__image_url__}
alt={image.__image_prompt__ || 'Slide Visual'}
className="w-full h-full object-cover"
style={{ objectPosition: '52.9% 44.07%' }}
/>
)}
</div>
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,478 @@
/**
* Zod Schema for the slide content.
* Enhanced with multiple chart type support
*/
import * as z from 'zod'
import React from 'react';
import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Bar, LabelList, LineChart, Line, PieChart, Pie, Cell, AreaChart, Area, ScatterChart, Scatter, ReferenceLine } from 'recharts';
const chartTypeEnum = z.enum([
'bar',
'horizontalBar',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter'
]).default('bar');
export const Schema = z.object({
title: z.string().max(25).describe("The main heading of the slide").default("Barchart with Description & metrix"),
description: z.string().max(180).describe("Supporting description text").default("Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies."),
metricCards: z.array(z.object({
heading: z.string().max(25).describe("Label text for the metric card"),
value: z.string().max(8).describe("Value displayed in the metric card")
})).max(4).describe("List of metric cards displayed in a grid").default([
{ heading: "Main Challenge: Delayed Client", value: "85%" },
{ heading: "Total Registered Users", value: ">500 M" },
]),
chartTitle: z.string().max(12).describe("Title text for the chart").default("Campaign A"),
chartCategory: z.string().max(12).describe("Secondary label text").default("Top Campaign"),
chartFooterLabel: z.string().max(15).describe("Footer label text for the chart").default("Engangment Rate"),
chartType: chartTypeEnum.describe('Type of chart to display'),
chartData: z.object({
columns: z.array(z.string()).max(2).describe("Names of the chart data series"),
rows: z.array(z.object({
label: z.string().describe("The x-axis category label"),
value1: z.number().describe("The first series value"),
value2: z.number().optional().describe("The second series value (optional for single-series charts)")
})).max(4).describe("The data rows for the chart")
}).describe("The data used to render the chart").default({
columns: ["Planned Budget", "Actual Budget"],
rows: [
{ label: "Paid Social", value1: 920, value2: 485 },
{ label: "Content Marketing", value1: 380, value2: 412 },
{ label: "Events & Sponsorships", value1: 450, value2: 468 },
{ label: "SEO & Organic", value1: 280, value2: 276 }
]
})
});
export const layoutId = "title-description-metrics-chart";
export const layoutName = "Title Description Metrics Chart";
export const layoutDescription = "A slide featuring a main title, description, metric cards grid on the left, and a chart panel on the right. Supports bar, grouped bar, stacked bar, clustered bar, diverging bar, horizontal bar, line, area, pie, donut, and scatter chart types.";
const CHART_COLORS = ['#244CD9', '#6B89E6', '#4169E1', '#7B9FFF', '#EC4899', '#10B981'];
// Custom tooltip matching TitleWithFullWidthChart style
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-sm font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-xs" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
// Helper function for graph colors
const graphColors = (index: number, fallbackColor?: string) => {
const fallback = fallbackColor || CHART_COLORS[index % CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const {
title,
description,
metricCards,
chartTitle,
chartCategory,
chartFooterLabel,
chartData,
chartType = 'bar'
} = data;
const renderChart = () => {
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 11, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.7,
};
const hasValue2 = (chartData?.rows?.some(row => (row.value2 ?? 0) > 0)) ?? false;
switch (chartType) {
case 'line':
return (
<ResponsiveContainer width="100%" height={350}>
<LineChart data={chartData?.rows} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="label" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{chartData?.columns?.map((column, index) => (
<Line type="monotone" dataKey={column} name={column} stroke={graphColors(index)} strokeWidth={3} dot={{ r: 5, strokeWidth: 0 }} activeDot={{ r: 7 }} />
))}
</LineChart>
</ResponsiveContainer>
);
case 'horizontalBar':
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData?.rows} layout="vertical" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="label" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px', color: '#6B7280' }} formatter={(value, entry, index) => chartData?.columns?.[index] || value} />
<Bar dataKey="value1" stackId="a" fill={graphColors(0)} barSize={35} radius={hasValue2 ? undefined : [0, 4, 4, 0]} label={{ position: 'inside', fill: 'var(--primary-text, #fff)', fontSize: 12 }} />
{hasValue2 && <Bar dataKey="value2" stackId="a" fill={graphColors(1)} radius={[0, 4, 4, 0]} barSize={35} label={{ position: 'inside', fill: 'var(--primary-text, #fff)', fontSize: 12 }} />}
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical':
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData?.rows} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="label" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chartData?.columns?.map((column, index) => (
<Bar dataKey={column} name={column} fill={graphColors(index)} radius={[4, 4, 0, 0]}>
<LabelList dataKey={column} position="top" fill="var(--background-text, #4B5563)" style={{ fontSize: '10px', fontWeight: 600 }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-horizontal':
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData?.rows} layout="vertical" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="label" {...axisProps} width={80} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chartData?.columns?.map((column, index) => (
<Bar dataKey={column} name={column} fill={graphColors(index)} radius={[0, 4, 4, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-vertical':
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData?.rows} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="label" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry, index) => chartData?.columns?.[index] || value} />
<Bar dataKey="value1" stackId="stack" fill={graphColors(0)} barSize={50} radius={hasValue2 ? undefined : [4, 4, 0, 0]} />
{hasValue2 && <Bar dataKey="value2" stackId="stack" fill={graphColors(1)} radius={[4, 4, 0, 0]} barSize={50} />}
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-horizontal':
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData?.rows} layout="vertical" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="label" {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry, index) => chartData?.columns?.[index] || value} />
<Bar dataKey="value1" stackId="stack" fill={graphColors(0)} barSize={30} radius={hasValue2 ? undefined : [0, 4, 4, 0]} />
{hasValue2 && <Bar dataKey="value2" stackId="stack" fill={graphColors(1)} radius={[0, 4, 4, 0]} barSize={30} />}
</BarChart>
</ResponsiveContainer>
);
case 'bar-clustered':
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData?.rows} barGap={2} barCategoryGap="20%" margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="label" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chartData?.columns?.map((column, index) => (
<Bar dataKey={column} name={column} fill={graphColors(index)} radius={[4, 4, 0, 0]} barSize={30}>
<LabelList dataKey={column} position="top" fill="var(--background-text, #4B5563)" style={{ fontSize: '9px', fontWeight: 600 }} />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
);
case 'bar-diverging': {
const divergingData = chartData?.rows?.map(row => ({
label: row.label,
positive: row.value1,
negative: -(row.value2 ?? 0),
})) || [];
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={divergingData} layout="vertical" stackOffset="sign" margin={{ top: 15, right: 30, left: 10, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="label" {...axisProps} width={80} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="var(--background-text, #9CA3AF)" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chartData?.columns?.map((column, index) => (
<Bar dataKey="positive" name={column} fill={graphColors(index)} stackId="stack" radius={[0, 4, 4, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'area':
return (
<ResponsiveContainer width="100%" height={350}>
<AreaChart data={chartData?.rows} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="label" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
{chartData?.columns?.map((column, index) => (
<linearGradient key={`metricsArea-${index}`} id={`metricsArea-${index}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(index)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(index)} stopOpacity={0.05} />
</linearGradient>
))}
</defs>
{chartData?.columns?.map((column, index) => (
<Area type="monotone" dataKey={column} name={column} stroke={graphColors(index)} strokeWidth={2} fill={`url(#metricsArea${index})`} />
))}
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked':
return (
<ResponsiveContainer width="100%" height={350}>
<AreaChart data={chartData?.rows} margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="label" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
{chartData?.columns?.map((column, index) => (
<Area type="monotone" dataKey={column} name={column} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.5} />
))}
</AreaChart>
</ResponsiveContainer>
);
case 'pie': {
const pieData = chartData?.rows?.map((row) => ({
name: row.label,
value: row.value1 + (row.value2 ?? 0),
})) || [];
return (
<ResponsiveContainer width="100%" height={350}>
<PieChart margin={{ top: 15, right: 15, left: 15, bottom: 15 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie data={pieData} cx="50%" cy="50%" outerRadius={120} dataKey="value" label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}>
{pieData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
case 'donut': {
const donutData = chartData?.rows?.map((row) => ({
name: row.label,
value: row.value1 + (row.value2 ?? 0),
})) || [];
return (
<ResponsiveContainer width="100%" height={350}>
<PieChart>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie data={donutData} cx="50%" cy="50%" innerRadius={60} outerRadius={120} dataKey="value" paddingAngle={2} label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}>
{donutData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
case 'scatter': {
const scatterData = chartData?.rows?.map((row) => ({
x: row.value1,
y: row.value2 ?? 0,
name: row.label,
})) || [];
return (
<ResponsiveContainer width="100%" height={350}>
<ScatterChart margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" dataKey="x" name={chartData?.columns?.[0]} {...axisProps} tickFormatter={formatComma} />
<YAxis type="number" dataKey="y" name={chartData?.columns?.[1]} {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3', fill: 'transparent' }} />
<Scatter data={scatterData} fill={graphColors(0)}>
{scatterData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
}
case 'bar':
default:
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData?.rows} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="label" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px', color: '#6B7280' }} formatter={(value, entry, index) => chartData?.columns?.[index] || value} />
<Bar dataKey="value1" stackId="a" fill={graphColors(0)} radius={hasValue2 ? [0, 0, 0, 0] : [2, 2, 0, 0]} barSize={70} label={{ position: 'inside', fill: 'var(--primary-text, #fff)', fontSize: 12 }} />
{hasValue2 && <Bar dataKey="value2" stackId="a" fill={graphColors(1)} radius={[2, 2, 0, 0]} barSize={70} label={{ position: 'inside', fill: 'var(--primary-text, #fff)', fontSize: 12 }} />}
</BarChart>
</ResponsiveContainer>
);
}
};
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex font-['Montserrat'] font-normal"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
<div className="flex w-full h-full p-[60px] gap-[40px]">
{/* Left Section */}
<div className="flex flex-col basis-[45%] justify-center h-full">
<h1 className="text-[42.7px] font-bold leading-[1.1] tracking-[-1.6px] mb-[20px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
<p className="text-[16px] font-normal leading-[1.6] mb-[60px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
<div className="grid grid-cols-2 gap-[18px]">
{metricCards?.map((metric, index) => {
const isEven = index % 2 === 0;
return (
<div
key={index}
className="p-[25px] rounded-[4px] h-[152px] flex flex-col justify-between"
style={{ backgroundColor: isEven ? 'var(--card-color,#6B89E6)' : 'var(--card-color,#F7F8FF)' }}
>
<span
className="text-[17.8px] leading-[1.3]"
style={{ color: isEven ? 'var(--background-text,#FFFFFF)' : 'var(--background-text,#244CD9)' }}
>
{metric.heading}
</span>
<span
className="text-[39.3px] font-bold leading-[1.1]"
style={{ color: isEven ? 'var(--background-text,#FFFFFF)' : 'var(--background-text,#244CD9)' }}
>
{metric.value}
</span>
</div>
);
})}
</div>
</div>
{/* Right Section */}
<div className="flex flex-col basis-[55%] h-full justify-center">
<div className="flex justify-between items-end mb-[20px] px-[20px]">
<h2 className="text-[28.4px] font-bold"
style={{ color: 'var(--background-text,#244CD9)' }}
>
{chartTitle}
</h2>
<span className="text-[18.7px] font-normal opacity-70"
style={{ color: 'var(--background-text,#244CD9)' }}
>
{chartCategory}
</span>
</div>
<div className="flex-grow border rounded-[6px] p-[20px] relative"
style={{
backgroundColor: 'var(--card-color,#FFFFFF)',
borderColor: 'var(--stroke,#F0F0F2)',
}}
>
<ResponsiveContainer width="100%" height="100%">
{renderChart()}
</ResponsiveContainer>
<div className="absolute bottom-[-15px] right-[20px] text-[16px] font-normal italic opacity-80"
style={{ color: 'var(--background-text,#244CD9)' }}
>
{chartFooterLabel}
</div>
</div>
</div>
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,131 @@
// Zod Schema for the content elements
import * as z from 'zod'
export const Schema = z.object({
title: z.string().max(25).describe("The main heading of the slide").default("Title Description Metrics Image"),
description: z.string().max(150).describe("Supporting description text").default("A clean professional slide featuring a title, description, a 2x2 grid of highlight metrics, and a large vertical image on the right."),
metrics: z.array(z.object({
label: z.string().max(15).describe("Label text for the metric"),
value: z.string().max(10).describe("Value displayed for the metric")
})).max(4).describe("Collection of metric items displayed in a grid").default([
{ label: "Main Challenge: Delayed Client", value: "85%" },
{ label: "Total Registered Users", value: ">500 M" },
{ label: "Main Challenge: Delayed Client", value: "85%" },
{ label: "Total Registered Users", value: ">500 M" }
]),
image: z.object({
__image_url__: z.string(),
__image_prompt__: z.string().max(100)
}).default({
__image_url__: 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png',
__image_prompt__: 'A professional business collaboration scene with people putting their hands together in the center.'
}).describe("The primary visual image displayed on the right side of the slide.")
})
// Layout ID, Name and Description
export const layoutId = "title-description-metrics-image";
export const layoutName = "Title Description Metrics Image";
export const layoutDescription = "A slide featuring a title, description, and 2x2 metrics grid on the left with a large vertical image on the right. The alternating metric cards create visual hierarchy.";
// React Component
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, metrics, image } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
<div className="flex h-full w-full">
{/* Left Side Content Section */}
<div className="flex-[1.2] flex flex-col justify-center pl-[52px] pr-[40px] py-[60px]">
{/* Header Section */}
<div className="mb-8">
{title && (
<h1 className="text-[#002BB2] font-bold text-[42.7px] leading-[1.1] tracking-[-1.6px] mb-6"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
)}
{description && (
<p className="text-[#244CD9] font-normal text-[16px] leading-[1.6] max-w-[520px]"
style={{ color: 'var(--background-text,#244CD9)' }}
>
{description}
</p>
)}
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-2 gap-x-[26px] gap-y-[20px] max-w-[550px]">
{metrics?.map((metric, index) => {
const isBlueBackground = index % 2 === 0;
return (
<div
key={index}
className={`flex flex-col justify-center p-[28px] rounded-[3.5px] w-[259px] h-[153px] ${isBlueBackground ? 'bg-[#6B89E6]' : 'bg-[#F7F8FF]'
}`}
style={{
backgroundColor: isBlueBackground ? 'var(--card-color,#6B89E6)' : 'var(--card-color,#F7F8FF)',
}}
>
<div
className={` font-normal text-[17.8px] leading-[1.4] mb-4 ${isBlueBackground ? 'text-white' : 'text-[#244CD9]'
}`}
style={{
color: isBlueBackground ? 'var(--background-text,#FFFFFF)' : 'var(--background-text,#244CD9)',
}}
>
{metric.label}
</div>
<div
className={` font-bold text-[39.3px] leading-tight ${isBlueBackground ? 'text-white' : 'text-[#244CD9]'
}`}
style={{
color: isBlueBackground ? 'var(--background-text,#FFFFFF)' : 'var(--background-text,#244CD9)',
}}
>
{metric.value}
</div>
</div>
);
})}
</div>
</div>
{/* Right Side Image Section */}
<div className="flex-1 flex items-center justify-center pr-[52px]">
<div className="w-[531px] h-[559px] overflow-hidden rounded-sm">
<img
src={image?.__image_url__ || 'https://presenton-public-assets.s3.ap-southeast-1.amazonaws.com/replaceable_template_image.png'}
alt={image?.__image_prompt__ || 'Layout visual content'}
className="w-full h-full object-cover"
style={{ objectPosition: '52.9% 44.07%' }}
/>
</div>
</div>
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,533 @@
/**
* Neo-modern layout: title, description, and 16 charts in a responsive grid.
* Same schema and chart types as neo-general MultiChartGridSlideLayout.
*/
import React from 'react';
import * as z from "zod";
import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
Cell,
ReferenceLine,
} from "recharts";
export const layoutId = 'title-description-multi-chart-grid';
export const layoutName = 'Title Description With Multi-Chart Grid';
export const layoutDescription = 'A neo-modern dashboard layout with title, description, and 16 auto-arranged charts. Supports bar (vertical, horizontal, grouped, stacked, clustered, diverging), line, area, pie, donut, and scatter charts.';
const DEFAULT_CHART_COLORS = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899'];
const ChartTypeEnum = z.enum([
'bar-vertical',
'bar-horizontal',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter',
]);
const SimpleDataPointSchema = z.object({
name: z.string(),
value: z.number(),
});
const MultiSeriesDataPointSchema = z.object({
name: z.string(),
values: z.any(),
});
const DivergingDataPointSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
const ScatterDataPointSchema = z.object({
x: z.number(),
y: z.number(),
name: z.string().optional(),
});
const ChartItemSchema = z.object({
title: z.string().max(40).default("Chart Title"),
type: ChartTypeEnum.default('bar-vertical'),
data: z.union([
z.array(SimpleDataPointSchema),
z.array(MultiSeriesDataPointSchema),
z.array(DivergingDataPointSchema),
z.array(ScatterDataPointSchema),
]).default([
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
]),
series: z.array(z.string()).optional(),
colorPalette: z.enum(['vibrant', 'ocean', 'forest', 'sunset', 'professional']).default('vibrant'),
});
export const Schema = z.object({
title: z.string().min(3).max(50).default('Data Analytics Dashboard'),
description: z.string().min(10).max(200).default('Comprehensive overview of key metrics and performance indicators across multiple data dimensions.'),
charts: z.array(ChartItemSchema).min(1).max(6).default([
{ title: 'Revenue by Quarter', type: 'bar-vertical', data: [{ name: 'Q1', value: 125000 }, { name: 'Q2', value: 158000 }, { name: 'Q3', value: 142000 }, { name: 'Q4', value: 189000 }], colorPalette: 'vibrant' },
{ title: 'Market Distribution', type: 'donut', data: [{ name: 'North America', value: 35 }, { name: 'Europe', value: 28 }, { name: 'Asia Pacific', value: 25 }, { name: 'Others', value: 12 }], colorPalette: 'ocean' },
{ title: 'Growth Trend', type: 'line', data: [{ name: 'Jan', value: 30 }, { name: 'Feb', value: 45 }, { name: 'Mar', value: 52 }, { name: 'Apr', value: 48 }, { name: 'May', value: 67 }, { name: 'Jun', value: 82 }], colorPalette: 'professional' },
{ title: 'Department Performance', type: 'bar-horizontal', data: [{ name: 'Sales', value: 87 }, { name: 'Marketing', value: 72 }, { name: 'Engineering', value: 95 }, { name: 'Support', value: 68 }], colorPalette: 'sunset' },
{ title: 'Product Comparison', type: 'bar-clustered', data: [{ name: 'Q1', values: { 'Product A': 45, 'Product B': 62 } }, { name: 'Q2', values: { 'Product A': 58, 'Product B': 71 } }, { name: 'Q3', values: { 'Product A': 72, 'Product B': 65 } }], series: ['Product A', 'Product B'], colorPalette: 'vibrant' },
{ title: 'Customer Feedback', type: 'bar-diverging', data: [{ name: 'Quality', positive: 78, negative: 22 }, { name: 'Service', positive: 65, negative: 35 }, { name: 'Price', positive: 42, negative: 58 }], series: ['Satisfied', 'Unsatisfied'], colorPalette: 'professional' },
]),
showLegend: z.boolean().default(true),
showGrid: z.boolean().default(true),
});
export type MultiChartGridSlideData = z.infer<typeof Schema>;
interface MultiChartGridSlideLayoutProps {
data?: Partial<MultiChartGridSlideData>;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border rounded-lg shadow-lg px-3 py-2" style={{ backgroundColor: 'var(--card-color, #ffffff)', borderColor: 'var(--stroke, #e5e7eb)' }}>
<p className="text-[10px] font-semibold mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[9px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
const MiniChartRenderer: React.FC<{
chart: z.infer<typeof ChartItemSchema>;
showLegend: boolean;
showGrid: boolean;
}> = ({ chart, showLegend, showGrid }) => {
const data = chart.data as any[];
const series = chart.series || [];
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 9, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.5,
};
const transformMultiSeriesData = (data: any[], series: string[]) => {
return data.map(item => {
const result: Record<string, any> = { name: item.name };
series.forEach(s => { result[s] = item.values?.[s] ?? 0; });
return result;
});
};
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
const renderPieLabel = (props: any) => {
const { percent } = props;
if (percent < 0.08) return null;
return `${(percent * 100).toFixed(0)}%`;
};
const graphColors = (index: number) => {
const fallback = DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'bar-vertical':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-horizontal':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-grouped-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[0, 3, 3, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [0, 3, 3, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-clustered': {
if (series.length === 0) {
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} barSize={32}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} barSize={Math.max(12, 40 / series.length)} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-diverging': {
const hasDivergingFormat = data.length > 0 && ('positive' in data[0] || 'negative' in data[0]);
const transformedData = hasDivergingFormat
? transformDivergingData(data)
: data.map((item: any, idx: number) => ({
name: item.name,
positive: idx % 2 === 0 ? Math.abs(item.value) : 0,
negative: idx % 2 === 1 ? -Math.abs(item.value) : 0,
}));
const seriesLabels = chart.series || ['Positive', 'Negative'];
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" stackOffset="sign" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="var(--background-text, #9CA3AF)" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
<Bar dataKey="positive" name={seriesLabels[0]} fill={graphColors(0)} stackId="stack" radius={[0, 3, 3, 0]} />
<Bar dataKey="negative" name={seriesLabels[1]} fill={graphColors(3)} stackId="stack" radius={[3, 0, 0, 3]} />
</BarChart>
</ResponsiveContainer>
);
}
case 'line':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<LineChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Line type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} dot={{ fill: graphColors(0), r: 3 }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
<linearGradient id={`areaGrad-${chart.title}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} fill={`url(#areaGrad-${chart.title})`} />
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Area key={s} type="monotone" dataKey={s} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.4} />
))}
</AreaChart>
</ResponsiveContainer>
);
}
case 'pie':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'donut':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" innerRadius="40%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false} paddingAngle={2}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<ScatterChart margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" dataKey="x" {...axisProps} tickFormatter={formatComma} />
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Scatter data={data}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
default:
return <div className="flex items-center justify-center h-full text-gray-400 text-sm">Unsupported chart type</div>;
}
};
const getGridLayout = (count: number): { className: string } => {
switch (count) {
case 1: return { className: 'grid-cols-1' };
case 2: return { className: 'grid-cols-2' };
case 3: return { className: 'grid-cols-3' };
case 4: return { className: 'grid-cols-2' };
case 5:
case 6: return { className: 'grid-cols-3' };
default: return { className: 'grid-cols-2' };
}
};
const TitleDescriptionMultiChartGridLayout: React.FC<MultiChartGridSlideLayoutProps> = ({ data: slideData }) => {
const title = slideData?.title || 'Data Analytics Dashboard';
const description = slideData?.description || 'Comprehensive overview of key metrics and performance indicators.';
const charts = slideData?.charts || [];
const showLegend = slideData?.showLegend ?? true;
const showGrid = slideData?.showGrid ?? true;
const chartCount = charts.length;
const gridLayout = getGridLayout(chartCount);
const getChartHeight = () => {
if (chartCount <= 2) return 280;
if (chartCount <= 3) return 260;
return 180;
};
return (
<>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet" />
<div
className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex font-normal"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4 z-10">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }} className="w-[2px] h-4" />
{(slideData as any)?.__companyName__ && (
<span className="text-sm font-semibold" style={{ color: 'var(--background-text, #002BB2)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>
)}
</div>
</div>
</div>
)}
<div className="relative w-full h-full p-[60px] pt-[50px] flex flex-col">
<div className="mb-5">
<h1
className="text-[42.7px] font-bold leading-[1.1] tracking-[-1.6px] mb-[12px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
<p
className="text-[16px] font-normal leading-[1.6]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
</div>
<div className={`flex-1 grid ${gridLayout.className} gap-4 min-h-0`} style={{ height: 'calc(100% - 140px)' }}>
{charts.map((chart, index) => (
<div
key={index}
className="rounded-[6px] border flex flex-col overflow-hidden"
style={{ borderColor: 'var(--stroke,#F0F0F2)', backgroundColor: 'var(--card-color,#FFFFFF)' }}
>
<div className="px-4 pt-3 pb-1">
<h3
className="text-sm font-semibold truncate"
style={{ color: 'var(--background-text,#374151)' }}
>
{chart.title}
</h3>
</div>
<div className="flex-1 px-2 pb-2 min-h-0" style={{ height: `${getChartHeight()}px` }}>
<MiniChartRenderer
chart={chart}
showLegend={showLegend && chartCount <= 4}
showGrid={showGrid}
/>
</div>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default TitleDescriptionMultiChartGridLayout;

View file

@ -0,0 +1,557 @@
/**
* Neo-modern layout: title, description, up to 6 bullets, and 14 charts in a responsive grid.
*/
import React from 'react';
import * as z from "zod";
import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
Cell,
ReferenceLine,
} from "recharts";
export const layoutId = 'title-description-multi-chart-grid-bullets';
export const layoutName = 'Title Description With Multi-Chart Grid + Bullets';
export const layoutDescription = 'A neo-modern dashboard with title, description, up to 6 bullet points, and 14 auto-arranged charts. Supports bar (vertical, horizontal, grouped, stacked, clustered, diverging), line, area, pie, donut, and scatter charts.';
const DEFAULT_CHART_COLORS = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899'];
const ChartTypeEnum = z.enum([
'bar-vertical',
'bar-horizontal',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter',
]);
const SimpleDataPointSchema = z.object({
name: z.string(),
value: z.number(),
});
const MultiSeriesDataPointSchema = z.object({
name: z.string(),
values: z.any(),
});
const DivergingDataPointSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
const ScatterDataPointSchema = z.object({
x: z.number(),
y: z.number(),
name: z.string().optional(),
});
const ChartItemSchema = z.object({
title: z.string().max(40).default("Chart Title"),
type: ChartTypeEnum.default('bar-vertical'),
data: z.union([
z.array(SimpleDataPointSchema),
z.array(MultiSeriesDataPointSchema),
z.array(DivergingDataPointSchema),
z.array(ScatterDataPointSchema),
]).default([
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
]),
series: z.array(z.string()).optional(),
colorPalette: z.enum(['vibrant', 'ocean', 'forest', 'sunset', 'professional']).default('vibrant'),
});
export const Schema = z.object({
title: z.string().min(3).max(50).default('Data Analytics Dashboard'),
description: z.string().min(10).max(200).default('Comprehensive overview of key metrics and performance indicators across multiple data dimensions.'),
bullets: z.array(z.string().max(80)).max(6).default([
'Pipeline coverage above 3x target.',
'CAC payback under 6 months.',
'Enterprise conversion improved QoQ.',
'Expansion revenue driving growth.',
'Retention above 95% across cohorts.',
'Forecast accuracy improved this quarter.',
]),
charts: z.array(ChartItemSchema).min(1).max(4).default([
{ title: 'Revenue by Quarter', type: 'bar-vertical', data: [{ name: 'Q1', value: 125000 }, { name: 'Q2', value: 158000 }, { name: 'Q3', value: 142000 }, { name: 'Q4', value: 189000 }], colorPalette: 'vibrant' },
{ title: 'Market Distribution', type: 'donut', data: [{ name: 'North America', value: 35 }, { name: 'Europe', value: 28 }, { name: 'Asia Pacific', value: 25 }, { name: 'Others', value: 12 }], colorPalette: 'ocean' },
{ title: 'Growth Trend', type: 'line', data: [{ name: 'Jan', value: 30 }, { name: 'Feb', value: 45 }, { name: 'Mar', value: 52 }, { name: 'Apr', value: 48 }, { name: 'May', value: 67 }, { name: 'Jun', value: 82 }], colorPalette: 'professional' },
{ title: 'Department Performance', type: 'bar-horizontal', data: [{ name: 'Sales', value: 87 }, { name: 'Marketing', value: 72 }, { name: 'Engineering', value: 95 }, { name: 'Support', value: 68 }], colorPalette: 'sunset' },
]),
showLegend: z.boolean().default(true),
showGrid: z.boolean().default(true),
});
export type MultiChartGridSlideData = z.infer<typeof Schema>;
interface MultiChartGridSlideLayoutProps {
data?: Partial<MultiChartGridSlideData>;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border rounded-lg shadow-lg px-3 py-2" style={{ backgroundColor: 'var(--card-color, #ffffff)', borderColor: 'var(--stroke, #e5e7eb)' }}>
<p className="text-[10px] font-semibold mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[9px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
const MiniChartRenderer: React.FC<{
chart: z.infer<typeof ChartItemSchema>;
showLegend: boolean;
showGrid: boolean;
}> = ({ chart, showLegend, showGrid }) => {
const data = chart.data as any[];
const series = chart.series || [];
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 9, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.5,
};
const transformMultiSeriesData = (data: any[], series: string[]) => {
return data.map(item => {
const result: Record<string, any> = { name: item.name };
series.forEach(s => { result[s] = item.values?.[s] ?? 0; });
return result;
});
};
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
const renderPieLabel = (props: any) => {
const { percent } = props;
if (percent < 0.08) return null;
return `${(percent * 100).toFixed(0)}%`;
};
const graphColors = (index: number) => {
const fallback = DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'bar-vertical':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-horizontal':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-grouped-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[0, 3, 3, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [0, 3, 3, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-clustered': {
if (series.length === 0) {
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} barSize={32}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} barSize={Math.max(12, 40 / series.length)} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-diverging': {
const hasDivergingFormat = data.length > 0 && ('positive' in data[0] || 'negative' in data[0]);
const transformedData = hasDivergingFormat
? transformDivergingData(data)
: data.map((item: any, idx: number) => ({
name: item.name,
positive: idx % 2 === 0 ? Math.abs(item.value) : 0,
negative: idx % 2 === 1 ? -Math.abs(item.value) : 0,
}));
const seriesLabels = chart.series || ['Positive', 'Negative'];
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" stackOffset="sign" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="var(--background-text, #9CA3AF)" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
<Bar dataKey="positive" name={seriesLabels[0]} fill={graphColors(0)} stackId="stack" radius={[0, 3, 3, 0]} />
<Bar dataKey="negative" name={seriesLabels[1]} fill={graphColors(3)} stackId="stack" radius={[3, 0, 0, 3]} />
</BarChart>
</ResponsiveContainer>
);
}
case 'line':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<LineChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Line type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} dot={{ fill: graphColors(0), r: 3 }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
<linearGradient id={`areaGrad-${chart.title}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} fill={`url(#areaGrad-${chart.title})`} />
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Area key={s} type="monotone" dataKey={s} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.4} />
))}
</AreaChart>
</ResponsiveContainer>
);
}
case 'pie':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'donut':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" innerRadius="40%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false} paddingAngle={2}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<ScatterChart margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" dataKey="x" {...axisProps} tickFormatter={formatComma} />
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Scatter data={data}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
default:
return <div className="flex items-center justify-center h-full text-gray-400 text-sm">Unsupported chart type</div>;
}
};
const getGridLayout = (count: number): { className: string } => {
switch (count) {
case 1: return { className: 'grid-cols-1' };
case 2: return { className: 'grid-cols-2' };
case 3:
case 4: return { className: 'grid-cols-2' };
default: return { className: 'grid-cols-2' };
}
};
const TitleDescriptionMultiChartGridWithBulletsLayout: React.FC<MultiChartGridSlideLayoutProps> = ({ data: slideData }) => {
const title = slideData?.title || 'Data Analytics Dashboard';
const description = slideData?.description || 'Comprehensive overview of key metrics and performance indicators.';
const charts = slideData?.charts || [];
const bullets = (slideData?.bullets || []).slice(0, 6);
const showLegend = slideData?.showLegend ?? true;
const showGrid = slideData?.showGrid ?? true;
const chartCount = charts.length;
const gridLayout = getGridLayout(chartCount);
const hasBullets = bullets.length > 0;
const getChartHeight = () => {
if (chartCount <= 2) return 260;
return 200;
};
return (
<>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet" />
<div
className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex font-normal"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4 z-10">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }} className="w-[2px] h-4" />
{(slideData as any)?.__companyName__ && (
<span className="text-sm font-semibold" style={{ color: 'var(--background-text, #002BB2)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>
)}
</div>
</div>
</div>
)}
<div className="relative w-full h-full p-[60px] pt-[50px] flex flex-col">
<div className="mb-5">
<h1
className="text-[42.7px] font-bold leading-[1.1] tracking-[-1.6px] mb-[12px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
<p
className="text-[16px] font-normal leading-[1.6]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
</div>
<div className="flex-1 flex gap-8 min-h-0">
<div className={`flex-1 grid ${gridLayout.className} gap-4 min-h-0`} style={{ height: '525px' }}>
{charts.map((chart, index) => (
<div
key={index}
className="rounded-[6px] border flex flex-col overflow-hidden"
style={{ borderColor: 'var(--stroke,#F0F0F2)', backgroundColor: 'var(--card-color,#FFFFFF)' }}
>
<div className="px-4 pt-3 pb-1">
<h3
className="text-sm font-semibold truncate"
style={{ color: 'var(--background-text,#374151)' }}
>
{chart.title}
</h3>
</div>
<div className="flex-1 px-2 pb-2 min-h-0" style={{ height: `${getChartHeight()}px` }}>
<MiniChartRenderer
chart={chart}
showLegend={showLegend && chartCount <= 4}
showGrid={showGrid}
/>
</div>
</div>
))}
</div>
{hasBullets && (
<div
className="w-full max-w-[300px] rounded-[6px] border p-4 flex flex-col gap-3 self-start flex-shrink-0"
style={{ backgroundColor: 'var(--card-color,#FFFFFF)', borderColor: 'var(--stroke,#F0F0F2)' }}
>
{bullets.map((bullet, index) => (
<div key={index} className="flex items-start gap-2">
<span
className="mt-[6px] h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: 'var(--primary-color,#244CD9)' }}
/>
<span className="text-[14px] leading-relaxed" style={{ color: 'var(--background-text,#002BB2)' }}>
{bullet}
</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
</>
);
};
export default TitleDescriptionMultiChartGridWithBulletsLayout;

View file

@ -0,0 +1,561 @@
/**
* Neo-modern layout: title, description, up to 4 metrics, and 16 charts in a responsive grid.
*/
import React from 'react';
import * as z from "zod";
import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
Cell,
ReferenceLine,
} from "recharts";
export const layoutId = 'title-description-multi-chart-grid-metrics';
export const layoutName = 'Title Description With Multi-Chart Grid + Metrics';
export const layoutDescription = 'A neo-modern dashboard with title, description, up to 4 KPI metrics, and 16 auto-arranged charts. Supports bar (vertical, horizontal, grouped, stacked, clustered, diverging), line, area, pie, donut, and scatter charts.';
const DEFAULT_CHART_COLORS = ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899'];
const ChartTypeEnum = z.enum([
'bar-vertical',
'bar-horizontal',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter',
]);
const SimpleDataPointSchema = z.object({
name: z.string(),
value: z.number(),
});
const MultiSeriesDataPointSchema = z.object({
name: z.string(),
values: z.any(),
});
const DivergingDataPointSchema = z.object({
name: z.string(),
positive: z.number(),
negative: z.number(),
});
const ScatterDataPointSchema = z.object({
x: z.number(),
y: z.number(),
name: z.string().optional(),
});
const ChartItemSchema = z.object({
title: z.string().max(40).default("Chart Title"),
type: ChartTypeEnum.default('bar-vertical'),
data: z.union([
z.array(SimpleDataPointSchema),
z.array(MultiSeriesDataPointSchema),
z.array(DivergingDataPointSchema),
z.array(ScatterDataPointSchema),
]).default([
{ name: 'Q1', value: 45 },
{ name: 'Q2', value: 72 },
{ name: 'Q3', value: 58 },
{ name: 'Q4', value: 89 },
]),
series: z.array(z.string()).optional(),
colorPalette: z.enum(['vibrant', 'ocean', 'forest', 'sunset', 'professional']).default('vibrant'),
});
export const Schema = z.object({
title: z.string().min(3).max(50).default('Data Analytics Dashboard'),
description: z.string().min(10).max(200).default('Comprehensive overview of key metrics and performance indicators across multiple data dimensions.'),
metrics: z.array(z.object({
value: z.string().max(12).default('$3.5M'),
label: z.string().max(24).default('Pipeline'),
})).max(4).default([
{ value: '$3.5M', label: 'Pipeline' },
{ value: '28%', label: 'Conversion' },
{ value: '1.9x', label: 'ROI' },
{ value: '42', label: 'Accounts' },
]),
charts: z.array(ChartItemSchema).min(1).max(6).default([
{ title: 'Revenue by Quarter', type: 'bar-vertical', data: [{ name: 'Q1', value: 125000 }, { name: 'Q2', value: 158000 }, { name: 'Q3', value: 142000 }, { name: 'Q4', value: 189000 }], colorPalette: 'vibrant' },
{ title: 'Market Distribution', type: 'donut', data: [{ name: 'North America', value: 35 }, { name: 'Europe', value: 28 }, { name: 'Asia Pacific', value: 25 }, { name: 'Others', value: 12 }], colorPalette: 'ocean' },
{ title: 'Growth Trend', type: 'line', data: [{ name: 'Jan', value: 30 }, { name: 'Feb', value: 45 }, { name: 'Mar', value: 52 }, { name: 'Apr', value: 48 }, { name: 'May', value: 67 }, { name: 'Jun', value: 82 }], colorPalette: 'professional' },
{ title: 'Department Performance', type: 'bar-horizontal', data: [{ name: 'Sales', value: 87 }, { name: 'Marketing', value: 72 }, { name: 'Engineering', value: 95 }, { name: 'Support', value: 68 }], colorPalette: 'sunset' },
{ title: 'Product Comparison', type: 'bar-clustered', data: [{ name: 'Q1', values: { 'Product A': 45, 'Product B': 62 } }, { name: 'Q2', values: { 'Product A': 58, 'Product B': 71 } }, { name: 'Q3', values: { 'Product A': 72, 'Product B': 65 } }], series: ['Product A', 'Product B'], colorPalette: 'vibrant' },
{ title: 'Customer Feedback', type: 'bar-diverging', data: [{ name: 'Quality', positive: 78, negative: 22 }, { name: 'Service', positive: 65, negative: 35 }, { name: 'Price', positive: 42, negative: 58 }], series: ['Satisfied', 'Unsatisfied'], colorPalette: 'professional' },
]),
showLegend: z.boolean().default(true),
showGrid: z.boolean().default(true),
});
export type MultiChartGridSlideData = z.infer<typeof Schema>;
interface MultiChartGridSlideLayoutProps {
data?: Partial<MultiChartGridSlideData>;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border rounded-lg shadow-lg px-3 py-2" style={{ backgroundColor: 'var(--card-color, #ffffff)', borderColor: 'var(--stroke, #e5e7eb)' }}>
<p className="text-[10px] font-semibold mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-[9px]" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
const MiniChartRenderer: React.FC<{
chart: z.infer<typeof ChartItemSchema>;
showLegend: boolean;
showGrid: boolean;
}> = ({ chart, showLegend, showGrid }) => {
const data = chart.data as any[];
const series = chart.series || [];
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 9, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.5,
};
const transformMultiSeriesData = (data: any[], series: string[]) => {
return data.map(item => {
const result: Record<string, any> = { name: item.name };
series.forEach(s => { result[s] = item.values?.[s] ?? 0; });
return result;
});
};
const transformDivergingData = (data: any[]) => {
return data.map(item => ({
name: item.name,
positive: item.positive,
negative: -Math.abs(item.negative),
}));
};
const renderPieLabel = (props: any) => {
const { percent } = props;
if (percent < 0.08) return null;
return `${(percent * 100).toFixed(0)}%`;
};
const graphColors = (index: number) => {
const fallback = DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
switch (chart.type) {
case 'bar-vertical':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-horizontal':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-grouped-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[0, 3, 3, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-vertical': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} />
<YAxis {...axisProps} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-stacked-horizontal': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} stackId="stack" fill={graphColors(index)} radius={index === series.length - 1 ? [0, 3, 3, 0] : [0, 0, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-clustered': {
if (series.length === 0) {
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={data} barGap={2} barCategoryGap="20%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} barSize={32}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Bar key={s} dataKey={s} fill={graphColors(index)} radius={[3, 3, 0, 0]} barSize={Math.max(12, 40 / series.length)} />
))}
</BarChart>
</ResponsiveContainer>
);
}
case 'bar-diverging': {
const hasDivergingFormat = data.length > 0 && ('positive' in data[0] || 'negative' in data[0]);
const transformedData = hasDivergingFormat
? transformDivergingData(data)
: data.map((item: any, idx: number) => ({
name: item.name,
positive: idx % 2 === 0 ? Math.abs(item.value) : 0,
negative: idx % 2 === 1 ? -Math.abs(item.value) : 0,
}));
const seriesLabels = chart.series || ['Positive', 'Negative'];
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<BarChart data={transformedData} layout="vertical" stackOffset="sign" margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} width={50} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
<Bar dataKey="positive" name={seriesLabels[0]} fill={graphColors(0)} stackId="stack" radius={[0, 3, 3, 0]} />
<Bar dataKey="negative" name={seriesLabels[1]} fill={graphColors(3)} stackId="stack" radius={[3, 0, 0, 3]} />
</BarChart>
</ResponsiveContainer>
);
}
case 'line':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<LineChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Line type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} dot={{ fill: graphColors(0), r: 3 }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<defs>
<linearGradient id={`areaGrad-${chart.title}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="value" stroke={graphColors(0)} strokeWidth={2} fill={`url(#areaGrad-${chart.title})`} />
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked': {
const transformedData = transformMultiSeriesData(data, series);
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<AreaChart data={transformedData} margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis dataKey="name" {...axisProps} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
{showLegend && <Legend wrapperStyle={{ fontSize: '9px' }} />}
{series.map((s: string, index: number) => (
<Area key={s} type="monotone" dataKey={s} stackId="1" stroke={graphColors(index)} fill={graphColors(index)} fillOpacity={0.4} />
))}
</AreaChart>
</ResponsiveContainer>
);
}
case 'pie':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'donut':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Pie data={data} cx="50%" cy="50%" innerRadius="40%" outerRadius="75%" dataKey="value" label={renderPieLabel} labelLine={false} paddingAngle={2}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%" maxHeight={400}>
<ScatterChart margin={{ top: 10, right: 10, left: -10, bottom: 5 }}>
{showGrid && <CartesianGrid {...gridProps} />}
<XAxis type="number" dataKey="x" {...axisProps} tickFormatter={formatComma} />
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Scatter data={data}>
{data.map((_, index) => <Cell key={`cell-${index}`} fill={graphColors(index)} />)}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
default:
return <div className="flex items-center justify-center h-full text-gray-400 text-sm">Unsupported chart type</div>;
}
};
const getGridLayout = (count: number): { className: string } => {
switch (count) {
case 1: return { className: 'grid-cols-1' };
case 2: return { className: 'grid-cols-2' };
case 3: return { className: 'grid-cols-3' };
case 4: return { className: 'grid-cols-2' };
case 5:
case 6: return { className: 'grid-cols-3' };
default: return { className: 'grid-cols-2' };
}
};
const TitleDescriptionMultiChartGridWithMetricsLayout: React.FC<MultiChartGridSlideLayoutProps> = ({ data: slideData }) => {
const title = slideData?.title || 'Data Analytics Dashboard';
const description = slideData?.description || 'Comprehensive overview of key metrics and performance indicators.';
const charts = slideData?.charts || [];
const metrics = (slideData?.metrics || []).slice(0, 4);
const showLegend = slideData?.showLegend ?? true;
const showGrid = slideData?.showGrid ?? true;
const chartCount = charts.length;
const gridLayout = getGridLayout(chartCount);
const hasMetrics = metrics.length > 0;
const getChartHeight = () => {
if (chartCount <= 2) return hasMetrics ? 230 : 280;
if (chartCount <= 3) return hasMetrics ? 210 : 260;
return hasMetrics ? 160 : 180;
};
return (
<>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet" />
<div
className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex font-normal"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 pt-4 z-10">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }} className="w-[2px] h-4" />
{(slideData as any)?.__companyName__ && (
<span className="text-sm font-semibold" style={{ color: 'var(--background-text, #002BB2)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>
)}
</div>
</div>
</div>
)}
<div className="relative w-full h-full p-[60px] pt-[50px] flex flex-col">
<div className="mb-5">
<h1
className="text-[42.7px] font-bold leading-[1.1] tracking-[-1.6px] mb-[12px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
<p
className="text-[16px] font-normal leading-[1.6]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
{hasMetrics && (
<div className="mt-4 grid grid-cols-4 gap-3">
{metrics.map((metric, index) => (
<div
key={index}
className="rounded-[6px] border px-3 py-2"
style={{ borderColor: 'var(--stroke,#F0F0F2)', backgroundColor: 'var(--card-color,#FFFFFF)' }}
>
<div className="text-[18px] font-semibold leading-tight" style={{ color: 'var(--background-text,#002BB2)' }}>
{metric.value}
</div>
<div className="text-[10px] font-medium mt-1 opacity-80" style={{ color: 'var(--background-text,#002BB2)' }}>
{metric.label}
</div>
</div>
))}
</div>
)}
</div>
<div className={`flex-1 grid ${gridLayout.className} gap-4 min-h-0`} style={{ height: hasMetrics ? '450px' : 'calc(100% - 140px)' }}>
{charts.map((chart, index) => (
<div
key={index}
className="rounded-[6px] border flex flex-col overflow-hidden"
style={{ borderColor: 'var(--stroke,#F0F0F2)', backgroundColor: 'var(--card-color,#FFFFFF)' }}
>
<div className="px-4 pt-3 pb-1">
<h3
className="text-sm font-semibold truncate"
style={{ color: 'var(--background-text,#374151)' }}
>
{chart.title}
</h3>
</div>
<div className="flex-1 px-2 pb-2 min-h-0" style={{ height: `${getChartHeight()}px` }}>
<MiniChartRenderer
chart={chart}
showLegend={showLegend && chartCount <= 4}
showGrid={showGrid}
/>
</div>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default TitleDescriptionMultiChartGridWithMetricsLayout;

View file

@ -0,0 +1,127 @@
import * as z from 'zod'
export const Schema = z.object({
title: z.string().max(12).describe("The main heading of the slide").default("TABLE"),
description: z.string().max(250).describe("Supporting description text").default("Focus on companies with 500+ employees in Financial Services, Healthcare, and Technology sectors. Target $3.5M in new pipeline with sub-$150 CAC through account-based marketing and content-led strategies."),
table: z.object({
columns: z.array(z.string().max(15)).max(3).describe("Column headers for the table"),
rows: z.array(z.array(z.string().max(60)).max(3)).max(3).describe("Data rows for the table")
}).default({
columns: ["Problem", "Description", "Solution"],
rows: [
["Self-motivation\nReference: Book and Inspirational Videos", "Self-motivation\nReference: Book and Inspirational Videos", "Self-motivation\nReference: Book and Inspirational Videos"],
["Self-motivation\nReference: Book and Inspirational Videos", "Self-motivation\nReference: Book and Inspirational Videos", "Self-motivation\nReference: Book and Inspirational Videos"],
["Self-motivation\nReference: Book and Inspirational Videos", "Self-motivation\nReference: Book and Inspirational Videos", "Self-motivation\nReference: Book and Inspirational Videos",],
]
})
});
/**
* Layout ID, Name and Description.
*/
export const layoutId = "title-description-table";
export const layoutName = "Title Description Table";
export const layoutDescription = "A slide featuring a bold title, description, and a clean 3-column table with color-highlighted headers. The header row provides visual hierarchy while rounded cell backgrounds maintain a modern appearance.";
/**
* React Component for the slide layout.
*/
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, description, table } = data;
const { columns, rows } = table || {};
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-[#FFFFFE] z-20 mx-auto overflow-hidden flex flex-col p-[72px]"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{/* Header Section */}
<div className="flex justify-between items-start mb-[45px]">
<div className="w-[30%]">
<h1 className="text-[42.7px] font-bold leading-tight tracking-[-1.6px] uppercase"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
</div>
<div className="w-[45%]">
<p className="text-[16px] font-normal leading-[1.6]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{description}
</p>
</div>
</div>
{/* Table Section */}
<div className="flex flex-col gap-[17px] w-full mx-auto"
style={{ width: columns?.length === 1 ? '60%' : '100%' }}
>
{/* Table Header Row */}
<div
className="bg-[#1F4CD9] h-[64px] rounded-[4px] flex justify-between px-8 gap-[17px] items-center"
style={{
backgroundColor: 'var(--primary-color,#1F4CD9)',
}}
>
{columns?.map((column, index) => (
<div key={index} className="text-center w-full">
<span className="text-[21.4px] font-bold"
style={{ color: 'var(--primary-text,#FFFFFE)' }}
>
{column}
</span>
</div>
))}
</div>
{/* Table Body Rows */}
<div className="flex flex-col gap-[17px]">
{rows?.map((row, rowIndex) => (
<div key={rowIndex} className="flex justify-between gap-[17px]">
{row.map((cell, cellIndex) => (
<div
key={cellIndex}
className="bg-[#F7F8FF] w-full rounded-[12px] h-[105px] flex flex-col justify-center items-center px-6 text-center"
style={{ backgroundColor: 'var(--card-color,#F7F8FF)' }}
>
{cell.split('\n').map((line, lineIndex) => (
<span
key={lineIndex}
className="text-[20.3px] font-normal leading-[1.4]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{line}
</span>
))}
</div>
))}
</div>
))}
</div>
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,109 @@
/**
* Zod Schema for the slide content.
*/
import * as z from 'zod'
export const Schema = z.object({
mainTitle: z.string().max(30).describe("The main heading of the slide").default("Text Comparison"),
comparisonSections: z.array(
z.object({
heading: z.string().max(20).describe("The title for the item"),
description: z.string().max(200).describe("The detailed text description for the item"),
})
).max(2).describe("A list of up to 2 items").default([
{
heading: "Problem",
description: "Presentation are communication tools that can be used as demontrations, lectures, reports, and more. it is mostly presented before an audience."
},
{
heading: "Solution",
description: "Presentation are communication tools that can be used as demontrations, lectures, reports, and more. it is mostly presented before an audience."
}
])
});
type DataType = z.infer<typeof Schema>;
export const layoutId = "title-dual-comparison-cards";
export const layoutName = "Title Dual Comparison Cards";
export const layoutDescription = "A comparison slide with a centered title and two equal-sized cards below. Each card contains a heading, decorative accent line, and detailed description. The symmetrical layout emphasizes balanced evaluation of two items.";
/**
* Dynamic Slide Layout Component
*/
const dynamicSlideLayout: React.FC<{ data: Partial<DataType> }> = ({ data }) => {
const { mainTitle, comparisonSections } = data;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex flex-col items-center"
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{/* Header Section */}
<div className="mt-[69px] w-full flex justify-center">
<h1
className="text-[42.7px] text-[#002BB2] font-['Montserrat'] font-bold"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{mainTitle}
</h1>
</div>
{/* Comparison Sections Container */}
<div className="mt-[60px] flex gap-[36.5px] px-[108.5px] w-full justify-center">
{comparisonSections?.map((section, index) => (
<div
key={index}
className="flex flex-col p-[41.5px] w-[531.5px] h-[363.5px] bg-[#F7F8FF]"
style={{ borderRadius: '3.4px', backgroundColor: 'var(--card-color,#F7F8FF)', borderColor: 'var(--stroke,#F7F8FF)' }}
>
{/* Section Heading */}
<div className="mb-[6px]">
<h2
className="text-[28.4px] font-bold"
style={{ fontFamily: 'Montserrat', letterSpacing: '-1.0px', color: 'var(--background-text,#002BB2)' }}
>
{section?.heading}
</h2>
</div>
{/* Decorative Line */}
<div
className="w-[116.6px] h-[3.9px] bg-[#1F4CD9] mb-[20px]"
style={{ borderRadius: '2px', backgroundColor: 'var(--primary-color,#1F4CD9)' }}
/>
{/* Section Description */}
<div className="w-[423.5px]">
<p
className="text-[19.5px] leading-[31.3px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{section?.description}
</p>
</div>
</div>
))}
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

View file

@ -0,0 +1,485 @@
/**
* Enhanced Comparison Slide with multiple chart type support
*/
import * as z from 'zod'
import React from 'react';
import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Bar, LabelList, LineChart, Line, PieChart, Pie, Cell, AreaChart, Area, ScatterChart, Scatter, ReferenceLine } from 'recharts';
const chartTypeEnum = z.enum([
'bar',
'horizontalBar',
'bar-grouped-vertical',
'bar-grouped-horizontal',
'bar-stacked-vertical',
'bar-stacked-horizontal',
'bar-clustered',
'bar-diverging',
'line',
'area',
'area-stacked',
'pie',
'donut',
'scatter'
]).default('bar');
export const Schema = z.object({
title: z.string().max(50).describe("The main title of the slide").default("Competitive Comparison"),
comparisonCards: z.array(z.object({
heading: z.string().max(20).describe("The title of the item"),
subHeading: z.string().max(20).optional().describe("An optional badge or subtitle"),
footerText: z.string().max(25).describe("The text displayed at the bottom of the chart area"),
chartType: chartTypeEnum.describe('Type of chart to display'),
chart: z.object({
columns: z.array(z.string()).max(5).describe("The labels for the X-axis categories"),
rows: z.array(
z.array(
z.object({
label: z.string().max(30).describe("The name of the data series"),
value: z.number().describe("The numerical value for this series segment")
})
).min(1).max(2).describe("1 or 2 series per category; second series is optional for single-series charts")
).max(5).describe("Data for the chart. Each inner array represents a category on the X-axis with multiple series.")
})
})).max(2).describe("A list of up to 2 items").default([
{
heading: "Campaign A",
subHeading: "Top Campaign",
footerText: "Engagement Rate",
chartType: "bar",
chart: {
columns: ["Paid Social", "Content Marketing", "Events & Sponsorships", "SEO & Organic"],
rows: [
[{ label: "Planned", value: 520 }, { label: "Actual", value: 485 }],
[{ label: "Planned", value: 380 }, { label: "Actual", value: 412 }],
[{ label: "Planned", value: 400 }, { label: "Actual", value: 468 }],
[{ label: "Planned", value: 280 }, { label: "Actual", value: 276 }]
]
}
},
{
heading: "Campaign B",
footerText: "Engagement Rate",
chartType: "bar",
chart: {
columns: ["Paid Social", "Content Marketing", "Events & Sponsorships", "SEO & Organic"],
rows: [
[{ label: "Planned", value: 520 }, { label: "Actual", value: 485 }],
[{ label: "Planned", value: 380 }, { label: "Actual", value: 412 }],
[{ label: "Planned", value: 400 }, { label: "Actual", value: 468 }],
[{ label: "Planned", value: 280 }, { label: "Actual", value: 276 }]
]
}
}
])
});
export const layoutId = "title-dual-comparison-charts";
export const layoutName = "Title Dual Comparison Charts";
export const layoutDescription = "A comparison slide with a main title and two side-by-side chart panels, each supporting bar, grouped bar, stacked bar, clustered bar, diverging bar, horizontal bar, line, area, pie, donut, and scatter chart types.";
const CHART_COLORS = ['#244CD9', '#6A89E6', '#4169E1', '#7B9FFF', '#EC4899', '#10B981'];
// Custom tooltip matching TitleWithFullWidthChart style
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg px-3 py-2"
style={{
backgroundColor: 'var(--card-color, #ffffff)',
borderColor: 'var(--stroke, #e5e7eb)',
}}
>
<p className="text-sm font-semibold text-gray-800 mb-1" style={{ color: 'var(--background-text, #111827)' }}>{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-xs" style={{ color: 'var(--background-text, #111827)' }}>
{entry.name}: <span className="font-medium">{entry.value?.toLocaleString()}</span>
</p>
))}
</div>
);
}
return null;
};
// Helper function for graph colors
const graphColors = (index: number, fallbackColor?: string) => {
const fallback = fallbackColor || CHART_COLORS[index % CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
const dynamicSlideLayout: React.FC<{ data: Partial<z.infer<typeof Schema>> }> = ({ data }) => {
const { title, comparisonCards } = data;
const renderChart = (card: NonNullable<typeof comparisonCards>[number]) => {
const chartType = card.chartType || 'bar';
const formatComma = (value: number) => {
return value.toLocaleString('en-US');
};
// Transform schema data to Recharts format
const chartData = card.chart?.columns?.map((col, cIdx) => ({
name: col,
series1: card.chart?.rows?.[cIdx]?.[0]?.value ?? 0,
series2: card.chart?.rows?.[cIdx]?.[1]?.value ?? 0,
series1Name: card.chart?.rows?.[cIdx]?.[0]?.label || "Series 1",
series2Name: card.chart?.rows?.[cIdx]?.[1]?.label || "Series 2",
})) || [];
const hasSeries2 = chartData.some(item => (item.series2 ?? 0) > 0);
const axisProps = {
tick: { fill: 'var(--background-text, #7f8491)', fontSize: 11, fontWeight: 500 },
axisLine: { stroke: 'var(--background-text, #7f8491)' },
tickLine: { stroke: 'var(--background-text, #7f8491)' },
};
const gridProps = {
strokeDasharray: "3 3",
stroke: "var(--background-text, #7f8491)",
opacity: 0.7,
};
switch (chartType) {
case 'line':
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData} margin={{ top: 40, right: 10, left: -20, bottom: 20 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} interval={0} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Line type="monotone" dataKey="series1" stroke={graphColors(0)} strokeWidth={3} dot={{ fill: graphColors(0), r: 5 }} />
<Line type="monotone" dataKey="series2" stroke={graphColors(1)} strokeWidth={3} dot={{ fill: graphColors(1), r: 5 }} />
</LineChart>
</ResponsiveContainer>
);
case 'horizontalBar':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData} layout="vertical" margin={{ top: 40, right: 10, left: 60, bottom: 20 }} barGap={0}>
<CartesianGrid horizontal={false} stroke="var(--background-text, #7f8491)" />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Bar dataKey="series1" stackId="a" fill={graphColors(0)} barSize={30} radius={hasSeries2 ? undefined : [0, 4, 4, 0]}>
<LabelList dataKey="series1" position="center" fill="var(--primary-text, #FFFFFF)" fontSize={12} />
</Bar>
{hasSeries2 && (
<Bar dataKey="series2" stackId="a" fill={graphColors(1)} barSize={30} radius={[0, 4, 4, 0]}>
<LabelList dataKey="series2" position="center" fill="var(--primary-text, #FFFFFF)" fontSize={12} />
</Bar>
)}
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-vertical':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData} margin={{ top: 40, right: 10, left: -20, bottom: 20 }} barGap={2}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} interval={0} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Bar dataKey="series1" fill={graphColors(0)} radius={[4, 4, 0, 0]} barSize={30} />
<Bar dataKey="series2" fill={graphColors(1)} radius={[4, 4, 0, 0]} barSize={30} />
</BarChart>
</ResponsiveContainer>
);
case 'bar-grouped-horizontal':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData} layout="vertical" margin={{ top: 40, right: 10, left: 60, bottom: 20 }} barGap={2}>
<CartesianGrid horizontal={false} stroke="var(--background-text, #7f8491)" />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Bar dataKey="series1" fill={graphColors(0)} radius={[0, 4, 4, 0]} barSize={15} />
<Bar dataKey="series2" fill={graphColors(1)} radius={[0, 4, 4, 0]} barSize={15} />
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-vertical':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData} margin={{ top: 40, right: 10, left: -20, bottom: 20 }} barGap={0}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} interval={0} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Bar dataKey="series1" stackId="stack" fill={graphColors(0)} barSize={50} radius={hasSeries2 ? undefined : [4, 4, 0, 0]} />
{hasSeries2 && <Bar dataKey="series2" stackId="stack" fill={graphColors(1)} radius={[4, 4, 0, 0]} barSize={50} />}
</BarChart>
</ResponsiveContainer>
);
case 'bar-stacked-horizontal':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData} layout="vertical" margin={{ top: 40, right: 10, left: 60, bottom: 20 }} barGap={0}>
<CartesianGrid horizontal={false} stroke="var(--background-text, #7f8491)" />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Bar dataKey="series1" stackId="stack" fill={graphColors(0)} barSize={25} radius={hasSeries2 ? undefined : [0, 4, 4, 0]} />
{hasSeries2 && <Bar dataKey="series2" stackId="stack" fill={graphColors(1)} radius={[0, 4, 4, 0]} barSize={25} />}
</BarChart>
</ResponsiveContainer>
);
case 'bar-clustered':
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData} margin={{ top: 40, right: 10, left: -20, bottom: 20 }} barGap={1} barCategoryGap="20%">
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} interval={0} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Bar dataKey="series1" fill={graphColors(0)} radius={[4, 4, 0, 0]} barSize={25}>
<LabelList dataKey="series1" position="top" fill="var(--background-text, #333)" fontSize={10} />
</Bar>
<Bar dataKey="series2" fill={graphColors(1)} radius={[4, 4, 0, 0]} barSize={25}>
<LabelList dataKey="series2" position="top" fill="var(--background-text, #333)" fontSize={10} />
</Bar>
</BarChart>
</ResponsiveContainer>
);
case 'bar-diverging': {
const divergingData = chartData.map(item => ({
name: item.name,
positive: item.series1,
negative: -(item.series2 ?? 0),
}));
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={divergingData} layout="vertical" margin={{ top: 40, right: 10, left: 60, bottom: 20 }} stackOffset="sign">
<CartesianGrid horizontal={false} stroke="var(--background-text, #7f8491)" />
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
<YAxis type="category" dataKey="name" {...axisProps} tickFormatter={formatComma} />
<ReferenceLine x={0} stroke="var(--background-text, #9CA3AF)" strokeWidth={1} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} />
<Bar dataKey="positive" name={chartData[0]?.series1Name || 'Positive'} fill={graphColors(0)} stackId="stack" radius={hasSeries2 ? [0, 4, 4, 0] : [4, 4, 4, 4]} barSize={20} />
{hasSeries2 && <Bar dataKey="negative" name={chartData[0]?.series2Name || 'Negative'} fill={graphColors(1)} stackId="stack" radius={[4, 0, 0, 4]} barSize={20} />}
</BarChart>
</ResponsiveContainer>
);
}
case 'area':
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={chartData} margin={{ top: 40, right: 10, left: -20, bottom: 20 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} interval={0} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<defs>
<linearGradient id="dualArea1" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(0)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(0)} stopOpacity={0.05} />
</linearGradient>
<linearGradient id="dualArea2" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={graphColors(1)} stopOpacity={0.4} />
<stop offset="95%" stopColor={graphColors(1)} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="series1" stroke={graphColors(0)} strokeWidth={2} fill="url(#dualArea1)" />
<Area type="monotone" dataKey="series2" stroke={graphColors(1)} strokeWidth={2} fill="url(#dualArea2)" />
</AreaChart>
</ResponsiveContainer>
);
case 'area-stacked':
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={chartData} margin={{ top: 40, right: 10, left: -20, bottom: 20 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} interval={0} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Area type="monotone" dataKey="series1" stackId="1" stroke={graphColors(0)} fill={graphColors(0)} fillOpacity={0.6} />
<Area type="monotone" dataKey="series2" stackId="1" stroke={graphColors(1)} fill={graphColors(1)} fillOpacity={0.6} />
</AreaChart>
</ResponsiveContainer>
);
case 'pie': {
const pieData = chartData.map((item) => ({
name: item.name,
value: item.series1 + item.series2,
}));
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart margin={{ top: 15, right: 15, left: 15, bottom: 15 }}>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie data={pieData} cx="50%" cy="50%" outerRadius={100} dataKey="value" label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}>
{pieData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
case 'donut': {
const donutData = chartData.map((item) => ({
name: item.name,
value: item.series1 + item.series2,
}));
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
<Legend />
<Pie data={donutData} cx="50%" cy="50%" innerRadius={40} outerRadius={100} dataKey="value" paddingAngle={2} label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}>
{donutData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} stroke="white" strokeWidth={2} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}
case 'scatter': {
const scatterData = chartData.map((item) => ({
x: item.series1,
y: item.series2,
name: item.name,
}));
return (
<ResponsiveContainer width="100%" height={400}>
<ScatterChart margin={{ top: 15, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis type="number" dataKey="x" name={chartData[0]?.series1Name} {...axisProps} />
<YAxis type="number" dataKey="y" name={chartData[0]?.series2Name} {...axisProps} />
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3', fill: 'transparent' }} />
<Scatter data={scatterData} fill={graphColors(0)}>
{scatterData.map((_, index) => (
<Cell key={`cell-${index}`} fill={graphColors(index)} />
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
);
}
case 'bar':
default:
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData} margin={{ top: 40, right: 10, left: -20, bottom: 20 }} barGap={0}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="name" {...axisProps} interval={0} tickFormatter={formatComma} />
<YAxis {...axisProps} tickFormatter={formatComma} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '12px' }} formatter={(value, entry: any) => { if (entry.dataKey === 'series1') return chartData[0]?.series1Name; if (entry.dataKey === 'series2') return chartData[0]?.series2Name; return value; }} />
<Bar dataKey="series1" stackId="a" fill={graphColors(0)} barSize={70} radius={hasSeries2 ? undefined : [4, 4, 0, 0]}>
<LabelList dataKey="series1" position="center" fill="var(--primary-text, #FFFFFF)" fontSize={12} />
</Bar>
{hasSeries2 && (
<Bar dataKey="series2" stackId="a" fill={graphColors(1)} barSize={70} radius={[4, 4, 0, 0]}>
<LabelList dataKey="series2" position="center" fill="var(--primary-text, #FFFFFF)" fontSize={12} />
</Bar>
)}
</BarChart>
</ResponsiveContainer>
);
}
};
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div className="relative w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white z-20 mx-auto overflow-hidden flex flex-col p-10 "
style={{
backgroundColor: 'var(--background-color,#FFFFFF)',
fontFamily: 'var(--body-font-family,Montserrat)',
}}
>
{/* Main Title */}
<div className="w-full text-center mb-10">
<h1 className="text-[42.7px] font-bold tracking-[-1.5px]"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{title}
</h1>
</div>
{/* Comparison Sections */}
<div className=" flex-1 flex gap-8 justify-center px-4">
{comparisonCards?.map((card, idx) => {
return (
<div key={idx} className="flex-1 bg-white border border-[#F0F0F2] rounded-lg p-4 flex flex-col relative"
style={{ backgroundColor: 'var(--card-color,#FFFFFF)', borderColor: 'var(--stroke,#F0F0F2)' }}
>
{/* Card Header */}
<div className="flex justify-between items-center mb-2">
<h2 className="text-[28px] font-bold"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{card.heading}
</h2>
{card.subHeading && (
<span className="text-[18px] font-normal"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{card.subHeading}
</span>
)}
</div>
{/* Chart Container */}
<div className=" w-full min-h-0">
{renderChart(card)}
</div>
{/* Footer Text */}
<div className=" absolute bottom-4 right-6 text-right ">
<p className="text-[16px] font-normal"
style={{ color: 'var(--background-text,#002BB2)' }}
>
{card.footerText}
</p>
</div>
</div>
);
})}
</div>
{(data as any)?.__companyName__ || (data as any)?._logo_url__ && <div className="flex items-center gap-1 absolute top-5 left-5 z-40">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-[60px] object-contain" />}
<span
style={{ backgroundColor: 'var(--stroke, #F0F0F0)' }}
className=' w-[2px] h-4'></span>
{(data as any)?.__companyName__ && <span className="text-sm font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>}
</div>
</>
);
};
export default dynamicSlideLayout;

Some files were not shown because too many files have changed in this diff Show more