diff --git a/.codex b/.codex new file mode 100644 index 00000000..e69de29b diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/Components/MarketOpportunitySlide.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/Components/MarketOpportunitySlide.tsx new file mode 100644 index 00000000..55028a94 --- /dev/null +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/Components/MarketOpportunitySlide.tsx @@ -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; + + +const COLORS = [ + "var(--graph-0,#5f7f79)", + "var(--graph-1,#1f5a4f)", + "var(--graph-2,#0d4f43)", + "var(--graph-3,#06463d)", +]; + +const MarketOpportunitySlide = ({ data }: { data: Partial }) => { + const { title, subtitle, bullets, values } = data; + + return ( +
+
+

+ {title} +

+

+ {subtitle} +

+
+ +
+ {bullets?.map((bullet, index) => ( +
+ +

+ {bullet.text} +

+ + +
+ ))} +
+ +
+ {values?.map((value, index) => ( +
+

+ {value} +

+
+ ))} +
+
+ ); +}; + +export default MarketOpportunitySlide; diff --git a/electron/servers/nextjs/app/globals.css b/electron/servers/nextjs/app/globals.css index ac589850..9674bec6 100644 --- a/electron/servers/nextjs/app/globals.css +++ b/electron/servers/nextjs/app/globals.css @@ -83,6 +83,9 @@ strong { + + + ::selection { background-color: hsl(var(--chart-1)); color: white; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/APIRequestResponseSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/APIRequestResponseSlide.tsx new file mode 100644 index 00000000..2091120c --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/APIRequestResponseSlide.tsx @@ -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 "]) + .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; + +const CodeSlide03ApiRequestResponse = ({ + data, +}: { + data: Partial; +}) => { + + return ( + <> + +
+ +
+

{data.title}

+ +
+
+
+
+

+ {data.method} +

+

{data.endpoint}

+
+

Headers

+
+ {data.headers?.map((item) => ( +

{item}

+ ))} +
+
+ +
+

+ {data.requestSnippet?.fileName} +

+
+
+                  
+                    {data.requestSnippet?.content}
+                  
+                
+
+
+ +
+

+ {data.responseSnippet?.fileName} +

+
+
+                
+                  {data.responseSnippet?.content}
+                
+              
+
+
+
+
+ + ); +}; + +export default CodeSlide03ApiRequestResponse; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CardsGridSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CardsGridSlide.tsx new file mode 100644 index 00000000..683afe62 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/CardsGridSlide.tsx @@ -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; + +const CodeSlide04FeatureGrid = ({ data }: { data: Partial }) => { + + + return ( + <> + +
+ + +

{data.title}

+ +
+ {data?.features?.map((feature) => ( +
+
+

{feature.title}

+ + {/* {feature.icon.__icon_query__} */} + + +
+

{feature.description}

+
+ ))} +
+
+ + ); +}; + +export default CodeSlide04FeatureGrid; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx new file mode 100644 index 00000000..258e3057 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx @@ -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; + +const CodeSlide02CodeExplanationSplit = ({ + data, +}: { + data: Partial; +}) => { + const normalizedCodeContent = normalizeCodeContent( + data.codeSnippet?.language, + data.codeSnippet?.content + ); + const codeTypography = getCodeBlockTypography(normalizedCodeContent); + const codeLineRuns = getCodeLineRuns(normalizedCodeContent, codeTypography.lineHeight); + + return ( + <> + +
+ +
+

{data.title}

+ +
+
+

+ {data.codeSnippet?.fileName} +

+
+ {codeLineRuns.map((codeLineRun, index) => ( +
+ {codeLineRun.text} +
+ ))} +
+
+ +
+

{data.descriptionTitle}

+

+ {data.description} +

+
+
+
+
+ + ); +}; + +export default CodeSlide02CodeExplanationSplit; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CoverSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CoverSlide.tsx new file mode 100644 index 00000000..367c3e39 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/CoverSlide.tsx @@ -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; + +const CodeSlide01RoadmapCover = ({ data }: { data: Partial }) => { + + return (<> + +
+
+

{data.companyName}

+

+ {data.title} +

+

{data.subtitle}

+
+
+ + ); +}; + +export default CodeSlide01RoadmapCover; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/DescriptionAndMetricsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/DescriptionAndMetricsSlide.tsx new file mode 100644 index 00000000..cbf1fbaf --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/DescriptionAndMetricsSlide.tsx @@ -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; + +const CodeSlide10MetricsSplit = ({ data }: { data: Partial }) => { + + + return ( + <> + +
+ + +

{data.title}

+
+
+

{data.explanationTitle}

+

{data.explanation}

+
+ +
+ {data?.metrics?.map((metric, index) => ( +
+

{metric.value}

+

{metric.label}

+

{metric.subtext}

+
+ ))} +
+
+
+ + ); +}; + +export default CodeSlide10MetricsSplit; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/DescriptionTextSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/DescriptionTextSlide.tsx new file mode 100644 index 00000000..a897992c --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/DescriptionTextSlide.tsx @@ -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; + +const CodeSlide08CodeExplanationText = ({ data }: { data: Partial }) => { + + return ( + <> + +
+ + +

{data.title}

+
+

{data.descriptionTitle}

+

{data.description}

+
+
+ + ); +}; + +export default CodeSlide08CodeExplanationText; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/MetricsGridSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/MetricsGridSlide.tsx new file mode 100644 index 00000000..6218e233 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/MetricsGridSlide.tsx @@ -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; + +const CodeSlide11MetricsGrid = ({ data }: { data: Partial }) => { + + return ( + <> + +
+ + + +

{data.title}

+ +
+ {data?.metrics?.map((metric, index) => ( +
+

{metric.value}

+

{metric.label}

+

{metric.subtext}

+
+ ))} +
+
+ + ); +}; + +export default CodeSlide11MetricsGrid; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/TableOfContentSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/TableOfContentSlide.tsx new file mode 100644 index 00000000..a5cc7539 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/TableOfContentSlide.tsx @@ -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; + +function TocColumn({ items }: { items: { number: string; label: string, description?: string }[] }) { + return ( +
+ {items.map((item, index) => { + + + return ( +
+
+
+ +
+

{item.number}

+
+
+

{item.label}

+ {item.description &&

{item.description}

} +
+
+ ); + })} +
+ ); +} + +const CodeSlide09TableOfContent = ({ data }: { data: Partial }) => { + const leftItems = data?.items?.slice(0, data?.items?.length / 2); + const rightItems = data?.items?.slice(data?.items?.length / 2); + + + return ( + <> + +
+ + +
+

{data.title}

+ +
+ + +
+
+
+ + ); +}; + +export default CodeSlide09TableOfContent; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/TableSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/TableSlide.tsx new file mode 100644 index 00000000..579feac6 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/TableSlide.tsx @@ -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; + +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 ; + } + + return ( + + {value} + + ); +} + +const CodeSlide05ComparisonTable = ({ data }: { data: Partial }) => { + 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 ( + <> + +
+ +

{data.title}

+ +
+
+ + {tableColumns.map((column, columnIndex) => ( +

+ {column} +

+ ))} +
+ +
+ {rows.map((row, rowIndex) => ( +
+ {row.cells.map((cell, cellIndex) => ( +
+ {renderCell(cell, cellIndex === 0)} +
+ ))} +
+ ))} +
+ +
+
+ + ); +}; + +export default CodeSlide05ComparisonTable; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/TwoColumnBulletListSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/TwoColumnBulletListSlide.tsx new file mode 100644 index 00000000..cfdab86f --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/TwoColumnBulletListSlide.tsx @@ -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; + +const CodeSlide07UseCaseList = ({ data }: { data: Partial }) => { + + return ( + <> + +
+ + +

{data.title}

+ +
+ {data?.items?.map((item, index) => ( +
+ + {index + 1} + +

{item}

+
+ ))} +
+
+ + ); +}; + +export default CodeSlide07UseCaseList; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/WorkflowSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/WorkflowSlide.tsx new file mode 100644 index 00000000..a9e45f54 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/WorkflowSlide.tsx @@ -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; + +const CodeSlide06Workflow = ({ data }: { data: Partial }) => { + + return ( + <> + +
+ +

{data.title}

+ +
+ {data?.steps?.map((step, index) => ( + +
+
+ + {/* {step.icon.__icon_query__} */} +
+

{step.title}

+

{step.description}

+
+ {index < (data?.steps?.length || 0) - 1 && ( + + + + + )} +
+ ))} + +
+
+ + ); +}; + +export default CodeSlide06Workflow; diff --git a/electron/servers/nextjs/app/presentation-templates/Code/settings.json b/electron/servers/nextjs/app/presentation-templates/Code/settings.json new file mode 100644 index 00000000..e73cc025 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Code/settings.json @@ -0,0 +1,5 @@ +{ + "description": "Developer-focused layouts for roadmaps, APIs, code explanations, and technical metrics", + "ordered": false, + "default": false +} diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationAboutSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationAboutSlide.tsx new file mode 100644 index 00000000..5e2434a9 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationAboutSlide.tsx @@ -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; + +const EducationAboutSlide = ({ data }: { data: Partial }) => { + + + return (<> + + +
+
+
+

+ {data.name} +

+

+ {data.intro} +

+

+ {data.body} +

+
+ +
+
+ {data.topFeatureImage?.__image_prompt__} +
+
+

+ {data.topPanelText} +

+
+
+ +
+
+ +
+ {data.bottomFeatureImage?.__image_prompt__} +
+ +
+
+
+

+ {data.bottomPanelText} +

+
+
+
+
+
+ + ); +}; + +export default EducationAboutSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationChartPrimitives.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationChartPrimitives.tsx new file mode 100644 index 00000000..e0921663 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationChartPrimitives.tsx @@ -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; +}; + +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 ( + + {labelText} + + ); +}; + + + + + +function getChartColor(index: number) { + return DEFAULT_COLORS[index % DEFAULT_COLORS.length]; +} + + +function ChartLegend({ showLegend }: { showLegend: boolean }) { + if (!showLegend) { + return null; + } + + return ; +} + +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 ( + + + + + + {simpleData.map((_, index) => ( + + ))} + + + + + + ); + + case "bar-horizontal": + return ( + + + + + + + + + {simpleData.map((_, index) => ( + + ))} + + + + ); + + + + + + + + + + + case "line": + return ( + + + + + + + + + + + ); + + case "area": + return ( + + + + + + + + + + + + + + + + + ); + + + + case "pie": + return ( + + + + + {simpleData.map((_, index) => ( + + ))} + + + + ); + + case "donut": + return ( + + + + + {simpleData.map((_, index) => ( + + ))} + + + + ); + + case "scatter": { + const scatterData = toScatterData(chartData); + const labelMap = new Map(); + 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 ( + + + + labelMap.get(Number(value)) ?? String(value)} + /> + + + + + {scatterData.map((_, index) => ( + + ))} + + + + ); + } + + default: + return ( +
+ Unsupported chart type +
+ ); + } + })(); + + return {chart}; +} diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationContentSplitSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationContentSplitSlide.tsx new file mode 100644 index 00000000..015ef894 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationContentSplitSlide.tsx @@ -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; + +const EducationContentSplitSlide = ({ data }: { data: Partial }) => { + const { heading, tagline, body, images } = data; + + return ( + <> + + +
+
+
+
+ {images?.[0]?.__image_prompt__} +
+
+
{images?.[1]?.__image_prompt__}
+
{images?.[2]?.__image_prompt__}
+
+
+ +
+

{heading}

+

+ {tagline} +

+

{body}

+
+
+
+ + ); +}; + +export default EducationContentSplitSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationCoverSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationCoverSlide.tsx new file mode 100644 index 00000000..88e1c4f8 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationCoverSlide.tsx @@ -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; + +const EducationCoverSlide = ({ data }: { data: Partial }) => { + const { name, title, backgroundImage } = data; + + return ( + <> + + +
+ {backgroundImage?.__image_prompt__} + +
+ + +
+ {name &&

{name}

+ }

+ {title} +

+
+
+ + ); +}; + +export default EducationCoverSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationImageGallerySlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationImageGallerySlide.tsx new file mode 100644 index 00000000..e1f6bd38 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationImageGallerySlide.tsx @@ -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; + +const EducationImageGallerySlide = ({ data }: { data: Partial }) => { + + const { title, body, galleryImages } = data; + + return ( + <> + + +
+
+
+ {galleryImages?.[0].__image_prompt__} + {galleryImages?.[1].__image_prompt__} + {galleryImages?.[2].__image_prompt__} + {galleryImages?.[3].__image_prompt__} + {galleryImages?.[4].__image_prompt__} +
+ +
+

+ {title} +

+

+ {body} +

+
+
+
+ + ); +}; + +export default EducationImageGallerySlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationReportChartSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationReportChartSlide.tsx new file mode 100644 index 00000000..b8387f3a --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationReportChartSlide.tsx @@ -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; + + + +const EducationReportChartSlide = ({ data }: { data: Partial }) => { + const slideData = data; + + const chartHeightClass = slideData.showStatusMessage ? "h-[372px]" : "h-[486px]"; + + return ( + <> + + +
+
+
+
+

+ {slideData.title} +

+

+ {slideData.body} +

+
+ +

+ {slideData.footnote} +

+
+ +
+

+ {slideData.chartTitle} +

+

+ {slideData.dateRange} +

+ +
+ +
+
+
+
+ + ); +}; + +export default EducationReportChartSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationServicesSplitSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationServicesSplitSlide.tsx new file mode 100644 index 00000000..fa3b9323 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationServicesSplitSlide.tsx @@ -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; + +const EducationServicesSplitSlide = ({ data }: { data: Partial }) => { + const { title, sections } = data; + + + return ( + <> + + +
+
+
+

+ {title} +

+
+ + + +
+ {sections?.map((section, index) => ( +
+
+ + {section.image?.__image_prompt__} +
+
+

{section.heading}

+

+ {section.tagline} +

+

+ {section.body} +

+
+
+ ))} +
+
+
+ + ); +}; + +export default EducationServicesSplitSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationStatisticsGridSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationStatisticsGridSlide.tsx new file mode 100644 index 00000000..3a76a0e5 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationStatisticsGridSlide.tsx @@ -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; + + + +const EducationStatisticsGridSlide = ({ data }: { data: Partial }) => { + + + return ( + <> + + +
+
+
+
+

+ {data.title} +

+

+ {data.description} +

+
+
+ + {data.stats && data.stats?.length <= 4 &&
+ {data.stats?.map((stat, index) => ( +
+

+ {stat?.value} +

+

+ {stat?.label} +

+
+ ))} +
} + + + + {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 ( +
+
+ + {leftArray?.map((stat: any, index: number) => ( +
+

+ {stat?.value} +

+

+ {stat?.label} +

+
+ ))} +
+
+ + {rightArray?.map((stat: any, index: number) => ( +
+

+ {stat.value} +

+

+ {stat.label} +

+
+ ))} +
+
+ ); + })()} +
+
+ + ); +}; + +export default EducationStatisticsGridSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationTableOfContentsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationTableOfContentsSlide.tsx new file mode 100644 index 00000000..5c3f25cd --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationTableOfContentsSlide.tsx @@ -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; + +const EducationTableOfContentsSlide = ({ data }: { data: Partial }) => { + + return ( + <> + + +
+ + +
+
+

+ {data.title} +

+
+ +
+
+ {data.items?.map((item, index) => ( +
+ + {item.number} + + + {item.label} + +
+ ))} +
+
+
+
+ + ); +}; + +export default EducationTableOfContentsSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationTimelineSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationTimelineSlide.tsx new file mode 100644 index 00000000..01de94d1 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationTimelineSlide.tsx @@ -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; + +const EducationTimelineSlide = ({ data }: { data: Partial }) => { + + const { title, milestones } = data; + + const isSixOrLess = milestones?.length && milestones?.length <= 6; + + return ( + <> + + +
+
+

+ {title} +

+
+ + {isSixOrLess ? ( + + ) : ( + + )} +
+ + ); +}; + +function TimelineUpToSix({ + milestones, +}: { + milestones: any; +}) { + + + return ( + +
+
+ {milestones.map((milestone: any, index: number) => ( +
+
+ +
+ {index !== milestones.length - 1 &&
} +
+

+ {milestone.heading} +

+

+ {milestone.description} +

+
+ ))} +
+ +
+ ); +} + +function TimelineMoreThanSix({ + milestones, +}: { + milestones: SchemaType["milestones"]; +}) { + const topItems = milestones.slice(0, 6); + const bottomItems = milestones.slice(6); + + return ( +
+ {/* vertical connector on left */} + + + + + + + {/* bottom horizontal line */} + {/*
*/} +
+
+ {topItems.map((milestone: any, index: number) => ( +
+
+ +
+ {index !== milestones.length - 1 &&
} +
+
+ +

+ {milestone.heading} +

+

+ {milestone.description} +

+
+
+ ))} +
+ +
+ + {/* bottom row */} +
+ {bottomItems.map((_, colIndex) => { + const item = bottomItems[colIndex]; + if (!item) return
; + + return ( +
+
+ {/* {colIndex === 0 &&
} */} +
+
+
+
+ +

+ {item.heading} +

+

+ {item.description} +

+
+
+ ); + })} +
+
+ ); +} + +export default EducationTimelineSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/settings.json b/electron/servers/nextjs/app/presentation-templates/Education/settings.json new file mode 100644 index 00000000..42d837e2 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/settings.json @@ -0,0 +1,5 @@ +{ + "description": "School and training layouts for covers, outlines, timelines, statistics, and visual storytelling", + "ordered": false, + "default": false +} diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/BusinessChallengesCardsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/BusinessChallengesCardsSlide.tsx new file mode 100644 index 00000000..39726418 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/BusinessChallengesCardsSlide.tsx @@ -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; + +const BusinessChallengesCardsSlide = ({ data }: { data: Partial }) => { + const { title, taglineLabel, taglineBody, heroImage, cards } = data; + + return ( + <> + +
+
+

+ {title} +

+ +
+

+ {taglineLabel} +

+

+ {taglineBody} +

+
+
+ + {heroImage?.__image_url__ && ( + {heroImage.__image_prompt__} + )} + +
+ {cards?.map((card, index) => ( +
+

+ {card.heading} +

+

+ {card.body} +

+
+ ))} +
+
+ + ); +}; + +export default BusinessChallengesCardsSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/BusinessChallengesGridSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/BusinessChallengesGridSlide.tsx new file mode 100644 index 00000000..56e8e510 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/BusinessChallengesGridSlide.tsx @@ -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; + +const BusinessChallengesGridSlide = ({ data }: { data: Partial }) => { + const { title, blocks } = data; + + return ( + <> + +
+
+

+ {title} +

+
+ +
+ {blocks?.map((block, index) => ( +
+

+ {block.heading} +

+

+ {block.body} +

+
+ ))} +
+
+ + ); +}; + +export default BusinessChallengesGridSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonChartSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonChartSlide.tsx new file mode 100644 index 00000000..5d1f5e59 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonChartSlide.tsx @@ -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; +type CellStatus = z.infer; + +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 ; + } + + if (status === "cross") { + return {crossIconAlt}; + } + if (status === 'check') { + + + return {checkIconAlt}; + } + return

{status}

+} + +const ComparisonChartSlide = ({ data }: { data: Partial }) => { + 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 ( + <> + +
+
+

+ {title} +

+

+ {subtitle} +

+
+ +
+
+
+ {safeColumns.map((column: any, index: any) => ( +
+ {column} +
+ ))} +
+ + {normalizedRows.map((row, index) => { + return ( +
+
+ {row.label} +
+ + {row.cells.map((status, cellIndex) => ( +
+ +
+ ))} +
+ ); + })} +
+
+ + ); +}; + +export default ComparisonChartSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonTableWithTextSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonTableWithTextSlide.tsx new file mode 100644 index 00000000..d14ccb54 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonTableWithTextSlide.tsx @@ -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[] = [ + { + 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; + +const ComparisonTableWithTextSlide = ({ data }: { data: Partial }) => { + 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 ( + <> + +
+
+

+ {title} +

+

+ {subtitle} +

+
+ +
+ + + + {safeColumns.map((column, index) => { + const isHighlighted = index + 1 === resolvedHighlightedHeaderIndex; + return ( + + ); + })} + + + + + {normalizedRows.map((cells, rowIndex) => { + return ( + + {cells?.map((cell, cellIndex) => ( + + ))} + + ); + })} + +
+ {column} +
+ {cell} +
+
+
+ + ); +}; + +export default ComparisonTableWithTextSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/CoverSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/CoverSlide.tsx new file mode 100644 index 00000000..f1629236 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/CoverSlide.tsx @@ -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; + +const CoverSlide = ({ data }: { data: Partial }) => { + + + return ( + <> + +
+
+
+ + {data.image?.__image_url__ ? {data.image?.__image_prompt__ :

} + +

+ {data.label || ''} +

+
+ +
+

+

{data.titleLine1}

+

{data.titleLine2}

+

+
+
+ + {data.backgroundImage?.__image_url__ && ( + {data.backgroundImage.__image_prompt__ + )} + +
+
+ + ); +}; + +export default CoverSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ImageGallerySlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ImageGallerySlide.tsx new file mode 100644 index 00000000..6639e3d2 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ImageGallerySlide.tsx @@ -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; + +const ImageGallerySlide = ({ data }: { data: Partial }) => { + const { + title, + description, + topCenterImage, + topRightImage, + bottomWideImage, + bottomCenterImage, + bottomRightImage, + } = data; + + return ( + <> + +
+
+ +
+

+ {title} +

+

+ {description} +

+
+
+ {topCenterImage?.__image_prompt__ + {topRightImage?.__image_prompt__ +
+
+ + +
+ {bottomWideImage?.__image_prompt__} + {bottomCenterImage?.__image_prompt__} + {bottomRightImage?.__image_prompt__} +
+
+ + ); +}; + +export default ImageGallerySlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/IntroductionSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/IntroductionSlide.tsx new file mode 100644 index 00000000..b87bbc34 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/IntroductionSlide.tsx @@ -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; + +const IntroductionSlide = ({ data }: { data: Partial }) => { + const { title, portraitImage, blocks } = data; + + return ( + <> + +
+
+
+ + {portraitImage?.__image_url__ && ( + {portraitImage.__image_prompt__} + )} +
+ +
+

+ {title} +

+ +
+ {blocks?.map((block, index) => ( +
+

+ {block.label} +

+

+ {block.body} +

+
+ ))} +
+
+
+
+ + ); +}; + +export default IntroductionSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/KpiCardsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/KpiCardsSlide.tsx new file mode 100644 index 00000000..03c31739 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/KpiCardsSlide.tsx @@ -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; + +const KpiCardsSlide = ({ data }: { data: Partial }) => { + const { title, kpiIcon, backgroundImage, items } = data; + + return ( + <> + +
+ {backgroundImage?.__image_url__ && ( + {backgroundImage?.__image_prompt__} + )} + +
+ +
+

+ {title} +

+
+ +
+ {items?.map((item, index) => ( +
+
+ {/* {kpiIcon?.__icon_query__} */} + + +
+

+ {item.value} +

+

+ {item.body} +

+
+ ))} +
+
+ + ); +}; + +export default KpiCardsSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/MarketOpportunitySlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/MarketOpportunitySlide.tsx new file mode 100644 index 00000000..cdaac001 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/MarketOpportunitySlide.tsx @@ -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; + +const COLORS = [ + "var(--graph-0,#5f7f79)", + "var(--graph-1,#1f5a4f)", + "var(--graph-2,#0d4f43)", + "var(--graph-3,#06463d)", +]; + +const MarketOpportunitySlide = ({ data }: { data: Partial }) => { + const { title, subtitle, bullets, values } = data; + + return ( + <> + +
+
+

+ {title} +

+

+ {subtitle} +

+
+ +
+ {bullets?.map((bullet, index) => ( +
+ +

+ {bullet.text} +

+ + +
+ ))} +
+ +
+ {values?.map((value, index) => ( +
+

+ {value} +

+
+ ))} +
+
+ + ); +}; + +export default MarketOpportunitySlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/MeetTeamSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/MeetTeamSlide.tsx new file mode 100644 index 00000000..6598ca40 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/MeetTeamSlide.tsx @@ -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; + +const MeetTeamSlide = ({ data }: { data: Partial }) => { + const { title, taglineLabel, taglineBody, members } = data; + + return ( + <> + +
+
+

+ {title} +

+ +
+

+ {taglineLabel} +

+

+ {taglineBody} +

+
+
+ +
+ {members?.map((member, index) => ( +
+ {member.image.__image_prompt__} +
+

+ {member.title} +

+

+ {member.name} +

+
+
+ ))} +
+
+ + ); +}; + +export default MeetTeamSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/MissionVisionSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/MissionVisionSlide.tsx new file mode 100644 index 00000000..d1ff0566 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/MissionVisionSlide.tsx @@ -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; + +const MissionVisionSlide = ({ data }: { data: Partial }) => { + + return ( + <> + +
+
+
+

+ {data.title} +

+
+ +
+

+ {data.topleftTextBlockLabel} +

+

+ {data.topleftTextBlockBody} +

+
+ +
+

+ {data.bottomleftTextBlockLabel} +

+

+ {data.bottomleftTextBlockBody} +

+
+
+ {data.image?.__image_url__ && ( + {data.image.__image_prompt__} + )} +
+
+
+ + ); +}; + +export default MissionVisionSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/OurServicesSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/OurServicesSlide.tsx new file mode 100644 index 00000000..90051b0c --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/OurServicesSlide.tsx @@ -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; + +const OurServicesSlide = ({ data }: { data: Partial }) => { + const { title, taglineLabel, taglineBody, featureImage, services } = data; + + return ( + <> + +
+
+
+ +

+ {title} +

+ +
+

+ {taglineLabel} +

+

+ {taglineBody} +

+
+
+
+ + {featureImage?.__image_url__ && ( + {featureImage?.__image_prompt__} + )} +
+
+ + + +
+ {services?.map((card, index) => ( +
+

+ {card.heading} +

+

+ {card.body} +

+
+ ))} +
+
+ + ); +}; + +export default OurServicesSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/PricingPlanSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/PricingPlanSlide.tsx new file mode 100644 index 00000000..eacce1ec --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/PricingPlanSlide.tsx @@ -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; + +const PricingPlanSlide = ({ data }: { data: Partial }) => { + const { title, featureIcon, plans } = data; + + return ( + <> + +
+
+

+ {title} +

+
+ +
+ {plans?.map((plan, index) => { + const active = plan.highlighted; + return ( +
+

+ {plan.price} +

+

+ {plan.description} +

+ +
+ {plan.features.map((feature, featureIndex) => ( +
+ + {/* {featureIcon?.__icon_query__} */} +

+ {feature} +

+
+ ))} +
+
+ ); + })} +
+
+ + ); +}; + +export default PricingPlanSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ProcessSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ProcessSlide.tsx new file mode 100644 index 00000000..f8b848a1 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ProcessSlide.tsx @@ -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; + + +const ProcessSlide = ({ data }: { data: Partial }) => { + const { title, steps } = data; + + return ( + <> + +
+
+

+ {title} +

+
+
+ {steps?.map((step, index) => { + if (index % 2 === 0) { + return ( +
+
+
+
+ + +
+ + + + +
+
+ + + +
+
+
+

+ {step.label} +

+

+ {step.body} +

+
+
+ ) + } + else { + return ( +
+
+

+ {step.label} +

+

+ {step.body} +

+
+ +
+
+ + + +
+ +
+
+
+ + + +
+
+
+ ) + } + })} +
+
+ + ); +}; + +export default ProcessSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ReportSnapshotSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ReportSnapshotSlide.tsx new file mode 100644 index 00000000..34d31063 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ReportSnapshotSlide.tsx @@ -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; + +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 = () => ( + +); + +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 ( + + + + {`${Math.round((percent ?? 0) * 100)}%`} + + + ); +}; + +const ReportSnapshotSlide = ({ data }: { data: Partial }) => { + 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 ( + <> + +
+
+ + + {sideImage?.__image_url__ && ( + {sideImage.__image_prompt__} + )} +
+
+

+ {title} +

+ +
+

+ {taglineLabel} +

+

+ {taglineBody} +

+
+
+
+
+

+ {chartTitle} +

+ + + {activeChartStyle === "mini-bars" && ( + <> +
+ + + + + `$${value}`} + axisLine={false} + tickLine={false} + tick={{ fill: "var(--background-text,#6C7271)", fontSize: 10 }} + /> + + + + +
+ +
+

+ {footerLabel} +

+

+ {footerValue} +

+
+ + )} + + {activeChartStyle === "donut" && ( +
+
+ + + + {(donutData ?? []).map((entry, index) => ( + + ))} + + + +
+ +
+ {(donutData ?? []).map((entry, index) => { + const percent = Math.round((entry.value / donutTotal) * 100); + return ( +
+
+ +

+ {legendLabels?.[index] ?? entry.name} +

+
+

+ {percent}% +

+
+ ); + })} +
+
+ )} + + {activeChartStyle === "grouped-bars" && ( +
+
+ + + + + + + + + + + + + +
+ +
+
+ +

+ {legendLabels?.[0] ?? "Option A"} +

+
+
+ +

+ {legendLabels?.[1] ?? "Option B"} +

+
+
+
+ )} + + {activeChartStyle === "dual-line" && ( +
+
+ + + + + + + + + +
+ +
+
+ +

+ {legendLabels?.[0] ?? "Option A"} +

+
+
+ +

+ {legendLabels?.[1] ?? "Option B"} +

+
+
+
+ )} +
+ +
+
+ {visibleMetricCards.map((metric, index) => ( +
+
+
+ {usePulseFallback ? ( + + ) : ( + + )} +
+

+ {metric.value} +

+
+

+ {metric.body} +

+
+ ))} +
+
+
+
+
+
+ + ); +}; + +export default ReportSnapshotSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/TableOfContentSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/TableOfContentSlide.tsx new file mode 100644 index 00000000..65857c42 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/TableOfContentSlide.tsx @@ -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; + +const TableOfContentSlide = ({ data }: { data: Partial }) => { + const { title, description, sections } = data; + + return ( + <> + +
+
+
+
3 ? 'space-y-[28px]' : 'space-y-[40px]'}`}> + {sections?.map((section, index) => ( +
+
+ +

+ {section.title} +

+ {section.description &&

+ {section.description} +

} +
+

+ {section.number} +

+
+ ))} +
+
+ +
+

+ {title} +

+

+ {description} +

+
+
+
+ + ); +}; + +export default TableOfContentSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/settings.json b/electron/servers/nextjs/app/presentation-templates/ProductOverview/settings.json new file mode 100644 index 00000000..64702b29 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/settings.json @@ -0,0 +1,5 @@ +{ + "description": "Product pitch layouts for cover, narrative, comparisons, KPIs, pricing, and team slides", + "ordered": false, + "default": false +} diff --git a/electron/servers/nextjs/app/presentation-templates/Report/BarChartWithBulletListWithTitleDescriptionIconSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/BarChartWithBulletListWithTitleDescriptionIconSlide.tsx new file mode 100644 index 00000000..762a6f19 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/BarChartWithBulletListWithTitleDescriptionIconSlide.tsx @@ -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; + +const DataAnalysisBarSlide = ({ data }: { data: Partial }) => { + const { title, itemIcon, items, chartData, legendLabel } = data; + const rows = chartData?.data ?? []; + const chartType = chartData?.type ?? "bar"; + const series = chartData?.series ?? []; + + return ( + <> + + +
+
+ +
+

+ {title} +

+
+ +
+
+ {items?.map((item, index) => ( +
+
+
+ + {/* {itemIcon?.__icon_query__} */} +
+

+ {item.title} +

+
+

+ {item.description} +

+
+ ))} +
+ +
+
+ + + +
+
+ +

{legendLabel}

+
+
+
+
+ + ); +}; + +export default DataAnalysisBarSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/BulletListWithIconTitleDescriptionSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/BulletListWithIconTitleDescriptionSlide.tsx new file mode 100644 index 00000000..67d4d84c --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/BulletListWithIconTitleDescriptionSlide.tsx @@ -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; + +const DataAnalysisListSlide = ({ data }: { data: Partial }) => { + const { title, itemIcon, items } = data; + + return ( + <> + +
+
+ +
+

+ {title} +

+
+ +
+ {items?.map((item, index) => ( +
+
+
+ + {/* {itemIcon?.__icon_query__} */} +
+

+ {item.title} +

+
+

+ {item.description} +

+
+ ))} +
+
+ + ); +}; + +export default DataAnalysisListSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx new file mode 100644 index 00000000..92f1ac42 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx @@ -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; + + + + + + + + +function SummaryCard({ + value, + label, + iconUrl, + iconAlt, +}: { + value: string; + label: string; + iconUrl?: string; + iconAlt?: string; +}) { + return ( +
+
+ + {/* {iconAlt */} +
+
+

+ {value} +

+

+ {label} +

+
+
+ ); +} + + + + + +const DataAnalysisDashboardSlide = ({ data }: { data: Partial }) => { + 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 ( + <> + +
+
+ +
+

+ {title} +

+
+ + {summaryCards && summaryCards.length > 0 &&
+ {summaryCards?.map((card, index) => ( + + ))} +
} +
+ + {halfChart && halfChart.length > 0 &&
+
+ {halfChart?.map((chart, index) => ( +
+ +
+ +
+
+ ))} +
+
} + {otherHalfChart && otherHalfChart.length > 0 &&
+
+ {otherHalfChart?.map((chart, index) => ( +
+
+ + + + +
+
+ ))} +
+
} +
+
+ + ); +}; + +export default DataAnalysisDashboardSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/HorizontalHeightSpanningImagesWithTitleSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/HorizontalHeightSpanningImagesWithTitleSlide.tsx new file mode 100644 index 00000000..bec43bff --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/HorizontalHeightSpanningImagesWithTitleSlide.tsx @@ -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; + +const TeamSlide = ({ data }: { data: Partial }) => { + + return ( + <> + +
+
+ {data?.members?.map((member, index) => ( +
+ {member.image.__image_prompt__} +
+
+

+ {member.title} +

+

+ {member.subtext} +

+
+
+ ))} +
+
+ + ); +}; + +export default TeamSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/IntroCoverSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/IntroCoverSlide.tsx new file mode 100644 index 00000000..a53a5a45 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/IntroCoverSlide.tsx @@ -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; +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 }) => { + const { titleFirstLine, titleSecondLine, name, position } = data; + + return ( + <> + +
+ + + + + + + + + +
+

+ {titleFirstLine} +

+

+ {titleSecondLine} +

+
+
+
+

{name}

+

{position}

+
+ +
+ + ) +} + +export default IntroSlide diff --git a/electron/servers/nextjs/app/presentation-templates/Report/MetricsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/MetricsSlide.tsx new file mode 100644 index 00000000..ae8e5f6e --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/MetricsSlide.tsx @@ -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; + +type StatMetric = { + value: string; + label?: string; + description?: string; +}; + +function StatPill({ + metrics, + +}: { + metrics: StatMetric[]; + +}) { + + + return ( +
+ + {metrics.map((metric, index) => ( + +
+

+ {metric.value} +

+ {metric.label &&

{metric.label}

} + {metric.description &&

+ {metric.description} +

} +
+ {index === 0 &&
+ + + + +
+ } +
+ ))} + + +
+ ); +} + +const IntroductionStatsSlide = ({ data }: { data: Partial }) => { + + const { title, body, bullets, statColumns } = data; + + return ( + <> + +
+
+ +
+

+ {title} +

+
+ +
+
+

+ {body} +

+ +
+ {bullets?.map((bullet, index) => ( +
+

+ {bullet} +

+
+ ))} +
+
+ +
+ {statColumns?.map((column, index) => ( + + ))} +
+
+
+ + ); +}; + +export default IntroductionStatsSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx new file mode 100644 index 00000000..3cccddc7 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx @@ -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; + +const MilestoneSlide = ({ data }: { data: Partial }) => { + const { title, activeIndex, items } = data; + + return ( + <> + +
+
+ +
+

+ {title} +

+
+ +
+
+ {items?.map((item, index) => { + const isActive = index === activeIndex; + + return ( +
+ +
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)", + }} + > + + {item.bulletNumber} + + +
+
0 ? 'pr-[33px]' : ''} ${index === 0 ? 'px-[33px]' : ''}`} + style={{ color: "var(--background-text,#232223)" }} + > +

+ {item.heading} +

+

+ {item.description} +

+
+
+ ); + })} +
+
+
+ + ); +}; + +export default MilestoneSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/TitleChartWithMetricsCardsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/TitleChartWithMetricsCardsSlide.tsx new file mode 100644 index 00000000..fa51c24e --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/TitleChartWithMetricsCardsSlide.tsx @@ -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; + +type StatMetric = { + value: string; + label?: string; + description?: string; +}; + +function StatPill({ metrics }: { metrics: StatMetric[] }) { + return ( +
+ {metrics.map((metric, index) => ( +
+
+

{metric.value}

+ {metric.label &&

{metric.label}

} + {metric.description &&

+ {metric.description} +

} +
+ {index === 0 && ( +
+ + + +
+ )} +
+ ))} +
+ ); +} + +const DataAnalysisLineStatsSlide = ({ data }: { data: Partial }) => { + const { title, seriesALabel, seriesBLabel, chartData, statColumns, legendLabel } = data; + const rows = chartData?.data ?? []; + const chartType = chartData?.type ?? "line-dual"; + const series = chartData?.series ?? []; + + return ( + <> + +
+
+ +
+

+ {title} +

+
+ +
+
+ {chartType === "line-dual" &&
+ + + {seriesALabel} + + + + {seriesBLabel} + +
} + +
+ + + +
+ +
+ +

{data.legendLabel}

+
+
+ +
+ {statColumns?.map((column, index) => ( + + ))} +
+
+
+ + ); +}; + +export default DataAnalysisLineStatsSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/TitleDescriptionChartSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/TitleDescriptionChartSlide.tsx new file mode 100644 index 00000000..b623161a --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/TitleDescriptionChartSlide.tsx @@ -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; + +const DataAnalysisInsightBarSlide = ({ + data, +}: { + data: Partial; +}) => { + const chartData = data?.chartData?.data ?? []; + const chartType = data?.chartData?.type ?? "bar"; + const series = data?.chartData?.series ?? []; + + return ( + <> + +
+
+ +
+

+ {data.title} +

+
+ +
+
+
+
+ + + {/* {data.insightIcon?.__icon_query__} */} +
+
+

+ {data.insightBody} +

+
+ +
+ + + +
+ +

{data.legendLabel}

+
+
+
+
+ + ); +}; + +export default DataAnalysisInsightBarSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/TitleDescriptionImageSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/TitleDescriptionImageSlide.tsx new file mode 100644 index 00000000..eff3ed25 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/TitleDescriptionImageSlide.tsx @@ -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; + +const IntroductionImageSlide = ({ data }: { data: Partial }) => { + const { title, body, bullets, featureImage } = data; + + return ( + <> + +
+
+ +
+

+ {title} +

+
+ +
+
+

+ {body} +

+ +
+ {bullets?.map((bullet, index) => ( +
+

+ {bullet} +

+
+ ))} +
+
+ +
+
+ {featureImage?.__image_prompt__} +
+
+
+
+ + ); +}; + +export default IntroductionImageSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/TitleImageBulletCardsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/TitleImageBulletCardsSlide.tsx new file mode 100644 index 00000000..912f9ba6 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/TitleImageBulletCardsSlide.tsx @@ -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; + +type SolutionSlideProps = { + data: Partial; +}; + +function SolutionCard({ + stepNumber, + description, +}: { + stepNumber: string; + description: string; +}) { + return ( +
+

{stepNumber}

+

+ {description} +

+
+ ); +} + +const SolutionSlide = ({ data }: SolutionSlideProps) => { + const { title, showImage, featureImage, cards } = data; + const visibleCards = showImage ? cards?.slice(0, 2) : cards; + + + return ( + <> +
+
+ +
+ {title && ( +

+ {title} +

+ )} + +
+ {showImage ? ( +
+ {featureImage?.__image_url__ && ( +
+ {featureImage?.__image_prompt__} +
+ )} + +
+ {visibleCards?.map((card, index) => ( + + ))} +
+
+ ) : ( +
+ {visibleCards?.map((card, index) => ( + + ))} +
+ )} +
+
+
+ + ); +}; + +export default SolutionSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/TitleMetricsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/TitleMetricsSlide.tsx new file mode 100644 index 00000000..a35b2107 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/TitleMetricsSlide.tsx @@ -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; + +type StatMetric = { + value: string; + label?: string; + description?: string; +}; + +function StatPill({ + metrics, + +}: { + metrics: StatMetric[]; + +}) { + return ( + +
+ + {metrics.map((metric, index) => ( + +
+

+ {metric.value} +

+ {metric.label &&

{metric.label}

} + {metric.description &&

+ {metric.description} +

} +
+ {index === 0 &&
+ + + + +
+ } +
+ ))} + + +
+ ); +} + +const PerformanceSnapshotSlide = ({ data }: { data: Partial }) => { + const { title, columns } = data; + + return ( + <> + +
+
+ +
+

+ {title} +

+
+ +
+ {columns?.map((column, index) => ( + + ))} +
+
+ + ); +}; + +export default PerformanceSnapshotSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/TitleWorkflowWithTitleDescriptionSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/TitleWorkflowWithTitleDescriptionSlide.tsx new file mode 100644 index 00000000..2a877409 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/TitleWorkflowWithTitleDescriptionSlide.tsx @@ -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; + +function ServiceGlyph({ + iconUrl, + iconAlt, + isActive, +}: { + iconUrl: string; + iconAlt: string; + isActive: boolean; +}) { + return ( + {iconAlt} + ); +} + +const ServicesSlide = ({ data }: { data: Partial }) => { + + const { title, activeIndex, items } = data; + + return ( + <> + +
+
+ +
+

+ {title} +

+
+ +
+ {items?.map((item, index) => { + const isActive = index === activeIndex; + + return ( + +
+
+ {/* */} + +
+ +

+ {item.heading} +

+

+ {item.description} +

+
+ + {index < items?.length - 1 && ( +
+ {/*
+ + + */} + + + +
+ )} + + ); + })} +
+
+ + ); +}; + +export default ServicesSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx b/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx new file mode 100644 index 00000000..37164a1f --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx @@ -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; + +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 = { 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 ( + + + {/* */} + + {labelText} + + + ); + }; + + const commonProps = { + margin: ui.margin, + + }; + + switch (chartType) { + case "bar": + return ( + + + + + + + + + + + + + ); + + case "bar-horizontal": + return ( + + + + + + + + + + + + ); + + case "bar-grouped-vertical": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-grouped-horizontal": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-stacked-vertical": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-stacked-horizontal": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-clustered": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-diverging": { + const transformedData = transformDivergingData(normalizedData as any[]); + return ( + + + + + + + + + + + + + + + ); + } + + case "line": + return ( + + + + + + + + + + ); + + case "line-dual": + return ( + + + + + + + + + + ); + + case "area": + return ( + + + + + + + + + + + + + + + + + ); + + case "area-stacked": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + ))} + + + ); + } + + case "pie": + return ( + + + + + {(normalizedData as any[]).map((_, index) => ( + + ))} + + + + ); + + case "donut": + return ( + + + + + {(normalizedData as any[]).map((_, index) => ( + + ))} + + + + ); + + case "scatter": + return ( + + + + + + + + {scatterPoints.map((_, index) => ( + + ))} + + + + ); + + default: + return ( +
Unsupported chart type
+ ); + } +} diff --git a/electron/servers/nextjs/app/presentation-templates/Report/settings.json b/electron/servers/nextjs/app/presentation-templates/Report/settings.json new file mode 100644 index 00000000..d8f8abde --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/settings.json @@ -0,0 +1,5 @@ +{ + "description": "Data and narrative report layouts for intros, analysis charts, dashboards, and closing slides", + "ordered": false, + "default": false +} diff --git a/electron/servers/nextjs/app/presentation-templates/index.tsx b/electron/servers/nextjs/app/presentation-templates/index.tsx index 4af28183..a195904a 100644 --- a/electron/servers/nextjs/app/presentation-templates/index.tsx +++ b/electron/servers/nextjs/app/presentation-templates/index.tsx @@ -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, + }, + ];