Merge pull request #505 from presenton/feat/new_templates
feat/new templates
This commit is contained in:
commit
09d7b300fc
59 changed files with 9303 additions and 30 deletions
0
.codex
Normal file
0
.codex
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "product-overview-market-opportunity-slide";
|
||||
export const slideLayoutName = "Product Overview Market Opportunity Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A market opportunity slide with title and intro text on the left, four bullet lines extending toward the right, and concentric value circles as the visual focal point.";
|
||||
|
||||
const BulletSchema = z.object({
|
||||
text: z.string().min(12).max(46).meta({
|
||||
description: "Bullet text shown on the left side of a line.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(22).default("Market Opportunity").meta({
|
||||
description: "Main heading shown at the top-left.",
|
||||
}),
|
||||
subtitle: z.string().min(40).max(110).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt."
|
||||
).meta({
|
||||
description: "Supporting text under the main heading.",
|
||||
}),
|
||||
bullets: z
|
||||
.array(BulletSchema)
|
||||
.min(4)
|
||||
.max(4)
|
||||
.default([
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
])
|
||||
.meta({
|
||||
description: "Four bullet-line entries shown on the left.",
|
||||
}),
|
||||
values: z
|
||||
.array(z.string().min(2).max(6))
|
||||
.min(4)
|
||||
.max(4)
|
||||
.default(["$33", "$20", "$120", "$200"])
|
||||
.meta({
|
||||
description: "Four values shown from outer to inner circles.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
|
||||
const COLORS = [
|
||||
"var(--graph-0,#5f7f79)",
|
||||
"var(--graph-1,#1f5a4f)",
|
||||
"var(--graph-2,#0d4f43)",
|
||||
"var(--graph-3,#06463d)",
|
||||
];
|
||||
|
||||
const MarketOpportunitySlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, subtitle, bullets, values } = data;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="px-[56px] pt-[72px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className="mt-[20px] w-[730px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-[56px] top-[368px] space-y-[42px]">
|
||||
{bullets?.map((bullet, index) => (
|
||||
<div key={index} className="relative flex items-center">
|
||||
<span
|
||||
className="mr-[14px] h-[14px] w-[14px] rounded-full bg-[#0a4a3f]"
|
||||
style={{ backgroundColor: "var(--graph-0,#0a4a3f)" }}
|
||||
/>
|
||||
<p
|
||||
className="w-[640px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{bullet.text}
|
||||
</p>
|
||||
<span
|
||||
className="ml-[8px] h-[2px] w-[80px] bg-[#8ea8a5]"
|
||||
style={{ backgroundColor: "var(--stroke,#8ea8a5)" }}
|
||||
/>
|
||||
<span
|
||||
className="h-[6px] w-[6px] rounded-full bg-[#edf2f1]"
|
||||
style={{ backgroundColor: "var(--primary-text,#edf2f1)" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[58px] right-[48px] h-[474px] w-[474px]">
|
||||
{values?.map((value, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: 237 + (index * 50),
|
||||
height: 237 + (index * 50),
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundColor: COLORS[index],
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="pt-[24px] text-center text-[24px] font-normal text-white"
|
||||
style={{ color: "var(--primary-text,#ffffff)" }}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketOpportunitySlide;
|
||||
|
|
@ -83,6 +83,9 @@ strong {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
::selection {
|
||||
background-color: hsl(var(--chart-1));
|
||||
color: white;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "api-request-response-slide";
|
||||
export const slideLayoutName = "API Request Response Slide";
|
||||
export const slideLayoutDescription =
|
||||
"An API-focused slide with endpoint metadata, request payload, and response payload.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(26).default("API Request / Response").meta({
|
||||
description: "Main heading shown at the top-left.",
|
||||
}),
|
||||
method: z.enum(["GET", "POST", "PATCH", "DELETE"]).default("POST").meta({
|
||||
description: "HTTP method badge text.",
|
||||
}),
|
||||
endpoint: z.string().min(8).max(48).default("/api/v1/users/authenticate").meta({
|
||||
description: "Endpoint path text.",
|
||||
}),
|
||||
headers: z
|
||||
.array(z.string().max(10))
|
||||
.min(2)
|
||||
.max(2)
|
||||
.default(["Content-Type: application/json", "Authorization: Bearer <token>"])
|
||||
.meta({
|
||||
description: "Two header lines shown in the endpoint card.",
|
||||
}),
|
||||
requestSnippet: z.object({
|
||||
language: z.string().min(2).max(10),
|
||||
fileName: z.string().min(3).max(24),
|
||||
content: z.string().min(20).max(500),
|
||||
}).default({
|
||||
language: "json",
|
||||
fileName: "request.json",
|
||||
content: `{
|
||||
"email": "user@example.com user@example.com user@example.com user@example.com user@example.com" ,
|
||||
"password": "securepassword123"
|
||||
}`,
|
||||
}).meta({
|
||||
description: "Request payload example.",
|
||||
}),
|
||||
responseSnippet: z.object({
|
||||
language: z.string().min(2).max(10),
|
||||
fileName: z.string().min(3).max(24),
|
||||
content: z.string().min(20).max(620),
|
||||
}).default({
|
||||
language: "json",
|
||||
fileName: "response.json",
|
||||
content: `{
|
||||
"success": true,
|
||||
"user": {
|
||||
"id": "usr_1234567890",
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "admin"
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expiresIn": 3600
|
||||
}`,
|
||||
}).meta({
|
||||
description: "Response payload example.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide03ApiRequestResponse = ({
|
||||
data,
|
||||
}: {
|
||||
data: Partial<SchemaType>;
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[22px] grid flex-1 grid-cols-2 gap-[22px]">
|
||||
<div className="flex flex-col gap-[12px] ">
|
||||
<div
|
||||
className="rounded-[14px] border p-[14px]"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-5 pb-[14px] border-b" style={{ borderColor: "var(--stroke,#1D293D80)" }}>
|
||||
<p
|
||||
className="rounded-[12px] px-[23px] py-[10px] text-[14px] uppercase tracking-[0.06em]"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#2B7FFF33)",
|
||||
color: "var(--primary-text,#51A2FF)",
|
||||
}}
|
||||
>
|
||||
{data.method}
|
||||
</p>
|
||||
<p className="text-[23px]" style={{ color: "var(--background-text,#dde5ff)" }}>{data.endpoint}</p>
|
||||
</div>
|
||||
<p className="mt-[21px] text-[18px] uppercase tracking-[0.08em]" style={{ color: "var(--background-text,#90a1d8)" }}>Headers</p>
|
||||
<div className="mt-[15px] space-y-[4px] text-[24px]" style={{ color: "var(--background-text,#cbd4f8)" }}>
|
||||
{data.headers?.map((item) => (
|
||||
<p key={item} className="text-[18px]" style={{ color: "var(--background-text,#CAD5E2)" }}>{item}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className=" flex-1 border rounded-[18px]"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[18px] capitalize rounded-t-[18px] border p-[14px]"
|
||||
style={{
|
||||
color: "var(--background-text,#CAD5E2)",
|
||||
backgroundColor: "var(--card-color,#1D293D80)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
}}
|
||||
>
|
||||
{data.requestSnippet?.fileName}
|
||||
</p>
|
||||
<pre className=" w-full px-[14px] py-[20px] whitespace-pre-wrap break-words overflow-hidden" style={{ color: "var(--background-text,#ffffff)" }}>
|
||||
|
||||
<code className="w-full ">
|
||||
{data.requestSnippet?.content}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className=" flex-1 border rounded-[18px]"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[18px] capitalize rounded-t-[18px] border p-[14px]"
|
||||
style={{
|
||||
color: "var(--background-text,#CAD5E2)",
|
||||
backgroundColor: "var(--card-color,#1D293D80)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
}}
|
||||
>
|
||||
{data.responseSnippet?.fileName}
|
||||
</p>
|
||||
<pre className=" w-full px-[14px] py-[20px] whitespace-pre-wrap break-words overflow-hidden" style={{ color: "var(--background-text,#ffffff)" }}>
|
||||
|
||||
<code className="w-full ">
|
||||
{data.responseSnippet?.content}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide03ApiRequestResponse;
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
import * as z from "zod";
|
||||
|
||||
const FeatureCardSchema = z.object({
|
||||
title: z.string().min(3).max(17).meta({
|
||||
description: "Title shown on each card.",
|
||||
}),
|
||||
description: z.string().min(18).max(80).meta({
|
||||
description: "Description shown on each card.",
|
||||
}),
|
||||
icon: z.object({
|
||||
__icon_url__: z.string().meta({
|
||||
description: "URL to icon",
|
||||
}),
|
||||
__icon_query__: z.string().meta({
|
||||
description: "Query used to search the icon",
|
||||
}),
|
||||
}).default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
}).meta({
|
||||
description: "Suiting icon used for each bullet in plan cards.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "cards-grid-slide";
|
||||
export const slideLayoutName = "Cards Grid Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A list of cards in grid with title, icon and compact description in each.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(20).default("Feature Grid").meta({
|
||||
description: "Slide title shown above the grid.",
|
||||
}),
|
||||
features: z
|
||||
.array(FeatureCardSchema)
|
||||
.min(1)
|
||||
.max(6)
|
||||
.default([
|
||||
{
|
||||
title: "Modern Stack",
|
||||
description: "Built with React, TypeScript, and Tailwind CSS for maximum developer experience.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Component Library ",
|
||||
description: "Reusable UI components with consistent design patterns.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "CLI Tools",
|
||||
description: "Command-line utilities for scaffolding and automation.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
description: "Built-in tracking and performance monitoring.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Version Control",
|
||||
description: "Git-based workflow with automated deployments.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Best Practices",
|
||||
description: "Following industry standards and modern development patterns.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Six feature cards displayed in a 3x2 grid.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide04FeatureGrid = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<h2 className="text-[64px] font-medium tracking-[-0.03em]" style={{ color: "var(--background-text,#f2f4ff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[26px] grid flex-1 grid-cols-3 items-center h-fit gap-[26px]">
|
||||
{data?.features?.map((feature) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="rounded-[18px] border p-[26px]"
|
||||
style={{
|
||||
boxShadow: "0 33.333px 66.667px -16px rgba(0, 0, 0, 0.25)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-[8px]">
|
||||
<h3 className="text-[26px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{feature.title}</h3>
|
||||
<span
|
||||
className="flex h-[52px] w-[52px] items-center justify-center rounded-full border text-[18px]"
|
||||
style={{
|
||||
borderColor: "var(--primary-color,#2B7FFF4D)",
|
||||
backgroundColor: "var(--primary-color,#2B7FFF33)",
|
||||
}}
|
||||
>
|
||||
{/* <img src={feature.icon.__icon_url__} alt={feature.icon.__icon_query__} className="h-[24px] w-[24px] object-contain"
|
||||
style={{
|
||||
filter: "invert(1)",
|
||||
}}
|
||||
/> */}
|
||||
<RemoteSvgIcon
|
||||
url={feature.icon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[24px] w-[24px] object-contain"
|
||||
color="var(--primary-text, #000000)"
|
||||
title={feature.icon.__icon_query__}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-[12px] text-[18px] leading-[136%]" style={{ color: "var(--background-text,#90A1B9)" }}>{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide04FeatureGrid;
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const CODE_BLOCK_MAX_FONT_SIZE = 16;
|
||||
const CODE_BLOCK_MIN_FONT_SIZE = 8;
|
||||
const CODE_BLOCK_WIDTH = 506;
|
||||
const CODE_BLOCK_HEIGHT = 430;
|
||||
const CODE_CHAR_WIDTH_RATIO = 0.62;
|
||||
const CODE_LINE_HEIGHT_RATIO = 1.25;
|
||||
const CODE_FONT_FAMILY = "var(--code-font-family,'Liberation Mono', monospace)";
|
||||
|
||||
function splitCollapsedPythonImports(line: string) {
|
||||
const importSegments = line
|
||||
.split(/(?=\sfrom\s+[A-Za-z0-9_.]+\s+import\s+)/g)
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return importSegments.length > 1 ? importSegments : [line];
|
||||
}
|
||||
|
||||
function expandInlinePythonStatement(line: string) {
|
||||
const inlineReturnMatch = line.match(/^(\s*def\s+[^(]+\([^)]*\):)\s+return\s+(.+)$/);
|
||||
|
||||
if (!inlineReturnMatch) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
return [inlineReturnMatch[1], ` return ${inlineReturnMatch[2]}`];
|
||||
}
|
||||
|
||||
function expandPathListAssignment(line: string) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (!trimmedLine.startsWith("urlpatterns = [") || !trimmedLine.endsWith("]")) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
const pathCalls = trimmedLine.match(/path\([^)]*\)/g);
|
||||
|
||||
if (!pathCalls?.length) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
return [
|
||||
"urlpatterns = [",
|
||||
...pathCalls.map((pathCall) => ` ${pathCall},`),
|
||||
"]",
|
||||
];
|
||||
}
|
||||
|
||||
function normalizePythonCode(content: string) {
|
||||
const normalizedLines: string[] = [];
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
const importLines = splitCollapsedPythonImports(line);
|
||||
|
||||
for (const importLine of importLines) {
|
||||
const expandedPathLines = expandPathListAssignment(importLine);
|
||||
|
||||
for (const expandedPathLine of expandedPathLines) {
|
||||
normalizedLines.push(...expandInlinePythonStatement(expandedPathLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedLines.join("\n").replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
|
||||
function normalizeCodeContent(language?: string, content?: string) {
|
||||
let normalizedContent = (content || "")
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/\\\[/g, "[")
|
||||
.replace(/\\\]/g, "]")
|
||||
.trimEnd();
|
||||
|
||||
if (language?.toLowerCase() === "python") {
|
||||
normalizedContent = normalizePythonCode(normalizedContent);
|
||||
}
|
||||
|
||||
return normalizedContent;
|
||||
}
|
||||
|
||||
function getCodeBlockTypography(content?: string) {
|
||||
const normalizedLines = (content || "").replace(/\t/g, " ").split("\n");
|
||||
const longestLineLength = Math.max(
|
||||
1,
|
||||
...normalizedLines.map((line) => line.length)
|
||||
);
|
||||
|
||||
for (let fontSize = CODE_BLOCK_MAX_FONT_SIZE; fontSize >= CODE_BLOCK_MIN_FONT_SIZE; fontSize -= 0.5) {
|
||||
const lineHeight = Math.round(fontSize * CODE_LINE_HEIGHT_RATIO);
|
||||
const fitsWidth = longestLineLength * fontSize * CODE_CHAR_WIDTH_RATIO <= CODE_BLOCK_WIDTH;
|
||||
const fitsHeight = normalizedLines.length * lineHeight <= CODE_BLOCK_HEIGHT;
|
||||
|
||||
if (fitsWidth && fitsHeight) {
|
||||
return { fontSize, lineHeight };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fontSize: CODE_BLOCK_MIN_FONT_SIZE,
|
||||
lineHeight: Math.round(CODE_BLOCK_MIN_FONT_SIZE * CODE_LINE_HEIGHT_RATIO),
|
||||
};
|
||||
}
|
||||
|
||||
function getCodeLineRuns(content: string, lineHeight: number) {
|
||||
const codeLineRuns: { text: string; marginTop: number }[] = [];
|
||||
let blankLineCount = 0;
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
if (line.length === 0) {
|
||||
blankLineCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
codeLineRuns.push({
|
||||
text: line,
|
||||
marginTop: blankLineCount * lineHeight,
|
||||
});
|
||||
blankLineCount = 0;
|
||||
}
|
||||
|
||||
return codeLineRuns;
|
||||
}
|
||||
|
||||
export const slideLayoutId = "code-explanation-split-slide";
|
||||
export const slideLayoutName = "Code Explanation Split Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A two-column slide with a code panel on the left and description on the right.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(24).default("Code + Explanation").meta({
|
||||
description: "Slide heading shown at the top-left.",
|
||||
}),
|
||||
codeSnippet: z.object({
|
||||
language: z.string().min(2).max(10).meta({
|
||||
description: "Programming language of the snippet",
|
||||
}),
|
||||
fileName: z.string().min(3).max(30).meta({
|
||||
description: "File name label shown above the code snippet.",
|
||||
}),
|
||||
content: z.string().min(20).max(520).meta({
|
||||
description: "The actual code content to be displayed.",
|
||||
}),
|
||||
}).default({
|
||||
language: "tsx",
|
||||
fileName: "components/UserAuth.tsx",
|
||||
content: `import { useState } from "react";
|
||||
import { login } from "@/lib/auth";
|
||||
|
||||
export function UserAuth() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const user = await login(email, password);
|
||||
console.log("Logged in:", user);
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
`,
|
||||
}).meta({
|
||||
description: "Code sample shown in the left panel.",
|
||||
}),
|
||||
descriptionTitle: z.string().min(4).max(20).default("Description").meta({
|
||||
description: "Heading shown above the paragraph.",
|
||||
}),
|
||||
description: z
|
||||
.string()
|
||||
.min(40)
|
||||
.max(360)
|
||||
.default(
|
||||
"This component manages credentials as local state and submits them through an async handler. The login utility abstracts network details while the handler keeps the UI flow predictable. Keep validation and side effects isolated so changes remain safe when authentication requirements evolve. "
|
||||
)
|
||||
.meta({
|
||||
description: "Description paragraph shown in the right column.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide02CodeExplanationSplit = ({
|
||||
data,
|
||||
}: {
|
||||
data: Partial<SchemaType>;
|
||||
}) => {
|
||||
const normalizedCodeContent = normalizeCodeContent(
|
||||
data.codeSnippet?.language,
|
||||
data.codeSnippet?.content
|
||||
);
|
||||
const codeTypography = getCodeBlockTypography(normalizedCodeContent);
|
||||
const codeLineRuns = getCodeLineRuns(normalizedCodeContent, codeTypography.lineHeight);
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[22px] grid min-h-0 flex-1 grid-cols-2 gap-[34px]">
|
||||
<div
|
||||
className="flex h-full min-h-0 flex-col overflow-hidden rounded-[18px] border"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[18px] capitalize rounded-t-[18px] border px-[26px] py-3"
|
||||
style={{
|
||||
color: "var(--background-text,#CAD5E2)",
|
||||
backgroundColor: "var(--card-color,#0F172BCC)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
}}
|
||||
>
|
||||
{data.codeSnippet?.fileName}
|
||||
</p>
|
||||
<div
|
||||
className="min-h-0 w-full flex-1 overflow-hidden px-[32px] py-[20px]"
|
||||
style={{
|
||||
color: "var(--background-text,#ffffff)",
|
||||
fontFamily: CODE_FONT_FAMILY,
|
||||
}}
|
||||
>
|
||||
{codeLineRuns.map((codeLineRun, index) => (
|
||||
<div
|
||||
key={`code-line-${index}`}
|
||||
style={{
|
||||
marginTop: codeLineRun.marginTop ? `${codeLineRun.marginTop}px` : undefined,
|
||||
fontSize: `${codeTypography.fontSize}px`,
|
||||
lineHeight: `${codeTypography.lineHeight}px`,
|
||||
whiteSpace: "pre",
|
||||
}}
|
||||
>
|
||||
{codeLineRun.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=" ">
|
||||
<h3 className="text-[24px] font-medium" style={{ color: "var(--background-text,#f1f4ff)" }}>{data.descriptionTitle}</h3>
|
||||
<p className="mt-[18px] text-[22px] leading-[145%]" style={{ color: "var(--background-text,#d2d9ff)" }}>
|
||||
{data.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide02CodeExplanationSplit;
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "cover-slide";
|
||||
export const slideLayoutName = "Cover Slide";
|
||||
export const slideLayoutDescription =
|
||||
"Opening/cover/intro slide with organization/institution/presenter, presentation title/heading , and supporting subtitle.";
|
||||
|
||||
export const Schema = z.object({
|
||||
companyName: z.string().min(2).max(18).optional().default("COMPANY NAME").meta({
|
||||
description: "Optional organization/institution/presenter name shown above the slide title.",
|
||||
}),
|
||||
title: z.string().min(8).max(28).default("Development Roadmap").meta({
|
||||
description: "Title/heading of the slide.",
|
||||
}),
|
||||
subtitle: z
|
||||
.string()
|
||||
.min(24)
|
||||
.max(40)
|
||||
.default(
|
||||
"We transform ideas into market-ready solutions through systematic development processes."
|
||||
)
|
||||
.meta({
|
||||
description: "Supporting subtitle shown under the heading.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide01RoadmapCover = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
<div className="relative z-10 flex h-full flex-col items-center justify-center px-[200px] text-center">
|
||||
<p className="text-[22px]" style={{ color: "var(--background-text,#d7dcff)" }}>{data.companyName}</p>
|
||||
<h2 className="mt-[10px] text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>
|
||||
{data.title}
|
||||
</h2>
|
||||
<p className="mt-[35px] text-[26px] leading-[132%]" style={{ color: "var(--background-text,#d8ddff)" }}>{data.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide01RoadmapCover;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(2).max(6).meta({
|
||||
description: "Primary metric value.",
|
||||
}),
|
||||
label: z.string().min(3).max(15).meta({
|
||||
description: "Metric label text.",
|
||||
}),
|
||||
subtext: z.string().min(3).max(30).meta({
|
||||
description: "Metric subtext/description.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "description-and-metrics-slide";
|
||||
export const slideLayoutName = "Description and Metrics Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A metrics slide with description text on the left and metric cards on the right.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(18).default("Metrics").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
explanationTitle: z.string().min(4).max(16).default("Explanation").meta({
|
||||
description: "Heading above the explanatory paragraph.",
|
||||
}),
|
||||
explanation: z
|
||||
.string()
|
||||
.max(320)
|
||||
.default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
)
|
||||
.meta({
|
||||
description: "Body text for the narrative section.",
|
||||
}),
|
||||
metrics: z
|
||||
.array(MetricSchema)
|
||||
.min(0)
|
||||
.max(4)
|
||||
.default([
|
||||
{ value: "50k+", label: "Active Users", subtext: "Last 12 months" },
|
||||
{ value: "50k+", label: "Active Users", subtext: "Last 12 months" },
|
||||
{ value: "50k+", label: "Active Users", subtext: "Last 12 months" },
|
||||
|
||||
])
|
||||
.meta({
|
||||
description: "Metric cards shown in the right column.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide10MetricsSplit = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
color: "var(--background-text,#ffffff)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<h2 className="text-[64px] font-medium tracking-[-0.03em]" style={{ color: "var(--background-text,#f2f4ff)" }}>{data.title}</h2>
|
||||
<div className="relative z-10 flex min-h-[520px] gap-10">
|
||||
<div className="w-1/2">
|
||||
<h3 className="mt-[28px] text-[24px] font-medium" style={{ color: "var(--background-text,#f1f4ff)" }}>{data.explanationTitle}</h3>
|
||||
<p className="mt-[16px] text-[22px] leading-[145%]" style={{ color: "var(--background-text,#d2d9ff)" }}>{data.explanation}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid w-1/2 grid-cols-2 auto-rows-max place-content-center justify-items-center gap-x-[16px] gap-y-[25px]">
|
||||
{data?.metrics?.map((metric, index) => (
|
||||
<div
|
||||
key={`metric-grid-${index}`}
|
||||
className="rounded-[16px] w-[280px] border pt-[26px] px-[26px] pb-[16px] text-center"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
}}
|
||||
>
|
||||
<p className="text-[64px] font-semibold leading-none" style={{ color: "var(--graph-0,#8bb4ff)" }}>{metric.value}</p>
|
||||
<p className="mt-[13px] text-[26px]" style={{ color: "var(--background-text,#edf1ff)" }}>{metric.label}</p>
|
||||
<p className="mt-[13px] text-[18px]" style={{ color: "var(--background-text,#8fa2d8)" }}>{metric.subtext}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide10MetricsSplit;
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "description-text-slide";
|
||||
export const slideLayoutName = "Description Text Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A text-only description slide tihe title/heading.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(30).default("Code + Explanation").meta({
|
||||
description: "Main slide title shown at the top-left.",
|
||||
}),
|
||||
descriptionTitle: z.string().min(4).max(20).default("Explanation").meta({
|
||||
description: "Subheading above the paragraph body.",
|
||||
}),
|
||||
description: z
|
||||
.string()
|
||||
|
||||
.max(360)
|
||||
.default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
)
|
||||
.meta({
|
||||
description: "Long-form explanation body.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide08CodeExplanationText = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#f2f4ff)" }}>{data.title}</h2>
|
||||
<div className="relative z-10 h-full max-w-[560px]">
|
||||
<h3 className="mt-[34px] text-[24px] font-medium" style={{ color: "var(--background-text,#f1f4ff)" }}>{data.descriptionTitle}</h3>
|
||||
<p className="mt-[16px] text-[22px] leading-[145%]" style={{ color: "var(--background-text,#d2d9ff)" }}>{data.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide08CodeExplanationText;
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(2).max(6).meta({
|
||||
description: "Primary metric value.",
|
||||
}),
|
||||
label: z.string().min(3).max(15).meta({
|
||||
description: "Metric label text.",
|
||||
}),
|
||||
subtext: z.string().min(3).max(30).meta({
|
||||
description: "Metric subtext/description.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "metrics-grid-slide";
|
||||
export const slideLayoutName = "Metrics Grid Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with metrics card grid and title at the top.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(18).default("Metrics").meta({
|
||||
description: "Slide heading shown above the KPI cards.",
|
||||
}),
|
||||
metrics: z
|
||||
.array(MetricSchema)
|
||||
.min(1)
|
||||
.max(6)
|
||||
.default([
|
||||
{ value: "99.9%", label: "Uptime", subtext: "Last 12 months" },
|
||||
{ value: "<100ms", label: "Response Time", subtext: "Last 12 months" },
|
||||
{ value: "50k+", label: "Active Users", subtext: "Last 12 months" },
|
||||
{ value: "99.9%", label: "Uptime", subtext: "Last 12 months" },
|
||||
{ value: "<100ms", label: "Response Time", subtext: "Last 12 months" },
|
||||
{ value: "50k+", label: "Active Users", subtext: "Last 12 months" },
|
||||
])
|
||||
.meta({
|
||||
description: "Metrics cards in a grid.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide11MetricsGrid = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] border p-[40px]"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#243272)",
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
color: "var(--background-text,#edf1ff)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[53px] grid flex-1 grid-cols-3 gap-[14px]">
|
||||
{data?.metrics?.map((metric, index) => (
|
||||
<div
|
||||
key={`metric-grid-${index}`}
|
||||
className="rounded-[16px] border pt-[26px] px-[26px] pb-[16px] text-center"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
}}
|
||||
>
|
||||
<p className="text-[64px] font-semibold leading-none" style={{ color: "var(--graph-0,#8bb4ff)" }}>{metric.value}</p>
|
||||
<p className="mt-[13px] text-[26px]" style={{ color: "var(--background-text,#edf1ff)" }}>{metric.label}</p>
|
||||
<p className="mt-[13px] text-[18px]" style={{ color: "var(--background-text,#8fa2d8)" }}>{metric.subtext}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide11MetricsGrid;
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "table-of-content-slide";
|
||||
export const slideLayoutName = "Table Of Content Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A two-column table of contents with numbered entries, labels and description.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(24).default("Table of Content").meta({
|
||||
description: "Slide heading shown above the index list.",
|
||||
}),
|
||||
items: z
|
||||
.array(z.object({
|
||||
number: z.string().min(2).max(2).meta({ "description": "Bullet Serial Number" }),
|
||||
label: z.string().min(3).max(30).meta({ "description": "Page/Content Name" }),
|
||||
description: z.string().min(3).max(100).optional().meta({ "description": "Short description for the content section." }),
|
||||
}))
|
||||
.min(12)
|
||||
.max(12)
|
||||
.default([
|
||||
{ number: "01", label: "Content Section summary", description: "A quick brown fox jumps over a lazy dog." },
|
||||
{ number: "01", label: "Content Section summary", description: "A quick brown fox jumps over a lazy dog." },
|
||||
{ number: "01", label: "Content Section summary", description: "A quick brown fox jumps over a lazy dog." },
|
||||
{ number: "01", label: "Content Section summary", description: "A quick brown fox jumps over a lazy dog." },
|
||||
{ number: "01", label: "Content Section summary", description: "A quick brown fox jumps over a lazy dog." },
|
||||
{ number: "06", label: "Content 6", description: "Section summary" },
|
||||
{ number: "07", label: "Content 7", description: "Section summary" },
|
||||
{ number: "08", label: "Content 8", description: "Section summary" },
|
||||
{ number: "09", label: "Content 9", description: "Section summary" },
|
||||
{ number: "10", label: "Content 10", description: "Section summary" },
|
||||
{ number: "11", label: "Content 11", description: "Section summary" },
|
||||
{ number: "12", label: "Content 12", description: "Section summary" },
|
||||
])
|
||||
.meta({
|
||||
description: "Table of contents entries.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
function TocColumn({ items }: { items: { number: string; label: string, description?: string }[] }) {
|
||||
return (
|
||||
<div className="space-y-[26px]">
|
||||
{items.map((item, index) => {
|
||||
|
||||
|
||||
return (
|
||||
<div key={`${item.number}-${item.label}`} className="flex items-start gap-[10px]" style={{ color: "var(--background-text,#d5dcff)" }}>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<div className=""><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22" fill="none">
|
||||
<path d="M17.7778 17.7776C18.2493 17.7776 18.7015 17.5903 19.0349 17.2569C19.3683 16.9235 19.5556 16.4713 19.5556 15.9998V7.11095C19.5556 6.63945 19.3683 6.18727 19.0349 5.85387C18.7015 5.52047 18.2493 5.33317 17.7778 5.33317H10.7556C10.4583 5.33609 10.165 5.26438 9.90254 5.12462C9.6401 4.98486 9.41691 4.7815 9.25339 4.53317L8.53339 3.4665C8.37151 3.2207 8.15114 3.01893 7.89205 2.8793C7.63296 2.73967 7.34326 2.66655 7.04894 2.6665H3.55561C3.08411 2.6665 2.63193 2.8538 2.29853 3.1872C1.96513 3.5206 1.77783 3.97279 1.77783 4.44428V15.9998C1.77783 16.4713 1.96513 16.9235 2.29853 17.2569C2.63193 17.5903 3.08411 17.7776 3.55561 17.7776H17.7778Z" stroke="var(--primary-color,#51A2FF)" strokeWidth="1.77778" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg></div>
|
||||
<p className="w-[36px] text-[18px] leading-none" style={{ color: "var(--primary-text,#8EC5FF)" }}>{item.number}</p>
|
||||
</div>
|
||||
<div className="flex gap-[26px]">
|
||||
<p className="text-[18px]" style={{ color: "var(--background-text,#ffffff)" }}>{item.label}</p>
|
||||
{item.description && <p className="text-[18px]" style={{ color: "var(--background-text,#ffffff)" }}>{item.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CodeSlide09TableOfContent = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const leftItems = data?.items?.slice(0, data?.items?.length / 2);
|
||||
const rightItems = data?.items?.slice(data?.items?.length / 2);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#f2f4ff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[53px] grid flex-1 grid-cols-2 gap-[24px]">
|
||||
<TocColumn items={leftItems || []} />
|
||||
<TocColumn items={rightItems || []} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide09TableOfContent;
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const DEFAULT_TABLE_COLUMNS = ["Feature", "Column 1", "Column 2", "Column 3"];
|
||||
|
||||
const DEFAULT_ROWS = [
|
||||
{ cells: ["Component-based", "check", "check", "check"] },
|
||||
{ cells: ["TypeScript Support", "check", "check", "check"] },
|
||||
{ cells: ["Learning Curve", "Medium", "Easy", "Steep"] },
|
||||
{ cells: ["Bundle Size", "40KB", "34KB", "167KB"] },
|
||||
{ cells: ["Performance", "Excellent", "Excellent", "Good"] },
|
||||
{ cells: ["Community Size", "Huge", "Large", "Large"] },
|
||||
];
|
||||
|
||||
const ComparisonRowSchema = z.object({
|
||||
cells: z.array(z.string().max(24)).min(1).max(6).meta({
|
||||
description: "Cell values for this row in left-to-right order. Match the number of table columns.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "table-slide";
|
||||
export const slideLayoutName = "Table Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with title and a table.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(18).default("Comparison").meta({
|
||||
description: "Slide title shown above the table.",
|
||||
}),
|
||||
tableColumns: z.array(z.string().max(18)).min(1).max(6).meta({
|
||||
description: "Table columns shown in the first row.",
|
||||
}).default(DEFAULT_TABLE_COLUMNS),
|
||||
rows: z
|
||||
.array(ComparisonRowSchema)
|
||||
.min(1)
|
||||
.max(6)
|
||||
.default(DEFAULT_ROWS)
|
||||
.meta({
|
||||
description: "Table rows where each row contains a cells array matching the table columns.",
|
||||
}),
|
||||
}).superRefine((value, ctx) => {
|
||||
value.rows.forEach((row, rowIndex) => {
|
||||
if (row.cells.length !== value.tableColumns.length) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["rows", rowIndex, "cells"],
|
||||
message: "Each row must contain the same number of cells as tableColumns.",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
function getGridTemplateColumns(columnCount: number) {
|
||||
if (columnCount <= 1) {
|
||||
return "minmax(0, 1fr)";
|
||||
}
|
||||
|
||||
return `minmax(0, 1.4fr) repeat(${columnCount - 1}, minmax(0, 1fr))`;
|
||||
}
|
||||
|
||||
function renderCell(value: string, isFirstColumn: boolean) {
|
||||
if (!isFirstColumn && value && value.toLowerCase() === "check") {
|
||||
return <span className="text-[26px] px-[32px]" style={{ color: "var(--graph-2,#37f08e)" }}>✓</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className="text-[18px] px-[32px]"
|
||||
style={{
|
||||
color: isFirstColumn
|
||||
? "var(--background-text,#d5dcff)"
|
||||
: "var(--background-text,#CAD5E2)",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const CodeSlide05ComparisonTable = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const tableColumns = data.tableColumns?.length ? data.tableColumns : DEFAULT_TABLE_COLUMNS;
|
||||
const rows = data.rows?.length ? data.rows : DEFAULT_ROWS;
|
||||
const gridTemplateColumns = getGridTemplateColumns(tableColumns.length);
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{data.title}</h2>
|
||||
|
||||
<div
|
||||
className="mt-[22px] min-h-0 flex-1 rounded-[16px] border"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#0F172BCC)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="grid items-center"
|
||||
style={{
|
||||
color: "var(--background-text,#8ea1da)",
|
||||
gridTemplateColumns,
|
||||
}}
|
||||
>
|
||||
|
||||
{tableColumns.map((column, columnIndex) => (
|
||||
<p
|
||||
key={`${column}-${columnIndex}`}
|
||||
className="px-[32px] py-[16px] text-[18px] text-center border-b border-r"
|
||||
style={{
|
||||
color: "var(--background-text,#ffffff)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
borderRightWidth: columnIndex === tableColumns.length - 1 ? "0px" : undefined,
|
||||
}}
|
||||
>
|
||||
{column}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div
|
||||
key={`row-${rowIndex}`}
|
||||
className="grid"
|
||||
style={{
|
||||
gridTemplateColumns,
|
||||
}}
|
||||
>
|
||||
{row.cells.map((cell, cellIndex) => (
|
||||
<div
|
||||
key={`row-${rowIndex}-cell-${cellIndex}`}
|
||||
className="flex items-center justify-center border-b border-r px-[20px] py-[20px] text-center"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
borderRightWidth: cellIndex === row.cells.length - 1 ? "0px" : undefined,
|
||||
}}
|
||||
>
|
||||
{renderCell(cell, cellIndex === 0)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide05ComparisonTable;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "bullet-list-slide";
|
||||
export const slideLayoutName = "Two Column Bullet List Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A two-column numbered string list with items.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(30).default("Usecase").meta({
|
||||
description: "Slide title shown above the numbered list.",
|
||||
}),
|
||||
items: z
|
||||
.array(z.string().min(1).max(200))
|
||||
.min(1)
|
||||
.max(8)
|
||||
.default([
|
||||
"Use pre-built component library for UI consistency",
|
||||
"Integrate REST API with TypeScript for type safety",
|
||||
"Implement real-time updates using WebSocket",
|
||||
"Deploy to production with automated CI/CD pipeline",
|
||||
"Enable role-based permissions for protected actions",
|
||||
"Generate docs automatically from route contracts",
|
||||
"Track release health with telemetry dashboards",
|
||||
"Add rollback strategy for high-risk deployments",
|
||||
])
|
||||
.meta({
|
||||
description: "Eight use-case items shown in two columns.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide07UseCaseList = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#f2f4ff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[53px] grid flex-1 grid-cols-2 gap-[21px]">
|
||||
{data?.items?.map((item, index) => (
|
||||
<div
|
||||
key={`use-case-${index}`}
|
||||
className="flex items-center gap-[21px] rounded-[18px] border p-[28px]"
|
||||
style={{
|
||||
boxShadow: "0 33.333px 66.667px -16px rgba(0, 0, 0, 0.25)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="flex h-[42px] w-[42px] shrink-0 items-center justify-center rounded-full border text-[18px]"
|
||||
style={{
|
||||
borderColor: "var(--primary-color,#2B7FFF4D)",
|
||||
backgroundColor: "var(--primary-color,#2B7FFF33)",
|
||||
color: "var(--primary-text,#51A2FF)",
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<p className="text-[18px]" style={{ color: "var(--background-text,#d5dcff)" }}>{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide07UseCaseList;
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
import { Fragment } from "react";
|
||||
import * as z from "zod";
|
||||
|
||||
const WorkflowStepSchema = z.object({
|
||||
title: z.string().min(3).max(12).meta({
|
||||
description: "Step title shown in each workflow card.",
|
||||
}),
|
||||
description: z.string().min(18).max(50).meta({
|
||||
description: "Short step description text.",
|
||||
}),
|
||||
icon: z.object({
|
||||
__icon_url__: z.string().meta({
|
||||
description: "URL to icon",
|
||||
}),
|
||||
__icon_query__: z.string().meta({
|
||||
description: "Query used to search the icon",
|
||||
}),
|
||||
}).default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "workflow-slide";
|
||||
export const slideLayoutName = "Workflow Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A workflow slide with cards and directional arrows between steps.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(16).default("Workflow").meta({
|
||||
description: "Slide title shown above the workflow row.",
|
||||
}),
|
||||
steps: z
|
||||
.array(WorkflowStepSchema)
|
||||
.min(1)
|
||||
.max(4)
|
||||
.default([
|
||||
{
|
||||
title: "Design",
|
||||
description: "Create wireframes and design system components.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Develop",
|
||||
description: "Build features using modern frameworks and best practices.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Test & QA",
|
||||
description: "Run automated tests and quality assurance checks.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Deploy",
|
||||
description: "Ship to production with CI and CD pipeline automation.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
},
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Workflow steps shown in sequence.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CodeSlide06Workflow = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#101B37)",
|
||||
fontFamily: "var(--body-font-family,Nunito Sans)",
|
||||
}}
|
||||
>
|
||||
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[52px] grid flex-1 grid-cols-[1fr_auto_1fr_auto_1fr_auto_1fr] items-center gap-[12px]">
|
||||
{data?.steps?.map((step, index) => (
|
||||
<Fragment key={`${step.title}-${index}`}>
|
||||
<div
|
||||
className="rounded-[18px] border p-[21px] text-center"
|
||||
style={{
|
||||
boxShadow: "0 33.333px 66.667px -16px rgba(0, 0, 0, 0.25)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto flex h-[75px] w-[75px] items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: "var(--primary-color,#2B7FFF80)",
|
||||
backgroundColor: "var(--primary-color,#2B7FFF33)",
|
||||
}}
|
||||
>
|
||||
<RemoteSvgIcon
|
||||
url={step.icon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[37px] w-[37px] object-contain"
|
||||
color="var(--primary-text, #ffffff)"
|
||||
title={step.icon.__icon_query__}
|
||||
/>
|
||||
{/* <img src={step.icon.__icon_url__} alt={step.icon.__icon_query__} className="h-[37px] w-[37px] object-contain"
|
||||
style={{
|
||||
filter: "invert(1)",
|
||||
}}
|
||||
/> */}
|
||||
</div>
|
||||
<h3 className="mt-[12px] text-[24px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{step.title}</h3>
|
||||
<p className="mt-[12px] text-[18px]" style={{ color: "var(--background-text,#90A1B9)" }}>{step.description}</p>
|
||||
</div>
|
||||
{index < (data?.steps?.length || 0) - 1 && (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="43" height="43" viewBox="0 0 43 43" fill="none">
|
||||
<path d="M8.88892 21.3333H33.7778" stroke="var(--primary-color,#51A2FF)" strokeWidth="3.55556" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M21.3334 8.88892L33.7778 21.3334L21.3334 33.7778" stroke="var(--primary-color,#51A2FF)" strokeWidth="3.55556" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeSlide06Workflow;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"description": "Developer-focused layouts for roadmaps, APIs, code explanations, and technical metrics",
|
||||
"ordered": false,
|
||||
"default": false
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "about-slide";
|
||||
export const slideLayoutName = "About Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A left text column with company/instructor/presenter/institute name and title introduction and a right-side visual grid made from one repeated image and tinted text panels.";
|
||||
|
||||
export const Schema = z.object({
|
||||
name: z.string().min(3).max(22).default("Company Name").meta({
|
||||
description: "Main heading in the left content column.",
|
||||
}),
|
||||
intro: z.string().min(40).max(100).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et."
|
||||
).meta({
|
||||
description: "Bold intro text shown beneath the company heading.",
|
||||
}),
|
||||
body: z.string().min(120).max(280).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Body paragraph in the left content section.",
|
||||
}),
|
||||
topPanelText: z.string().min(20).max(70).default("Insert info about the company.").meta({
|
||||
description: "Short text inside the top-right dark panel. ",
|
||||
}),
|
||||
bottomPanelText: z.string().min(20).max(70).default("Insert info about the company and your mission statement.").meta({
|
||||
description: "Short text inside the bottom-right dark panel.",
|
||||
}),
|
||||
topFeatureImage: z.object({
|
||||
__image_url__: z.string().default("https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80"),
|
||||
__image_prompt__: z.string().default("Office team collaboration"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Office team collaboration",
|
||||
}).meta({
|
||||
description: "Single image reused in the top right-side visual grid.",
|
||||
}),
|
||||
bottomFeatureImage: z.object({
|
||||
__image_url__: z.string().default("https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80"),
|
||||
__image_prompt__: z.string().default("Office team collaboration"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Office team collaboration",
|
||||
}).meta({
|
||||
description: "Single image reused in the bottom right-side visual grid.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationAboutSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
|
||||
return (<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#efeff1)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
<div className="grid items-end grid-cols-[1fr_1fr]">
|
||||
<div className="px-[53px] pb-[56px] ">
|
||||
<h2 className="font-serif text-[64px] leading-[98%] tracking-[-0.02em]" style={{ color: "var(--primary-color,#101C3D)" }}>
|
||||
{data.name}
|
||||
</h2>
|
||||
<p className="mt-[30px] max-w-[610px] text-[22px] font-semibold leading-[1.24]" style={{ color: "var(--background-text,#34394C)" }}>
|
||||
{data.intro}
|
||||
</p>
|
||||
<p className="mt-[18px] max-w-[620px] text-[22px] leading-[1.28]" style={{ color: "var(--background-text,#46474C)" }}>
|
||||
{data.body}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className=" ">
|
||||
<div className="relative flex overflow-hidden h-[360px]">
|
||||
<img
|
||||
src={data.topFeatureImage?.__image_url__}
|
||||
alt={data.topFeatureImage?.__image_prompt__}
|
||||
className="absolute inset-0 h-full w-full object-cover z-1 "
|
||||
/>
|
||||
<div className="w-1/2 z-10 flex justify-center items-center relative">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#28256f)",
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
<p className="relative z-10 text-[24px] leading-[1.22] px-[42px]" style={{ color: "var(--primary-text,#f5f7ff)" }}>
|
||||
{data.topPanelText}
|
||||
</p>
|
||||
</div>
|
||||
<div className=" w-1/2 ">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex overflow-hidden h-[360px]">
|
||||
<img
|
||||
src={data.bottomFeatureImage?.__image_url__}
|
||||
alt={data.bottomFeatureImage?.__image_prompt__}
|
||||
className="absolute inset-0 h-full w-full object-cover "
|
||||
/>
|
||||
<div className=" w-1/2 ">
|
||||
|
||||
</div>
|
||||
<div className="w-1/2 z-10 flex justify-center items-center relative">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#28256f)",
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
<p className="relative z-10 text-[24px] leading-[1.22] px-[42px]" style={{ color: "var(--primary-text,#f5f7ff)" }}>
|
||||
{data.bottomPanelText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationAboutSlide;
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
LabelList,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
Pie,
|
||||
PieChart,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Scatter,
|
||||
ScatterChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
export type EducationChartType =
|
||||
| "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";
|
||||
|
||||
export type SimpleDatum = {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type MultiSeriesDatum = {
|
||||
name: string;
|
||||
values: Record<string, number>;
|
||||
};
|
||||
|
||||
export type DivergingDatum = {
|
||||
name: string;
|
||||
positive: number;
|
||||
negative: number;
|
||||
};
|
||||
|
||||
export type ScatterDatum = {
|
||||
x: number;
|
||||
y: number;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type EducationChartDatum = SimpleDatum | MultiSeriesDatum | DivergingDatum | ScatterDatum;
|
||||
|
||||
type TooltipPayloadItem = {
|
||||
color?: string;
|
||||
dataKey?: string | number;
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
"var(--graph-0,#4A15A8)",
|
||||
"var(--graph-1,#5B45AD)",
|
||||
"var(--graph-2,#7E6CC0)",
|
||||
"var(--graph-3,#9F94CD)",
|
||||
"var(--graph-4,#6A31B8)",
|
||||
"var(--graph-5,#4D2A97)",
|
||||
];
|
||||
|
||||
const AXIS = "var(--background-text,#7C7A83)";
|
||||
const GRID = "var(--stroke,#CFCBD8)";
|
||||
const PRIMARY_TEXT = "var(--background-text,#3E3C45)";
|
||||
|
||||
function formatComma(value: number | string) {
|
||||
if (typeof value === "number") {
|
||||
return value.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function isSimpleDatum(item: EducationChartDatum): item is SimpleDatum {
|
||||
return typeof (item as SimpleDatum).name === "string" && typeof (item as SimpleDatum).value === "number";
|
||||
}
|
||||
|
||||
|
||||
|
||||
function isScatterDatum(item: EducationChartDatum): item is ScatterDatum {
|
||||
return typeof (item as ScatterDatum).x === "number" && typeof (item as ScatterDatum).y === "number";
|
||||
}
|
||||
|
||||
function toSimpleData(data: EducationChartDatum[]) {
|
||||
return data
|
||||
.filter(isSimpleDatum)
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
}));
|
||||
}
|
||||
|
||||
function toScatterData(data: EducationChartDatum[]) {
|
||||
const scatterData = data.filter(isScatterDatum);
|
||||
|
||||
if (scatterData.length > 0) {
|
||||
return scatterData.map((item, index) => ({
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
name: item.name ?? String(index + 1),
|
||||
}));
|
||||
}
|
||||
|
||||
return data
|
||||
.filter(isSimpleDatum)
|
||||
.map((item, index) => ({
|
||||
x: index + 1,
|
||||
y: item.value,
|
||||
name: item.name,
|
||||
}));
|
||||
}
|
||||
|
||||
const renderPieInsideLabel = (props: any) => {
|
||||
const {
|
||||
cx = 0,
|
||||
cy = 0,
|
||||
midAngle = 0,
|
||||
innerRadius: ir = 0,
|
||||
outerRadius: or = 0,
|
||||
percent = 0,
|
||||
name,
|
||||
} = props;
|
||||
if (percent < 0.08) return null;
|
||||
const toNum = (v: unknown) => {
|
||||
if (typeof v === "number" && Number.isFinite(v)) return v;
|
||||
if (typeof v === "string" && v.trim().endsWith("%")) return NaN;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : NaN;
|
||||
};
|
||||
let inner = toNum(ir);
|
||||
let outer = toNum(or);
|
||||
if (!Number.isFinite(outer)) {
|
||||
outer = 140;
|
||||
inner = Number.isFinite(inner) ? inner : 0;
|
||||
}
|
||||
if (!Number.isFinite(inner)) inner = 0;
|
||||
const midR = inner + (outer - inner) * 0.5;
|
||||
const rad = (-midAngle * Math.PI) / 180;
|
||||
const x = cx + midR * Math.cos(rad);
|
||||
const y = cy + midR * Math.sin(rad);
|
||||
const nm = String(name ?? "");
|
||||
const short = nm.length <= 10;
|
||||
const pct = `${(percent * 100).toFixed(0)}%`;
|
||||
const fontSize = short ? 10 : 9;
|
||||
const labelText = short ? `${name} ${pct}` : pct;
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="var(--primary-text,#ffffff)"
|
||||
fontSize={fontSize}
|
||||
fontWeight={600}
|
||||
style={{
|
||||
paintOrder: "stroke fill",
|
||||
stroke: "rgba(0,0,0,0.28)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function getChartColor(index: number) {
|
||||
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
||||
}
|
||||
|
||||
|
||||
function ChartLegend({ showLegend }: { showLegend: boolean }) {
|
||||
if (!showLegend) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Legend wrapperStyle={{ fontSize: "12px", color: AXIS, paddingTop: "8px" }} iconType="circle" />;
|
||||
}
|
||||
|
||||
export default function EducationChartPrimitives({
|
||||
chartType,
|
||||
chartData,
|
||||
series,
|
||||
showLegend,
|
||||
|
||||
divergingLabels,
|
||||
}: {
|
||||
chartType: EducationChartType;
|
||||
chartData: EducationChartDatum[];
|
||||
series: string[];
|
||||
showLegend: boolean;
|
||||
showTooltip: boolean;
|
||||
divergingLabels: [string, string];
|
||||
}) {
|
||||
const axisProps = {
|
||||
tick: { fill: AXIS, fontSize: 12, fontFamily: "var(--body-font-family,'Times New Roman')" },
|
||||
axisLine: { stroke: GRID },
|
||||
tickLine: { stroke: GRID },
|
||||
} as const;
|
||||
|
||||
const gridProps = {
|
||||
strokeDasharray: "0",
|
||||
stroke: GRID,
|
||||
opacity: 1,
|
||||
} as const;
|
||||
|
||||
const commonMargin = { top: 10, right: 12, left: 6, bottom: 8 };
|
||||
|
||||
const simpleData = toSimpleData(chartData);
|
||||
|
||||
const chart = (() => {
|
||||
switch (chartType) {
|
||||
case "bar":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={simpleData} margin={commonMargin}>
|
||||
<XAxis dataKey="name" {...axisProps} axisLine={false} tickLine={false} />
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Bar dataKey="value" radius={[18, 18, 18, 18]} barSize={30} isAnimationActive={false}>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`bar-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
<LabelList
|
||||
dataKey="value"
|
||||
position="top"
|
||||
fill={AXIS}
|
||||
fontSize={12}
|
||||
offset={10}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
);
|
||||
|
||||
case "bar-horizontal":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={simpleData} layout="vertical" margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} horizontal={false} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
|
||||
<YAxis type="category" dataKey="name" {...axisProps} width={74} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Bar dataKey="value" radius={[0, 10, 10, 0]} isAnimationActive={false}>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`barh-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case "line":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={simpleData} margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} horizontal={false} />
|
||||
<XAxis dataKey="name" {...axisProps} axisLine={false} tickLine={false} />
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} axisLine={false} tickLine={false} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "area":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={simpleData} margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} vertical={false} />
|
||||
<XAxis dataKey="name" {...axisProps} axisLine={false} tickLine={false} />
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} axisLine={false} tickLine={false} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<defs>
|
||||
<linearGradient id="education-area-fill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={getChartColor(0)} stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor={getChartColor(0)} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={2}
|
||||
fill="url(#education-area-fill)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
|
||||
|
||||
case "pie":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart margin={commonMargin}>
|
||||
<Pie
|
||||
data={simpleData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
|
||||
label={renderPieInsideLabel}
|
||||
labelLine={false}
|
||||
>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`pie-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "donut":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart margin={commonMargin}>
|
||||
<Pie
|
||||
data={simpleData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={90}
|
||||
label={renderPieInsideLabel}
|
||||
paddingAngle={2}
|
||||
labelLine={false}
|
||||
>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`donut-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "scatter": {
|
||||
const scatterData = toScatterData(chartData);
|
||||
const labelMap = new Map<number, string>();
|
||||
const xTicks = Array.from(new Set(scatterData.map((item) => item.x))).sort((a, b) => a - b);
|
||||
const minTick = xTicks[0] ?? 0;
|
||||
const maxTick = xTicks[xTicks.length - 1] ?? 1;
|
||||
|
||||
scatterData.forEach((item) => {
|
||||
labelMap.set(item.x, item.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} vertical={false} />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="x"
|
||||
{...axisProps}
|
||||
ticks={xTicks}
|
||||
domain={[minTick - 0.5, maxTick + 0.5]}
|
||||
tickFormatter={(value) => labelMap.get(Number(value)) ?? String(value)}
|
||||
/>
|
||||
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Scatter data={scatterData} fill={getChartColor(0)} isAnimationActive={false}>
|
||||
{scatterData.map((_, index) => (
|
||||
<Cell key={`scatter-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-[14px]" style={{ color: PRIMARY_TEXT }}>
|
||||
Unsupported chart type
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
return <ResponsiveContainer width="100%" height="100%">{chart}</ResponsiveContainer>;
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "content-split-slide";
|
||||
export const slideLayoutName = "Content Split Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A left collage built from one repeated image and a right content block containing heading, tagline, and paragraph text.";
|
||||
|
||||
export const Schema = z.object({
|
||||
heading: z.string().max(24).default("Heading").meta({
|
||||
description: "Main right-side heading.",
|
||||
}),
|
||||
tagline: z.string().max(12).default("TAGLINE").meta({
|
||||
description: "Small uppercase label shown under the heading.",
|
||||
}),
|
||||
body: z.string().max(300).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Main descriptive paragraph on the right side.",
|
||||
}),
|
||||
images: z.array(z.object({
|
||||
__image_url__: z.string().default("https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80"),
|
||||
__image_prompt__: z.string().default("Business team around a laptop"),
|
||||
})).min(1).max(3).default([{
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Business team around a laptop",
|
||||
},
|
||||
{
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Business team around a laptop",
|
||||
},
|
||||
{
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Business team around a laptop",
|
||||
},
|
||||
]).meta({
|
||||
description: "Array of images reused to create the left collage composition.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationContentSplitSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { heading, tagline, body, images } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#E6E7E8)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center h-full">
|
||||
<div className="w-[660px] h-full">
|
||||
<div className="h-[394px] w-full mb-[6px]">
|
||||
<img
|
||||
src={images?.[0]?.__image_url__}
|
||||
alt={images?.[0]?.__image_prompt__}
|
||||
className=" h-full w-full object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full gap-[6px] h-[320px] ">
|
||||
<div className="w-[330px]"> <img
|
||||
src={images?.[1]?.__image_url__}
|
||||
alt={images?.[1]?.__image_prompt__}
|
||||
className="h-full w-full object-cover "
|
||||
/></div>
|
||||
<div className="w-[330px]"> <img
|
||||
src={images?.[2]?.__image_url__}
|
||||
alt={images?.[2]?.__image_prompt__}
|
||||
className="h-full w-full object-cover "
|
||||
/></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-1/2 px-[56px]">
|
||||
<h2 className="text-[24px] font-medium leading-none" style={{ color: "var(--background-text,#34394C)" }}>{heading}</h2>
|
||||
<p className=" text-[14px] font-medium mt-1 leading-none tracking-[0.04em]" style={{ color: "var(--background-text,#454962)" }}>
|
||||
{tagline}
|
||||
</p>
|
||||
<p className="mt-[18px] text-[22px] leading-[1.28]" style={{ color: "var(--background-text,#34394C)" }}>{body}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationContentSplitSlide;
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "cover-slide";
|
||||
export const slideLayoutName = "Cover Slide";
|
||||
export const slideLayoutDescription =
|
||||
"Opening/cover/intro slide with organization/institution/presenter, presentation title/heading , and supporting subtitle.";
|
||||
|
||||
export const Schema = z.object({
|
||||
name: z.string().min(3).max(16).optional().default("Name").meta({
|
||||
description: "Optional organization/institution/presenter name shown above the slide title.",
|
||||
}),
|
||||
title: z.string().min(6).max(32).default("PowerPoint Template").meta({
|
||||
description: "Main centered title of the cover slide.",
|
||||
}),
|
||||
backgroundImage: z.object({
|
||||
__image_url__: z.string().default("https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1920&q=80"),
|
||||
__image_prompt__: z.string().default("City business district buildings"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1920&q=80",
|
||||
__image_prompt__: "City business district buildings",
|
||||
}).meta({
|
||||
description: "Single background image used across the cover.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationCoverSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { name, title, backgroundImage } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#2e0a8a)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={backgroundImage?.__image_url__}
|
||||
alt={backgroundImage?.__image_prompt__}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#3b0bb6)",
|
||||
opacity: 0.85,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<div
|
||||
className="relative z-10 flex h-full flex-col items-center justify-center text-center"
|
||||
style={{ color: "var(--primary-text,#ffffff)" }}
|
||||
>
|
||||
{name && <p className="text-[22px] font-normal tracking-[0.64px]">{name}</p>
|
||||
} <h1 className="mt-[12px] px-[53px] text-[64px] font-medium leading-[98%]">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationCoverSlide;
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "image-gallery-slide";
|
||||
export const slideLayoutName = "Image Gallery Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a left image gallery and right text block for gallery heading and description.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(24).default("Image Gallery").meta({
|
||||
description: "Heading on the right side.",
|
||||
}),
|
||||
body: z.string().max(300).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Supporting paragraph shown below the heading.",
|
||||
}),
|
||||
galleryImages: z.array(z.object({
|
||||
__image_url__: z.string(),
|
||||
__image_prompt__: z.string(),
|
||||
})).max(5).min(5).default(Array(5).fill({
|
||||
__image_url__: "https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Office team collaboration",
|
||||
})).meta({
|
||||
description: "Image gallery images.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationImageGallerySlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
const { title, body, galleryImages } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#E6E7E8)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full items-end grid-cols-[590px_1fr]">
|
||||
<div className="grid h-full grid-cols-2 grid-rows-[245px_245px_230px] gap-[2px]">
|
||||
<img
|
||||
src={galleryImages?.[0].__image_url__}
|
||||
alt={galleryImages?.[0].__image_prompt__}
|
||||
className="h-full w-full object-cover object-left"
|
||||
/>
|
||||
<img
|
||||
src={galleryImages?.[1].__image_url__}
|
||||
alt={galleryImages?.[1].__image_prompt__}
|
||||
className="h-full w-full object-cover object-right"
|
||||
/>
|
||||
<img
|
||||
src={galleryImages?.[2].__image_url__}
|
||||
alt={galleryImages?.[2].__image_prompt__}
|
||||
className="h-full w-full object-cover object-top"
|
||||
/>
|
||||
<img
|
||||
src={galleryImages?.[3].__image_url__}
|
||||
alt={galleryImages?.[3].__image_prompt__}
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
<img
|
||||
src={galleryImages?.[4].__image_url__}
|
||||
alt={galleryImages?.[4].__image_prompt__}
|
||||
className="col-span-2 h-full w-full object-cover object-bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-[64px] pb-[56px] ">
|
||||
<h2 className="font-serif text-[64px] font-medium leading-[98%]" style={{ color: "var(--primary-color,#101C3D)" }}>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-[37px] text-[22px]" style={{ color: "var(--background-text,#34394C)" }}>
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationImageGallerySlide;
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import EducationChartPrimitives, {
|
||||
type EducationChartDatum,
|
||||
type EducationChartType,
|
||||
} from "./EducationChartPrimitives";
|
||||
|
||||
export const slideLayoutId = "report-chart-slide";
|
||||
export const slideLayoutName = "Report Chart Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A left text column with a report title, body, footnote and a right-side chart.";
|
||||
|
||||
const ChartTypeSchema = z.enum([
|
||||
"bar",
|
||||
|
||||
"line",
|
||||
"area",
|
||||
|
||||
"pie",
|
||||
"donut",
|
||||
"scatter",
|
||||
]);
|
||||
|
||||
const SimpleDataSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Simple chart category label.",
|
||||
}),
|
||||
value: z.number().meta({
|
||||
description: "Simple chart numeric value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const MultiSeriesDataSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Grouped/stacked category label.",
|
||||
}),
|
||||
values: z.record(z.string(), z.number()).meta({
|
||||
description: "Series-to-value map for grouped or stacked charts.",
|
||||
}),
|
||||
});
|
||||
|
||||
const DivergingDataSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Diverging chart category label.",
|
||||
}),
|
||||
positive: z.number().min(0).max(100000).meta({
|
||||
description: "Positive side value.",
|
||||
}),
|
||||
negative: z.number().min(0).max(100000).meta({
|
||||
description: "Negative side value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const ScatterDataSchema = z.object({
|
||||
x: z.number().min(-100000).max(100000).meta({
|
||||
description: "Scatter X coordinate.",
|
||||
}),
|
||||
y: z.number().min(-100000).max(100000).meta({
|
||||
description: "Scatter Y coordinate.",
|
||||
}),
|
||||
name: z.string().min(1).max(20).optional().meta({
|
||||
description: "Optional scatter tick label.",
|
||||
}),
|
||||
});
|
||||
|
||||
const UnifiedChartDataSchema = z.union([
|
||||
z.array(SimpleDataSchema),
|
||||
z.array(MultiSeriesDataSchema),
|
||||
z.array(DivergingDataSchema),
|
||||
z.array(ScatterDataSchema),
|
||||
]);
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(24).default("Report").meta({
|
||||
description: "Left-side report title.",
|
||||
}),
|
||||
body: z.string().min(80).max(260).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Left-side report body paragraph.",
|
||||
}),
|
||||
footnote: z.string().min(20).max(150).default(
|
||||
"(Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.)"
|
||||
).meta({
|
||||
description: "Left-side footnote line.",
|
||||
}),
|
||||
chartTitle: z.string().min(8).max(42).default("Students by Grade Level").meta({
|
||||
description: "Right-panel chart heading.",
|
||||
}),
|
||||
dateRange: z.string().min(8).max(22).default("Apr 10 - Apr 17").meta({
|
||||
description: "Right-panel date range label.",
|
||||
}),
|
||||
chartType: ChartTypeSchema.default("bar").meta({
|
||||
description:
|
||||
"Chart type selector. Supports bar, grouped, stacked, clustered, diverging, line, area, pie/donut, and scatter.",
|
||||
}),
|
||||
chartData: UnifiedChartDataSchema.default([
|
||||
{ name: "Option A", value: 17.07 },
|
||||
{ name: "Option B", value: 45.23 },
|
||||
{ name: "Option C", value: 21.61 },
|
||||
{ name: "Option D", value: 16.36 },
|
||||
]).meta({
|
||||
description: "Unified chart data payload. Shape depends on chartType.",
|
||||
}),
|
||||
series: z.array(z.string().min(1).max(20)).max(6).default(["Series A", "Series B"]).meta({
|
||||
description: "Series names for grouped/stacked/clustered/area-stacked charts.",
|
||||
}),
|
||||
divergingLabels: z.tuple([z.string().min(1).max(24), z.string().min(1).max(24)]).default([
|
||||
"Positive",
|
||||
"Negative",
|
||||
]).meta({
|
||||
description: "Legend labels for bar-diverging charts.",
|
||||
}),
|
||||
showLegend: z.boolean().default(true).meta({
|
||||
description: "Show or hide chart legend.",
|
||||
}),
|
||||
|
||||
showStatusMessage: z.boolean().default(false).meta({
|
||||
description: "Show callout message under chart (useful for weekly/performance styles).",
|
||||
}),
|
||||
statusMessageTitle: z.string().min(8).max(40).default("You are doing good!").meta({
|
||||
description: "Callout headline under chart.",
|
||||
}),
|
||||
statusMessageBody: z.string().min(10).max(80).default("You almost reached your goal").meta({
|
||||
description: "Callout subtitle under chart.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
|
||||
|
||||
const EducationReportChartSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const slideData = data;
|
||||
|
||||
const chartHeightClass = slideData.showStatusMessage ? "h-[372px]" : "h-[486px]";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#efeff1)",
|
||||
fontFamily: "var(--body-font-family,'Times New Roman')",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full grid-cols-[1fr_560px] items-center ">
|
||||
<div className="px-[52px] pb-[46px] mt-[111px] ">
|
||||
<div className="text-start">
|
||||
<h2 className=" text-[64px] font-medium leading-[98%]" style={{ color: "var(--primary-color,#101C3D)" }}>
|
||||
{slideData.title}
|
||||
</h2>
|
||||
<p className=" mt-[38px] max-w-[610px] text-[22px] leading-[1.22]" style={{ color: "var(--background-text,#3E3F4A)" }}>
|
||||
{slideData.body}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="max-w-[610px] mt-[96px] text-[18px] leading-[1.22]" style={{ color: "var(--background-text,#4E4F57)" }}>
|
||||
{slideData.footnote}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="px-[42px] h-full flex flex-col justify-center" style={{ backgroundColor: "var(--card-color,#eceaf0)" }}>
|
||||
<h3 className="text-center text-[24px] font-semibold leading-none" style={{ color: "var(--background-text,#33313A)" }}>
|
||||
{slideData.chartTitle}
|
||||
</h3>
|
||||
<p className="mt-1 text-center pb-6 text-[18px] leading-none" style={{ color: "var(--background-text,#4D4B55)" }}>
|
||||
{slideData.dateRange}
|
||||
</p>
|
||||
|
||||
<div className={` ${chartHeightClass} h-[372px]`}>
|
||||
<EducationChartPrimitives
|
||||
chartType={slideData.chartType as EducationChartType}
|
||||
chartData={slideData.chartData as EducationChartDatum[]}
|
||||
series={slideData.series || []}
|
||||
showLegend={slideData.showLegend || false}
|
||||
divergingLabels={slideData.divergingLabels || ['', '']}
|
||||
showTooltip={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationReportChartSlide;
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "services-split-slide";
|
||||
export const slideLayoutName = "Services Split Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A left text column with a heading, one image column, and stacked service description blocks on the right side.";
|
||||
|
||||
const ServiceSchema = z.object({
|
||||
image: z.object({
|
||||
__image_url__: z.string(),
|
||||
__image_prompt__: z.string(),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Team meeting image reused across two rows",
|
||||
}).meta({
|
||||
description: "Single image in the middle column.",
|
||||
}),
|
||||
heading: z.string().min(3).max(18).meta({
|
||||
description: "Heading shown in the right column.",
|
||||
}),
|
||||
tagline: z.string().min(3).max(12).meta({
|
||||
description: "Short label under each heading.",
|
||||
}),
|
||||
body: z.string().max(40).meta({
|
||||
description: "Description paragraph shown below the heading and tagline.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(16).default("Services").meta({
|
||||
description: "Main slide title shown on the left.",
|
||||
}),
|
||||
sections: z
|
||||
.array(ServiceSchema)
|
||||
.min(1)
|
||||
.max(4)
|
||||
.default([
|
||||
{
|
||||
image: {
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Team meeting image reused across two rows",
|
||||
},
|
||||
heading: "Service 1",
|
||||
tagline: "TAGLINE",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.",
|
||||
},
|
||||
{
|
||||
image: {
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Team meeting image reused across two rows",
|
||||
},
|
||||
heading: "Service 2",
|
||||
tagline: "TAGLINE",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.",
|
||||
},
|
||||
{
|
||||
image: {
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Team meeting image reused across two rows",
|
||||
},
|
||||
heading: "Service 3",
|
||||
tagline: "TAGLINE",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.",
|
||||
},
|
||||
{
|
||||
image: {
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Team meeting image reused across two rows",
|
||||
},
|
||||
heading: "Service 4",
|
||||
tagline: "TAGLINE",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.",
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Two stacked service content sections on the right side.",
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationServicesSplitSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, sections } = data;
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#E6E7E8)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full grid-cols-[365px_1fr]">
|
||||
<div className="px-[53px] pt-[53px]">
|
||||
<h2 className="font-serif text-[64px] leading-[98%] tracking-[-0.02em]" style={{ color: "var(--primary-color,#1a1752)" }}>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className=" grid "
|
||||
style={{
|
||||
gridTemplateRows: `repeat(${sections?.length}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{sections?.map((section, index) => (
|
||||
<div key={`${section.heading}-${index}`} className=" flex items-center"
|
||||
style={{
|
||||
borderBottom:
|
||||
index !== (sections?.length ?? 1) - 1
|
||||
? "5px solid var(--stroke,rgba(255, 255, 255, 0.10))"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
<div className=" min-w-[316px] max-w-[316px] "
|
||||
style={{
|
||||
height: sections?.length === 4 ? '175px' : sections?.length === 3 ? '240px' : '357px'
|
||||
}}
|
||||
>
|
||||
|
||||
<img
|
||||
src={section.image?.__image_url__}
|
||||
alt={section.image?.__image_prompt__}
|
||||
className="h-full w-full object-cover "
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`px-[56px] `}
|
||||
>
|
||||
<h3 className="text-[24px] font-medium leading-none" style={{ color: "var(--background-text,#34394C)" }}>{section.heading}</h3>
|
||||
<p className="mt-[10px] text-[14px] font-medium uppercase leading-none" style={{ color: "var(--background-text,#454962)" }}>
|
||||
{section.tagline}
|
||||
</p>
|
||||
<p className="mt-[18px] text-[22px] leading-[1.26] tracking-[0.04em]" style={{ color: "var(--background-text,#34394C)" }}>
|
||||
{section.body}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationServicesSplitSlide;
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "statistics-grid-slide";
|
||||
export const slideLayoutName = "Statistics Grid Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A left text column with a title, description and a right-side grid of statistics cards,value and label each in a card";
|
||||
|
||||
const StatisticSchema = z.object({
|
||||
value: z.string().max(8).meta({
|
||||
description: "Main metric value shown at the top of one card.",
|
||||
}),
|
||||
label: z.string().max(20).meta({
|
||||
description: "Label shown under the value.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(16).default("Statistics").meta({
|
||||
description: "Main title shown in the left column.",
|
||||
}),
|
||||
description: z.string().max(160).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
).meta({
|
||||
description: "Supporting line shown under the left title.",
|
||||
}),
|
||||
stats: z
|
||||
.array(StatisticSchema)
|
||||
.min(2)
|
||||
.max(8)
|
||||
.default([
|
||||
{ value: "120", label: "Sales Team Strength" },
|
||||
{ value: "15", label: "Senior Sales Officer" },
|
||||
{ value: "1", label: "National Manager" },
|
||||
{ value: "25", label: "Sales Officers" },
|
||||
{ value: "2", label: "Regional Manager" },
|
||||
{ value: "50", label: "Distributor Reps" },
|
||||
{ value: "5", label: "Zonal Manager" },
|
||||
{ value: "20", label: "Merchandising Team" },
|
||||
])
|
||||
.meta({
|
||||
description: "statistic cards, with value and label each in a card",
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
|
||||
|
||||
const EducationStatisticsGridSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#efeff1)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
<div className="relative z-10 grid h-full grid-cols-[490px_1fr]">
|
||||
<div className="px-[44px] pb-[78px] pt-[96px]">
|
||||
<div className="flex h-full flex-col justify-end">
|
||||
<h2 className="font-serif text-[64px] leading-[98%] tracking-[-0.02em]" style={{ color: "var(--primary-color,#1a1752)" }}>
|
||||
{data.title}
|
||||
</h2>
|
||||
<p className="mt-[40px] max-w-[330px] text-[22px] leading-[1.24]" style={{ color: "var(--background-text,#34394C)" }}>
|
||||
{data.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.stats && data.stats?.length <= 4 && <div className="grid h-full grid-cols-1">
|
||||
{data.stats?.map((stat, index) => (
|
||||
<div
|
||||
key={`${stat.value}-${index}`}
|
||||
className="px-[52px] pt-[22px]"
|
||||
style={{ backgroundColor: index % 2 === 1 ? 'var(--card-color,#5C0FD908)' : 'var(--card-color,white)' }}
|
||||
>
|
||||
<p className="font-serif text-[58px] leading-[56px]" style={{ color: "var(--background-text,#434A63)" }}>
|
||||
{stat?.value}
|
||||
</p>
|
||||
<p className="mt-[12px] text-[24px]" style={{ color: "var(--background-text,#434A63)" }}>
|
||||
{stat?.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
|
||||
|
||||
|
||||
{data.stats && data.stats?.length > 4 && data.stats?.length <= 8 && (() => {
|
||||
const rightArray = data.stats?.slice(0, Math.floor(data.stats?.length / 2));
|
||||
const leftArray = data.stats?.slice(Math.floor(data.stats?.length / 2));
|
||||
|
||||
return (
|
||||
<div className="h-full flex w-full">
|
||||
<div className="flex flex-col h-full flex-1">
|
||||
|
||||
{leftArray?.map((stat: any, index: number) => (
|
||||
<div
|
||||
key={`${stat?.value}-${index}`}
|
||||
className="px-[52px] pt-[22px] h-full"
|
||||
style={{ backgroundColor: index % 2 === 0 ? 'var(--card-color,#5C0FD908)' : 'var(--card-color,white)' }}
|
||||
>
|
||||
<p className=" text-[58px] leading-[56px]" style={{ color: "var(--background-text,#283E51)" }}>
|
||||
{stat?.value}
|
||||
</p>
|
||||
<p className="mt-[12px] text-[24px]" style={{ color: "var(--background-text,#434A63)" }}>
|
||||
{stat?.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col flex-1">
|
||||
|
||||
{rightArray?.map((stat: any, index: number) => (
|
||||
<div
|
||||
key={`${stat.value}-${index}`}
|
||||
className="px-[52px] pt-[22px] h-full"
|
||||
style={{ backgroundColor: index % 2 === 1 ? 'var(--card-color,#5C0FD908)' : 'var(--card-color,white)' }}
|
||||
>
|
||||
<p className=" text-[58px] leading-[56px]" style={{ color: "var(--background-text,#283E51)" }}>
|
||||
{stat.value}
|
||||
</p>
|
||||
<p className="mt-[12px] text-[24px]" style={{ color: "var(--background-text,#434A63)" }}>
|
||||
{stat.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationStatisticsGridSlide;
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "table-of-contents-slide";
|
||||
export const slideLayoutName = "Table Of Contents Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A split layout with a left title panel and a right list of numbered sections, with one subtle background image overlay.";
|
||||
|
||||
const TocItemSchema = z.object({
|
||||
number: z.string().min(2).max(3).meta({
|
||||
description: "Section number displayed before each section title.",
|
||||
}),
|
||||
label: z.string().min(3).max(30).meta({
|
||||
description: "Section title shown in the right column list.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(32).default("Table of Contents").meta({
|
||||
description: "Main centered title of the table of contents slide.",
|
||||
}),
|
||||
items: z
|
||||
.array(TocItemSchema)
|
||||
.min(1)
|
||||
.max(10)
|
||||
.default([
|
||||
{ number: "01", label: "ABOUT" },
|
||||
{ number: "02", label: "TIMELINE" },
|
||||
{ number: "03", label: "GROUP OF COMPANIES" },
|
||||
{ number: "04", label: "SERVICES" },
|
||||
{ number: "05", label: "IMAGE GALLERY" },
|
||||
{ number: "06", label: "STATISTICS" },
|
||||
{ number: "07", label: "REPORT" },
|
||||
{ number: "08", label: "CONCLUSION" },
|
||||
{ number: "09", label: "QUESTIONS" },
|
||||
{ number: "10", label: "CONTACT" },
|
||||
])
|
||||
.meta({
|
||||
description: "table-of-content entries listed on the right.",
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationTableOfContentsSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#efeff1)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<div className="relative z-10 grid h-full grid-cols-[430px_1fr]">
|
||||
<div className="px-[56px] pt-[74px]" style={{ backgroundColor: "var(--card-color,#f1efef)" }}>
|
||||
<h2 className="font-serif text-[64px] leading-[98%] tracking-[-0.02em]" style={{ color: "var(--primary-color,#1a1752)" }}>
|
||||
{data.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="px-[88px] pt-[84px]" style={{ backgroundColor: "var(--card-color,#FFFFFF80)" }}>
|
||||
<div className="space-y-[32px]">
|
||||
{data.items?.map((item, index) => (
|
||||
<div key={`${item.number}-${item.label}-${index}`} className="flex items-center gap-[16px]">
|
||||
<span className="w-[42px] text-[20px] font-semibold leading-none" style={{ color: "var(--background-text,#3f414a)" }}>
|
||||
{item.number}
|
||||
</span>
|
||||
<span className="text-[24px] font-medium leading-none" style={{ color: "var(--background-text,#3f414a)" }}>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationTableOfContentsSlide;
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "timeline-slide";
|
||||
export const slideLayoutName = "Timeline Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title, a horizontal progress line, and short heading and description pairs.";
|
||||
|
||||
const MilestoneSchema = z.object({
|
||||
heading: z.string().max(6).meta({
|
||||
description: "Heading displayed under each timeline marker.",
|
||||
}),
|
||||
description: z.string().max(50).meta({
|
||||
description: "Short text shown under each heading. with max 50 characters",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(4).max(14).default("Timeline").meta({
|
||||
description: "Main timeline heading shown at the top-left.",
|
||||
}),
|
||||
milestones: z
|
||||
.array(MilestoneSchema)
|
||||
.min(2)
|
||||
.max(12)
|
||||
.default([
|
||||
{ heading: "2022", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
|
||||
{ heading: "1994", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
|
||||
{ heading: "1993", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
|
||||
{ heading: "1991", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
|
||||
{ heading: "1991", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
|
||||
{ heading: "1988", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " },
|
||||
{ heading: "1988", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
|
||||
{ heading: "1988", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
|
||||
])
|
||||
.meta({
|
||||
description: "Timeline milestones displayed left to right.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationTimelineSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
const { title, milestones } = data;
|
||||
|
||||
const isSixOrLess = milestones?.length && milestones?.length <= 6;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden flex flex-col"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#efeff1)",
|
||||
fontFamily: "var(--body-font-family,'Source Serif 4')",
|
||||
}}
|
||||
>
|
||||
<div className="relative z-10 px-[56px] pt-[86px]">
|
||||
<h2 className="font-serif text-[84px] leading-none tracking-[-0.02em]" style={{ color: "var(--primary-color,#1a1752)" }}>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{isSixOrLess ? (
|
||||
<TimelineUpToSix milestones={milestones || []} />
|
||||
) : (
|
||||
<TimelineMoreThanSix milestones={milestones || []} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function TimelineUpToSix({
|
||||
milestones,
|
||||
}: {
|
||||
milestones: any;
|
||||
}) {
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="relative z-10 mt-[160px] px-[56px]">
|
||||
<div
|
||||
className="grid "
|
||||
style={{ gridTemplateColumns: `repeat(${milestones.length}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{milestones.map((milestone: any, index: number) => (
|
||||
<div key={`${milestone.heading}-${index}`} className="">
|
||||
<div className="flex items-center ">
|
||||
|
||||
<div className="h-[22px] z-10 relative w-[22px] rounded-full" style={{ backgroundColor: "var(--primary-color,#272272)" }} />
|
||||
{index !== milestones.length - 1 && <div className="h-[3px] flex-1" style={{ backgroundColor: "var(--stroke,#d8d8dd)" }} />}
|
||||
</div>
|
||||
<p className="mt-[18px] text-[20px] font-medium leading-none" style={{ color: "var(--background-text,#3c3f4b)" }}>
|
||||
{milestone.heading}
|
||||
</p>
|
||||
<p className=" text-[18px] leading-[1.2]" style={{ color: "var(--background-text,#3a3d4c)" }}>
|
||||
{milestone.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TimelineMoreThanSix({
|
||||
milestones,
|
||||
}: {
|
||||
milestones: SchemaType["milestones"];
|
||||
}) {
|
||||
const topItems = milestones.slice(0, 6);
|
||||
const bottomItems = milestones.slice(6);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 mt-[84px] px-[56px]">
|
||||
{/* vertical connector on left */}
|
||||
<svg className="absolute z-[-1] right-[100px] top-[10px]" xmlns="http://www.w3.org/2000/svg" width="139" height="231" viewBox="0 0 139 231" fill="none">
|
||||
<mask id="path-1-inside-1_220_636" fill="white">
|
||||
<path d="M0 0H128C133.891 0 138.667 4.77563 138.667 10.6667V220C138.667 225.891 133.891 230.667 128 230.667H0V0Z" />
|
||||
</mask>
|
||||
<path d="M0 -2.66667H128C135.364 -2.66667 141.333 3.30287 141.333 10.6667H136C136 6.24839 132.418 2.66667 128 2.66667H0V-2.66667ZM141.333 220C141.333 227.364 135.364 233.333 128 233.333H0V228H128C132.418 228 136 224.418 136 220H141.333ZM136 220M0 230.667V0V230.667M128 -2.66667C135.364 -2.66667 141.333 3.30287 141.333 10.6667V220C141.333 227.364 135.364 233.333 128 233.333V228C132.418 228 136 224.418 136 220V10.6667C136 6.24839 132.418 2.66667 128 2.66667V-2.66667Z" fill="var(--primary-color,#101C3D)" fillOpacity="0.1" mask="url(#path-1-inside-1_220_636)" />
|
||||
</svg>
|
||||
{/* bottom horizontal line */}
|
||||
{/* <div className="absolute z-[-1] right-[110px] top-[220px] w-[150px] h-[3px]" style={{ backgroundColor: "var(--stroke,#d8d8dd)" }} /> */}
|
||||
<div className="relative z-10 px-[56px]">
|
||||
<div
|
||||
className="grid "
|
||||
style={{ gridTemplateColumns: `repeat(${topItems.length}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{topItems.map((milestone: any, index: number) => (
|
||||
<div key={`${milestone.heading}-${index}`} className="">
|
||||
<div className="flex items-center ">
|
||||
|
||||
<div className="h-[22px] z-10 relative w-[22px] rounded-full" style={{ backgroundColor: "var(--primary-color,#272272)" }} />
|
||||
{index !== milestones.length - 1 && <div className="h-[3px] flex-1" style={{ backgroundColor: "var(--stroke,#d8d8dd)" }} />}
|
||||
</div>
|
||||
<div className="pr-2 mt-[18px]">
|
||||
|
||||
<p className=" text-[20px] font-medium leading-none" style={{ color: "var(--background-text,#3c3f4b)" }}>
|
||||
{milestone.heading}
|
||||
</p>
|
||||
<p className="mt-2 text-[18px] leading-[1.2]" style={{ color: "var(--background-text,#3a3d4c)" }}>
|
||||
{milestone.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className="mt-[95px] grid items-end px-[56px] pr-[180px] "
|
||||
|
||||
style={{ gridTemplateColumns: `repeat(${bottomItems.length}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{bottomItems.map((_, colIndex) => {
|
||||
const item = bottomItems[colIndex];
|
||||
if (!item) return <div key={colIndex} />;
|
||||
|
||||
return (
|
||||
<div key={`${item.heading}-${colIndex + 8}`} className="flex flex-col items-end">
|
||||
<div className="flex w-full items-center ">
|
||||
{/* {colIndex === 0 && <div className="absolute h-[3px] flex-1" style={{ backgroundColor: "var(--stroke,#d8d8dd)" }} />} */}
|
||||
<div className="h-[3px] flex-1" style={{ backgroundColor: colIndex === 0 ? "transparent" : "var(--stroke,#d8d8dd)" }} />
|
||||
<div className="h-[22px] z-10 relative w-[22px] rounded-full" style={{ backgroundColor: "var(--primary-color,#272272)" }} />
|
||||
</div>
|
||||
<div className="pl-2 mt-[18px]">
|
||||
|
||||
<p className=" text-right text-[20px] font-medium leading-none" style={{ color: "var(--background-text,#3c3f4b)" }}>
|
||||
{item.heading}
|
||||
</p>
|
||||
<p className="mt-2 text-[18px] text-right leading-[1.2]" style={{ color: "var(--background-text,#3a3d4c)" }}>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EducationTimelineSlide;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"description": "School and training layouts for covers, outlines, timelines, statistics, and visual storytelling",
|
||||
"ordered": false,
|
||||
"default": false
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
|
||||
export const slideLayoutId = "title-description-with-cards-text-slide";
|
||||
export const slideLayoutName = "Title Description with Cards to Text Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title on top and a description below, and a content section containing cards of text.";
|
||||
|
||||
const CardSchema = z.object({
|
||||
heading: z.string().max(16).meta({
|
||||
description: "Card heading for one challenge column.",
|
||||
}),
|
||||
body: z.string().max(45).meta({
|
||||
description: "Card body copy for one challenge column.",
|
||||
}),
|
||||
dark: z.boolean().default(false).meta({
|
||||
description: "Controls whether the card uses a dark emphasis style.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(16).default("Business Challenges").meta({
|
||||
description: "Main slide title. Max 16 characters.",
|
||||
}),
|
||||
taglineLabel: z.string().max(16).default("TAGLINE").meta({
|
||||
description: "Short label above the left-side paragraph.",
|
||||
}),
|
||||
taglineBody: z.string().max(100).default(
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea."
|
||||
).meta({
|
||||
description: "Supporting paragraph on the left side.",
|
||||
}),
|
||||
heroImage: z.object({
|
||||
__image_url__: z.string().url().default("https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=1400&q=80"),
|
||||
__image_prompt__: z.string().min(10).max(100).default("Team meeting and stressed analyst"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=1400&q=80",
|
||||
__image_prompt__: "Team meeting and stressed analyst",
|
||||
}).meta({
|
||||
description: "Primary image shown in the upper right area.",
|
||||
}),
|
||||
cards: z
|
||||
.array(CardSchema)
|
||||
|
||||
.max(3)
|
||||
.default([
|
||||
{
|
||||
heading: "HEADING 1",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur elit.",
|
||||
dark: false,
|
||||
},
|
||||
{
|
||||
heading: "HEADING 2",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur elit.",
|
||||
dark: false,
|
||||
},
|
||||
{
|
||||
heading: "HEADING 3",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur elit.",
|
||||
dark: true,
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Three vertical challenge cards rendered under the image.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const BusinessChallengesCardsSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, taglineLabel, taglineBody, heroImage, cards } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className=" pl-[66px] pt-[50px] pb-[28px] pr-[40px]">
|
||||
<h2
|
||||
className="text-[80px] max-w-[406px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="mt-[72px] w-[360px]">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{taglineLabel}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[16px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{taglineBody}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{heroImage?.__image_url__ && (
|
||||
<img
|
||||
src={heroImage.__image_url__}
|
||||
alt={heroImage.__image_prompt__}
|
||||
className="absolute right-0 top-[72px] h-[350px] w-[770px] object-cover bg-white"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-[72px] right-[40px] flex items-start gap-[16px]">
|
||||
{cards?.map((card, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className=" w-[248px] px-[34px] py-[34px]"
|
||||
style={{
|
||||
backgroundColor: card.dark
|
||||
? "var(--primary-color,#15342D)"
|
||||
: "var(--card-color,#ebebee)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{
|
||||
color: card.dark
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--primary-color,#15342D)",
|
||||
}}
|
||||
>
|
||||
{card.heading}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[18px] text-[28px] font-normal text-white"
|
||||
style={{
|
||||
color: card.dark
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--primary-color,#15342D)",
|
||||
}}
|
||||
>
|
||||
{card.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessChallengesCardsSlide;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "title-with-blocks-text-slide";
|
||||
export const slideLayoutName = "Title with Blocks to Text Grid Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title on top and a content section below containing blocks of text.";
|
||||
|
||||
const BlockSchema = z.object({
|
||||
heading: z.string().max(30).meta({
|
||||
description: "Short heading for a single block of text.",
|
||||
}),
|
||||
body: z.string().max(80).meta({
|
||||
description: "Description text for a single block of text.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(24).default("Business Challenges Business").meta({
|
||||
description: "Main title shown in the top.",
|
||||
}),
|
||||
blocks: z
|
||||
.array(BlockSchema)
|
||||
|
||||
.max(4)
|
||||
.default([
|
||||
{
|
||||
heading: "HEADING 1 HEADING 1 HEADING 1 HEADING 1",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.",
|
||||
},
|
||||
{
|
||||
heading: "HEADING 2",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.",
|
||||
},
|
||||
{
|
||||
heading: "HEADING 1",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.",
|
||||
},
|
||||
{
|
||||
heading: "HEADING 2",
|
||||
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.",
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Four challenge blocks rendered in a 2x2 arrangement.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const BusinessChallengesGridSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, blocks } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden flex flex-col"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className=" px-[60px] pt-[50px] pb-[28px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grid grid-cols-2 justify-between items-center flex-1 gap-y-[43px] px-[84px] py-[70px] gap-x-[63px]"
|
||||
style={{ backgroundColor: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{blocks?.map((block, index) => (
|
||||
<div key={index} className="">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{ color: "var(--primary-text,#edf2f1)" }}
|
||||
>
|
||||
{block.heading}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[24px] text-[28px] font-normal text-white"
|
||||
style={{ color: "var(--primary-text,#edf2f1)" }}
|
||||
>
|
||||
{block.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessChallengesGridSlide;
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-description-with-table-slide";
|
||||
export const slideLayoutName = "Title Description with Table Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title on top and a description below, and a content section containing a table with column headers and rows of check, cross and empty state of content.";
|
||||
|
||||
const CellStatusSchema = z.enum(["check", "cross", "empty"]);
|
||||
|
||||
const GeneralRowSchema = z.object({
|
||||
label: z.string().max(18).meta({
|
||||
description: "Row heading shown in the first column.",
|
||||
}),
|
||||
cells: z.array(CellStatusSchema).min(1).max(8).meta({
|
||||
description: "Status cells aligned with the table columns.",
|
||||
}),
|
||||
});
|
||||
|
||||
const LegacyRowSchema = z.object({
|
||||
label: z.string().max(18).meta({
|
||||
description: "Row heading shown in the first column.",
|
||||
}),
|
||||
cell1: CellStatusSchema.optional(),
|
||||
cell2: CellStatusSchema.optional(),
|
||||
cell3: CellStatusSchema.optional(),
|
||||
cell4: CellStatusSchema.optional(),
|
||||
});
|
||||
|
||||
const RowSchema = z.union([GeneralRowSchema, LegacyRowSchema]);
|
||||
|
||||
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(14).default("Comparison Chart").meta({
|
||||
description: "Main heading shown above the table.",
|
||||
}),
|
||||
subtitle: z.string().max(80).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt."
|
||||
).meta({
|
||||
description: "Short subtitle shown under the main heading.",
|
||||
}),
|
||||
columns: z
|
||||
.array(z.string().max(18))
|
||||
.min(1)
|
||||
.max(4)
|
||||
.default(["HEADING 1", "HEADING 2", "HEADING 3", "HEADING 4"])
|
||||
.meta({
|
||||
description: "Table column headings.",
|
||||
}),
|
||||
highlightedColumnIndex: z.number().int().min(1).max(8).default(4).meta({
|
||||
description: "1-based column index for the dark highlighted table header.",
|
||||
}),
|
||||
rows: z
|
||||
.array(RowSchema)
|
||||
.min(1)
|
||||
.max(3)
|
||||
.default([
|
||||
{
|
||||
label: "HEADING 1",
|
||||
cells: ["check", "cross", "check", "cross"],
|
||||
},
|
||||
{
|
||||
label: "HEADING 1",
|
||||
cells: ["check", "empty", "check", "empty"],
|
||||
},
|
||||
{
|
||||
label: "HEADING 2",
|
||||
cells: ["check", "check", "check", "check"],
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Table rows with status indicators. Prefer the `cells` array format.",
|
||||
}),
|
||||
checkIcon: z.object({
|
||||
__icon_url__: z.string(),
|
||||
__icon_query__: z.string(),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
}).meta({
|
||||
description: "Icon used for positive comparison status.",
|
||||
}),
|
||||
crossIcon: z.object({
|
||||
__icon_url__: z.string(),
|
||||
__icon_query__: z.string(),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "cross icon",
|
||||
}).meta({
|
||||
description: "Icon used for negative comparison status.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
type CellStatus = z.infer<typeof CellStatusSchema>;
|
||||
|
||||
function StatusIcon({
|
||||
status,
|
||||
checkIconUrl,
|
||||
checkIconAlt,
|
||||
crossIconUrl,
|
||||
crossIconAlt,
|
||||
}: {
|
||||
status: any;
|
||||
checkIconUrl: string | undefined;
|
||||
checkIconAlt: string | undefined;
|
||||
crossIconUrl: string | undefined;
|
||||
crossIconAlt: string | undefined;
|
||||
}) {
|
||||
if (status === "empty") {
|
||||
return <span className="h-[26px] w-[26px]" />;
|
||||
}
|
||||
|
||||
if (status === "cross") {
|
||||
return <img src={crossIconUrl} alt={crossIconAlt} className="h-[26px] w-[26px] object-contain" />;
|
||||
}
|
||||
if (status === 'check') {
|
||||
|
||||
|
||||
return <img src={checkIconUrl} alt={checkIconAlt} className="h-[26px] w-[26px] object-contain" />;
|
||||
}
|
||||
return <p className="text-base ">{status}</p>
|
||||
}
|
||||
|
||||
const ComparisonChartSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
columns,
|
||||
highlightedColumnIndex,
|
||||
rows,
|
||||
checkIcon,
|
||||
crossIcon,
|
||||
} = data;
|
||||
const safeColumns = columns && columns.length > 0 ? columns : [];
|
||||
const resolvedHighlightedColumnIndex =
|
||||
highlightedColumnIndex &&
|
||||
highlightedColumnIndex >= 1 &&
|
||||
highlightedColumnIndex <= safeColumns.length
|
||||
? highlightedColumnIndex
|
||||
: Math.min(4, safeColumns.length);
|
||||
const safeRows = rows && rows.length > 0 ? rows : [];
|
||||
const normalizedRows = safeRows.map((row) => {
|
||||
const rowCells =
|
||||
"cells" in row
|
||||
? row.cells
|
||||
: [row.cell1, row.cell2, row.cell3, row.cell4].filter(
|
||||
(cell): cell is CellStatus => typeof cell !== "undefined"
|
||||
);
|
||||
|
||||
return {
|
||||
label: row.label,
|
||||
cells: Array.from(
|
||||
{ length: safeColumns.length },
|
||||
(_, cellIndex) => rowCells[cellIndex] ?? "empty"
|
||||
),
|
||||
};
|
||||
});
|
||||
const tableGridColumns = `220px repeat(${safeColumns.length}, minmax(0, 1fr))`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="px-[56px] pt-[50px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className="mt-[20px] w-[740px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-[54px] mt-[20px] ">
|
||||
<div
|
||||
className="grid border-b"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#c5cccb)",
|
||||
gridTemplateColumns: tableGridColumns,
|
||||
}}
|
||||
>
|
||||
<div className=" " />
|
||||
{safeColumns.map((column: any, index: any) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center p-[33px] justify-center border-r text-[20px] font-semibold tracking-[0.2em]"
|
||||
style={{
|
||||
backgroundColor:
|
||||
index + 1 === resolvedHighlightedColumnIndex
|
||||
? "var(--primary-color,#15342D)"
|
||||
: "var(--card-color,#ffffff)",
|
||||
color:
|
||||
index + 1 === resolvedHighlightedColumnIndex
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--primary-color,#15342D)",
|
||||
borderColor: "var(--stroke,#c5cccb)",
|
||||
}}
|
||||
>
|
||||
{column}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{normalizedRows.map((row, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`grid ${index < normalizedRows.length - 1 ? "border-b" : ""}`}
|
||||
style={{
|
||||
borderColor: "var(--stroke,#c5cccb)",
|
||||
gridTemplateColumns: tableGridColumns,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center border-r pl-[34px] text-[20px] font-semibold tracking-[0.2em]"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#ffffff)",
|
||||
borderColor: "var(--stroke,#c5cccb)",
|
||||
color: "var(--primary-color,#15342D)",
|
||||
}}
|
||||
>
|
||||
{row.label}
|
||||
</div>
|
||||
|
||||
{row.cells.map((status, cellIndex) => (
|
||||
<div
|
||||
key={cellIndex}
|
||||
className="flex p-[23px] items-center justify-center border-r"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#ffffff)",
|
||||
borderColor: "var(--stroke,#c5cccb)",
|
||||
}}
|
||||
>
|
||||
<StatusIcon
|
||||
status={status}
|
||||
checkIconUrl={checkIcon?.__icon_url__}
|
||||
checkIconAlt={checkIcon?.__icon_query__}
|
||||
crossIconUrl={crossIcon?.__icon_url__}
|
||||
crossIconAlt={crossIcon?.__icon_query__}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComparisonChartSlide;
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "title-description-with-table-slide";
|
||||
export const slideLayoutName = "Title Description with Table Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A comparison table slide with a title, a subtitle, column headers, and rows of text content.";
|
||||
|
||||
const TableCellSchema = z.string().max(40).meta({
|
||||
description: "Table cell text.",
|
||||
});
|
||||
|
||||
const GeneralRowSchema = z.object({
|
||||
cells: z.array(TableCellSchema).min(1).max(8).meta({
|
||||
description: "Row cell values matching the table columns.",
|
||||
}),
|
||||
});
|
||||
|
||||
const LegacyRowSchema = z.object({
|
||||
cell1: TableCellSchema.optional(),
|
||||
cell2: TableCellSchema.optional(),
|
||||
cell3: TableCellSchema.optional(),
|
||||
cell4: TableCellSchema.optional(),
|
||||
});
|
||||
|
||||
const RowSchema = z.union([GeneralRowSchema, LegacyRowSchema]);
|
||||
|
||||
const DEFAULT_COLUMNS = ["HEADING 1", "HEADING 1", "HEADING 2", "HEADING 3"];
|
||||
const DEFAULT_ROWS: z.infer<typeof GeneralRowSchema>[] = [
|
||||
{
|
||||
cells: [
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
],
|
||||
},
|
||||
{
|
||||
cells: [
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
],
|
||||
},
|
||||
{
|
||||
cells: [
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(14).default("Comparison Chart").meta({
|
||||
description: "Main heading shown above the table.",
|
||||
}),
|
||||
subtitle: z
|
||||
.string()
|
||||
.max(80)
|
||||
.default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt."
|
||||
)
|
||||
.meta({
|
||||
description: "Short subtitle shown under the main heading.",
|
||||
}),
|
||||
columns: z
|
||||
.array(z.string().max(20))
|
||||
.min(1)
|
||||
.max(8)
|
||||
.default(DEFAULT_COLUMNS)
|
||||
.meta({
|
||||
description: "Table column headings.",
|
||||
}),
|
||||
highlightedHeaderIndex: z.number().int().min(1).max(8).default(4).meta({
|
||||
description: "1-based column index for the dark highlighted table header.",
|
||||
}),
|
||||
rows: z
|
||||
.array(RowSchema)
|
||||
.min(1)
|
||||
.max(6)
|
||||
.default(DEFAULT_ROWS)
|
||||
.meta({
|
||||
description: "Table rows of text content. Prefer the `cells` array format.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const ComparisonTableWithTextSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, subtitle, columns, highlightedHeaderIndex, rows } = data;
|
||||
const safeColumns = columns && columns.length > 0 ? columns : DEFAULT_COLUMNS;
|
||||
const resolvedHighlightedHeaderIndex =
|
||||
highlightedHeaderIndex &&
|
||||
highlightedHeaderIndex >= 1 &&
|
||||
highlightedHeaderIndex <= safeColumns.length
|
||||
? highlightedHeaderIndex
|
||||
: Math.min(4, safeColumns.length);
|
||||
const safeRows = rows && rows.length > 0 ? rows : DEFAULT_ROWS;
|
||||
const normalizedRows = safeRows.map((row) => {
|
||||
const rowCells =
|
||||
"cells" in row
|
||||
? row.cells
|
||||
: [row.cell1, row.cell2, row.cell3, row.cell4].filter(
|
||||
(cell): cell is string => typeof cell === "string"
|
||||
);
|
||||
|
||||
return Array.from(
|
||||
{ length: safeColumns.length },
|
||||
(_, cellIndex) => rowCells[cellIndex] ?? ""
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#c3cccc)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="px-[44px] pt-[50px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[1.02] tracking-[-0.03em] text-[#0a443b]"
|
||||
style={{ color: "var(--primary-color,#0a443b)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className="mt-[22px] max-w-[700px] text-[24px] leading-[1.22] text-[#2d5d56]"
|
||||
style={{ color: "var(--background-text,#2d5d56)" }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mx-[44px] mt-[30px] overflow-hidden border"
|
||||
style={{ borderColor: "var(--stroke,#bcc3c3)" }}
|
||||
>
|
||||
<table
|
||||
className="w-full table-fixed border-collapse"
|
||||
style={{ backgroundColor: "var(--card-color,#ffffff)" }}
|
||||
>
|
||||
<thead className="w-full">
|
||||
<tr className="w-full">
|
||||
{safeColumns.map((column, index) => {
|
||||
const isHighlighted = index + 1 === resolvedHighlightedHeaderIndex;
|
||||
return (
|
||||
<th
|
||||
key={`${column}-${index}`}
|
||||
className=" border-r p-[33px] text-left text-[20px] font-semibold uppercase tracking-[0.16em] last:border-r-0"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#bcc3c3)",
|
||||
backgroundColor: isHighlighted
|
||||
? "var(--primary-color,#05443a)"
|
||||
: "var(--card-color,#ffffff)",
|
||||
color: isHighlighted
|
||||
? "var(--primary-text,#eef2f0)"
|
||||
: "var(--primary-color,#123f38)",
|
||||
}}
|
||||
>
|
||||
{column}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{normalizedRows.map((cells, rowIndex) => {
|
||||
return (
|
||||
<tr key={`row-${rowIndex}`}>
|
||||
{cells?.map((cell, cellIndex) => (
|
||||
<td
|
||||
key={`cell-${rowIndex}-${cellIndex}`}
|
||||
className=" border-r border-t bg-white p-[33px] text-left text-[18px] leading-[1.2] last:border-r-0"
|
||||
style={{
|
||||
borderColor: "var(--stroke,#bcc3c3)",
|
||||
backgroundColor: "var(--card-color,#ffffff)",
|
||||
color: "var(--primary-color,#123f38)",
|
||||
}}
|
||||
>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComparisonTableWithTextSlide;
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
|
||||
export const slideLayoutId = "cover-slide";
|
||||
export const slideLayoutName = "Cover Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A cover slide with a compact logo in the top-left, a date/text/label in the top-right, a centered title, and a image anchored to the bottom with a soft fade into the background.";
|
||||
|
||||
export const Schema = z.object({
|
||||
image: z.object({
|
||||
__image_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg"),
|
||||
|
||||
__image_prompt__: z.string().default("Image of the company"),
|
||||
}).optional().default({
|
||||
__image_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Image of the company",
|
||||
}),
|
||||
label: z.string().min(3).max(16).optional().default("MARCH 2026").meta({
|
||||
description: "Date/text/label shown at the top-right corner.",
|
||||
}),
|
||||
titleLine1: z.string().min(3).max(18).default("Social Media").meta({
|
||||
description: "First line of the cover title.",
|
||||
}),
|
||||
titleLine2: z.string().min(3).max(20).default("Marketing Report").meta({
|
||||
description: "Second line of the cover title.",
|
||||
}),
|
||||
backgroundImage: z.object({
|
||||
__image_url__: z.string().url().default("https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1920&q=80"),
|
||||
__image_prompt__: z.string().min(10).max(100).default("Tall glass buildings from street view"),
|
||||
}).default({
|
||||
__image_url__: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1920&q=80",
|
||||
__image_prompt__: "Tall glass buildings from street view",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const CoverSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative z-20 flex h-full flex-col px-[36px] pt-[62px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
{data.image?.__image_url__ ? <img
|
||||
src={data.image?.__image_url__ ?? ''}
|
||||
alt={data.image?.__image_prompt__ || ''}
|
||||
className="h-[42px] w-[171px] object-cover"
|
||||
/> : <p></p>}
|
||||
|
||||
<p
|
||||
className="text-[18px] font-normal leading-[18.991px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{data.label || ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-center pb-[80px]">
|
||||
<h1
|
||||
className="text-center text-[100px] font-semibold leading-[108.4%] tracking-[-3.024px]"
|
||||
>
|
||||
<p> {data.titleLine1}</p>
|
||||
<p>{data.titleLine2} </p>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.backgroundImage?.__image_url__ && (
|
||||
<img
|
||||
src={data.backgroundImage.__image_url__ || ''}
|
||||
alt={data.backgroundImage.__image_prompt__ || ''}
|
||||
className="absolute bottom-0 left-0 z-0 h-[360px] w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="pointer-events-none absolute bottom-0 left-0 w-full z-10"
|
||||
style={{
|
||||
height: "365px",
|
||||
background:
|
||||
"linear-gradient(0deg, rgba(218, 225, 222, 0.00) 0%, var(--background-color,#DAE1DE) 80.33%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoverSlide;
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-description-with-image-gallery-slide";
|
||||
export const slideLayoutName = "Title Description with Image Gallery Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A text slide with a title on top and a description below, and a section containing a gallery of images.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(12).default("Image Gallery").meta({
|
||||
description: "Main Title of the slide",
|
||||
}),
|
||||
description: z.string().max(120).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore."
|
||||
).meta({
|
||||
description: "Supporting paragraph shown under the title.",
|
||||
}),
|
||||
topCenterImage: z.object({
|
||||
__image_url__:
|
||||
z.string().default("https://images.unsplash.com/photo-1521737711867-e3b97375f902?auto=format&fit=crop&w=800&q=80"),
|
||||
__image_prompt__: z.string().max(80).default("Design team discussing project board"),
|
||||
}).default({
|
||||
__image_url__: "https://images.unsplash.com/photo-1521737711867-e3b97375f902?auto=format&fit=crop&w=800&q=80",
|
||||
__image_prompt__: "Design team discussing project board",
|
||||
}).meta({
|
||||
description: "Top-middle gallery image.",
|
||||
}),
|
||||
topRightImage: z.object({
|
||||
__image_url__:
|
||||
z.string().default("https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&w=800&q=80"),
|
||||
__image_prompt__: z.string().max(80).default("Creative desk with notebook and photos"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&w=800&q=80",
|
||||
__image_prompt__: "Creative desk with notebook and photos",
|
||||
}).meta({
|
||||
description: "Top-right gallery image.",
|
||||
}),
|
||||
bottomWideImage: z.object({
|
||||
__image_url__:
|
||||
z.string().default("https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?auto=format&fit=crop&w=1300&q=80"),
|
||||
__image_prompt__: z.string().max(80).default("City skyline seen from office window"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?auto=format&fit=crop&w=1300&q=80",
|
||||
__image_prompt__: "City skyline seen from office window",
|
||||
}).meta({
|
||||
description: "Bottom-left wide gallery image.",
|
||||
}),
|
||||
bottomCenterImage: z.object({
|
||||
__image_url__:
|
||||
z.string().default("https://images.unsplash.com/photo-1517048676732-d65bc937f952?auto=format&fit=crop&w=900&q=80"),
|
||||
__image_prompt__: z.string().max(80).default("Art gallery wall with framed photos"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1517048676732-d65bc937f952?auto=format&fit=crop&w=900&q=80",
|
||||
__image_prompt__: "Art gallery wall with framed photos",
|
||||
}).meta({
|
||||
description: "Bottom-center gallery image.",
|
||||
}),
|
||||
bottomRightImage: z.object({
|
||||
__image_url__:
|
||||
z.string().default("https://images.unsplash.com/photo-1521791136064-7986c2920216?auto=format&fit=crop&w=900&q=80"),
|
||||
__image_prompt__: z.string().max(80).default("Office workshop with presentation board"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521791136064-7986c2920216?auto=format&fit=crop&w=900&q=80",
|
||||
__image_prompt__: "Office workshop with presentation board",
|
||||
}).meta({
|
||||
description: "Bottom-right gallery image.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const ImageGallerySlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
topCenterImage,
|
||||
topRightImage,
|
||||
bottomWideImage,
|
||||
bottomCenterImage,
|
||||
bottomRightImage,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[50px]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-1 ">
|
||||
|
||||
<div className=" ">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className="mt-[24px] w-[584px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-[22px]">
|
||||
<img
|
||||
src={topCenterImage?.__image_url__ ?? ''}
|
||||
alt={topCenterImage?.__image_prompt__ ?? ''}
|
||||
className="h-[294px] w-[270px] object-cover"
|
||||
/>
|
||||
<img
|
||||
src={topRightImage?.__image_url__}
|
||||
alt={topRightImage?.__image_prompt__ ?? ''}
|
||||
className="h-[294px] w-[270px] object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-[22px] w-full flex gap-[22px]">
|
||||
<img
|
||||
src={bottomWideImage?.__image_url__}
|
||||
alt={bottomWideImage?.__image_prompt__}
|
||||
className="h-[290px] w-[605px] object-cover"
|
||||
/>
|
||||
<img
|
||||
src={bottomCenterImage?.__image_url__}
|
||||
alt={bottomCenterImage?.__image_prompt__}
|
||||
className="h-[294px] w-[270px] object-cover"
|
||||
/>
|
||||
<img
|
||||
src={bottomRightImage?.__image_url__}
|
||||
alt={bottomRightImage?.__image_prompt__}
|
||||
className="h-[294px] w-[270px] object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGallerySlide;
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
|
||||
export const slideLayoutId = "introduction-slide";
|
||||
export const slideLayoutName = "Introduction Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A split slide with a large portrait image on the left and a structured introduction column on the right containing a title and two labeled body paragraphs.";
|
||||
|
||||
const IntroBlockSchema = z.object({
|
||||
label: z.string().min(3).max(12).meta({
|
||||
description: "Uppercase mini-heading shown above each introduction paragraph.",
|
||||
}),
|
||||
body: z.string().max(180).meta({
|
||||
description: "Supporting paragraph content for the introduction block.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(4).max(16).default("Introduction").meta({
|
||||
description: "Primary title in the right column.",
|
||||
}),
|
||||
portraitImage: z.object({
|
||||
__image_url__: z.string().url().default("https://images.unsplash.com/photo-1521119989659-a83eee488004?auto=format&fit=crop&w=1200&q=80"),
|
||||
__image_prompt__: z.string().min(10).max(100).default("Two business professionals in office"),
|
||||
}).optional().default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521119989659-a83eee488004?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Two business professionals in office",
|
||||
}).meta({
|
||||
description: "Main portrait image shown on the left half.",
|
||||
}),
|
||||
blocks: z
|
||||
.array(IntroBlockSchema)
|
||||
|
||||
.max(2)
|
||||
.default([
|
||||
{
|
||||
label: "TAGLINE",
|
||||
body: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea.",
|
||||
},
|
||||
{
|
||||
label: "TAGLINE",
|
||||
body: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea.",
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Two short intro content blocks shown in the text column.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const IntroductionSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, portraitImage, blocks } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full gap-[54px] items-center grid-cols-2">
|
||||
<div
|
||||
className="h-full w-full overflow-hidden bg-[#15342D]"
|
||||
style={{ backgroundColor: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
|
||||
{portraitImage?.__image_url__ && (
|
||||
<img
|
||||
src={portraitImage.__image_url__}
|
||||
alt={portraitImage.__image_prompt__}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="mt-[53px] space-y-[53px] pr-[33px]">
|
||||
{blocks?.map((block, index) => (
|
||||
<div key={`${block.label}-${index}`}>
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{block.label}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[14px] text-[24px] leading-[26.667px] text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{block.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroductionSlide;
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-with-kpi-cards-slide";
|
||||
export const slideLayoutName = "Title with KPI Cards Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title on top and a content section containing a grid of KPI cards.";
|
||||
|
||||
const KpiSchema = z.object({
|
||||
value: z.string().max(5).meta({
|
||||
description: "Primary KPI value shown in a card. Should be less than 5 characters.",
|
||||
}),
|
||||
body: z.string().max(16).meta({
|
||||
description: "Short KPI supporting text. Should be less than 16 characters.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(10).default("KPIs").meta({
|
||||
description: "Main title shown in the top-left corner.",
|
||||
}),
|
||||
kpiIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().min(3).max(30).default("pulse icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}).meta({
|
||||
description: "Icon shown in each KPI card badge.",
|
||||
}),
|
||||
backgroundImage: z.object({
|
||||
__image_url__: z.string().url().default("https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1600&q=80"),
|
||||
__image_prompt__: z.string().min(10).max(100).default("Business team using laptop in meeting"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1600&q=80",
|
||||
__image_prompt__: "Business team using laptop in meeting",
|
||||
}).meta({
|
||||
description: "Background image behind the KPI cards.",
|
||||
}),
|
||||
items: z
|
||||
.array(KpiSchema)
|
||||
.min(3)
|
||||
.max(6)
|
||||
.default([
|
||||
{ value: "X 5 Lorem", body: "Lorem ipsum dolor sit. " },
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
])
|
||||
.meta({
|
||||
description: "Six KPI cards displayed in a 3x2 grid.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const KpiCardsSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, kpiIcon, backgroundImage, items } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
{backgroundImage?.__image_url__ && (
|
||||
<img
|
||||
src={backgroundImage?.__image_url__}
|
||||
alt={backgroundImage?.__image_prompt__}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#15342D)",
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 px-[66px] pt-[52px] mb-[33px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#FEFEFF]"
|
||||
style={{ color: "var(--primary-text,#FEFEFF)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 grid grid-cols-3 gap-x-[30px] gap-y-[19px] px-[66px]">
|
||||
{items?.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className=" bg-[#FEFEFF] p-[33px]"
|
||||
style={{ backgroundColor: "var(--card-color,#FEFEFF)" }}
|
||||
>
|
||||
<div
|
||||
className="flex h-[55px] w-[55px] items-center justify-center rounded-full bg-[#15342D]"
|
||||
style={{ backgroundColor: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{/* <img
|
||||
src={kpiIcon?.__icon_url__}
|
||||
alt={kpiIcon?.__icon_query__}
|
||||
className="h-[25px] w-[25px] object-contain"
|
||||
s
|
||||
tyle={{ filter: "brightness(0) invert(1)" }}
|
||||
/> */}
|
||||
|
||||
<RemoteSvgIcon
|
||||
url={kpiIcon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="w-[25px] h-[25px] object-contain"
|
||||
color="var(--primary-text, #FEFEFF)"
|
||||
title={kpiIcon?.__icon_query__}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className="mt-[18px] text-[42px] font-semibold leading-none"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{item.value}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[18px] text-[28px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342D)" }}
|
||||
>
|
||||
{item.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default KpiCardsSlide;
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const slideLayoutId = "title-description-with-lines-and-circles-slide";
|
||||
export const slideLayoutName = "Title Description with Lines and Circles";
|
||||
export const slideLayoutDescription =
|
||||
"A text slide with a title on top and a description below, and a content section containing a list of bullet points and a grid of circles.";
|
||||
|
||||
const BulletSchema = z.object({
|
||||
text: z.string().min(12).max(46).meta({
|
||||
description: "Bullet text shown on the left side of a line.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(18).default("Market Opportunity").meta({
|
||||
description: "Main heading shown at the top-left.",
|
||||
}),
|
||||
subtitle: z.string().min(40).max(110).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt."
|
||||
).meta({
|
||||
description: "Supporting text under the main heading.",
|
||||
}),
|
||||
bullets: z
|
||||
.array(BulletSchema)
|
||||
.min(4)
|
||||
.max(4)
|
||||
.default([
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
{ text: "Ut enim ad minim veniam, quis" },
|
||||
])
|
||||
.meta({
|
||||
description: "Four bullet-line entries shown on the left.",
|
||||
}),
|
||||
values: z
|
||||
.array(z.string().min(2).max(6))
|
||||
.min(4)
|
||||
.max(4)
|
||||
.default(["$33", "$20", "$120", "$200"])
|
||||
.meta({
|
||||
description: "Four values shown from outer to inner circles.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const COLORS = [
|
||||
"var(--graph-0,#5f7f79)",
|
||||
"var(--graph-1,#1f5a4f)",
|
||||
"var(--graph-2,#0d4f43)",
|
||||
"var(--graph-3,#06463d)",
|
||||
];
|
||||
|
||||
const MarketOpportunitySlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, subtitle, bullets, values } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="px-[56px] pt-[50px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className="mt-[20px] w-[730px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-[56px] top-[368px] space-y-[42px]">
|
||||
{bullets?.map((bullet, index) => (
|
||||
<div key={index} className="relative flex items-center">
|
||||
<span
|
||||
className="mr-[14px] h-[14px] w-[14px] rounded-full bg-[#0a4a3f]"
|
||||
style={{ backgroundColor: "var(--graph-0,#0a4a3f)" }}
|
||||
/>
|
||||
<p
|
||||
className="w-[640px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{bullet.text}
|
||||
</p>
|
||||
<span
|
||||
className="ml-[8px] h-[2px] w-[80px] bg-[#8ea8a5]"
|
||||
style={{ backgroundColor: "var(--stroke,#8ea8a5)" }}
|
||||
/>
|
||||
<span
|
||||
className="h-[6px] w-[6px] rounded-full bg-[#edf2f1]"
|
||||
style={{ backgroundColor: "var(--primary-text,#edf2f1)" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[58px] right-[48px] h-[474px] w-[474px]">
|
||||
{values?.map((value, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: 237 + index * 50,
|
||||
height: 237 + index * 50,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundColor: COLORS[index],
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="pt-[24px] text-center text-[24px] font-normal text-white"
|
||||
style={{ color: "var(--primary-text,#ffffff)" }}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketOpportunitySlide;
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-description-with-cards-slide";
|
||||
export const slideLayoutName = "Title Description with Cards Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A team introduction slide with a title and intro text on top, followed by a grid of profile cards where one card can be highlighted with a footer style.";
|
||||
|
||||
const MemberSchema = z.object({
|
||||
title: z.string().min(2).max(12).meta({
|
||||
description: "Member role or short heading.",
|
||||
}),
|
||||
name: z.string().min(2).max(16).meta({
|
||||
description: "Member name shown in the card footer.",
|
||||
}),
|
||||
image: z.object({
|
||||
__image_url__: z.string().url().default("https://i.pravatar.cc/600?img=12"),
|
||||
__image_prompt__: z.string().min(10).max(100).default("Professional male portrait with suit"),
|
||||
}).default({
|
||||
__image_url__: "https://i.pravatar.cc/600?img=12",
|
||||
__image_prompt__: "Professional male portrait with suit",
|
||||
}).meta({
|
||||
description: "Portrait image for a team member card.",
|
||||
}),
|
||||
highlighted: z.boolean().default(false).meta({
|
||||
description: "Whether this card uses the dark highlight footer.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(18).default("Meet Our Team").meta({
|
||||
description: "Main title at the top-left.",
|
||||
}),
|
||||
taglineLabel: z.string().max(16).default("TAGLINE").meta({
|
||||
description: "Small heading above team description.",
|
||||
}),
|
||||
taglineBody: z.string().max(80).default(
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea."
|
||||
).meta({
|
||||
description: "Short descriptive paragraph at top-right.",
|
||||
}),
|
||||
members: z
|
||||
.array(MemberSchema)
|
||||
|
||||
.max(4)
|
||||
.default([
|
||||
{
|
||||
title: "CEO",
|
||||
name: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://i.pravatar.cc/600?img=12",
|
||||
__image_prompt__: "Professional male portrait with suit",
|
||||
},
|
||||
highlighted: false,
|
||||
},
|
||||
{
|
||||
title: "HEADING 2",
|
||||
name: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://i.pravatar.cc/600?img=13",
|
||||
__image_prompt__: "Professional male portrait with tie",
|
||||
},
|
||||
highlighted: false,
|
||||
},
|
||||
{
|
||||
title: "HEADING 3",
|
||||
name: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://i.pravatar.cc/600?img=14",
|
||||
__image_prompt__: "Professional male portrait smiling",
|
||||
},
|
||||
highlighted: true,
|
||||
},
|
||||
{
|
||||
title: "HEADING 2",
|
||||
name: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://i.pravatar.cc/600?img=15",
|
||||
__image_prompt__: "Professional male portrait office",
|
||||
},
|
||||
highlighted: false,
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Four team member cards shown in one row.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const MeetTeamSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, taglineLabel, taglineBody, members } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between px-[64px] pt-[50px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="w-[520px]">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{taglineLabel}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[14px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{taglineBody}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[40px] left-[64px] grid grid-cols-4 gap-[22px]">
|
||||
{members?.map((member, index) => (
|
||||
<div key={index} className="w-[252px] overflow-hidden">
|
||||
<img
|
||||
src={member.image.__image_url__}
|
||||
alt={member.image.__image_prompt__}
|
||||
className="h-[244px] w-full object-cover"
|
||||
/>
|
||||
<div
|
||||
className="h-full p-[23px]"
|
||||
style={{
|
||||
backgroundColor: member.highlighted
|
||||
? "var(--primary-color,#15342D)"
|
||||
: "var(--card-color,#FEFEFF)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{
|
||||
color: member.highlighted
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--primary-color,#15342D)",
|
||||
}}
|
||||
>
|
||||
{member.title}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[20px] text-[28px] font-normal text-white"
|
||||
style={{
|
||||
color: member.highlighted
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--primary-color,#15342D)",
|
||||
}}
|
||||
>
|
||||
{member.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeetTeamSlide;
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "text-blocks-with-image-block-slide";
|
||||
export const slideLayoutName = "Text Blocks with Image Block Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title in the top-left block, text in the top-right , another text block in the bottom-left , and an image in the bottom-right block.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(30).default("Mission & Vision").meta({
|
||||
description: "Primary heading shown in the top-left tile.",
|
||||
}),
|
||||
|
||||
topleftTextBlockLabel: z.string().min(3).max(20).default("MISSION").meta({
|
||||
description: "Mission section label.",
|
||||
}),
|
||||
topleftTextBlockBody: z.string().min(40).max(90).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore."
|
||||
).meta({
|
||||
description: "Mission paragraph content.",
|
||||
}),
|
||||
bottomleftTextBlockLabel: z.string().min(3).max(20).default("VISION").meta({
|
||||
description: "Vision section label.",
|
||||
}),
|
||||
bottomleftTextBlockBody: z.string().min(40).max(90).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore."
|
||||
).meta({
|
||||
description: "Vision paragraph content.",
|
||||
}),
|
||||
image: z.object({
|
||||
__image_url__: z.string(),
|
||||
__image_prompt__: z.string(),
|
||||
}).optional().meta({
|
||||
description: "Bottom-right supporting image. Optional.",
|
||||
}).optional().default({
|
||||
__image_url__: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?auto=format&fit=crop&w=1400&q=80",
|
||||
__image_prompt__: "Business silhouette at window skyline",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const MissionVisionSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full grid-cols-2 grid-rows-2">
|
||||
<div className="px-[74px] pt-[50px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{data.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="pl-[60px] pt-[76px]"
|
||||
style={{ backgroundColor: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{ color: "var(--primary-text,#edf2f1)" }}
|
||||
>
|
||||
{data.topleftTextBlockLabel}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[26px] text-[28px] font-normal text-white"
|
||||
style={{ color: "var(--primary-text,#edf2f1)" }}
|
||||
>
|
||||
{data.topleftTextBlockBody}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="pl-[53px] py-[53px]"
|
||||
style={{ backgroundColor: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{ color: "var(--primary-text,#edf2f1)" }}
|
||||
>
|
||||
{data.bottomleftTextBlockLabel}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[24px] text-[28px] font-normal text-white"
|
||||
style={{ color: "var(--primary-text,#edf2f1)" }}
|
||||
>
|
||||
{data.bottomleftTextBlockBody}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-hidden bg-white"
|
||||
style={{ backgroundColor: "var(--card-color,#ffffff)" }}
|
||||
>
|
||||
{data.image?.__image_url__ && (
|
||||
<img
|
||||
src={data.image.__image_url__}
|
||||
alt={data.image.__image_prompt__}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MissionVisionSlide;
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-description-with-image-block-slide";
|
||||
export const slideLayoutName = "Title Description with Image Block Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title on top and a description below, and a content section containing an image and a grid of cards of text.";
|
||||
|
||||
const CardSchema = z.object({
|
||||
heading: z.string().max(14).meta({
|
||||
description: "Card heading.",
|
||||
}),
|
||||
body: z.string().max(25).meta({
|
||||
description: "Card short description.",
|
||||
}),
|
||||
isHighlighted: z.boolean().default(false).meta({
|
||||
description: "Whether this card uses the dark style.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(16).default("Our Services").meta({
|
||||
description: "Main heading shown at the top-left.",
|
||||
}),
|
||||
taglineLabel: z.string().max(16).default("TAGLINE").meta({
|
||||
description: "Small label above left paragraph.",
|
||||
}),
|
||||
taglineBody: z.string().max(30).default(
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea."
|
||||
).meta({
|
||||
description: "Supporting text shown beneath the tagline label.",
|
||||
}),
|
||||
featureImage: z.object({
|
||||
__image_url__: z.string().url().default("https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80"),
|
||||
__image_prompt__: z.string().min(10).max(100).default("Customer support team in office"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80",
|
||||
__image_prompt__: "Customer support team in office",
|
||||
}).meta({
|
||||
description: "Main image shown at the lower left side.",
|
||||
}),
|
||||
services: z
|
||||
.array(CardSchema)
|
||||
|
||||
.max(4)
|
||||
.default([
|
||||
{ heading: "HEADING 1", body: "Lorem ipsum dolor sit amet, consectetur", isHighlighted: false },
|
||||
{ heading: "HEADING 2", body: "Lorem ipsum dolor sit amet, consectetur", isHighlighted: true },
|
||||
{ heading: "HEADING 3", body: "Lorem ipsum dolor sit amet, consectetur", isHighlighted: false },
|
||||
{ heading: "HEADING 4", body: "Lorem ipsum dolor sit amet, consectetur", isHighlighted: false },
|
||||
])
|
||||
.meta({
|
||||
description: "Cards rendered on the right side.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const OurServicesSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, taglineLabel, taglineBody, featureImage, services } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] flex items-end pb-[56px] justify-between overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className=" pt-[50px]">
|
||||
<div className="px-[68px]">
|
||||
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="mt-[26px] w-[560px]">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{taglineLabel}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[14px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{taglineBody}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="mt-[35px] h-[326px] w-[650px] bg-[#15342D]"
|
||||
style={{ backgroundColor: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
|
||||
{featureImage?.__image_url__ && (
|
||||
<img
|
||||
src={featureImage?.__image_url__}
|
||||
alt={featureImage?.__image_prompt__}
|
||||
className="h-[326px] w-[650px] object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-[22px] pr-[76px]">
|
||||
{services?.map((card, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className=" p-[33px]"
|
||||
style={{
|
||||
backgroundColor: card.isHighlighted
|
||||
? "var(--primary-color,#15342D)"
|
||||
: "var(--card-color,#ececee)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[4.354px] text-white"
|
||||
style={{
|
||||
color: card.isHighlighted
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--primary-color,#15342D)",
|
||||
}}
|
||||
>
|
||||
{card.heading}
|
||||
</p>
|
||||
<p
|
||||
className={`${card.isHighlighted ? "text-white" : "text-[#15342D]"} mt-[20px] text-[28px] font-normal`}
|
||||
style={{
|
||||
color: card.isHighlighted
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--background-text,#15342DCC)",
|
||||
}}
|
||||
>
|
||||
{card.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OurServicesSlide;
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-cards-list-with-text-slide";
|
||||
export const slideLayoutName = "Title with Cards List with Text";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title on top and a content section containing a list of cards with text content.";
|
||||
|
||||
const PlanSchema = z.object({
|
||||
price: z.string().min(4).max(12).meta({
|
||||
description: "Plan price label shown at the top of each card.",
|
||||
}),
|
||||
description: z.string().max(18).meta({
|
||||
description: "Short statement describing the plan.",
|
||||
}),
|
||||
features: z
|
||||
.array(z.string().max(16))
|
||||
|
||||
.max(4)
|
||||
.meta({
|
||||
description: "Four bullet features shown in the pricing card.",
|
||||
}),
|
||||
highlighted: z.boolean().default(false).meta({
|
||||
description: "Whether this card uses the highlighted dark style.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(18).default("Pricing Plan").meta({
|
||||
description: "Main slide title.",
|
||||
}),
|
||||
featureIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().min(3).max(30).default("check icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
}).meta({
|
||||
description: "Icon used for each feature bullet in plan cards.",
|
||||
}),
|
||||
plans: z
|
||||
.array(PlanSchema)
|
||||
.max(3)
|
||||
.default([
|
||||
{
|
||||
price: "$80/MONTH",
|
||||
description: "Lorem ipsum dolor sit.",
|
||||
features: [
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
],
|
||||
highlighted: false,
|
||||
},
|
||||
{
|
||||
price: "$80/MONTH",
|
||||
description: "Lorem ipsum dolor sit.",
|
||||
features: [
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
],
|
||||
highlighted: true,
|
||||
},
|
||||
{
|
||||
price: "$80/MONTH",
|
||||
description: "Lorem ipsum dolor sit.",
|
||||
features: [
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
"Lorem ipsum dolor sit.",
|
||||
],
|
||||
highlighted: false,
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Three pricing cards displayed across the slide.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const PricingPlanSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, featureIcon, plans } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden flex flex-col "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="px-[68px] pt-[50px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className=" px-[68px] mt-16 grid grid-cols-3 items-start">
|
||||
{plans?.map((plan, index) => {
|
||||
const active = plan.highlighted;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={` px-[20px] ${active ? "-mt-[30px] py-[60px]" : "py-[33px]"}`}
|
||||
style={{
|
||||
backgroundColor: active
|
||||
? "var(--primary-color,#15342D)"
|
||||
: "var(--card-color,#ececee)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-white"
|
||||
style={{
|
||||
color: active
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--primary-color,#15342D)",
|
||||
}}
|
||||
>
|
||||
{plan.price}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[18px] text-[28px] font-normal text-[#15342DCC]"
|
||||
style={{
|
||||
color: active
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--background-text,#15342DCC)",
|
||||
}}
|
||||
>
|
||||
{plan.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-[18px] space-y-[6px]">
|
||||
{plan.features.map((feature, featureIndex) => (
|
||||
<div key={featureIndex} className="flex items-center gap-[10px]">
|
||||
<RemoteSvgIcon
|
||||
url={featureIcon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="w-[28px] h-[28px] object-contain"
|
||||
color={active ? "var(--primary-text, #edf2f1)" : "var(--background-text, #15342DCC)"}
|
||||
title={featureIcon?.__icon_query__}
|
||||
/>
|
||||
{/* <img
|
||||
src={featureIcon?.__icon_url__}
|
||||
alt={featureIcon?.__icon_query__}
|
||||
className="h-[28px] w-[28px] object-contain"
|
||||
style={{ filter: active ? "brightness(0) invert(1)" : "none" }}
|
||||
/> */}
|
||||
<p
|
||||
className="text-[28px] font-normal text-[#15342DCC]"
|
||||
style={{
|
||||
color: active
|
||||
? "var(--primary-text,#edf2f1)"
|
||||
: "var(--background-text,#15342DCC)",
|
||||
}}
|
||||
>
|
||||
{feature}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingPlanSlide;
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-with-process-steps-slide";
|
||||
export const slideLayoutName = "Title with Process Steps Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title on top and a content section containing a process diagrams with connected hexagon steps and alternating caption blocks above and below the flow.";
|
||||
|
||||
const StepSchema = z.object({
|
||||
label: z.string().max(16).meta({
|
||||
description: "Short uppercase label for a process step.",
|
||||
}),
|
||||
body: z.string().max(32).meta({
|
||||
description: "Brief explanatory text for the process step.",
|
||||
}),
|
||||
icon: z.object({
|
||||
__icon_url__: z.string(),
|
||||
__icon_query__: z.string(),
|
||||
}).default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}),
|
||||
highlighted: z.boolean().default(false).meta({
|
||||
description: "Whether the hexagon is emphasized with dark fill.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(14).default("PROCESS").meta({
|
||||
description: "Main title shown in the top-left corner.",
|
||||
}),
|
||||
|
||||
steps: z
|
||||
.array(StepSchema)
|
||||
|
||||
.max(5)
|
||||
.default([
|
||||
{
|
||||
label: "TAGLINE TAGLINE", body: "Ut enim ad minim. Ut enim ad minim. ", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}, highlighted: false
|
||||
},
|
||||
{
|
||||
label: "TAGLINE", body: "Ut enim ad minim. Ut enim ad minim.", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "upload icon",
|
||||
}, highlighted: false
|
||||
},
|
||||
{
|
||||
label: "TAGLINE", body: "Ut enim ad minim.", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}, highlighted: false
|
||||
},
|
||||
{
|
||||
label: "TAGLINE", body: "Ut enim ad minim.", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "upload icon",
|
||||
}, highlighted: false
|
||||
},
|
||||
{
|
||||
label: "TAGLINE", body: "Ut enim ad minim.", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}, highlighted: true
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Process steps rendered from left to right.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
|
||||
const ProcessSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, steps } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden flex flex-col "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#DAE1DE)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="px-[66px] pt-[50px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex justify-center items-center w-full flex-1 px-[66px] ">
|
||||
{steps?.map((step, index) => {
|
||||
if (index % 2 === 0) {
|
||||
return (
|
||||
<div key={index} className="relative w-[230px] "
|
||||
style={{
|
||||
marginLeft: index === 0 ? '0' : '-10px',
|
||||
}}
|
||||
>
|
||||
<div className="relative flex justify-center items-center h-[276px]">
|
||||
<div className="relative">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[42px] h-[42px] flex items-center justify-center">
|
||||
|
||||
<RemoteSvgIcon
|
||||
url={step.icon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="w-full h-full object-contain"
|
||||
color={step.highlighted ? "var(--primary-text, #4C68DF)" : "var(--background-text,#315f58)"}
|
||||
title={step.icon.__icon_query__}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="162" height="187" viewBox="0 0 162 187" fill="none">
|
||||
<path d="M80.8291 0L161.658 46.6667V140L80.8291 186.667L2.28882e-05 140V46.6667L80.8291 0Z" fill={step.highlighted ? "var(--primary-color,#15342D)" : "var(--card-color,#FEFEFF)"} />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="231" height="134" viewBox="0 0 231 134" fill="none">
|
||||
<path d="M230.94 66.667L115.47 133.334L0 66.667V0H11.5469V60L115.47 120L219.393 60V0H230.94V66.667Z" fill="var(--card-color,#FEFEFF)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" absolute bottom-[-120px] left-0 text-center mt-[60px]">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{step.label}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[6px] text-[24px] leading-[1.2] text-[#315f58]"
|
||||
style={{ color: "var(--background-text,#315f58)" }}
|
||||
>
|
||||
{step.body}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div key={index} className="relative w-[230px]"
|
||||
style={{
|
||||
marginLeft: index === 0 ? '0' : '-11px',
|
||||
marginTop: '2px',
|
||||
}}
|
||||
>
|
||||
<div className=" absolute top-[-140px] left-0 text-center">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{step.label}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[6px] text-[24px] leading-[1.2] text-[#315f58]"
|
||||
style={{ color: "var(--background-text,#315f58)" }}
|
||||
>
|
||||
{step.body}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative w-[230px] flex justify-center items-center h-[276px]">
|
||||
<div className="relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="162" height="187" viewBox="0 0 162 187" fill="none">
|
||||
<path d="M80.8291 0L161.658 46.6667V140L80.8291 186.667L2.28882e-05 140V46.6667L80.8291 0Z" fill={step.highlighted ? "var(--primary-color,#15342D)" : "var(--card-color,#FEFEFF)"} />
|
||||
</svg>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[42px] h-[42px] flex items-center justify-center">
|
||||
<RemoteSvgIcon
|
||||
url={step.icon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="w-full h-full object-contain"
|
||||
color={step.highlighted ? "var(--primary-text, #4C68DF)" : "var(--background-text,#315f58)"}
|
||||
title={step.icon.__icon_query__}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-1 right-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="231" height="134" viewBox="0 0 231 134" fill="none">
|
||||
<path d="M230.94 66.667L115.47 0L0 66.667V133.333H11.5469V73.333L115.47 13.333L219.394 73.333V133.333H230.94V66.667Z" fill="var(--card-color,#FEFEFF)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessSlide;
|
||||
|
|
@ -0,0 +1,720 @@
|
|||
import * as z from "zod";
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
XAxis,
|
||||
YAxis,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
LineChart,
|
||||
Line,
|
||||
LabelList,
|
||||
} from "recharts";
|
||||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
|
||||
export const slideLayoutId = "title-description-with-chart-and-kpi-cards-slide";
|
||||
export const slideLayoutName = "Title Description with Chart and KPI Cards Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A text slide with a title on top and a description below, and a content section containing a chart and a grid of KPI cards.";
|
||||
|
||||
const LegacyBarSchema = z.object({
|
||||
value: z.number().min(10).max(100).meta({
|
||||
description: "Relative bar value used by legacy data.",
|
||||
}),
|
||||
});
|
||||
|
||||
const MiniBarPointSchema = z.object({
|
||||
label: z.string().min(1).max(10).meta({
|
||||
description: "Category label for the mini bar chart.",
|
||||
}),
|
||||
primary: z.number().min(0).max(1000).meta({
|
||||
description: "Primary series value.",
|
||||
}),
|
||||
secondary: z.number().min(0).max(1000).meta({
|
||||
description: "Secondary series value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const DonutPointSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Donut segment label.",
|
||||
}),
|
||||
value: z.number().min(1).max(100).meta({
|
||||
description: "Donut segment value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const GroupedBarPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "X-axis label for grouped bars.",
|
||||
}),
|
||||
optionA: z.number().min(0).max(220).meta({
|
||||
description: "Option A value.",
|
||||
}),
|
||||
optionB: z.number().min(0).max(220).meta({
|
||||
description: "Option B value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const TrendPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "X-axis label for trend lines.",
|
||||
}),
|
||||
optionA: z.number().min(0).max(100).meta({
|
||||
description: "Option A trend value.",
|
||||
}),
|
||||
optionB: z.number().min(0).max(100).meta({
|
||||
description: "Option B trend value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const MetricCardSchema = z.object({
|
||||
value: z.string().min(1).max(8).meta({
|
||||
description: "KPI value text.",
|
||||
}),
|
||||
body: z.string().min(10).max(40).meta({
|
||||
description: "KPI supporting sentence.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(4).max(12).default("Report").meta({
|
||||
description: "Slide heading text.",
|
||||
}),
|
||||
taglineLabel: z.string().min(3).max(10).default("TAGLINE").meta({
|
||||
description: "Small label above intro paragraph.",
|
||||
}),
|
||||
taglineBody: z.string().min(40).max(120).default(
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Intro paragraph shown beneath the heading.",
|
||||
}),
|
||||
sideImage: z.object({
|
||||
__image_url__: z.string().url().default("https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=700&q=80"),
|
||||
__image_prompt__: z.string().min(10).max(100).default("Team members reviewing charts together"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=700&q=80",
|
||||
__image_prompt__: "Team members reviewing charts together",
|
||||
}).meta({
|
||||
description: "Left-side vertical image strip.",
|
||||
}),
|
||||
chartStyle: z.enum(["mini-bars", "donut", "grouped-bars", "dual-line"]).default("donut").meta({
|
||||
description: "Chart style variant matching Image #1 to Image #4.",
|
||||
}),
|
||||
chartTitle: z.string().min(3).max(20).default("Sandro Tavares").meta({
|
||||
description: "Name displayed in the chart card.",
|
||||
}),
|
||||
miniBars: z
|
||||
.array(MiniBarPointSchema)
|
||||
.min(8)
|
||||
.max(9)
|
||||
.default([
|
||||
{ label: "1", primary: 320, secondary: 560 },
|
||||
{ label: "2", primary: 140, secondary: 840 },
|
||||
{ label: "3", primary: 230, secondary: 520 },
|
||||
{ label: "4", primary: 320, secondary: 660 },
|
||||
{ label: "5", primary: 150, secondary: 460 },
|
||||
{ label: "6", primary: 160, secondary: 850 },
|
||||
{ label: "7", primary: 640, secondary: 320 },
|
||||
{ label: "8", primary: 320, secondary: 440 },
|
||||
{ label: "9", primary: 420, secondary: 620 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #1 mini dual-series bar chart.",
|
||||
}),
|
||||
donutData: z
|
||||
.array(DonutPointSchema)
|
||||
.min(3)
|
||||
.max(3)
|
||||
.default([
|
||||
{ name: "Option A", value: 60 },
|
||||
{ name: "Option B", value: 20 },
|
||||
{ name: "Option C", value: 20 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #2 donut chart.",
|
||||
}),
|
||||
groupedBars: z
|
||||
.array(GroupedBarPointSchema)
|
||||
.min(4)
|
||||
.max(4)
|
||||
.default([
|
||||
{ label: "label", optionA: 120, optionB: 200 },
|
||||
{ label: "label", optionA: 150, optionB: 80 },
|
||||
{ label: "label", optionA: 70, optionB: 110 },
|
||||
{ label: "label", optionA: 130, optionB: 130 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #3 grouped bar chart.",
|
||||
}),
|
||||
trendLines: z
|
||||
.array(TrendPointSchema)
|
||||
.min(6)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "label", optionA: 8, optionB: 2 },
|
||||
{ label: "label", optionA: 45, optionB: 65 },
|
||||
{ label: "label", optionA: 35, optionB: 40 },
|
||||
{ label: "label", optionA: 95, optionB: 100 },
|
||||
{ label: "label", optionA: 50, optionB: 35 },
|
||||
{ label: "label", optionA: 5, optionB: 75 },
|
||||
{ label: "label", optionA: 55, optionB: 50 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #4 dual-line chart.",
|
||||
}),
|
||||
legendLabels: z.array(z.string().min(1).max(18)).min(2).max(3).default(["Option A", "Option B", "Option C"]).meta({
|
||||
description: "Legend labels used by donut/grouped/line variants.",
|
||||
}),
|
||||
xAxisName: z.string().min(3).max(16).default("X axis name").meta({
|
||||
description: "X axis title used in the dual-line variant.",
|
||||
}),
|
||||
yAxisName: z.string().min(3).max(16).default("Y axis name").meta({
|
||||
description: "Y axis title used in the dual-line variant.",
|
||||
}),
|
||||
footerLabel: z.string().min(10).max(60).default("Current margin: April Spendings").meta({
|
||||
description: "Footer label under mini bar chart.",
|
||||
}),
|
||||
footerValue: z.string().min(6).max(24).default("$350.00 / $640.00").meta({
|
||||
description: "Footer value under mini bar chart.",
|
||||
}),
|
||||
bars: z
|
||||
.array(LegacyBarSchema)
|
||||
.min(8)
|
||||
.max(9)
|
||||
.default([
|
||||
{ value: 52 },
|
||||
{ value: 24 },
|
||||
{ value: 35 },
|
||||
{ value: 48 },
|
||||
{ value: 26 },
|
||||
{ value: 72 },
|
||||
{ value: 47 },
|
||||
{ value: 55 },
|
||||
])
|
||||
.meta({
|
||||
description: "Legacy fallback bar values used when miniBars is not supplied.",
|
||||
}),
|
||||
metricValue: z.string().min(1).max(8).default("X 5").meta({
|
||||
description: "Legacy single KPI value.",
|
||||
}),
|
||||
metricBody: z.string().min(10).max(40).default("Lorem ipsum dolor sit.").meta({
|
||||
description: "Legacy single KPI body.",
|
||||
}),
|
||||
metricCards: z
|
||||
.array(MetricCardSchema)
|
||||
.min(1)
|
||||
.max(2)
|
||||
.default([
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
])
|
||||
.meta({
|
||||
description: "One or two KPI cards shown on the right.",
|
||||
}),
|
||||
metricIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().min(3).max(30).default("pulse icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}).meta({
|
||||
description: "Icon shown in the KPI callout card.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const MINI_BAR_DARK = "var(--graph-0,#0B4B40)";
|
||||
const MINI_BAR_LIGHT = "var(--graph-1,#CED3D1)";
|
||||
const DONUT_COLORS = [
|
||||
"var(--graph-0,#0B4B40)",
|
||||
"var(--graph-1,#4B6B61)",
|
||||
"var(--graph-2,#7B938C)",
|
||||
];
|
||||
const KPI_ICON_BG = "var(--graph-3,#063C73)";
|
||||
|
||||
const PulseIcon = () => (
|
||||
<svg viewBox="0 0 24 24" className="h-[22px] w-[22px]" aria-hidden="true">
|
||||
<path
|
||||
d="M2.5 12h4.6l1.7-4.4 3.1 9 2.7-6.2h6.9"
|
||||
fill="none"
|
||||
stroke="var(--primary-text,#ffffff)"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const renderDonutPercentLabel = ({ cx, cy, midAngle, outerRadius, percent }: any) => {
|
||||
const radius = (outerRadius ?? 0) + 18;
|
||||
const x = (cx ?? 0) + radius * Math.cos((-(midAngle ?? 0) * Math.PI) / 180);
|
||||
const y = (cy ?? 0) + radius * Math.sin((-(midAngle ?? 0) * Math.PI) / 180);
|
||||
|
||||
return (
|
||||
<g>
|
||||
<circle cx={x} cy={y} r={16} fill="var(--card-color,#ECEAF8)" />
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
style={{ padding: "4px" }}
|
||||
textAnchor="middle"
|
||||
fill="var(--background-text,#2C2B39)"
|
||||
fontSize={10}
|
||||
fontWeight={600}
|
||||
>
|
||||
{`${Math.round((percent ?? 0) * 100)}%`}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const ReportSnapshotSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const {
|
||||
title,
|
||||
taglineLabel,
|
||||
taglineBody,
|
||||
sideImage,
|
||||
chartStyle,
|
||||
chartTitle,
|
||||
miniBars,
|
||||
donutData,
|
||||
groupedBars,
|
||||
trendLines,
|
||||
legendLabels,
|
||||
xAxisName,
|
||||
yAxisName,
|
||||
footerLabel,
|
||||
footerValue,
|
||||
bars,
|
||||
metricValue,
|
||||
metricBody,
|
||||
metricCards,
|
||||
metricIcon,
|
||||
} = data;
|
||||
|
||||
const resolvedMiniBars =
|
||||
miniBars && miniBars.length >= 8
|
||||
? miniBars
|
||||
: (bars ?? []).map((bar, index) => {
|
||||
const scaledPrimary = Math.round(bar.value * 8 + 80);
|
||||
const scaledSecondary = Math.min(1000, scaledPrimary + 220);
|
||||
|
||||
return {
|
||||
label: `${index + 1}`,
|
||||
primary: scaledPrimary,
|
||||
secondary: scaledSecondary,
|
||||
};
|
||||
});
|
||||
|
||||
const fallbackMetric = {
|
||||
value: metricValue ?? "X 5",
|
||||
body: metricBody ?? "Lorem ipsum dolor sit.",
|
||||
};
|
||||
|
||||
const resolvedMetricCards =
|
||||
metricCards && metricCards.length > 0
|
||||
? metricCards
|
||||
: [fallbackMetric, fallbackMetric];
|
||||
|
||||
const visibleMetricCards =
|
||||
(chartStyle ?? "mini-bars") === "mini-bars"
|
||||
? resolvedMetricCards.slice(0, 1)
|
||||
: resolvedMetricCards.slice(0, 2);
|
||||
|
||||
const usePulseFallback = !metricIcon?.__icon_url__ || metricIcon.__icon_url__.includes("placeholder.svg");
|
||||
const activeChartStyle = chartStyle ?? "mini-bars";
|
||||
const donutTotal = (donutData ?? []).reduce((sum, item) => sum + item.value, 0) || 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#D7DEDB)",
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-7 h-full">
|
||||
|
||||
|
||||
{sideImage?.__image_url__ && (
|
||||
<img
|
||||
src={sideImage.__image_url__}
|
||||
alt={sideImage.__image_prompt__}
|
||||
className=" h-full w-[232px] object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col flex-1 justify-center">
|
||||
<div
|
||||
className=" text-[#083F37]"
|
||||
style={{ color: "var(--primary-color,#083F37)" }}
|
||||
>
|
||||
<h2 className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px]">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="mt-[14px] w-[560px]">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-[#083F37]"
|
||||
style={{ color: "var(--primary-color,#083F37)" }}
|
||||
>
|
||||
{taglineLabel}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[12px] text-[24px] mb-5 leading-[1.11] text-[#083F37]/75"
|
||||
style={{ color: "var(--background-text,#083F37BF)" }}
|
||||
>
|
||||
{taglineBody}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
className={` w-[580px] bg-[#F3F3F3] px-[28px] pb-[18px] pt-[20px] ${activeChartStyle === "mini-bars" ? "h-[308px]" : "h-[350px]"
|
||||
}`}
|
||||
style={{ backgroundColor: "var(--card-color,#F3F3F3)" }}
|
||||
>
|
||||
<p
|
||||
className="mt-[14px] text-[32px] font-normal leading-[1.1] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{chartTitle}
|
||||
</p>
|
||||
|
||||
|
||||
{activeChartStyle === "mini-bars" && (
|
||||
<>
|
||||
<div className="mt-[18px] h-[166px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={resolvedMiniBars}
|
||||
margin={{ top: 0, right: 8, left: -6, bottom: 0 }}
|
||||
barCategoryGap={16}
|
||||
>
|
||||
<CartesianGrid vertical={false} stroke="var(--stroke,#D7DCDA)" strokeDasharray="3 3" />
|
||||
<XAxis dataKey="label" tick={false} axisLine={false} tickLine={false} />
|
||||
<YAxis
|
||||
width={42}
|
||||
|
||||
|
||||
tickFormatter={(value) => `$${value}`}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "var(--background-text,#6C7271)", fontSize: 10 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="secondary"
|
||||
fill={MINI_BAR_LIGHT}
|
||||
radius={[5, 5, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
maxBarSize={26}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="primary"
|
||||
fill={MINI_BAR_DARK}
|
||||
radius={[5, 5, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
maxBarSize={26}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-[14px] flex items-center justify-between ">
|
||||
<p
|
||||
className="text-[#6D7371] text-[18px]"
|
||||
style={{ color: "var(--background-text,#6D7371)" }}
|
||||
>
|
||||
{footerLabel}
|
||||
</p>
|
||||
<p
|
||||
className="font-medium text-[#15342D] text-[18px]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{footerValue}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeChartStyle === "donut" && (
|
||||
<div className="mt-[6px] flex h-[250px] items-center">
|
||||
<div className="h-[220px] w-[250px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={donutData ?? []}
|
||||
dataKey="value"
|
||||
innerRadius={48}
|
||||
outerRadius={82}
|
||||
stroke="none"
|
||||
labelLine={false}
|
||||
label={renderDonutPercentLabel}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{(donutData ?? []).map((entry, index) => (
|
||||
<Cell key={`${entry.name}-${index}`} fill={DONUT_COLORS[index % DONUT_COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="ml-[8px] flex-1 space-y-[16px] pr-[8px]">
|
||||
{(donutData ?? []).map((entry, index) => {
|
||||
const percent = Math.round((entry.value / donutTotal) * 100);
|
||||
return (
|
||||
<div key={`${entry.name}-legend-${index}`} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
className="h-[14px] w-[14px] rounded-full"
|
||||
style={{ backgroundColor: DONUT_COLORS[index % DONUT_COLORS.length] }}
|
||||
/>
|
||||
<p
|
||||
className="text-[18px] font-bold text-[#767676]"
|
||||
style={{ color: "var(--background-text,#767676)" }}
|
||||
>
|
||||
{legendLabels?.[index] ?? entry.name}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className="text-[18px] font-bold text-[#404040]"
|
||||
style={{ color: "var(--background-text,#404040)" }}
|
||||
>
|
||||
{percent}%
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeChartStyle === "grouped-bars" && (
|
||||
<div className="mt-[12px] flex h-[236px] items-center justify-between">
|
||||
<div className="h-[210px] w-[362px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={groupedBars ?? []}
|
||||
margin={{ top: 12, right: 6, left: -12, bottom: 0 }}
|
||||
barCategoryGap={20}
|
||||
>
|
||||
<CartesianGrid vertical={false} stroke="var(--stroke,#D7DCDA)" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "var(--background-text,#42484A)", fontSize: 10 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
|
||||
tick={{ fill: "var(--background-text,#566061)", fontSize: 10 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="optionA"
|
||||
fill={MINI_BAR_DARK}
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={20}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
<LabelList
|
||||
dataKey="optionA"
|
||||
position="top"
|
||||
fill="var(--background-text,#5B6463)"
|
||||
fontSize={9}
|
||||
/>
|
||||
</Bar>
|
||||
<Bar
|
||||
dataKey="optionB"
|
||||
fill="var(--graph-2,#8A9A96)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={20}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
<LabelList
|
||||
dataKey="optionB"
|
||||
position="top"
|
||||
fill="var(--background-text,#5B6463)"
|
||||
fontSize={9}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="ml-[24px] space-y-[24px]">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
className="h-[14px] w-[14px] rounded-full bg-[#0B4B40]"
|
||||
style={{ backgroundColor: "var(--graph-0,#0B4B40)" }}
|
||||
/>
|
||||
<p
|
||||
className="text-[18px] font-medium leading-[1] text-[#6A6B6E]"
|
||||
style={{ color: "var(--background-text,#6A6B6E)" }}
|
||||
>
|
||||
{legendLabels?.[0] ?? "Option A"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
className="h-[14px] w-[14px] rounded-full bg-[#8A9A96]"
|
||||
style={{ backgroundColor: "var(--graph-2,#8A9A96)" }}
|
||||
/>
|
||||
<p
|
||||
className="text-[18px] font-medium leading-[1] text-[#6A6B6E]"
|
||||
style={{ color: "var(--background-text,#6A6B6E)" }}
|
||||
>
|
||||
{legendLabels?.[1] ?? "Option B"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeChartStyle === "dual-line" && (
|
||||
<div className="mt-[12px] flex h-[236px] items-center justify-between">
|
||||
<div className="h-[210px] w-[362px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={trendLines ?? []} margin={{ top: 12, right: 6, left: -6, bottom: 16 }}>
|
||||
<CartesianGrid vertical={false} stroke="var(--stroke,#D7DCDA)" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "var(--background-text,#42484A)", fontSize: 10 }}
|
||||
label={{
|
||||
value: xAxisName,
|
||||
position: "insideBottom",
|
||||
offset: -6,
|
||||
fill: "var(--background-text,#535B5C)",
|
||||
fontSize: 10,
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
|
||||
tick={{ fill: "var(--background-text,#566061)", fontSize: 10 }}
|
||||
label={{
|
||||
value: yAxisName,
|
||||
angle: -90,
|
||||
position: "insideLeft",
|
||||
fill: "var(--background-text,#535B5C)",
|
||||
fontSize: 10,
|
||||
dx: -8,
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="optionA"
|
||||
stroke={MINI_BAR_DARK}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="optionB"
|
||||
stroke="var(--graph-2,#8A9A96)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="ml-[18px] space-y-[24px]">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
className="h-[14px] w-[14px] rounded-full bg-[#0B4B40]"
|
||||
style={{ backgroundColor: "var(--graph-0,#0B4B40)" }}
|
||||
/>
|
||||
<p
|
||||
className="text-[18px] font-medium leading-[1] text-[#6A6B6E]"
|
||||
style={{ color: "var(--background-text,#6A6B6E)" }}
|
||||
>
|
||||
{legendLabels?.[0] ?? "Option A"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
className="h-[14px] w-[14px] rounded-full bg-[#8A9A96]"
|
||||
style={{ backgroundColor: "var(--graph-2,#8A9A96)" }}
|
||||
/>
|
||||
<p
|
||||
className="text-[18px] font-medium leading-[1] text-[#6A6B6E]"
|
||||
style={{ color: "var(--background-text,#6A6B6E)" }}
|
||||
>
|
||||
{legendLabels?.[1] ?? "Option B"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={` w-[362px] ${activeChartStyle === "mini-bars" ? "top-[382px]" : "top-[320px]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col justify-end items-end gap-[24px]">
|
||||
{visibleMetricCards.map((metric, index) => (
|
||||
<div
|
||||
key={`${metric.value}-${index}`}
|
||||
className="bg-[#F3F3F3] px-[33px] py-[24px]"
|
||||
style={{ backgroundColor: "var(--card-color,#F3F3F3)" }}
|
||||
>
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div
|
||||
className="flex h-[56px] w-[56px] items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: metricIcon?.__icon_url__ ? "var(--primary-color,#113F37)" : KPI_ICON_BG }}
|
||||
>
|
||||
{usePulseFallback ? (
|
||||
<PulseIcon />
|
||||
) : (
|
||||
<RemoteSvgIcon
|
||||
url={metricIcon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="w-[24px] h-[24px] object-contain"
|
||||
color="var(--primary-text, #FEFEFF)"
|
||||
title={metricIcon?.__icon_query__}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className="text-[48px] font-semibold leading-[1] text-[#113F37]"
|
||||
style={{ color: "var(--primary-color,#113F37)" }}
|
||||
>
|
||||
{metric.value}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className="mt-[18px] text-[28px] leading-[1.08] text-[#113F37]"
|
||||
style={{ color: "var(--primary-color,#113F37)" }}
|
||||
>
|
||||
{metric.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportSnapshotSlide;
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const PRODUCT_BG = "var(--background-color,#d7dddd)";
|
||||
const PRODUCT_DARK = "var(--primary-color,#05463d)";
|
||||
|
||||
|
||||
export const slideLayoutId = "table-of-content-slide";
|
||||
export const slideLayoutName = "Table of Content Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A two-column table of contents slide with section titles and numbers on a left panel and a title plus description paragraph on the right panel.";
|
||||
|
||||
const SectionSchema = z.object({
|
||||
title: z.string().min(4).max(25).meta({
|
||||
description: "Section label shown in the left navigation column.",
|
||||
}),
|
||||
number: z.string().min(2).max(3).meta({
|
||||
description: "Section number shown beside the section label.",
|
||||
}),
|
||||
description: z.string().min(4).max(60).optional().meta({
|
||||
description: "Section description shown in the right column.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(18).default("Table Of Content").meta({
|
||||
description: "Heading in the right-side content area.",
|
||||
}),
|
||||
description: z.string().min(50).max(160).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore."
|
||||
).meta({
|
||||
description: "Supporting descriptive paragraph under the heading.",
|
||||
}),
|
||||
sections: z
|
||||
.array(SectionSchema)
|
||||
.max(6)
|
||||
.default([
|
||||
{ title: "SECTION TITLE SECTION TITLE", number: "01", description: "Lorem ipsum dolor sit. Lorem ipsum dolor sit. Lorem ipsum dolor sit." },
|
||||
{ title: "SECTION TITLE SECTION TITLE", number: "02", description: "Lorem ipsum dolor sit. Lorem ipsum dolor sit. Lorem ipsum dolor sit." },
|
||||
{ title: "SECTION TITLE SECTION TITLE", number: "03", description: "Lorem ipsum dolor sit. Lorem ipsum dolor sit. Lorem ipsum dolor sit." },
|
||||
{ title: "SECTION TITLE SECTION TITLE", number: "04", description: "Lorem ipsum dolor sit. Lorem ipsum dolor sit. Lorem ipsum dolor sit." },
|
||||
{ title: "SECTION TITLE SECTION TITLE", number: "05", description: "Lorem ipsum dolor sit. Lorem ipsum dolor sit. Lorem ipsum dolor sit." },
|
||||
{ title: "SECTION TITLE SECTION TITLE", number: "06", description: "Lorem ipsum dolor sit. Lorem ipsum dolor sit. Lorem ipsum dolor sit." },
|
||||
|
||||
])
|
||||
.meta({
|
||||
description: "Six rows listed in the table of contents panel.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const TableOfContentSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, description, sections } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden "
|
||||
style={{
|
||||
backgroundColor: PRODUCT_BG,
|
||||
fontFamily: "var(--body-font-family,'Bricolage Grotesque')",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full grid-cols-[1fr_1fr]">
|
||||
<div className="px-[56px] pt-[50px]" style={{ backgroundColor: PRODUCT_DARK }}>
|
||||
<div className={`${sections && sections?.length > 3 ? 'space-y-[28px]' : 'space-y-[40px]'}`}>
|
||||
{sections?.map((section, index) => (
|
||||
<div key={index} className="flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[0.2em] text-[#ecf2f1]"
|
||||
style={{ color: "var(--primary-text,#ecf2f1)" }}
|
||||
>
|
||||
{section.title}
|
||||
</p>
|
||||
{section.description && <p
|
||||
className="mt-[6px] text-[18px] leading-[1.2] text-[#ecf2f1]"
|
||||
style={{ color: "var(--primary-text,#ecf2f1)" }}
|
||||
>
|
||||
{section.description}
|
||||
</p>}
|
||||
</div>
|
||||
<p
|
||||
className="text-[22px] font-medium text-[#ecf2f1]"
|
||||
style={{ color: "var(--primary-text,#ecf2f1)" }}
|
||||
>
|
||||
{section.number}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-[42px] pt-[118px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "var(--primary-color,#15342D)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className="mt-[28px] w-[560px] text-[24px] font-normal text-[#15342DCC]"
|
||||
style={{ color: "var(--background-text,#15342DCC)" }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableOfContentSlide;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"description": "Product pitch layouts for cover, narrative, comparisons, KPIs, pricing, and team slides",
|
||||
"ordered": false,
|
||||
"default": false
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
"use client";
|
||||
|
||||
import * as z from "zod";
|
||||
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart";
|
||||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
|
||||
const InsightItemSchema = z.object({
|
||||
title: z.string().min(3).max(80).meta({
|
||||
description: "Bullet point title shown next to the icon.",
|
||||
}),
|
||||
description: z.string().min(20).max(120).meta({
|
||||
description: "Bullet point description shown below the title.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "bar-chart-with-bullet-list-title-description-icon-slide";
|
||||
export const slideLayoutName = "Bar Chart with Bullet List with Title Description Icon Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top, a vertical list of three bullet points with icon, title and description on the left, and a bar chart on the right.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(80).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
itemIcon: z
|
||||
.object({
|
||||
__icon_url__: z
|
||||
.string()
|
||||
.default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
})
|
||||
.default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
})
|
||||
.meta({
|
||||
description: "Icon shown in each analysis item badge.",
|
||||
}),
|
||||
items: z
|
||||
.array(InsightItemSchema)
|
||||
.min(1)
|
||||
.max(3)
|
||||
.default([
|
||||
{ title: "Title 1", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 2", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 2", description: "Ut enim ad minima veniam, quis." },
|
||||
])
|
||||
.meta({
|
||||
description: "Three analysis points shown in the left column,maximum 3 items",
|
||||
}),
|
||||
chartData: flexibleChartDataSchema.default({
|
||||
type: "bar",
|
||||
data: [
|
||||
{ name: "Mon", value: 120 },
|
||||
{ name: "Tue", value: 200 },
|
||||
{ name: "Wed", value: 150 },
|
||||
{ name: "Thu", value: 80 },
|
||||
{ name: "Fri", value: 70 },
|
||||
{ name: "Sat", value: 110 },
|
||||
{ name: "Sun", value: 130 },
|
||||
],
|
||||
|
||||
}),
|
||||
legendLabel: z.string().min(3).max(50).default("Traditional Workflow").meta({
|
||||
description: "Legend label shown below the chart.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const DataAnalysisBarSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, itemIcon, items, chartData, legendLabel } = data;
|
||||
const rows = chartData?.data ?? [];
|
||||
const chartType = chartData?.type ?? "bar";
|
||||
const series = chartData?.series ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#4d4ef3]"
|
||||
style={{ height: 185, backgroundColor: "var(--graph-0,#4d4ef3)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-[85px] pt-[44px]">
|
||||
<div className="space-y-[38px] pt-[8px]">
|
||||
{items?.map((item, index) => (
|
||||
<div key={`${item.title}-${index}`}>
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div
|
||||
className="flex h-[55px] w-[55px] items-center justify-center rounded-full bg-[#4d4ef3] text-white"
|
||||
style={{
|
||||
backgroundColor: "var(--graph-0,#4d4ef3)",
|
||||
color: "var(--primary-text,#ffffff)",
|
||||
}}
|
||||
>
|
||||
<RemoteSvgIcon
|
||||
url={itemIcon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[25px] w-[25px] object-contain"
|
||||
color="var(--primary-text, #ffffff)"
|
||||
title={itemIcon?.__icon_query__}
|
||||
/>
|
||||
{/* <img
|
||||
src={itemIcon?.__icon_url__}
|
||||
alt={itemIcon?.__icon_query__}
|
||||
className="h-[25px] w-[25px] object-contain"
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
/> */}
|
||||
</div>
|
||||
<h3
|
||||
className="text-[20px] font-medium tracking-[2.074px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p
|
||||
className="mt-[20px] text-[24px] leading-[26.667px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="ml-[44px] flex flex-col items-center">
|
||||
<div className="h-[346px] w-[560px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<FlexibleReportChart chartType={chartType} data={rows} series={series} colorFallback="#4d4ef3" />
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div
|
||||
className="mt-[12px] flex items-center gap-[10px] text-[24px] tracking-[-0.03em] text-[#4d4ef3]"
|
||||
style={{ color: "var(--graph-0,#4d4ef3)" }}
|
||||
>
|
||||
<span
|
||||
className="h-[12px] w-[12px] rounded-full bg-[#4d4ef3]"
|
||||
style={{ backgroundColor: "var(--graph-0,#4d4ef3)" }}
|
||||
/>
|
||||
<p>{legendLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataAnalysisBarSlide;
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
import * as z from "zod";
|
||||
|
||||
|
||||
const AnalysisItemSchema = z.object({
|
||||
title: z.string().max(12).meta({
|
||||
description: "Short item title displayed next to the icon.",
|
||||
}),
|
||||
description: z.string().max(30).meta({
|
||||
description: "Supporting sentence shown below the title.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "bullet-list-with-icon-title-description-slide";
|
||||
export const slideLayoutName = "Bullet List with Icon Title Description Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top and a two-column list of bullets points underneath. Each point contains a small circular icon badge, a short title on the same row, and a supporting description directly below.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(12).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
itemIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}).meta({
|
||||
description: "Icon shown in each analysis list badge.",
|
||||
}),
|
||||
items: z
|
||||
.array(AnalysisItemSchema)
|
||||
|
||||
.max(6)
|
||||
.default([
|
||||
{ title: "Title 1", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 3", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 2", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 4", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 2", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 5", description: "Ut enim ad minima veniam, quis." },
|
||||
])
|
||||
.meta({
|
||||
description: "List of points contains a title and description.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const DataAnalysisListSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, itemIcon, items } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[58px] pt-[52px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-[112px] gap-y-[52px] px-[82px] pt-[58px]">
|
||||
{items?.map((item, index) => (
|
||||
<div key={`${item.title}-${index}`}>
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div
|
||||
className="flex h-[55px] w-[55px] items-center justify-center rounded-full bg-[#157CFF] text-white"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#157CFF)",
|
||||
color: "var(--primary-text,#ffffff)",
|
||||
}}
|
||||
>
|
||||
<RemoteSvgIcon
|
||||
url={itemIcon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[26px] w-[26px] object-contain"
|
||||
color="var(--primary-text, #ffffff)"
|
||||
title={itemIcon?.__icon_query__}
|
||||
/>
|
||||
{/* <img
|
||||
src={itemIcon?.__icon_url__}
|
||||
alt={itemIcon?.__icon_query__}
|
||||
className="h-[26px] w-[26px] object-contain"
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
/> */}
|
||||
</div>
|
||||
<h3
|
||||
className="text-[20px] font-medium tracking-[2.074px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p
|
||||
className="mt-5 max-w-[420px] text-[24px] leading-[26.667px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataAnalysisListSlide;
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import * as z from "zod";
|
||||
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
import {
|
||||
DivergingDataPointSchema,
|
||||
FlexibleReportChart,
|
||||
MultiSeriesDataPointSchema,
|
||||
ScatterDataPointSchema,
|
||||
SimpleDataPointSchema,
|
||||
flexibleChartDataSchema,
|
||||
flexibleChartTypeSchema,
|
||||
type FlexibleChartData,
|
||||
} from "./flexibleReportChart";
|
||||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
|
||||
const SummaryCardSchema = z.object({
|
||||
value: z.string().min(1).max(8).meta({
|
||||
description: "Primary metric value shown in the compact summary card.",
|
||||
}),
|
||||
label: z.string().min(3).max(20).meta({
|
||||
description: "Short summary card label.",
|
||||
}),
|
||||
icon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
}).optional().meta({
|
||||
description: "Icon shown in each compact summary card.",
|
||||
}).default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const slideLayoutId = "data-analysis-dashboard-slide";
|
||||
export const slideLayoutName = "Data Analysis Dashboard Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A dashboard-style slide with a title, summary cards, and a responsive grid of chart panels (1–9). Each panel uses the same flexible chart types as other report slides; labels and margins are compact for small cells.";
|
||||
|
||||
const ChartItemSchema = z.object({
|
||||
|
||||
type: flexibleChartTypeSchema.default('bar'),
|
||||
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(),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(12).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
|
||||
summaryCards: z
|
||||
.array(SummaryCardSchema)
|
||||
.min(2)
|
||||
.max(4)
|
||||
.optional()
|
||||
.default([
|
||||
{
|
||||
value: "5", label: "Text 1", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
{
|
||||
value: "52", label: "Text 2", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
{
|
||||
value: "4", label: "Text 3", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
{
|
||||
value: "80%", label: "Text 4", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Four compact summary cards displayed above the dashboard panels.",
|
||||
}),
|
||||
charts: z.array(ChartItemSchema).min(1).max(6).default([
|
||||
{ type: 'bar', data: [{ name: 'Q1', value: 125000 }, { name: 'Q2', value: 158000 }, { name: 'Q3', value: 142000 }, { name: 'Q4', value: 189000 }] },
|
||||
{ type: 'donut', data: [{ name: 'North America', value: 35 }, { name: 'Europe', value: 28 }, { name: 'Asia Pacific', value: 25 }, { name: 'Others', value: 12 }] },
|
||||
{ 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 }] },
|
||||
{ type: 'bar', data: [{ name: 'Sales', value: 87 }, { name: 'Marketing', value: 72 }, { name: 'Engineering', value: 95 }, { name: 'Support', value: 68 }] },
|
||||
{ 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'] },
|
||||
{ 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'] },
|
||||
]),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function SummaryCard({
|
||||
value,
|
||||
label,
|
||||
iconUrl,
|
||||
iconAlt,
|
||||
}: {
|
||||
value: string;
|
||||
label: string;
|
||||
iconUrl?: string;
|
||||
iconAlt?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-[10px] items-center rounded-[14px] py-[9px]">
|
||||
<div
|
||||
className="flex h-[36px] w-[36px] border border-[#ECF5FE] shrink-0 items-center justify-center rounded-full bg-[#ECF5FE] "
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#ECF5FE)",
|
||||
borderColor: "var(--stroke,#ECF5FE)",
|
||||
}}
|
||||
>
|
||||
<RemoteSvgIcon
|
||||
url={iconUrl ?? ""}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[18px] w-[18px] object-contain"
|
||||
color="var(--primary-text, #000000)"
|
||||
title={iconAlt ?? ""}
|
||||
/>
|
||||
{/* <img
|
||||
src={iconUrl ?? ""}
|
||||
alt={iconAlt ?? ""}
|
||||
className="h-[18px] w-[18px] object-contain"
|
||||
|
||||
/> */}
|
||||
</div>
|
||||
<div className="">
|
||||
<p
|
||||
className="text-[18px] leading-none tracking-[-0.04em] text-[#4A4D53]"
|
||||
style={{ color: "var(--background-text,#4A4D53)" }}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
<p
|
||||
className="mt-[4px] text-[14px] leading-none text-[#6C6C6C]"
|
||||
style={{ color: "var(--background-text,#6C6C6C)" }}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const DataAnalysisDashboardSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, summaryCards, charts } = data;
|
||||
const halfChart = charts?.slice(0, Math.ceil(charts.length / 2));
|
||||
const otherHalfChart = charts?.slice(Math.ceil(charts.length / 2));
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative flex flex-col h-[720px] w-[1280px] overflow-hidden bg-[#F9F8F8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#F9F8F8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{summaryCards && summaryCards.length > 0 && <div className=" mx-[64px] grid bg-white gap-[16px] p-[13px] mt-[22px] rounded-[14px] "
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${summaryCards.length}, minmax(220px, 1fr))`,
|
||||
backgroundColor: "var(--card-color,#ffffff)",
|
||||
}}>
|
||||
{summaryCards?.map((card, index) => (
|
||||
<SummaryCard
|
||||
key={`${card.label}-${index}`}
|
||||
value={card.value}
|
||||
label={card.label}
|
||||
iconUrl={card.icon?.__icon_url__}
|
||||
iconAlt={card.icon?.__icon_query__}
|
||||
/>
|
||||
))}
|
||||
</div>}
|
||||
<div className="flex-1 flex flex-col pb-[30px]">
|
||||
|
||||
{halfChart && halfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1">
|
||||
<div
|
||||
className={`grid h-full bg-white p-[13px] rounded-[14px] min-h-0 gap-[10px] `}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${halfChart.length}, minmax(150px, 1fr))`,
|
||||
backgroundColor: "var(--card-color,#ffffff)",
|
||||
}}
|
||||
>
|
||||
{halfChart?.map((chart, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-[6px] flex flex-col overflow-hidden"
|
||||
|
||||
>
|
||||
|
||||
<div className="flex-1 " >
|
||||
<FlexibleReportChart density="compact" chartType={chart.type} data={chart.data} series={chart.series} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
{otherHalfChart && otherHalfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1">
|
||||
<div
|
||||
className={`grid h-full bg-white p-[13px] rounded-[14px] min-h-0 gap-[10px] `}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${otherHalfChart.length}, minmax(150px, 1fr))`,
|
||||
backgroundColor: "var(--card-color,#ffffff)",
|
||||
}}
|
||||
>
|
||||
{otherHalfChart?.map((chart, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-[6px] flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex-1 " >
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<FlexibleReportChart density="compact" chartType={chart.type} data={chart.data} series={chart.series} />
|
||||
</ResponsiveContainer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataAnalysisDashboardSlide;
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
const MemberSchema = z.object({
|
||||
subtext: z.string().min(2).max(40).meta({
|
||||
description: "Subtext for the image.",
|
||||
}),
|
||||
title: z.string().min(2).max(40).meta({
|
||||
description: "Title/name/subject for the image",
|
||||
}),
|
||||
image: z.object({
|
||||
__image_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg"),
|
||||
__image_prompt__: z.string().default("Professional portrait of a team member"),
|
||||
}).default({
|
||||
__image_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Professional portrait of a team member",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "horizontal-height-spanning-images-with-title-slide";
|
||||
export const slideLayoutName = "Horizontal Height Spanning Images with Title Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide of portrait cards placed side by side from edge to edge. Each card uses a full-height image background with a content overlay at the bottom containing a short subtext line and a larger title line.";
|
||||
|
||||
export const Schema = z.object({
|
||||
members: z
|
||||
.array(MemberSchema)
|
||||
.min(2)
|
||||
.max(5)
|
||||
.default([
|
||||
{
|
||||
subtext: "Title",
|
||||
title: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Professional portrait of a male team member",
|
||||
},
|
||||
},
|
||||
{
|
||||
subtext: "Title",
|
||||
title: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Professional portrait of a female team member",
|
||||
},
|
||||
},
|
||||
{
|
||||
subtext: "Title",
|
||||
title: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Professional portrait of a business manager",
|
||||
},
|
||||
},
|
||||
{
|
||||
subtext: "Title",
|
||||
title: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Professional portrait of a senior employee",
|
||||
},
|
||||
},
|
||||
{
|
||||
subtext: "Title",
|
||||
title: "Lanny LA",
|
||||
image: {
|
||||
__image_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Professional portrait of a young executive",
|
||||
},
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "List of team members shown as portrait cards. Each member contains a title, subtext, and image.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const TeamSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-white"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#ffffff)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full "
|
||||
style={{ gridTemplateColumns: `repeat(${data?.members?.length}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{data?.members?.map((member, index) => (
|
||||
<div
|
||||
key={`${member.title}-${index}`}
|
||||
className="relative h-full overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={member.image.__image_url__}
|
||||
alt={member.image.__image_prompt__}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 h-[240px] bg-gradient-to-t from-[#4d4ef3] via-[#4d4ef3]/55 to-transparent"
|
||||
style={{
|
||||
backgroundImage: "linear-gradient(to top, var(--graph-1,#4d4ef3), rgba(77, 78, 243, 0.55), transparent)",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute left-0 bottom-0 p-[33px] text-white" style={{ color: "var(--primary-text,#ffffff)" }}>
|
||||
<p
|
||||
className="text-[21px] tracking-[2.074px] font-medium text-white/90"
|
||||
style={{ color: "var(--primary-text,#ffffff)", opacity: 0.9 }}
|
||||
>
|
||||
{member.title}
|
||||
</p>
|
||||
<p className="mt-[14px] text-[28px] ">
|
||||
{member.subtext}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamSlide;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const Schema = z.object({
|
||||
titleFirstLine: z.string().min(1).max(12).default("Company's ").meta({
|
||||
description: "First half of title or heading",
|
||||
}),
|
||||
titleSecondLine: z.string().min(1).max(12).default("Annual Report").meta({
|
||||
description: "Second half of title or heading",
|
||||
}),
|
||||
name: z.string().min(1).max(10).optional().default("John Doe").meta({
|
||||
description: "Name of the presenter/individual/company/organization.",
|
||||
}),
|
||||
position: z.string().min(1).max(20).default("Company Name | Strategy, Content, growth").meta({
|
||||
description: "Position or role of the presenter or address of the company/organization.",
|
||||
}),
|
||||
})
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
export const slideLayoutId = "intro-slide";
|
||||
export const slideLayoutName = "Intro/Cover Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A cover/intro slide with a two-line title section, a divider directly beneath the title, and a presenter information block below the divider containing a name line and a supporting role or company line.";
|
||||
const IntroSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { titleFirstLine, titleSecondLine, name, position } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className='relative w-[1280px] h-[720px] aspect-video flex flex-col justify-center items-center bg-white'
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#ffffff)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<svg className="absolute top-0 left-10" xmlns="http://www.w3.org/2000/svg" width="116" height="251" viewBox="0 0 116 251" fill="none">
|
||||
<path d="M0 0H44.6667V228.333C44.6667 240.668 34.6677 250.667 22.3333 250.667C9.99898 250.667 0 240.668 0 228.333V0Z" fill="var(--primary-color,#147CFE)" />
|
||||
<path d="M71.3334 0H116V163C116 175.334 106.001 185.333 93.6667 185.333C81.3324 185.333 71.3334 175.334 71.3334 163V0Z" fill="var(--primary-color,#147CFE)" />
|
||||
</svg>
|
||||
<svg className="absolute bottom-0 right-10 rotate-180" xmlns="http://www.w3.org/2000/svg" width="116" height="251" viewBox="0 0 116 251" fill="none">
|
||||
<path d="M0 0H44.6667V228.333C44.6667 240.668 34.6677 250.667 22.3333 250.667C9.99898 250.667 0 240.668 0 228.333V0Z" fill="var(--primary-color,#147CFE)" />
|
||||
<path d="M71.3334 0H116V163C116 175.334 106.001 185.333 93.6667 185.333C81.3324 185.333 71.3334 175.334 71.3334 163V0Z" fill="var(--primary-color,#147CFE)" />
|
||||
</svg>
|
||||
|
||||
<div>
|
||||
<h1
|
||||
className="text-[#232223] text-[133px] italic text-center font-bold capitalize tracking-[-2.8px]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{titleFirstLine}
|
||||
</h1>
|
||||
<p
|
||||
className="text-[#232223] text-[93px] text-center font-medium capitalize tracking-[-2.8px]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{titleSecondLine}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="bg-[#CD7721] w-[67px] h-0.5 my-[78px]"
|
||||
style={{ backgroundColor: "var(--graph-2,#CD7721)" }}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<h4 className="text-[#232223] text-[40px] pb-4" style={{ color: "var(--background-text,#232223)" }}>{name}</h4>
|
||||
<p className="text-[19px] text-[#232223]" style={{ color: "var(--background-text,#232223)" }}>{position}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntroSlide
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import { Fragment } from "react/jsx-runtime";
|
||||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(1).max(6).meta({
|
||||
description: "Primary metric value shown in the card.",
|
||||
}),
|
||||
label: z.string().min(0).max(10).optional().meta({
|
||||
description: "Short metric label shown below the value.",
|
||||
}),
|
||||
description: z.string().min(0).max(20).optional().meta({
|
||||
description: "Supporting text shown below the label.",
|
||||
}),
|
||||
});
|
||||
|
||||
const StatColumnSchema = z.object({
|
||||
metrics: z.array(MetricSchema).min(2).max(2).meta({
|
||||
description: "Two stacked metrics shown in one tall card.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "metrics-slide";
|
||||
export const slideLayoutName = "Metrics Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title and explanatory text on the left, an optional bulleted list underneath the text, and metric cards on the right. Each metric card contains two stacked metric blocks.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(12).default("Introduction").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
body: z.string().max(250).default(
|
||||
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis"
|
||||
).meta({
|
||||
description: "Primary paragraph shown below the title.",
|
||||
}),
|
||||
bullets: z
|
||||
.array(z.string().max(100))
|
||||
.max(4)
|
||||
.optional()
|
||||
.default([
|
||||
"Ut enim ad minima veniam, quis nostrum",
|
||||
"Exercitationem ullam corporis suscipit",
|
||||
"Ut enim ad minima veniam, quis nostrum",
|
||||
"exercitationem ullam corporis suscipit",
|
||||
])
|
||||
.meta({
|
||||
description: "Optional bullet list shown after the description if required.",
|
||||
}),
|
||||
statColumns: z
|
||||
.array(StatColumnSchema)
|
||||
|
||||
.max(2)
|
||||
.default([
|
||||
{
|
||||
metrics: [
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
],
|
||||
},
|
||||
{
|
||||
metrics: [
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
],
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Two stat cards shown on the right side of the slide.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
type StatMetric = {
|
||||
value: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
function StatPill({
|
||||
metrics,
|
||||
|
||||
}: {
|
||||
metrics: StatMetric[];
|
||||
|
||||
}) {
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className=" h-[438px] w-[248px] overflow-hidden rounded-[127px] bg-[#157CFF] px-[28px] py-[74px] text-center text-white"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#157CFF)",
|
||||
color: "var(--primary-text,#ffffff)",
|
||||
}}
|
||||
>
|
||||
|
||||
{metrics.map((metric, index) => (
|
||||
<Fragment key={`${metric.value}-${metric.label}-${index}`}>
|
||||
<div
|
||||
key={`${metric.value}-${metric.label}-${index}`}
|
||||
className={``}
|
||||
>
|
||||
<p className="text-[55px] leading-[44.353px] tracking-[-1.09px]">
|
||||
{metric.value}
|
||||
</p>
|
||||
{metric.label && <p className="mt-[6px] text-[20px] leading-none">{metric.label}</p>}
|
||||
{metric.description && <p className=" text-[20px] mt-1 leading-[1.15] text-white/90" style={{ color: "var(--primary-text,#ffffff)", opacity: 0.9 }}>
|
||||
{metric.description}
|
||||
</p>}
|
||||
</div>
|
||||
{index === 0 && <div className="py-[22px]">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="181" height="1" viewBox="0 0 181 1" fill="none">
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M0 0.487305H180.122"
|
||||
stroke="var(--primary-text,#ffffff)"
|
||||
strokeWidth="0.974913"
|
||||
strokeDasharray="3.9 1.95"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const IntroductionStatsSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
const { title, body, bullets, statColumns } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-[96px] pt-[38px]">
|
||||
<div className="">
|
||||
<p className="max-w-[400px] text-[24px] leading-[26.667px] text-[#232223]" style={{ color: "var(--background-text,#232223)" }}>
|
||||
{body}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="mt-[34px] list-disc pl-[28px] text-[24px] leading-[26.667px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{bullets?.map((bullet, index) => (
|
||||
<div key={`${bullet}-${index}`} className="mt-[8px] flex items-center gap-2">
|
||||
<div className="w-[8px] h-[8px] rounded-full bg-[#232223]" style={{ backgroundColor: "var(--background-text,#232223)" }} /> <p className="text-[24px] leading-[26.667px] text-[#232223]" style={{ color: "var(--background-text,#232223)" }}>
|
||||
{bullet}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-[48px] flex gap-[34px]">
|
||||
{statColumns?.map((column, index) => (
|
||||
<StatPill key={`intro-stat-column-${index}`} metrics={column.metrics} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroductionStatsSlide;
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const MilestoneItemSchema = z.object({
|
||||
bulletNumber: z.string().min(2).max(4).meta({
|
||||
description: "Short milestone number such as 01 or 05.",
|
||||
}),
|
||||
heading: z.string().min(3).max(30).meta({
|
||||
description: "Heading displayed below the milestone marker.",
|
||||
}),
|
||||
description: z.string().min(10).max(80).meta({
|
||||
description: "Supporting milestone description shown under the heading.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "milestone-slide";
|
||||
export const slideLayoutName = "Milestone Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top and a single horizontal milestone sequence below it. The sequence contains five circular markers aligned in one row, and each marker has a heading and description placed directly underneath. The activeIndex field controls which marker is emphasized while the remaining markers stay in the default state.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(12).default("Milestone").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
activeIndex: z.number().int().min(0).max(4).default(4).meta({
|
||||
description: "Zero-based index of the highlighted milestone.",
|
||||
}),
|
||||
items: z
|
||||
.array(MilestoneItemSchema)
|
||||
.min(1)
|
||||
.max(5)
|
||||
.default([
|
||||
{
|
||||
bulletNumber: "01",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
bulletNumber: "02",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
bulletNumber: "03",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
bulletNumber: "04",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
bulletNumber: "05",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Bullet with title and description.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const MilestoneSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, activeIndex, items } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#F9F8F8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#F9F8F8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[70px] pt-[56px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-[52px] pl-[74px] pr-[63px]">
|
||||
<div className="flex items-center justify-center">
|
||||
{items?.map((item, index) => {
|
||||
const isActive = index === activeIndex;
|
||||
|
||||
return (
|
||||
<div className=" " key={`${item.bulletNumber}-${index}`}>
|
||||
|
||||
<div
|
||||
className={`relative flex h-[270px] w-[270px] items-center justify-center rounded-full ${isActive
|
||||
? "z-10 bg-[#157CFF] text-white"
|
||||
: "border border-[#157CFF] bg-white text-[#157CFE]"
|
||||
} ${index > 0 ? "ml-[-45px]" : ""} `}
|
||||
style={{
|
||||
backgroundColor: isActive ? "var(--primary-color,#157CFF)" : "var(--card-color,#ffffff)",
|
||||
borderColor: "var(--primary-color,#157CFF)",
|
||||
color: isActive ? "var(--primary-text,#ffffff)" : "var(--primary-color,#157CFE)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`${isActive ? "text-white" : "text-[#157CFF]"} text-[42px] font-medium tracking-[0.18em]`}
|
||||
style={{ color: isActive ? "var(--primary-text,#ffffff)" : "var(--primary-color,#157CFF)" }}
|
||||
>
|
||||
{item.bulletNumber}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div
|
||||
key={`${item.heading}-${index}`}
|
||||
className={`text-center mt-[20px] text-[#232223] ${index > 0 ? 'pr-[33px]' : ''} ${index === 0 ? 'px-[33px]' : ''}`}
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
<h3 className="text-[20px] text-[#232223] font-medium tracking-[2.074px]" style={{ color: "var(--background-text,#232223)" }}>
|
||||
{item.heading}
|
||||
</h3>
|
||||
<p className="mt-[6px] text-[24px] leading-[26.667px] text-[#232223]" style={{ color: "var(--background-text,#232223)" }}>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MilestoneSlide;
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
"use client";
|
||||
|
||||
import * as z from "zod";
|
||||
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(1).max(12).meta({
|
||||
description: "Primary metric value shown in the stat card.",
|
||||
}),
|
||||
label: z.string().min(3).max(24).optional().meta({
|
||||
description: "Metric label shown below the value.",
|
||||
}),
|
||||
description: z.string().min(6).max(36).optional().meta({
|
||||
description: "Supporting description shown below the label.",
|
||||
}),
|
||||
});
|
||||
|
||||
const StatColumnSchema = z.object({
|
||||
metrics: z.array(MetricSchema).min(0).max(2).meta({
|
||||
description: "Two stacked metrics shown in one stat card.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "title-chart-metrics-cards-slide";
|
||||
export const slideLayoutName = "Title Chart with Metrics Cards Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top, chart in the left content area, and optional metric cards arranged side by side on the right.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(80).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
seriesALabel: z.string().min(3).max(20).default("Category A").meta({
|
||||
description: "Legend label for the first line series.",
|
||||
}),
|
||||
seriesBLabel: z.string().min(3).max(20).default("Category B").meta({
|
||||
description: "Legend label for the second line series.",
|
||||
}),
|
||||
chartData: flexibleChartDataSchema.default({
|
||||
type: "line-dual",
|
||||
data: [
|
||||
{ label: "label", valueA: 24, valueB: 40 },
|
||||
{ label: "label", valueA: 55, valueB: 72 },
|
||||
{ label: "label", valueA: 50, valueB: 98 },
|
||||
{ label: "label", valueA: 97, valueB: 86 },
|
||||
{ label: "label", valueA: 70, valueB: 52 },
|
||||
{ label: "label", valueA: 42, valueB: 78 },
|
||||
{ label: "label", valueA: 63, valueB: 51 },
|
||||
],
|
||||
}),
|
||||
legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({
|
||||
description: "Legend label shown below the chart.",
|
||||
}),
|
||||
statColumns: z
|
||||
.array(StatColumnSchema)
|
||||
.min(1)
|
||||
.max(2)
|
||||
.default([
|
||||
{
|
||||
metrics: [
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
],
|
||||
},
|
||||
{
|
||||
metrics: [
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
],
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Stat/metric cards shown on the right side of the slide.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
type StatMetric = {
|
||||
value: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
function StatPill({ metrics }: { metrics: StatMetric[] }) {
|
||||
return (
|
||||
<div
|
||||
className="h-[438px] w-[248px] overflow-hidden rounded-[127px] bg-[#157CFF] px-[28px] py-[74px] text-center text-white"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#157CFF)",
|
||||
color: "var(--primary-text,#ffffff)",
|
||||
}}
|
||||
>
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={`${metric.value}-${metric.label}-${index}`} className="flex flex-col items-center justify-between gap-2">
|
||||
<div key={`${metric.value}-${metric.label}-${index}`} className={``}>
|
||||
<p className="text-[55px] leading-[44.353px] tracking-[-1.09px]">{metric.value}</p>
|
||||
{metric.label && <p className="mt-[6px] text-[20px] leading-none">{metric.label}</p>}
|
||||
{metric.description && <p className="text-[20px] mt-1 leading-[1.15] text-white/90" style={{ color: "var(--primary-text,#ffffff)", opacity: 0.9 }}>
|
||||
{metric.description}
|
||||
</p>}
|
||||
</div>
|
||||
{index === 0 && (
|
||||
<div className="py-[22px]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="181" height="1" viewBox="0 0 181 1" fill="none">
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M0 0.487305H180.122"
|
||||
stroke="var(--primary-text,#ffffff)"
|
||||
strokeWidth="0.974913"
|
||||
strokeDasharray="3.9 1.95"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DataAnalysisLineStatsSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, seriesALabel, seriesBLabel, chartData, statColumns, legendLabel } = data;
|
||||
const rows = chartData?.data ?? [];
|
||||
const chartType = chartData?.type ?? "line-dual";
|
||||
const series = chartData?.series ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-[74px] pt-[40px]">
|
||||
<div className="w-[474px]">
|
||||
{chartType === "line-dual" && <div
|
||||
className="flex justify-center gap-[26px] text-[14px] text-[#353538]"
|
||||
style={{ color: "var(--background-text,#353538)" }}
|
||||
>
|
||||
<span className="flex items-center gap-[8px]">
|
||||
<span className="h-[2px] w-[20px] bg-[#9fb6ff]" style={{ backgroundColor: "var(--graph-0,#9fb6ff)" }} />
|
||||
{seriesALabel}
|
||||
</span>
|
||||
<span className="flex items-center gap-[8px]">
|
||||
<span className="h-[2px] w-[20px] bg-[#4d4ef3]" style={{ backgroundColor: "var(--graph-1,#4d4ef3)" }} />
|
||||
{seriesBLabel}
|
||||
</span>
|
||||
</div>}
|
||||
|
||||
<div className="mt-[12px] h-[356px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<FlexibleReportChart
|
||||
chartType={chartType}
|
||||
data={rows}
|
||||
series={series}
|
||||
colorFallback="#157CFF"
|
||||
density="default"
|
||||
dualLineColors={["var(--graph-0,#9fb6ff)", "var(--graph-1,#4d4ef3)"]}
|
||||
/>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-[12px] flex items-center gap-[10px] text-center justify-center text-[24px] tracking-[-0.03em] text-[#157CFF]"
|
||||
style={{ color: "var(--primary-color,#157CFF)" }}
|
||||
>
|
||||
<span
|
||||
className="h-[12px] w-[12px] rounded-full bg-[#157CFF]"
|
||||
style={{ backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
<p>{data.legendLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-[42px] flex gap-[30px]">
|
||||
{statColumns?.map((column, index) => (
|
||||
<StatPill key={`line-stat-column-${index}`} metrics={column.metrics} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataAnalysisLineStatsSlide;
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
"use client";
|
||||
|
||||
import * as z from "zod";
|
||||
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart";
|
||||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
|
||||
export const slideLayoutId = "title-description-chart-slide";
|
||||
export const slideLayoutName = "Title Description Chart Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top, description text in left and chart in the right.";
|
||||
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(80).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
insightIcon: z
|
||||
.object({
|
||||
__icon_url__: z
|
||||
.string()
|
||||
.default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
})
|
||||
.default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
})
|
||||
.meta({
|
||||
description: "Icon shown in the featured insight badge.",
|
||||
}),
|
||||
insightBody: z
|
||||
.string()
|
||||
.min(30)
|
||||
.max(320)
|
||||
.default(
|
||||
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis"
|
||||
)
|
||||
.meta({
|
||||
description: "Description text shown in the left content area.",
|
||||
}),
|
||||
chartData: flexibleChartDataSchema.default({
|
||||
type: "line-dual",
|
||||
data: [
|
||||
{ name: "Q1", value: 45 },
|
||||
{ name: "Q2", value: 72 },
|
||||
{ name: "Q3", value: 58 },
|
||||
{ name: "Q4", value: 89 },
|
||||
],
|
||||
|
||||
}),
|
||||
legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({
|
||||
description: "Legend label shown below the chart.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const DataAnalysisInsightBarSlide = ({
|
||||
data,
|
||||
}: {
|
||||
data: Partial<SchemaType>;
|
||||
}) => {
|
||||
const chartData = data?.chartData?.data ?? [];
|
||||
const chartType = data?.chartData?.type ?? "bar";
|
||||
const series = data?.chartData?.series ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{data.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-[74px] gap-10 pt-[96px]">
|
||||
<div className=" pt-[24px] w-1/2">
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div
|
||||
className="flex h-[55px] w-[55px] items-center justify-center rounded-full bg-[#157CFF] text-white"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#157CFF)",
|
||||
color: "var(--primary-text,#ffffff)",
|
||||
}}
|
||||
>
|
||||
|
||||
<RemoteSvgIcon
|
||||
url={data.insightIcon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[25px] w-[25px] object-contain"
|
||||
color="var(--primary-text, #ffffff)"
|
||||
title={data.insightIcon?.__icon_query__}
|
||||
/>
|
||||
{/* <img
|
||||
src={data.insightIcon?.__icon_url__}
|
||||
alt={data.insightIcon?.__icon_query__}
|
||||
className="h-[25px] w-[25px] object-contain"
|
||||
style={{ filter: "invert(1)" }}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="mt-[20px] text-[24px] leading-[26.667px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{data.insightBody}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="ml-[28px] flex w-1/2 flex-col items-center">
|
||||
<ResponsiveContainer height={400} width="100%">
|
||||
<FlexibleReportChart chartType={chartType} data={chartData} series={series} colorFallback="#157CFF" />
|
||||
</ResponsiveContainer>
|
||||
<div
|
||||
className="mt-[12px] flex items-center gap-[10px] text-[24px] tracking-[-0.03em] text-[#157CFF]"
|
||||
style={{ color: "var(--primary-color,#157CFF)" }}
|
||||
>
|
||||
<span
|
||||
className="h-[12px] w-[12px] rounded-full bg-[#157CFF]"
|
||||
style={{ backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
<p>{data.legendLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataAnalysisInsightBarSlide;
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-description-image-slide";
|
||||
export const slideLayoutName = "Title Description Image Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top-left, a paragraph block beneath the title, a large supporting image anchored on the right side of the slide.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(12).default("Introduction").meta({
|
||||
description: "Title/heading of the slide",
|
||||
}),
|
||||
body: z.string().max(250).default(
|
||||
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis"
|
||||
).meta({
|
||||
description: "Primary paragraph shown under the title.",
|
||||
}),
|
||||
bullets: z
|
||||
.array(z.string().max(100))
|
||||
.min(0)
|
||||
.max(4)
|
||||
.optional()
|
||||
.default([
|
||||
"Ut enim ad minima veniam, quis nostrum",
|
||||
"Exercitationem ullam corporis suscipit",
|
||||
"Ut enim ad minima veniam, quis nostrum",
|
||||
"exercitationem ullam corporis suscipit",
|
||||
])
|
||||
.meta({
|
||||
description: "Optional bullet list shown after the description if required.",
|
||||
}),
|
||||
featureImage: z.object({
|
||||
__image_url__: z.string(),
|
||||
__image_prompt__: z.string(),
|
||||
}).optional().meta({
|
||||
description: "Large image shown on the right side of the slide or optional.",
|
||||
}).default({
|
||||
__image_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Thoughtful woman portrait on a neutral backdrop",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const IntroductionImageSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, body, bullets, featureImage } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[74px] pt-[76px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-28 pl-[96px] pt-[30px]">
|
||||
<div className="flex flex-col">
|
||||
<p className=" text-[24px] leading-[26.667px] text-[#232223]" style={{ color: "var(--background-text,#232223)" }}>
|
||||
{body}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="mt-8 list-disc pl-[28px] text-[24px] leading-[26.667px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{bullets?.map((bullet, index) => (
|
||||
<div key={`${bullet}-${index}`} className="mt-[8px] flex items-center gap-2">
|
||||
<div className="w-[8px] h-[8px] rounded-full bg-[#232223]" style={{ backgroundColor: "var(--background-text,#232223)" }} /> <p className="text-[24px] leading-[26.667px] text-[#232223]" style={{ color: "var(--background-text,#232223)" }}>
|
||||
{bullet}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-end justify-end">
|
||||
<div
|
||||
className="h-[397px] w-[582px] overflow-hidden rounded-l-[106px] bg-[#157CFF]"
|
||||
style={{ backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
>
|
||||
<img
|
||||
src={featureImage?.__image_url__}
|
||||
alt={featureImage?.__image_prompt__}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroductionImageSlide;
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "title-image-bullet-cards-slide";
|
||||
export const slideLayoutName = "Title Image Bullet Cards Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with optional image on the left and cards with bullet on the right.";
|
||||
|
||||
const CardSchema = z.object({
|
||||
bulletNumber: z.string().min(2).max(4).meta({
|
||||
description: "Short description step number starting from 01...",
|
||||
}),
|
||||
description: z.string().min(10).max(80).meta({
|
||||
description: "Bullet point description shown inside the card.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(30).default("Solution").meta({
|
||||
description: "Slide heading shown in the top-left corner.",
|
||||
}),
|
||||
showImage: z.boolean().default(true).meta({
|
||||
description: "Whether the image should be shown.",
|
||||
}),
|
||||
featureImage: z.object({
|
||||
__image_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg"),
|
||||
__image_prompt__: z.string().default("Thinking woman portrait on a neutral background"),
|
||||
}).default({
|
||||
__image_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Thinking woman portrait on a neutral background",
|
||||
}).meta({
|
||||
description: "Optional image used on the left side of the slide.",
|
||||
}),
|
||||
cards: z
|
||||
.array(CardSchema)
|
||||
.min(1)
|
||||
.max(3)
|
||||
.default([
|
||||
{
|
||||
bulletNumber: "01",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
bulletNumber: "02",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
bulletNumber: "03",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description:
|
||||
"Three solution cards. When the image is enabled, only the first two cards are displayed.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
type SolutionSlideProps = {
|
||||
data: Partial<SchemaType>;
|
||||
};
|
||||
|
||||
function SolutionCard({
|
||||
stepNumber,
|
||||
description,
|
||||
}: {
|
||||
stepNumber: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex py-[60px] px-10 w-[312px] flex-col items-center justify-center rounded-[160px] bg-[#4d4ef3] text-center text-white"
|
||||
style={{
|
||||
backgroundColor: "var(--graph-1,#4d4ef3)",
|
||||
color: "var(--primary-text,#ffffff)",
|
||||
}}
|
||||
>
|
||||
<p className="text-[42px] font-medium tracking-[8.709px]">{stepNumber}</p>
|
||||
<p className="mt-[27px] text-[27px] min-h-[200px] ">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SolutionSlide = ({ data }: SolutionSlideProps) => {
|
||||
const { title, showImage, featureImage, cards } = data;
|
||||
const visibleCards = showImage ? cards?.slice(0, 2) : cards;
|
||||
|
||||
|
||||
return (
|
||||
<> <link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#F9F8F8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#F9F8F8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 h-full py-[58px]">
|
||||
{title && (
|
||||
<h2
|
||||
className="text-[80px] px-[64px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div className="mt-[70px]">
|
||||
{showImage ? (
|
||||
<div className="flex items-start gap-[40px]">
|
||||
{featureImage?.__image_url__ && (
|
||||
<div
|
||||
className="h-[396px] w-[534px] shrink-0 overflow-hidden rounded-r-[90px] bg-[#ece8dd]"
|
||||
style={{ backgroundColor: "var(--card-color,#ece8dd)" }}
|
||||
>
|
||||
<img
|
||||
src={featureImage?.__image_url__}
|
||||
alt={featureImage?.__image_prompt__}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-[40px]">
|
||||
{visibleCards?.map((card, index) => (
|
||||
<SolutionCard
|
||||
key={`${card.bulletNumber}-${index}`}
|
||||
stepNumber={card.bulletNumber}
|
||||
description={card.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center gap-[44px] pt-[6px]">
|
||||
{visibleCards?.map((card, index) => (
|
||||
<SolutionCard
|
||||
key={`${card.bulletNumber}-${index}`}
|
||||
stepNumber={card.bulletNumber}
|
||||
description={card.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SolutionSlide;
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { Fragment } from "react/jsx-runtime";
|
||||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(1).max(6).meta({
|
||||
description: "Primary metric value shown in the pill.",
|
||||
}),
|
||||
label: z.string().max(20).optional().meta({
|
||||
description: "Short label shown below the metric value.",
|
||||
}),
|
||||
description: z.string().min(6).max(50).optional().meta({
|
||||
description: "Supporting metric description shown below the label.",
|
||||
}),
|
||||
});
|
||||
|
||||
const MetricColumnSchema = z.object({
|
||||
metrics: z.array(MetricSchema).max(2).meta({
|
||||
description: "One or two metrics shown in a single snapshot pill. Maximum two metrics.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "title-metrics-slide";
|
||||
export const slideLayoutName = "Title Metrics Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top and tall metric cards arranged horizontally below it. Each card can contain one or two stacked metric blocks, and each block includes a main value, a label, and a supporting description.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(80).default("Performance Snapshot").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
columns: z
|
||||
.array(MetricColumnSchema)
|
||||
.min(1)
|
||||
.max(4)
|
||||
.default([
|
||||
{
|
||||
metrics: [
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
],
|
||||
},
|
||||
{
|
||||
metrics: [
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
],
|
||||
},
|
||||
{
|
||||
metrics: [
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
{ value: "25K", label: "Students", description: "Ut enim ad minima" },
|
||||
],
|
||||
}
|
||||
])
|
||||
.meta({
|
||||
description: "Three metric columns displayed beneath the title.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
type StatMetric = {
|
||||
value: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
function StatPill({
|
||||
metrics,
|
||||
|
||||
}: {
|
||||
metrics: StatMetric[];
|
||||
|
||||
}) {
|
||||
return (
|
||||
|
||||
<div
|
||||
className=" h-[438px] w-[248px] overflow-hidden rounded-[127px] bg-[#157CFF] px-[28px] py-[74px] text-center text-white"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color,#157CFF)",
|
||||
color: "var(--primary-text,#ffffff)",
|
||||
}}
|
||||
>
|
||||
|
||||
{metrics.map((metric, index) => (
|
||||
<Fragment key={`${metric.value}-${metric.label}-${index}`}>
|
||||
<div
|
||||
key={`${metric.value}-${metric.label}-${index}`}
|
||||
className={``}
|
||||
>
|
||||
<p className="text-[55px] leading-[44.353px] tracking-[-1.09px]">
|
||||
{metric.value}
|
||||
</p>
|
||||
{metric.label && <p className="mt-[10px] text-[20px] font-medium leading-none">{metric.label}</p>}
|
||||
{metric.description && <p className=" text-[20px] mt-1 leading-[1.15] text-white/90" style={{ color: "var(--primary-text,#ffffff)", opacity: 0.9 }}>
|
||||
{metric.description}
|
||||
</p>}
|
||||
</div>
|
||||
{index === 0 && <div className="py-[22px]">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="181" height="1" viewBox="0 0 181 1" fill="none">
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M0 0.487305H180.122"
|
||||
stroke="var(--primary-text,#ffffff)"
|
||||
strokeWidth="0.974913"
|
||||
strokeDasharray="3.9 1.95"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PerformanceSnapshotSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, columns } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-[44px] flex justify-center gap-[33px]">
|
||||
{columns?.map((column, index) => (
|
||||
<StatPill
|
||||
key={`snapshot-column-${index}`}
|
||||
metrics={column.metrics}
|
||||
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformanceSnapshotSlide;
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
|
||||
|
||||
const ServiceItemSchema = z.object({
|
||||
icon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}).meta({
|
||||
description: "Icon used for service circles.",
|
||||
}),
|
||||
heading: z.string().min(3).max(30).meta({
|
||||
description: "Heading shown below the service icon.",
|
||||
}),
|
||||
description: z.string().min(20).max(60).meta({
|
||||
description: "Supporting description below the service heading.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "title-workflow-with-title-description-slide";
|
||||
export const slideLayoutName = "Title Workflow with Title Description Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title and a horizontal flow. Each step contains a circular icon area, a heading, and a description placed underneath. Directional connectors between the circles indicate sequence, and the activeIndex field determines which step is emphasized.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(50).default("Services").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
|
||||
activeIndex: z.number().int().min(0).max(2).default(2).meta({
|
||||
description: "Zero-based index of the emphasized service step.",
|
||||
}),
|
||||
items: z
|
||||
.array(ServiceItemSchema)
|
||||
.min(1)
|
||||
.max(5)
|
||||
.default([
|
||||
{
|
||||
icon: {
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
},
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "upload icon",
|
||||
},
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
},
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
},
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
},
|
||||
|
||||
])
|
||||
.meta({
|
||||
description: "Three sequential service items displayed on the slide.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
function ServiceGlyph({
|
||||
iconUrl,
|
||||
iconAlt,
|
||||
isActive,
|
||||
}: {
|
||||
iconUrl: string;
|
||||
iconAlt: string;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
return (
|
||||
<img
|
||||
src={iconUrl}
|
||||
alt={iconAlt}
|
||||
className="h-[62px] w-[62px] object-contain"
|
||||
style={{ filter: isActive ? "brightness(0) invert(1)" : "none" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ServicesSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
const { title, activeIndex, items } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet" />
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-color,#f9f8f8)",
|
||||
fontFamily: "var(--body-font-family,'Source Sans 3')",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185, backgroundColor: "var(--primary-color,#157CFF)" }}
|
||||
/>
|
||||
|
||||
<div className="px-[70px] pt-[56px]">
|
||||
<h2
|
||||
className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]"
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-[56px] flex items-start justify-center px-[82px]">
|
||||
{items?.map((item, index) => {
|
||||
const isActive = index === activeIndex;
|
||||
|
||||
return (
|
||||
<Fragment key={`${item.heading}-${index}`}>
|
||||
<div key={`${item.heading}-${index}`} className="flex w-[302px] flex-col items-center text-center">
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full ${isActive
|
||||
? "bg-[#157CFF] text-white"
|
||||
: "border border-[#157CFF] bg-transparent text-[#157CFE]"
|
||||
}`}
|
||||
style={{
|
||||
width: items?.length === 3 ? '266px' : items?.length === 4 ? '192px' : items?.length === 5 ? '157px' : '266px',
|
||||
height: items?.length === 3 ? '266px' : items?.length === 4 ? '192px' : items?.length === 5 ? '157px' : '270px',
|
||||
backgroundColor: isActive ? "var(--primary-color,#157CFF)" : "transparent",
|
||||
borderColor: "var(--primary-color,#157CFF)",
|
||||
color: isActive ? "var(--primary-text,#ffffff)" : "var(--primary-color,#157CFE)",
|
||||
}}
|
||||
>
|
||||
{/* <ServiceGlyph
|
||||
iconUrl={
|
||||
item.icon?.__icon_url__
|
||||
}
|
||||
iconAlt={
|
||||
item.icon?.__icon_query__
|
||||
}
|
||||
isActive={isActive}
|
||||
/> */}
|
||||
<RemoteSvgIcon
|
||||
url={item.icon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[62px] w-[62px] object-contain"
|
||||
color={isActive ? "var(--primary-text, #ffffff)" : "var(--primary-color,#157CFE)"}
|
||||
title={item.icon?.__icon_query__}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3
|
||||
className="mt-[18px] text-[26px] font-medium tracking-[0.08em] text-[#232223]"
|
||||
style={{
|
||||
fontSize: items?.length === 3 ? '20px' : items?.length === 4 ? '16px' : items?.length === 5 ? '12px' : '20px',
|
||||
color: "var(--background-text,#232223)",
|
||||
}}
|
||||
>
|
||||
{item.heading}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-[12px] max-w-[290px] text-[18px] leading-[1.08] tracking-[-0.04em] text-[#353538]"
|
||||
style={{
|
||||
fontSize: items?.length === 3 ? '24px' : items?.length === 4 ? '17px' : items?.length === 5 ? '14px' : '24px',
|
||||
color: "var(--background-text,#353538)",
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{index < items?.length - 1 && (
|
||||
<div className=" flex items-center px-1 "
|
||||
style={{
|
||||
marginTop: items?.length === 3 ? '135px' : items?.length === 4 ? '93px' : items?.length === 5 ? '70px' : '135px',
|
||||
}}
|
||||
>
|
||||
{/* <div className="h-px mr-[-10px] w-full bg-[#4d4ef3]"
|
||||
style={{
|
||||
width: items?.length === 3 ? '117px' : items?.length === 4 ? '84px' : items?.length === 5 ? '60px' : '112px',
|
||||
}}
|
||||
/>
|
||||
<svg
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-[18px] w-[18px] text-[#4d4ef3]"
|
||||
>
|
||||
<path
|
||||
d="M5 4L12 9L5 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg> */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
style={{
|
||||
width: items?.length === 3 ? '117px' : items?.length === 4 ? '84px' : items?.length === 5 ? '60px' : '112px',
|
||||
}}
|
||||
height="15" viewBox="0 0 119 15" fill="none">
|
||||
<path
|
||||
d="M1 6.36401H0V8.36401H1V7.36401V6.36401ZM118.707 8.07112C119.098 7.6806 119.098 7.04743 118.707 6.65691L112.343 0.292946C111.953 -0.0975785 111.319 -0.0975785 110.929 0.292946C110.538 0.68347 110.538 1.31664 110.929 1.70716L116.586 7.36401L110.929 13.0209C110.538 13.4114 110.538 14.0446 110.929 14.4351C111.319 14.8256 111.953 14.8256 112.343 14.4351L118.707 8.07112ZM1 7.36401V8.36401H118V7.36401V6.36401H1V7.36401Z"
|
||||
fill="var(--primary-color,#157CFE)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesSlide;
|
||||
|
|
@ -0,0 +1,839 @@
|
|||
"use client";
|
||||
|
||||
import { useId } from "react";
|
||||
import * as z from "zod";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Line,
|
||||
LineChart,
|
||||
Pie,
|
||||
PieChart,
|
||||
ReferenceLine,
|
||||
Scatter,
|
||||
ScatterChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
LabelList,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
|
||||
export const simpleDataSchema = z.object({
|
||||
name: z.string().meta({ description: "Data point name" }),
|
||||
value: z.number().meta({ description: "Data point value" }),
|
||||
});
|
||||
|
||||
export 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)",
|
||||
}),
|
||||
});
|
||||
|
||||
export 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" }),
|
||||
});
|
||||
|
||||
export const scatterDataSchema = z.object({
|
||||
x: z.number().meta({ description: "X coordinate" }),
|
||||
y: z.number().meta({ description: "Y coordinate" }),
|
||||
});
|
||||
|
||||
/** Two series over categorical labels (line stats slide). */
|
||||
export const dualLinePointSchema = z.object({
|
||||
label: z.string().meta({ description: "Chart axis label" }),
|
||||
valueA: z.number().meta({ description: "First series value" }),
|
||||
valueB: z.number().meta({ description: "Second series value" }),
|
||||
});
|
||||
export const SimpleDataPointSchema = z.object({
|
||||
name: z.string(),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
export const MultiSeriesDataPointSchema = z.object({
|
||||
name: z.string(),
|
||||
values: z.any(),
|
||||
});
|
||||
|
||||
export const DivergingDataPointSchema = z.object({
|
||||
name: z.string(),
|
||||
positive: z.number(),
|
||||
negative: z.number(),
|
||||
});
|
||||
|
||||
export const ScatterDataPointSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
export const flexibleChartTypeSchema = z.enum([
|
||||
"bar",
|
||||
"bar-horizontal",
|
||||
"bar-grouped-vertical",
|
||||
"bar-grouped-horizontal",
|
||||
"bar-stacked-vertical",
|
||||
"bar-stacked-horizontal",
|
||||
"bar-clustered",
|
||||
"bar-diverging",
|
||||
"line",
|
||||
"line-dual",
|
||||
"area",
|
||||
"area-stacked",
|
||||
"pie",
|
||||
"donut",
|
||||
"scatter",
|
||||
]);
|
||||
|
||||
export const flexibleChartDataSchema = z.object({
|
||||
type: flexibleChartTypeSchema.default("bar"),
|
||||
data: z.union([
|
||||
z.array(simpleDataSchema),
|
||||
z.array(multiSeriesDataSchema),
|
||||
z.array(divergingDataSchema),
|
||||
z.array(scatterDataSchema),
|
||||
z.array(dualLinePointSchema),
|
||||
]),
|
||||
series: z.array(z.string()).optional().meta({ description: "Series names for grouped/stacked charts" }),
|
||||
divergingLabels: z.tuple([z.string(), z.string()]).optional(),
|
||||
});
|
||||
|
||||
export type FlexibleChartData = z.infer<typeof flexibleChartDataSchema>;
|
||||
|
||||
const formatComma = (value: number) => value.toLocaleString("en-US");
|
||||
|
||||
export function deriveSeriesNames(data: any[], explicit: string[]): string[] {
|
||||
if (explicit.length > 0) return explicit;
|
||||
const first = data[0];
|
||||
if (!first) return [];
|
||||
if (first.values != null && typeof first.values === "object" && !Array.isArray(first.values)) {
|
||||
return Object.keys(first.values);
|
||||
}
|
||||
if (typeof first.value === "number") return ["value"];
|
||||
return [];
|
||||
}
|
||||
|
||||
export function transformMultiSeriesData(data: any[], series: string[]) {
|
||||
return data.map((item) => {
|
||||
const result: Record<string, any> = { name: item.name };
|
||||
series.forEach((s) => {
|
||||
if (item.values != null && typeof item.values === "object" && s in item.values) {
|
||||
result[s] = Number(item.values[s]) || 0;
|
||||
} else if (s === "value" && typeof item.value === "number") {
|
||||
result[s] = item.value;
|
||||
} else if (typeof item[s] === "number") {
|
||||
result[s] = item[s];
|
||||
} else {
|
||||
result[s] = Number(item.values?.[s]) || 0;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
export function transformDivergingData(data: any[]) {
|
||||
return data.map((item) => {
|
||||
if (typeof item.positive === "number" && typeof item.negative === "number") {
|
||||
return {
|
||||
name: item.name,
|
||||
positive: item.positive,
|
||||
negative: -Math.abs(item.negative),
|
||||
};
|
||||
}
|
||||
const v = Number(item.value);
|
||||
if (!Number.isNaN(v)) {
|
||||
return {
|
||||
name: item.name,
|
||||
positive: Math.max(0, v),
|
||||
negative: v < 0 ? v : 0,
|
||||
};
|
||||
}
|
||||
return { name: item.name, positive: 0, negative: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeScatterPoints(data: any[]) {
|
||||
return data.map((item, i) => {
|
||||
if (typeof item.x === "number" && typeof item.y === "number") {
|
||||
return { ...item, x: item.x, y: item.y };
|
||||
}
|
||||
if (typeof item.value === "number") {
|
||||
return { ...item, x: typeof item.x === "number" ? item.x : i + 1, y: item.value };
|
||||
}
|
||||
return { ...item, x: i + 1, y: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
/** Line-stats style rows: categorical `label` + two metrics (not a single `value` series). */
|
||||
function dataIsDualLineShape(data: any[]): boolean {
|
||||
const row = data[0];
|
||||
return (
|
||||
!!row &&
|
||||
typeof row === "object" &&
|
||||
typeof row.label === "string" &&
|
||||
typeof row.valueA === "number" &&
|
||||
typeof row.valueB === "number" &&
|
||||
typeof row.value !== "number"
|
||||
);
|
||||
}
|
||||
|
||||
const MULTI_SERIES_CHART_TYPES: FlexibleChartData["type"][] = [
|
||||
"bar-grouped-vertical",
|
||||
"bar-grouped-horizontal",
|
||||
"bar-stacked-vertical",
|
||||
"bar-stacked-horizontal",
|
||||
"bar-clustered",
|
||||
"area-stacked",
|
||||
];
|
||||
|
||||
/**
|
||||
* Aligns `data`/`series` with `chartType`. Line-stats slides often keep `{ label, valueA, valueB }`
|
||||
* while bar/line/pie/etc. expect `name`/`value` or `values` + series keys.
|
||||
*/
|
||||
export function normalizeFlexibleChartData(
|
||||
chartType: FlexibleChartData["type"],
|
||||
data: any[],
|
||||
seriesIn: string[],
|
||||
): { data: any[]; series: string[] } {
|
||||
const series = seriesIn ?? [];
|
||||
const rows = data ?? [];
|
||||
|
||||
if (chartType === "line-dual") {
|
||||
if (dataIsDualLineShape(rows)) return { data: rows, series };
|
||||
return {
|
||||
data: rows.map((r, i) => ({
|
||||
label: r.label ?? r.name ?? `P${i + 1}`,
|
||||
valueA: typeof r.valueA === "number" ? r.valueA : typeof r.value === "number" ? r.value : 0,
|
||||
valueB: typeof r.valueB === "number" ? r.valueB : typeof r.value === "number" ? r.value : 0,
|
||||
})),
|
||||
series,
|
||||
};
|
||||
}
|
||||
|
||||
if (!dataIsDualLineShape(rows)) {
|
||||
return { data: rows, series };
|
||||
}
|
||||
|
||||
const dual = rows as Array<{ label: string; valueA: number; valueB: number }>;
|
||||
|
||||
if (MULTI_SERIES_CHART_TYPES.includes(chartType)) {
|
||||
const keys = series.length >= 2 ? [series[0], series[1]] : ["A", "B"];
|
||||
const mapped = dual.map((r) => ({
|
||||
name: r.label,
|
||||
values: { [keys[0]]: r.valueA, [keys[1]]: r.valueB },
|
||||
}));
|
||||
return { data: mapped, series: keys };
|
||||
}
|
||||
|
||||
if (chartType === "bar-diverging") {
|
||||
const mapped = dual.map((r) => ({
|
||||
name: r.label,
|
||||
positive: Math.max(0, r.valueA),
|
||||
negative: Math.max(0, r.valueB),
|
||||
}));
|
||||
return { data: mapped, series };
|
||||
}
|
||||
|
||||
const mapped = dual.map((r) => ({
|
||||
name: r.label,
|
||||
value: r.valueA + r.valueB,
|
||||
}));
|
||||
return { data: mapped, series };
|
||||
}
|
||||
|
||||
const graphVar = (index: number, fallback: string) => `var(--graph-${index % 10}, ${fallback})`;
|
||||
|
||||
export type ChartDensity = "default" | "compact";
|
||||
|
||||
export type FlexibleReportChartProps = {
|
||||
chartType: FlexibleChartData["type"];
|
||||
data: any[];
|
||||
series?: string[];
|
||||
colorFallback?: string;
|
||||
/** For `line-dual` only */
|
||||
dualLineColors?: [string, string];
|
||||
/** Smaller type, margins, and labels for multi-chart dashboards */
|
||||
density?: ChartDensity;
|
||||
};
|
||||
|
||||
export function FlexibleReportChart({
|
||||
chartType,
|
||||
data: chartData,
|
||||
series = [],
|
||||
colorFallback = "#157CFF",
|
||||
dualLineColors = ["var(--graph-0,#9fb6ff)", "var(--graph-1,#4d4ef3)"],
|
||||
density = "default",
|
||||
}: FlexibleReportChartProps) {
|
||||
const areaGradientId = `flex-area-${useId().replace(/:/g, "")}`;
|
||||
const compact = density === "compact";
|
||||
|
||||
const { data: normalizedData, series: normalizedSeries } = normalizeFlexibleChartData(
|
||||
chartType,
|
||||
chartData,
|
||||
series,
|
||||
);
|
||||
const effectiveSeries = deriveSeriesNames(normalizedData as any[], normalizedSeries);
|
||||
const scatterPoints = normalizeScatterPoints(normalizedData as any[]);
|
||||
|
||||
const ui = {
|
||||
tickFs: compact ? 6 : 10,
|
||||
catAxisW: compact ? 36 : 60,
|
||||
barSize: compact ? 20 : 35,
|
||||
labelFs: compact ? 8 : 14,
|
||||
labelOffTop: compact ? 2 : 10,
|
||||
labelOffSide: compact ? 2 : 8,
|
||||
margin: compact
|
||||
? { top: 10, right: 15, left: -2, bottom: 0 }
|
||||
: { top: 20, right: 20, left: 0, bottom: 5 },
|
||||
lineStroke: compact ? 2 : 3,
|
||||
dotR: compact ? 2.5 : 4,
|
||||
dotStroke: compact ? 1 : 2,
|
||||
barRadiusLg: compact ? ([4, 4, 0, 0] as const) : ([8, 8, 0, 0] as const),
|
||||
barRadiusMd: compact ? ([2, 2, 0, 0] as const) : ([4, 4, 0, 0] as const),
|
||||
barRadiusH: compact ? ([0, 4, 4, 0] as const) : ([0, 6, 6, 0] as const),
|
||||
pieOuter: compact ? "100%" : "100%",
|
||||
donutOuter: compact ? "90%" : "100%",
|
||||
donutInner: compact ? "60%" : "70%",
|
||||
pieMargin: compact
|
||||
? { top: 0, right: 0, left: 0, bottom: 0 }
|
||||
: { top: 8, right: 8, left: 8, bottom: 8 },
|
||||
pieLabelMinPct: compact ? 0.12 : 0.06,
|
||||
pieMaxNameLen: compact ? 6 : 10,
|
||||
};
|
||||
|
||||
const axisProps = {
|
||||
tick: { fill: "var(--background-text, #232223)", fontSize: ui.tickFs, fontWeight: 500 },
|
||||
axisLine: { stroke: "var(--background-text, #232223)" },
|
||||
tickLine: { stroke: "var(--background-text, #232223)" },
|
||||
};
|
||||
|
||||
const gridProps = {
|
||||
strokeDasharray: "3 3",
|
||||
stroke: "var(--background-text, #232223)",
|
||||
opacity: 0.7,
|
||||
};
|
||||
|
||||
const renderPieInsideLabel = (props: any) => {
|
||||
const {
|
||||
cx = 0,
|
||||
cy = 0,
|
||||
midAngle = 0,
|
||||
innerRadius: ir = 0,
|
||||
outerRadius: or = 0,
|
||||
percent = 0,
|
||||
name,
|
||||
} = props;
|
||||
if (percent < ui.pieLabelMinPct) return null;
|
||||
const toNum = (v: unknown) => {
|
||||
if (typeof v === "number" && Number.isFinite(v)) return v;
|
||||
if (typeof v === "string" && v.trim().endsWith("%")) return NaN;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : NaN;
|
||||
};
|
||||
let inner = toNum(ir);
|
||||
let outer = toNum(or);
|
||||
if (!Number.isFinite(outer)) {
|
||||
outer = compact ? 56 : 140;
|
||||
inner = Number.isFinite(inner) ? inner : 0;
|
||||
}
|
||||
if (!Number.isFinite(inner)) inner = 0;
|
||||
const midR = inner + (outer - inner) * 0.5;
|
||||
const rad = (-midAngle * Math.PI) / 180;
|
||||
const x = cx + midR * Math.cos(rad);
|
||||
const y = cy + midR * Math.sin(rad);
|
||||
const nm = String(name ?? "");
|
||||
const short = nm.length <= ui.pieMaxNameLen;
|
||||
const pct = `${(percent * 100).toFixed(0)}%`;
|
||||
const fontSize = compact ? (short ? 6 : 5) : short ? 12 : 12;
|
||||
const labelText = compact ? pct : short ? `${name} ${pct}` : pct;
|
||||
return (
|
||||
|
||||
<g>
|
||||
{/* <circle cx={x} cy={y} fill="var(--card-color,#ECEAF8)" /> */}
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
style={{ padding: "4px" }}
|
||||
textAnchor="middle"
|
||||
fill="var(--background-text,#2C2B39)"
|
||||
fontSize={fontSize}
|
||||
fontWeight={600}
|
||||
>
|
||||
{labelText}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
margin: ui.margin,
|
||||
|
||||
};
|
||||
|
||||
switch (chartType) {
|
||||
case "bar":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
|
||||
<BarChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Bar dataKey="value" fill={graphVar(0, colorFallback)} barSize={ui.barSize} radius={[...ui.barRadiusLg]}>
|
||||
<LabelList
|
||||
dataKey="value"
|
||||
position="top"
|
||||
fill={graphVar(0, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "bar-horizontal":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={normalizedData as any[]} layout="vertical" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Bar dataKey="value" barSize={ui.barSize} fill={graphVar(0, colorFallback)} radius={[...ui.barRadiusH]}>
|
||||
<LabelList
|
||||
dataKey="value"
|
||||
position="right"
|
||||
fill={graphVar(0, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffSide}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "bar-grouped-vertical": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar key={s} dataKey={s} barSize={ui.barSize} fill={graphVar(index, colorFallback)} radius={[...ui.barRadiusMd]}>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="top"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-grouped-horizontal": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} layout="vertical" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar key={s} dataKey={s} barSize={ui.barSize} fill={graphVar(index, colorFallback)} radius={[...ui.barRadiusH]}>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="right"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffSide}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-stacked-vertical": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar
|
||||
key={s}
|
||||
dataKey={s}
|
||||
stackId="stack"
|
||||
barSize={ui.barSize}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
radius={index === effectiveSeries.length - 1 ? [...ui.barRadiusMd] : [0, 0, 0, 0]}
|
||||
>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="top"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-stacked-horizontal": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} layout="vertical" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar
|
||||
key={s}
|
||||
dataKey={s}
|
||||
stackId="stack"
|
||||
barSize={ui.barSize}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
radius={index === effectiveSeries.length - 1 ? [...ui.barRadiusH] : [0, 0, 0, 0]}
|
||||
>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="right"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffSide}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-clustered": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar
|
||||
key={s}
|
||||
dataKey={s}
|
||||
barSize={Math.max(
|
||||
compact ? 6 : 15,
|
||||
(compact ? 22 : 50) / Math.max(1, effectiveSeries.length),
|
||||
)}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
radius={compact ? [2, 2, 0, 0] : [3, 3, 0, 0]}
|
||||
>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="top"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-diverging": {
|
||||
const transformedData = transformDivergingData(normalizedData as any[]);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={transformedData} layout="vertical" stackOffset="sign" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ReferenceLine x={0} stroke="var(--stroke,#9CA3AF)" strokeWidth={1} />
|
||||
<Bar dataKey="positive" barSize={ui.barSize} fill={graphVar(0, colorFallback)} stackId="stack" radius={[...ui.barRadiusH]}>
|
||||
<LabelList dataKey="positive" position="right" fill={graphVar(0, colorFallback)} fontSize={ui.labelFs} offset={ui.labelOffSide} />
|
||||
</Bar>
|
||||
<Bar dataKey="negative" fill={graphVar(3, colorFallback)} stackId="stack" radius={compact ? [2, 0, 0, 2] : [4, 0, 0, 4]}>
|
||||
<LabelList dataKey="negative" position="left" fill={graphVar(3, colorFallback)} fontSize={ui.labelFs} offset={ui.labelOffSide} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "line":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<LineChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={graphVar(0, colorFallback)}
|
||||
strokeWidth={ui.lineStroke}
|
||||
dot={{ fill: graphVar(0, colorFallback), strokeWidth: ui.dotStroke, r: ui.dotR }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "line-dual":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueA"
|
||||
stroke={dualLineColors[0]}
|
||||
strokeWidth={ui.lineStroke}
|
||||
dot={{ fill: dualLineColors[0], strokeWidth: ui.dotStroke, r: ui.dotR }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueB"
|
||||
stroke={dualLineColors[1]}
|
||||
strokeWidth={ui.lineStroke}
|
||||
dot={{ fill: dualLineColors[1], strokeWidth: ui.dotStroke, r: ui.dotR }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "area":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
|
||||
<AreaChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<defs>
|
||||
<linearGradient id={areaGradientId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={graphVar(0, colorFallback)} stopOpacity={0.4} />
|
||||
<stop offset="95%" stopColor={graphVar(0, colorFallback)} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={graphVar(0, colorFallback)}
|
||||
strokeWidth={compact ? 1.5 : 2}
|
||||
fill={`url(#${areaGradientId})`}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "area-stacked": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<AreaChart data={transformedData} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Area
|
||||
key={s}
|
||||
type="monotone"
|
||||
dataKey={s}
|
||||
stackId="1"
|
||||
stroke={graphVar(index, colorFallback)}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fillOpacity={0.4}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "pie":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart {...commonProps} margin={ui.pieMargin}>
|
||||
<Pie
|
||||
data={normalizedData as any[]}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={ui.pieOuter}
|
||||
innerRadius={0}
|
||||
label={renderPieInsideLabel}
|
||||
labelLine={false}
|
||||
>
|
||||
{(normalizedData as any[]).map((_, index) => (
|
||||
<Cell key={`pie-cell-${index}`} fill={graphVar(index, colorFallback)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "donut":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart {...commonProps} margin={ui.pieMargin}>
|
||||
<Pie
|
||||
data={normalizedData as any[]}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={ui.donutOuter}
|
||||
innerRadius={ui.donutInner}
|
||||
label={renderPieInsideLabel}
|
||||
paddingAngle={compact ? 1 : 2}
|
||||
labelLine={false}
|
||||
>
|
||||
{(normalizedData as any[]).map((_, index) => (
|
||||
<Cell key={`donut-cell-${index}`} fill={graphVar(index, colorFallback)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "scatter":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<ScatterChart {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis dataKey="x" type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis dataKey="y" type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Scatter data={scatterPoints} name="Series">
|
||||
{scatterPoints.map((_, index) => (
|
||||
<Cell key={`scatter-cell-${index}`} fill={graphVar(index, colorFallback)} />
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">Unsupported chart type</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"description": "Data and narrative report layouts for intros, analysis charts, dashboards, and closing slides",
|
||||
"ordered": false,
|
||||
"default": false
|
||||
}
|
||||
|
|
@ -3,6 +3,63 @@ import { TemplateWithData, TemplateGroupSettings, createTemplateEntry, TemplateL
|
|||
|
||||
// TODO: Step 1: Import All templates Layouts Here (like the ones below)
|
||||
|
||||
// Code templates
|
||||
import CodeSlide01RoadmapCover, { Schema as CodeRoadmapCoverSchema, slideLayoutId as CodeRoadmapCoverId, slideLayoutName as CodeRoadmapCoverName, slideLayoutDescription as CodeRoadmapCoverDesc } from "./Code/CoverSlide";
|
||||
import CodeSlide02CodeExplanationSplit, { Schema as CodeExplanationSplitSchema, slideLayoutId as CodeExplanationSplitId, slideLayoutName as CodeExplanationSplitName, slideLayoutDescription as CodeExplanationSplitDesc } from "./Code/CodeExplanationSplitSlide";
|
||||
import CodeSlide03ApiRequestResponse, { Schema as CodeApiRequestResponseSchema, slideLayoutId as CodeApiRequestResponseId, slideLayoutName as CodeApiRequestResponseName, slideLayoutDescription as CodeApiRequestResponseDesc } from "./Code/APIRequestResponseSlide";
|
||||
import CodeSlide04FeatureGrid, { Schema as CodeFeatureGridSchema, slideLayoutId as CodeFeatureGridId, slideLayoutName as CodeFeatureGridName, slideLayoutDescription as CodeFeatureGridDesc } from "./Code/CardsGridSlide";
|
||||
import CodeSlide05ComparisonTable, { Schema as CodeComparisonTableSchema, slideLayoutId as CodeComparisonTableId, slideLayoutName as CodeComparisonTableName, slideLayoutDescription as CodeComparisonTableDesc } from "./Code/TableSlide";
|
||||
import CodeSlide06Workflow, { Schema as CodeWorkflowSchema, slideLayoutId as CodeWorkflowId, slideLayoutName as CodeWorkflowName, slideLayoutDescription as CodeWorkflowDesc } from "./Code/WorkflowSlide";
|
||||
import CodeSlide07UseCaseList, { Schema as CodeUseCaseListSchema, slideLayoutId as CodeUseCaseListId, slideLayoutName as CodeUseCaseListName, slideLayoutDescription as CodeUseCaseListDesc } from "./Code/TwoColumnBulletListSlide";
|
||||
import CodeSlide08CodeExplanationText, { Schema as CodeExplanationTextSchema, slideLayoutId as CodeExplanationTextId, slideLayoutName as CodeExplanationTextName, slideLayoutDescription as CodeExplanationTextDesc } from "./Code/DescriptionTextSlide";
|
||||
import CodeSlide09TableOfContent, { Schema as CodeTableOfContentSchema, slideLayoutId as CodeTableOfContentId, slideLayoutName as CodeTableOfContentName, slideLayoutDescription as CodeTableOfContentDesc } from "./Code/TableOfContentSlide";
|
||||
import CodeSlide10MetricsSplit, { Schema as CodeMetricsSplitSchema, slideLayoutId as CodeMetricsSplitId, slideLayoutName as CodeMetricsSplitName, slideLayoutDescription as CodeMetricsSplitDesc } from "./Code/DescriptionAndMetricsSlide";
|
||||
import CodeSlide11MetricsGrid, { Schema as CodeMetricsGridSchema, slideLayoutId as CodeMetricsGridId, slideLayoutName as CodeMetricsGridName, slideLayoutDescription as CodeMetricsGridDesc } from "./Code/MetricsGridSlide";
|
||||
|
||||
// Education templates
|
||||
import EducationCoverSlide, { Schema as EduCoverSchema, slideLayoutId as EduCoverId, slideLayoutName as EduCoverName, slideLayoutDescription as EduCoverDesc } from "./Education/EducationCoverSlide";
|
||||
import EducationTableOfContentsSlide, { Schema as EduTocSchema, slideLayoutId as EduTocId, slideLayoutName as EduTocName, slideLayoutDescription as EduTocDesc } from "./Education/EducationTableOfContentsSlide";
|
||||
import EducationAboutSlide, { Schema as EduAboutSchema, slideLayoutId as EduAboutId, slideLayoutName as EduAboutName, slideLayoutDescription as EduAboutDesc } from "./Education/EducationAboutSlide";
|
||||
import EducationContentSplitSlide, { Schema as EduContentSplitSchema, slideLayoutId as EduContentSplitId, slideLayoutName as EduContentSplitName, slideLayoutDescription as EduContentSplitDesc } from "./Education/EducationContentSplitSlide";
|
||||
import EducationImageGallerySlide, { Schema as EduImageGallerySchema, slideLayoutId as EduImageGalleryId, slideLayoutName as EduImageGalleryName, slideLayoutDescription as EduImageGalleryDesc } from "./Education/EducationImageGallerySlide";
|
||||
import EducationReportDonutSlide, { Schema as EduReportDonutSchema, slideLayoutId as EduReportDonutId, slideLayoutName as EduReportDonutName, slideLayoutDescription as EduReportDonutDesc } from "./Education/EducationReportChartSlide";
|
||||
import EducationServicesSplitSlide, { Schema as EduServicesSplitSchema, slideLayoutId as EduServicesSplitId, slideLayoutName as EduServicesSplitName, slideLayoutDescription as EduServicesSplitDesc } from "./Education/EducationServicesSplitSlide";
|
||||
import EducationStatisticsGridSlide, { Schema as EduStatisticsGridSchema, slideLayoutId as EduStatisticsGridId, slideLayoutName as EduStatisticsGridName, slideLayoutDescription as EduStatisticsGridDesc } from "./Education/EducationStatisticsGridSlide";
|
||||
import EducationTimelineSlide, { Schema as EduTimelineSchema, slideLayoutId as EduTimelineId, slideLayoutName as EduTimelineName, slideLayoutDescription as EduTimelineDesc } from "./Education/EducationTimelineSlide";
|
||||
|
||||
// Product Overview templates
|
||||
import BusinessChallengesCardsSlide, { Schema as PoBizChallengesCardsSchema, slideLayoutId as PoBizChallengesCardsId, slideLayoutName as PoBizChallengesCardsName, slideLayoutDescription as PoBizChallengesCardsDesc } from "./ProductOverview/BusinessChallengesCardsSlide";
|
||||
import BusinessChallengesGridSlide, { Schema as PoBizChallengesGridSchema, slideLayoutId as PoBizChallengesGridId, slideLayoutName as PoBizChallengesGridName, slideLayoutDescription as PoBizChallengesGridDesc } from "./ProductOverview/BusinessChallengesGridSlide";
|
||||
import ComparisonChartSlide, { Schema as PoComparisonChartSchema, slideLayoutId as PoComparisonChartId, slideLayoutName as PoComparisonChartName, slideLayoutDescription as PoComparisonChartDesc } from "./ProductOverview/ComparisonChartSlide";
|
||||
import ComparisonTableWithTextSlide, { Schema as PoComparisonTableSchema, slideLayoutId as PoComparisonTableId, slideLayoutName as PoComparisonTableName, slideLayoutDescription as PoComparisonTableDesc } from "./ProductOverview/ComparisonTableWithTextSlide";
|
||||
import CoverSlide, { Schema as PoCoverSchema, slideLayoutId as PoCoverId, slideLayoutName as PoCoverName, slideLayoutDescription as PoCoverDesc } from "./ProductOverview/CoverSlide";
|
||||
import ImageGallerySlide, { Schema as PoImageGallerySchema, slideLayoutId as PoImageGalleryId, slideLayoutName as PoImageGalleryName, slideLayoutDescription as PoImageGalleryDesc } from "./ProductOverview/ImageGallerySlide";
|
||||
import IntroductionSlide, { Schema as PoIntroductionSchema, slideLayoutId as PoIntroductionId, slideLayoutName as PoIntroductionName, slideLayoutDescription as PoIntroductionDesc } from "./ProductOverview/IntroductionSlide";
|
||||
import KpiCardsSlide, { Schema as PoKpiCardsSchema, slideLayoutId as PoKpiCardsId, slideLayoutName as PoKpiCardsName, slideLayoutDescription as PoKpiCardsDesc } from "./ProductOverview/KpiCardsSlide";
|
||||
// import MarketOpportunitySlide, { Schema as PoMarketOpportunitySchema, slideLayoutId as PoMarketOpportunityId, slideLayoutName as PoMarketOpportunityName, slideLayoutDescription as PoMarketOpportunityDesc } from "./ProductOverview/MarketOpportunitySlide";
|
||||
import MeetTeamSlide, { Schema as PoMeetTeamSchema, slideLayoutId as PoMeetTeamId, slideLayoutName as PoMeetTeamName, slideLayoutDescription as PoMeetTeamDesc } from "./ProductOverview/MeetTeamSlide";
|
||||
import MissionVisionSlide, { Schema as PoMissionVisionSchema, slideLayoutId as PoMissionVisionId, slideLayoutName as PoMissionVisionName, slideLayoutDescription as PoMissionVisionDesc } from "./ProductOverview/MissionVisionSlide";
|
||||
import OurServicesSlide, { Schema as PoOurServicesSchema, slideLayoutId as PoOurServicesId, slideLayoutName as PoOurServicesName, slideLayoutDescription as PoOurServicesDesc } from "./ProductOverview/OurServicesSlide";
|
||||
import PricingPlanSlide, { Schema as PoPricingPlanSchema, slideLayoutId as PoPricingPlanId, slideLayoutName as PoPricingPlanName, slideLayoutDescription as PoPricingPlanDesc } from "./ProductOverview/PricingPlanSlide";
|
||||
import ProcessSlide, { Schema as PoProcessSchema, slideLayoutId as PoProcessId, slideLayoutName as PoProcessName, slideLayoutDescription as PoProcessDesc } from "./ProductOverview/ProcessSlide";
|
||||
import ReportSnapshotSlide, { Schema as PoReportSnapshotSchema, slideLayoutId as PoReportSnapshotId, slideLayoutName as PoReportSnapshotName, slideLayoutDescription as PoReportSnapshotDesc } from "./ProductOverview/ReportSnapshotSlide";
|
||||
import TableOfContentSlide, { Schema as PoTableOfContentSchema, slideLayoutId as PoTableOfContentId, slideLayoutName as PoTableOfContentName, slideLayoutDescription as PoTableOfContentDesc } from "./ProductOverview/TableOfContentSlide";
|
||||
|
||||
// Report templates
|
||||
import ReportIntroSlide, { Schema as RepIntroSchema, slideLayoutId as RepIntroId, slideLayoutName as RepIntroName, slideLayoutDescription as RepIntroDesc } from "./Report/IntroCoverSlide";
|
||||
import TitleDescriptionImageSlide, { Schema as RepIntroductionImageSchema, slideLayoutId as RepIntroductionImageId, slideLayoutName as RepIntroductionImageName, slideLayoutDescription as RepIntroductionImageDesc } from "./Report/TitleDescriptionImageSlide";
|
||||
import IntroductionStatsSlide, { Schema as RepIntroductionStatsSchema, slideLayoutId as RepIntroductionStatsId, slideLayoutName as RepIntroductionStatsName, slideLayoutDescription as RepIntroductionStatsDesc } from "./Report/MetricsSlide";
|
||||
import SolutionSlide, { Schema as RepSolutionSchema, slideLayoutId as RepSolutionId, slideLayoutName as RepSolutionName, slideLayoutDescription as RepSolutionDesc } from "./Report/TitleImageBulletCardsSlide";
|
||||
import MilestoneSlide, { Schema as RepMilestoneSchema, slideLayoutId as RepMilestoneId, slideLayoutName as RepMilestoneName, slideLayoutDescription as RepMilestoneDesc } from "./Report/MilestoneSlide";
|
||||
import DataAnalysisListSlide, { Schema as RepDataAnalysisListSchema, slideLayoutId as RepDataAnalysisListId, slideLayoutName as RepDataAnalysisListName, slideLayoutDescription as RepDataAnalysisListDesc } from "./Report/BulletListWithIconTitleDescriptionSlide";
|
||||
import DataAnalysisBarSlide, { Schema as RepDataAnalysisBarSchema, slideLayoutId as RepDataAnalysisBarId, slideLayoutName as RepDataAnalysisBarName, slideLayoutDescription as RepDataAnalysisBarDesc } from "./Report/BarChartWithBulletListWithTitleDescriptionIconSlide";
|
||||
import DataAnalysisInsightBarSlide, { Schema as RepDataAnalysisInsightBarSchema, slideLayoutId as RepDataAnalysisInsightBarId, slideLayoutName as RepDataAnalysisInsightBarName, slideLayoutDescription as RepDataAnalysisInsightBarDesc } from "./Report/TitleDescriptionChartSlide";
|
||||
import DataAnalysisLineStatsSlide, { Schema as RepDataAnalysisLineStatsSchema, slideLayoutId as RepDataAnalysisLineStatsId, slideLayoutName as RepDataAnalysisLineStatsName, slideLayoutDescription as RepDataAnalysisLineStatsDesc } from "./Report/TitleChartWithMetricsCardsSlide";
|
||||
import DataAnalysisDashboardSlide, { Schema as RepDataAnalysisDashboardSchema, slideLayoutId as RepDataAnalysisDashboardId, slideLayoutName as RepDataAnalysisDashboardName, slideLayoutDescription as RepDataAnalysisDashboardDesc } from "./Report/DataAnalysisDashboardSlide";
|
||||
import PerformanceSnapshotSlide, { Schema as RepPerformanceSnapshotSchema, slideLayoutId as RepPerformanceSnapshotId, slideLayoutName as RepPerformanceSnapshotName, slideLayoutDescription as RepPerformanceSnapshotDesc } from "./Report/TitleMetricsSlide";
|
||||
import ReportServicesSlide, { Schema as RepServicesSchema, slideLayoutId as RepServicesId, slideLayoutName as RepServicesName, slideLayoutDescription as RepServicesDesc } from "./Report/TitleWorkflowWithTitleDescriptionSlide";
|
||||
import ReportTeamSlide, { Schema as RepTeamSchema, slideLayoutId as RepTeamId, slideLayoutName as RepTeamName, slideLayoutDescription as RepTeamDesc } from "./Report/HorizontalHeightSpanningImagesWithTitleSlide";
|
||||
|
||||
// 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";
|
||||
|
|
@ -175,6 +232,10 @@ 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";
|
||||
import codeSettings from "./Code/settings.json";
|
||||
import educationSettings from "./Education/settings.json";
|
||||
import productOverviewSettings from "./ProductOverview/settings.json";
|
||||
import reportSettings from "./Report/settings.json";
|
||||
|
||||
|
||||
// Helper to create template entry
|
||||
|
|
@ -182,6 +243,67 @@ import neoSwiftSettings from "./neo-swift/settings.json";
|
|||
|
||||
// TODO: Step 3: Create template entries for each template (like the ones below)
|
||||
|
||||
export const codeTemplates: TemplateWithData[] = [
|
||||
createTemplateEntry(CodeSlide01RoadmapCover, CodeRoadmapCoverSchema, CodeRoadmapCoverId, CodeRoadmapCoverName, CodeRoadmapCoverDesc, "code", "CoverSlide"),
|
||||
createTemplateEntry(CodeSlide02CodeExplanationSplit, CodeExplanationSplitSchema, CodeExplanationSplitId, CodeExplanationSplitName, CodeExplanationSplitDesc, "code", "CodeExplanationSplitSlide"),
|
||||
createTemplateEntry(CodeSlide03ApiRequestResponse, CodeApiRequestResponseSchema, CodeApiRequestResponseId, CodeApiRequestResponseName, CodeApiRequestResponseDesc, "code", "APIRequestResponseSlide"),
|
||||
createTemplateEntry(CodeSlide04FeatureGrid, CodeFeatureGridSchema, CodeFeatureGridId, CodeFeatureGridName, CodeFeatureGridDesc, "code", "CardsGridSlide"),
|
||||
createTemplateEntry(CodeSlide05ComparisonTable, CodeComparisonTableSchema, CodeComparisonTableId, CodeComparisonTableName, CodeComparisonTableDesc, "code", "TableSlide"),
|
||||
createTemplateEntry(CodeSlide06Workflow, CodeWorkflowSchema, CodeWorkflowId, CodeWorkflowName, CodeWorkflowDesc, "code", "WorkflowSlide"),
|
||||
createTemplateEntry(CodeSlide07UseCaseList, CodeUseCaseListSchema, CodeUseCaseListId, CodeUseCaseListName, CodeUseCaseListDesc, "code", "TwoColumnBulletListSlide"),
|
||||
createTemplateEntry(CodeSlide08CodeExplanationText, CodeExplanationTextSchema, CodeExplanationTextId, CodeExplanationTextName, CodeExplanationTextDesc, "code", "DescriptionTextSlide"),
|
||||
createTemplateEntry(CodeSlide09TableOfContent, CodeTableOfContentSchema, CodeTableOfContentId, CodeTableOfContentName, CodeTableOfContentDesc, "code", "TableOfContentSlide"),
|
||||
createTemplateEntry(CodeSlide10MetricsSplit, CodeMetricsSplitSchema, CodeMetricsSplitId, CodeMetricsSplitName, CodeMetricsSplitDesc, "code", "DescriptionAndMetricsSlide"),
|
||||
createTemplateEntry(CodeSlide11MetricsGrid, CodeMetricsGridSchema, CodeMetricsGridId, CodeMetricsGridName, CodeMetricsGridDesc, "code", "MetricsGridSlide"),
|
||||
];
|
||||
|
||||
export const educationTemplates: TemplateWithData[] = [
|
||||
createTemplateEntry(EducationCoverSlide, EduCoverSchema, EduCoverId, EduCoverName, EduCoverDesc, "education", "EducationCoverSlide"),
|
||||
createTemplateEntry(EducationTableOfContentsSlide, EduTocSchema, EduTocId, EduTocName, EduTocDesc, "education", "EducationTableOfContentsSlide"),
|
||||
createTemplateEntry(EducationAboutSlide, EduAboutSchema, EduAboutId, EduAboutName, EduAboutDesc, "education", "EducationAboutSlide"),
|
||||
createTemplateEntry(EducationContentSplitSlide, EduContentSplitSchema, EduContentSplitId, EduContentSplitName, EduContentSplitDesc, "education", "EducationContentSplitSlide"),
|
||||
createTemplateEntry(EducationImageGallerySlide, EduImageGallerySchema, EduImageGalleryId, EduImageGalleryName, EduImageGalleryDesc, "education", "EducationImageGallerySlide"),
|
||||
createTemplateEntry(EducationReportDonutSlide, EduReportDonutSchema, EduReportDonutId, EduReportDonutName, EduReportDonutDesc, "education", "EducationReportDonutSlide"),
|
||||
createTemplateEntry(EducationServicesSplitSlide, EduServicesSplitSchema, EduServicesSplitId, EduServicesSplitName, EduServicesSplitDesc, "education", "EducationServicesSplitSlide"),
|
||||
createTemplateEntry(EducationStatisticsGridSlide, EduStatisticsGridSchema, EduStatisticsGridId, EduStatisticsGridName, EduStatisticsGridDesc, "education", "EducationStatisticsGridSlide"),
|
||||
createTemplateEntry(EducationTimelineSlide, EduTimelineSchema, EduTimelineId, EduTimelineName, EduTimelineDesc, "education", "EducationTimelineSlide"),
|
||||
];
|
||||
|
||||
export const productOverviewTemplates: TemplateWithData[] = [
|
||||
createTemplateEntry(CoverSlide, PoCoverSchema, PoCoverId, PoCoverName, PoCoverDesc, "product-overview", "CoverSlide"),
|
||||
createTemplateEntry(TableOfContentSlide, PoTableOfContentSchema, PoTableOfContentId, PoTableOfContentName, PoTableOfContentDesc, "product-overview", "TableOfContentSlide"),
|
||||
createTemplateEntry(IntroductionSlide, PoIntroductionSchema, PoIntroductionId, PoIntroductionName, PoIntroductionDesc, "product-overview", "IntroductionSlide"),
|
||||
createTemplateEntry(MissionVisionSlide, PoMissionVisionSchema, PoMissionVisionId, PoMissionVisionName, PoMissionVisionDesc, "product-overview", "MissionVisionSlide"),
|
||||
// createTemplateEntry(MarketOpportunitySlide, PoMarketOpportunitySchema, PoMarketOpportunityId, PoMarketOpportunityName, PoMarketOpportunityDesc, "product-overview", "MarketOpportunitySlide"),
|
||||
createTemplateEntry(BusinessChallengesGridSlide, PoBizChallengesGridSchema, PoBizChallengesGridId, PoBizChallengesGridName, PoBizChallengesGridDesc, "product-overview", "BusinessChallengesGridSlide"),
|
||||
createTemplateEntry(BusinessChallengesCardsSlide, PoBizChallengesCardsSchema, PoBizChallengesCardsId, PoBizChallengesCardsName, PoBizChallengesCardsDesc, "product-overview", "BusinessChallengesCardsSlide"),
|
||||
createTemplateEntry(OurServicesSlide, PoOurServicesSchema, PoOurServicesId, PoOurServicesName, PoOurServicesDesc, "product-overview", "OurServicesSlide"),
|
||||
createTemplateEntry(ProcessSlide, PoProcessSchema, PoProcessId, PoProcessName, PoProcessDesc, "product-overview", "ProcessSlide"),
|
||||
createTemplateEntry(ComparisonChartSlide, PoComparisonChartSchema, PoComparisonChartId, PoComparisonChartName, PoComparisonChartDesc, "product-overview", "ComparisonChartSlide"),
|
||||
createTemplateEntry(ComparisonTableWithTextSlide, PoComparisonTableSchema, PoComparisonTableId, PoComparisonTableName, PoComparisonTableDesc, "product-overview", "ComparisonTableWithTextSlide"),
|
||||
createTemplateEntry(KpiCardsSlide, PoKpiCardsSchema, PoKpiCardsId, PoKpiCardsName, PoKpiCardsDesc, "product-overview", "KpiCardsSlide"),
|
||||
createTemplateEntry(ReportSnapshotSlide, PoReportSnapshotSchema, PoReportSnapshotId, PoReportSnapshotName, PoReportSnapshotDesc, "product-overview", "ReportSnapshotSlide"),
|
||||
createTemplateEntry(PricingPlanSlide, PoPricingPlanSchema, PoPricingPlanId, PoPricingPlanName, PoPricingPlanDesc, "product-overview", "PricingPlanSlide"),
|
||||
createTemplateEntry(MeetTeamSlide, PoMeetTeamSchema, PoMeetTeamId, PoMeetTeamName, PoMeetTeamDesc, "product-overview", "MeetTeamSlide"),
|
||||
createTemplateEntry(ImageGallerySlide, PoImageGallerySchema, PoImageGalleryId, PoImageGalleryName, PoImageGalleryDesc, "product-overview", "ImageGallerySlide"),
|
||||
];
|
||||
|
||||
export const reportTemplates: TemplateWithData[] = [
|
||||
createTemplateEntry(ReportIntroSlide, RepIntroSchema, RepIntroId, RepIntroName, RepIntroDesc, "report", "IntroCoverSlide"),
|
||||
createTemplateEntry(TitleDescriptionImageSlide, RepIntroductionImageSchema, RepIntroductionImageId, RepIntroductionImageName, RepIntroductionImageDesc, "report", "TitleDescriptionImageSlide"),
|
||||
createTemplateEntry(IntroductionStatsSlide, RepIntroductionStatsSchema, RepIntroductionStatsId, RepIntroductionStatsName, RepIntroductionStatsDesc, "report", "MetricsSlide"),
|
||||
createTemplateEntry(SolutionSlide, RepSolutionSchema, RepSolutionId, RepSolutionName, RepSolutionDesc, "report", "TitleImageBulletCardsSlide"),
|
||||
createTemplateEntry(MilestoneSlide, RepMilestoneSchema, RepMilestoneId, RepMilestoneName, RepMilestoneDesc, "report", "MilestoneSlide"),
|
||||
createTemplateEntry(DataAnalysisListSlide, RepDataAnalysisListSchema, RepDataAnalysisListId, RepDataAnalysisListName, RepDataAnalysisListDesc, "report", "BulletListWithIconTitleDescriptionSlide"),
|
||||
createTemplateEntry(DataAnalysisBarSlide, RepDataAnalysisBarSchema, RepDataAnalysisBarId, RepDataAnalysisBarName, RepDataAnalysisBarDesc, "report", "BarChartWithBulletListWithTitleDescriptionIconSlide"),
|
||||
createTemplateEntry(DataAnalysisInsightBarSlide, RepDataAnalysisInsightBarSchema, RepDataAnalysisInsightBarId, RepDataAnalysisInsightBarName, RepDataAnalysisInsightBarDesc, "report", "TitleDescriptionChartSlide"),
|
||||
createTemplateEntry(DataAnalysisLineStatsSlide, RepDataAnalysisLineStatsSchema, RepDataAnalysisLineStatsId, RepDataAnalysisLineStatsName, RepDataAnalysisLineStatsDesc, "report", "TitleChartWithMetricsCardsSlide"),
|
||||
createTemplateEntry(DataAnalysisDashboardSlide, RepDataAnalysisDashboardSchema, RepDataAnalysisDashboardId, RepDataAnalysisDashboardName, RepDataAnalysisDashboardDesc, "report", "DataAnalysisDashboardSlide"),
|
||||
createTemplateEntry(PerformanceSnapshotSlide, RepPerformanceSnapshotSchema, RepPerformanceSnapshotId, RepPerformanceSnapshotName, RepPerformanceSnapshotDesc, "report", "TitleMetricsSlide"),
|
||||
createTemplateEntry(ReportServicesSlide, RepServicesSchema, RepServicesId, RepServicesName, RepServicesDesc, "report", "TitleWorkflowWithTitleDescriptionSlide"),
|
||||
createTemplateEntry(ReportTeamSlide, RepTeamSchema, RepTeamId, RepTeamName, RepTeamDesc, "report", "HorizontalHeightSpanningImagesWithTitleSlide"),
|
||||
];
|
||||
|
||||
export const neoGeneralTemplates: TemplateWithData[] = [
|
||||
|
||||
createTemplateEntry(TextSplitWithEmphasisBlockLayout, TextSplitWithEmphasisBlockSchema, TextSplitWithEmphasisBlockId, TextSplitWithEmphasisBlockName, TextSplitWithEmphasisBlockDesc, 'neo-general', 'TextSplitWithEmphasisBlock'),
|
||||
|
|
@ -352,42 +474,16 @@ export const allLayouts: TemplateWithData[] = [
|
|||
...modernTemplates,
|
||||
...standardTemplates,
|
||||
...swiftTemplates,
|
||||
|
||||
|
||||
...codeTemplates,
|
||||
...educationTemplates,
|
||||
...productOverviewTemplates,
|
||||
...reportTemplates,
|
||||
];
|
||||
|
||||
|
||||
// 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",
|
||||
|
|
@ -416,6 +512,63 @@ export const templates: TemplateLayoutsWithSettings[] = [
|
|||
settings: swiftSettings as TemplateGroupSettings,
|
||||
layouts: swiftTemplates,
|
||||
},
|
||||
{
|
||||
id: "code",
|
||||
name: "Code",
|
||||
description: codeSettings.description,
|
||||
settings: codeSettings as TemplateGroupSettings,
|
||||
layouts: codeTemplates,
|
||||
},
|
||||
{
|
||||
id: "education",
|
||||
name: "Education",
|
||||
description: educationSettings.description,
|
||||
settings: educationSettings as TemplateGroupSettings,
|
||||
layouts: educationTemplates,
|
||||
},
|
||||
{
|
||||
id: "product-overview",
|
||||
name: "Product Overview",
|
||||
description: productOverviewSettings.description,
|
||||
settings: productOverviewSettings as TemplateGroupSettings,
|
||||
layouts: productOverviewTemplates,
|
||||
},
|
||||
{
|
||||
id: "report",
|
||||
name: "Report",
|
||||
description: reportSettings.description,
|
||||
settings: reportSettings as TemplateGroupSettings,
|
||||
layouts: reportTemplates,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue