diff --git a/servers/nextjs/app/api/layouts/route.ts b/servers/nextjs/app/api/layouts/route.ts new file mode 100644 index 00000000..8ab38ce6 --- /dev/null +++ b/servers/nextjs/app/api/layouts/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server' +import { promises as fs } from 'fs' +import path from 'path' + +export async function GET() { + try { + // Get the path to the layouts directory + const layoutsDirectory = path.join(process.cwd(), 'components', 'layouts') + + // Read all files in the layouts directory + const files = await fs.readdir(layoutsDirectory) + + // Filter for .tsx files and exclude any non-layout files + const layoutFiles = files.filter(file => + file.endsWith('.tsx') && + !file.startsWith('.') && + !file.includes('.test.') && + !file.includes('.spec.') + ) + + return NextResponse.json(layoutFiles) + } catch (error) { + console.error('Error reading layouts directory:', error) + return NextResponse.json( + { error: 'Failed to read layouts directory' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/page.tsx b/servers/nextjs/app/layout-preview/page.tsx new file mode 100644 index 00000000..651b17b4 --- /dev/null +++ b/servers/nextjs/app/layout-preview/page.tsx @@ -0,0 +1,463 @@ +'use client' +import { toast } from '@/hooks/use-toast' +import React, { useState, useEffect } from 'react' + +interface LayoutInfo { + name: string + component: React.ComponentType + schema: any + sampleData: any + fileName: string +} + +const LayoutPreview = () => { + const [layouts, setLayouts] = useState([]) + const [loading, setLoading] = useState(true) + const [currentLayout, setCurrentLayout] = useState(0) + const [error, setError] = useState(null) + + const generateSampleDataFromSchema = (schema: any, layoutName: string): any => { + if (!schema) return {} + + try { + // First, try to get defaults from schema + let sampleData = {} + try { + sampleData = schema.parse({}) + } catch { + // If parsing fails, we'll build the data manually + } + + // Generate realistic sample data based on schema shape + const enhancedData = generateRealisticData(schema._def?.shape || schema.shape, layoutName) + + // Merge defaults with enhanced data, giving priority to defaults + return { ...enhancedData, ...sampleData } + } catch (error) { + console.error(`Error generating sample data for ${layoutName}:`, error) + return {} + } + } + + const generateRealisticData = (shape: any, layoutName: string): any => { + if (!shape) return {} + + const data: any = {} + + for (const [key, fieldSchema] of Object.entries(shape as any)) { + const field = fieldSchema as any + + // Skip if field has a default value (will be handled by schema.parse) + if (field._def?.defaultValue !== undefined) { + continue + } + + data[key] = generateFieldValue(key, field, layoutName) + } + + return data + } + + const generateFieldValue = (fieldName: string, fieldSchema: any, layoutName: string): any => { + const fieldType = fieldSchema._def?.typeName + + // Handle optional fields (might not generate value for some) + if (fieldSchema._def?.innerType && Math.random() > 0.7) { + return undefined + } + + // Handle different field types + switch (fieldType) { + case 'ZodString': + return generateStringValue(fieldName, fieldSchema, layoutName) + case 'ZodArray': + return generateArrayValue(fieldName, fieldSchema, layoutName) + case 'ZodObject': + return generateObjectValue(fieldName, fieldSchema, layoutName) + case 'ZodEnum': + const options = fieldSchema._def?.values || [] + return options[Math.floor(Math.random() * options.length)] + case 'ZodBoolean': + return Math.random() > 0.5 + case 'ZodNumber': + return Math.floor(Math.random() * 100) + 1 + default: + return generateStringValue(fieldName, fieldSchema, layoutName) + } + } + + const generateStringValue = (fieldName: string, fieldSchema: any, layoutName: string): string => { + const lowerField = fieldName.toLowerCase() + + // Handle URLs (images, logos, backgrounds, etc.) + if (lowerField.includes('url') || lowerField.includes('image') || lowerField.includes('logo')) { + if (lowerField.includes('logo')) { + return 'https://images.unsplash.com/photo-1611224923853-80b023f02d71?w=200&h=200&fit=crop' + } + if (lowerField.includes('background')) { + const backgrounds = [ + 'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=1920&h=1080&fit=crop', + 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1920&h=1080&fit=crop', + 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=1920&h=1080&fit=crop' + ] + return backgrounds[Math.floor(Math.random() * backgrounds.length)] + } + // Regular images + const images = [ + 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=800&h=600&fit=crop', + 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop', + 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=800&h=600&fit=crop', + 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=800&h=600&fit=crop' + ] + return images[Math.floor(Math.random() * images.length)] + } + + // Handle email + if (lowerField.includes('email')) { + const domains = ['example.com', 'company.com', 'business.org'] + const names = ['contact', 'info', 'hello', 'support'] + return `${names[Math.floor(Math.random() * names.length)]}@${domains[Math.floor(Math.random() * domains.length)]}` + } + + // Handle phone + if (lowerField.includes('phone')) { + return `+1 (555) ${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 9000) + 1000}` + } + + // Handle website + if (lowerField.includes('website')) { + const sites = ['https://example.com', 'https://company.com', 'https://business.org'] + return sites[Math.floor(Math.random() * sites.length)] + } + + // Handle LinkedIn + if (lowerField.includes('linkedin')) { + return 'https://linkedin.com/company/example' + } + + // Handle specific field names + if (lowerField.includes('title')) { + const titles = [ + 'Welcome to Our Presentation', + 'Key Business Insights', + 'Product Overview', + 'Market Analysis', + 'Future Vision', + 'Strategic Goals' + ] + return titles[Math.floor(Math.random() * titles.length)] + } + + if (lowerField.includes('subtitle')) { + const subtitles = [ + 'Driving innovation through technology', + 'Transforming the way we work', + 'Building solutions for tomorrow', + 'Excellence in every detail', + 'Your success is our mission' + ] + return subtitles[Math.floor(Math.random() * subtitles.length)] + } + + if (lowerField.includes('author') || lowerField.includes('name')) { + const names = ['Alex Johnson', 'Sarah Chen', 'Michael Rodriguez', 'Emily Davis', 'David Kim'] + return names[Math.floor(Math.random() * names.length)] + } + + if (lowerField.includes('organization') || lowerField.includes('company')) { + const orgs = ['Tech Innovations Inc.', 'Future Solutions Ltd.', 'Global Dynamics Corp.', 'NextGen Enterprises'] + return orgs[Math.floor(Math.random() * orgs.length)] + } + + if (lowerField.includes('date')) { + return new Date().toLocaleDateString() + } + + if (lowerField.includes('content')) { + const contents = [ + 'Our innovative approach combines cutting-edge technology with proven methodologies to deliver exceptional results. We focus on scalability, reliability, and user experience.', + 'Through strategic partnerships and continuous innovation, we\'ve established ourselves as leaders in the industry. Our solutions are designed to meet evolving market demands.', + 'With over a decade of experience, our team brings deep expertise and fresh perspectives to every project. We\'re committed to exceeding expectations and driving growth.' + ] + return contents[Math.floor(Math.random() * contents.length)] + } + + if (lowerField.includes('caption')) { + const captions = [ + 'Innovative solutions driving business transformation', + 'Real-time analytics and insights at your fingertips', + 'Seamless integration with existing workflows', + 'Empowering teams to achieve more' + ] + return captions[Math.floor(Math.random() * captions.length)] + } + + if (lowerField.includes('action') || lowerField.includes('cta')) { + const actions = [ + 'Get Started Today!', + 'Schedule a Demo', + 'Contact Our Team', + 'Learn More', + 'Try It Free' + ] + return actions[Math.floor(Math.random() * actions.length)] + } + + // Default text based on field length constraints + const minLength = fieldSchema._def?.checks?.find((c: any) => c.kind === 'min')?.value || 10 + const maxLength = fieldSchema._def?.checks?.find((c: any) => c.kind === 'max')?.value || 100 + + if (maxLength <= 50) { + return 'Sample short text content' + } else if (maxLength <= 150) { + return 'This is sample medium-length text content for preview purposes' + } else { + return 'This is sample long-form text content that demonstrates how the layout will look with realistic data. It provides a good representation of the final presentation slide.' + } + } + + const generateArrayValue = (fieldName: string, fieldSchema: any, layoutName: string): any[] => { + const itemSchema = fieldSchema._def?.type + const minItems = fieldSchema._def?.minLength?.value || 2 + const maxItems = Math.min(fieldSchema._def?.maxLength?.value || 5, 6) + const itemCount = Math.floor(Math.random() * (maxItems - minItems + 1)) + minItems + + const lowerField = fieldName.toLowerCase() + + if (lowerField.includes('bullet') || lowerField.includes('point')) { + const bulletPoints = [ + 'Increased efficiency and productivity', + 'Cost-effective solutions', + 'Enhanced user experience', + 'Scalable architecture', + 'Real-time analytics', + '24/7 customer support', + 'Seamless integration capabilities', + 'Advanced security features' + ] + return bulletPoints.slice(0, itemCount) + } + + if (lowerField.includes('takeaway') || lowerField.includes('key')) { + const takeaways = [ + 'Strategic advantage through innovation', + 'Proven ROI within 6 months', + 'Comprehensive support included', + 'Future-ready technology stack', + 'Industry-leading performance' + ] + return takeaways.slice(0, itemCount) + } + + // Generate generic array items + const items = [] + for (let i = 0; i < itemCount; i++) { + if (itemSchema) { + items.push(generateFieldValue(`${fieldName}Item`, itemSchema, layoutName)) + } else { + items.push(`Sample item ${i + 1}`) + } + } + return items + } + + const generateObjectValue = (fieldName: string, fieldSchema: any, layoutName: string): any => { + const shape = fieldSchema._def?.shape + if (!shape) return {} + + const obj: any = {} + for (const [key, subSchema] of Object.entries(shape)) { + obj[key] = generateFieldValue(key, subSchema, layoutName) + } + return obj + } + + const loadAllLayouts = async () => { + try { + setLoading(true) + setError(null) + + const response = await fetch('/api/layouts') + if (!response.ok) { + throw new Error('Failed to fetch layout files') + } + + const layoutFiles: string[] = await response.json() + const loadedLayouts: LayoutInfo[] = [] + + for (const fileName of layoutFiles) { + try { + const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`../../components/layouts/${layoutName}`) + + if (!module.default) { + toast({ + title: `${layoutName} has no default export`, + description: 'Please ensure the layout file exports a default component', + + }) + console.warn(`${layoutName} has no default export`) + continue + } + + if (!module.Schema) { + toast({ + title: `${layoutName} is missing required Schema export`, + description: 'Please ensure the layout file exports a Schema', + variant: 'destructive' + }) + console.error(`${layoutName} is missing required Schema export`) + continue + } + + const sampleData = generateSampleDataFromSchema(module.Schema, layoutName) + + loadedLayouts.push({ + name: layoutName, + component: module.default, + schema: module.Schema, + sampleData, + fileName + }) + + } catch (importError) { + console.error(`Failed to import ${fileName}:`, importError) + + // Try alternative import path + try { + const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`@/components/layouts/${layoutName}`) + + if (module.default && module.Schema) { + const sampleData = generateSampleDataFromSchema(module.Schema, layoutName) + loadedLayouts.push({ + name: layoutName, + component: module.default, + schema: module.Schema, + sampleData, + fileName + }) + } else { + console.error(`${layoutName} is missing required exports (default component or Schema)`) + } + } catch (altError) { + console.error(`Alternative import also failed for ${fileName}:`, altError) + } + } + } + + if (loadedLayouts.length === 0) { + setError('No valid layouts found. Make sure your layout files export both a default component and a Schema.') + } else { + setLayouts(loadedLayouts) + } + + } catch (error) { + console.error('Error loading layouts:', error) + setError(error instanceof Error ? error.message : 'Failed to load layouts') + } finally { + setLoading(false) + } + } + + useEffect(() => { + loadAllLayouts() + }, []) + + if (loading) { + return ( +
+
Loading layouts...
+
+ ) + } + + if (error) { + return ( +
+
Error: {error}
+
+ ) + } + + if (layouts.length === 0) { + return ( +
+
No layouts found
+
+ ) + } + + const CurrentLayoutComponent = layouts[currentLayout]?.component + + return ( +
+ {/* Navigation */} +
+
+

Layout Preview

+
+ +
+ + +
+
+
+
+ + {/* Layout Display */} +
+ {CurrentLayoutComponent && ( + + )} +
+ + {/* Layout Info */} +
+
+

+ Current Layout: {layouts[currentLayout]?.name} +

+

+ {currentLayout + 1} of {layouts.length} layouts ({layouts[currentLayout]?.fileName}) +

+ {layouts[currentLayout]?.sampleData && ( +
+ + Sample Data Structure + +
+                                {JSON.stringify(layouts[currentLayout].sampleData, null, 2)}
+                            
+
+ )} +
+
+
+ ) +} + +export default LayoutPreview diff --git a/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx b/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx new file mode 100644 index 00000000..90c08022 --- /dev/null +++ b/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import { z } from "zod"; + +export const Schema = z.object({ + title: z.string().min(3).max(100).default('Key Points'), + subtitle: z.string().min(3).max(150).optional().describe("Optional subtitle or section header"), + bulletPoints: z.array(z.string().min(5).max(200)).min(1).max(8).default([ + 'First important point', + 'Second key insight', + 'Third crucial element' + ]).describe("List of bullet points (1-8 items)"), + backgroundImage: z.string().url().optional().default("https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4").describe("URL to background image for the slide") +}) + + + +export type BulletPointSlideData = z.infer + +interface BulletPointSlideLayoutProps { + data?: Partial +} + +const BulletPointSlideLayout: React.FC = ({ data }) => { + const slideData = Schema.parse(data || {}) + + return ( +
+ {slideData.backgroundImage && ( +
+ )} + +
+ {/* Header */} +
+

+ {slideData.title} +

+ {slideData.subtitle && ( +

+ {slideData.subtitle} +

+ )} +
+
+ + {/* Bullet Points */} +
+
+
    + {slideData.bulletPoints.map((point, index) => ( +
  • +
    + + {point} + +
  • + ))} +
+
+
+
+ + {/* Decorative accent */} +
+
+ ) +} + +export default BulletPointSlideLayout \ No newline at end of file diff --git a/servers/nextjs/components/layouts/ConclusionSlideLayout.tsx b/servers/nextjs/components/layouts/ConclusionSlideLayout.tsx new file mode 100644 index 00000000..11af8fc4 --- /dev/null +++ b/servers/nextjs/components/layouts/ConclusionSlideLayout.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import { z } from "zod"; + +const conclusionSlideSchema = z.object({ + title: z.string().min(3).max(100).default('Conclusion'), + subtitle: z.string().min(3).max(150).optional().describe("Optional subtitle or closing message"), + keyTakeaways: z.array(z.string().min(5).max(200)).min(1).max(5).default([ + 'First key takeaway', + 'Second important point', + 'Third crucial insight' + ]).describe("List of key takeaways (1-5 items)"), + callToAction: z.string().min(5).max(200).optional().describe("Call to action or next steps"), + contactInfo: z.object({ + email: z.string().email().optional(), + phone: z.string().optional(), + website: z.string().url().optional(), + linkedin: z.string().url().optional() + }).optional(), + backgroundImage: z.string().url().optional().default("https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4").describe("URL to background image for the slide") +}) + +// Standardized schema export +export const Schema = conclusionSlideSchema + +export type ConclusionSlideData = z.infer + +interface ConclusionSlideLayoutProps { + data?: Partial +} + +const ConclusionSlideLayout: React.FC = ({ data }) => { + const slideData = conclusionSlideSchema.parse(data || {}) + + return ( +
+ {slideData.backgroundImage && ( +
+ )} + +
+ {/* Main Title */} +

+ {slideData.title} +

+ + {/* Subtitle */} + {slideData.subtitle && ( +

+ {slideData.subtitle} +

+ )} + + {/* Key Takeaways */} +
+

+ Key Takeaways +

+
+ {slideData.keyTakeaways.map((takeaway, index) => ( +
+
+
+ {index + 1} +
+
+

+ {takeaway} +

+
+ ))} +
+
+ + {/* Call to Action */} + {slideData.callToAction && ( +
+
+

+ {slideData.callToAction} +

+
+
+ )} + + {/* Contact Information */} + {slideData.contactInfo && ( +
+

+ Get in Touch +

+
+ {slideData.contactInfo.email && ( +
+ Email: + {slideData.contactInfo.email} +
+ )} + {slideData.contactInfo.phone && ( +
+ Phone: + {slideData.contactInfo.phone} +
+ )} + {slideData.contactInfo.website && ( +
+ Website: + {slideData.contactInfo.website} +
+ )} + {slideData.contactInfo.linkedin && ( +
+ LinkedIn: + {slideData.contactInfo.linkedin} +
+ )} +
+
+ )} +
+ + {/* Decorative accent */} +
+
+ ) +} + +export default ConclusionSlideLayout \ No newline at end of file diff --git a/servers/nextjs/components/layouts/ContentSlideLayout.tsx b/servers/nextjs/components/layouts/ContentSlideLayout.tsx new file mode 100644 index 00000000..3570887d --- /dev/null +++ b/servers/nextjs/components/layouts/ContentSlideLayout.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { z } from "zod"; + +const contentSlideSchema = z.object({ + title: z.string().min(3).max(100).default('Content Title'), + content: z.string().min(10).max(2000).default('Your main content goes here...').describe("Main content text for the slide"), + subtitle: z.string().min(3).max(150).optional().default('Subtitle of the slide').describe("Optional subtitle or section header"), + backgroundImage: z.string().url().default("https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4").optional().describe("URL to background image for the slide") +}) + +// Standardized schema export +export const Schema = contentSlideSchema + +export type ContentSlideData = z.infer + +interface ContentSlideLayoutProps { + data?: Partial +} + +const ContentSlideLayout: React.FC = ({ data }) => { + const slideData = contentSlideSchema.parse(data || {}) + console.log(slideData) + + return ( +
+ {slideData.backgroundImage && ( +
+ )} + +
+ {/* Header */} +
+

+ {slideData.title} +

+ {slideData.subtitle && ( +

+ {slideData.subtitle} +

+ )} +
+
+ + {/* Content Area */} +
+
+
+ {slideData.content} +
+
+
+
+ + {/* Decorative accent */} +
+
+ ) +} + +export default ContentSlideLayout \ No newline at end of file diff --git a/servers/nextjs/components/layouts/FirstSlideLayout.tsx b/servers/nextjs/components/layouts/FirstSlideLayout.tsx new file mode 100644 index 00000000..7b1b029d --- /dev/null +++ b/servers/nextjs/components/layouts/FirstSlideLayout.tsx @@ -0,0 +1,89 @@ + + + +import React from 'react' +import { z } from "zod"; + +const firstSlideSchema = z.object({ + title: z.string().min(3).max(100).default('Welcome to Your Presentation'), + subtitle: z.string().min(3).max(150).optional().describe("Optional subtitle or tagline"), + author: z.string().min(2).max(50).default('Your Name').describe("Author or presenter name"), + organization: z.string().min(2).max(100).optional().describe("Organization or company name"), + date: z.string().default(new Date().toLocaleDateString()).describe("Presentation date"), + logoUrl: z.string().url().optional().describe("URL to company/organization logo"), + backgroundImage: z.string().url().default("https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4").optional().describe("URL to background image for the slide") +}) + +// Standardized schema export +export const Schema = firstSlideSchema + +export type FirstSlideData = z.infer + +interface FirstSlideLayoutProps { + data?: Partial +} + +const FirstSlideLayout: React.FC = ({ data }) => { + const slideData = firstSlideSchema.parse(data || {}) + + return ( +
+ {slideData.backgroundImage && ( +
+ )} + +
+ {/* Logo */} + {slideData.logoUrl && ( +
+ Logo +
+ )} + + {/* Main Title */} +

+ {slideData.title} +

+ + {/* Subtitle */} + {slideData.subtitle && ( +

+ {slideData.subtitle} +

+ )} + + {/* Author and Organization */} +
+
{slideData.author}
+ {slideData.organization && ( +
+ {slideData.organization} +
+ )} +
+ + {/* Date */} +
+ {slideData.date} +
+
+ + {/* Decorative accent */} +
+
+ ) +} + +export default FirstSlideLayout + diff --git a/servers/nextjs/components/layouts/ImageSlideLayout.tsx b/servers/nextjs/components/layouts/ImageSlideLayout.tsx new file mode 100644 index 00000000..9df23fed --- /dev/null +++ b/servers/nextjs/components/layouts/ImageSlideLayout.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import { z } from "zod"; + +const imageSlideSchema = z.object({ + title: z.string().min(3).max(100).default('Image Slide'), + subtitle: z.string().min(3).max(150).optional().describe("Optional subtitle or caption"), + imageUrl: z.string().url().default('https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4').describe("URL to the main image"), + caption: z.string().min(5).max(300).optional().describe("Image caption or description"), + layout: z.enum(['full', 'centered', 'left', 'right']).default('centered').describe("Image layout style"), + backgroundImage: z.string().url().default("https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4").optional().describe("URL to background image for the slide") +}) + +// Standardized schema export +export const Schema = imageSlideSchema + +export type ImageSlideData = z.infer + +interface ImageSlideLayoutProps { + data?: Partial +} + +const ImageSlideLayout: React.FC = ({ data }) => { + const slideData = imageSlideSchema.parse(data || {}) + + const getImageClasses = () => { + switch (slideData.layout) { + case 'full': + return 'w-full h-full object-cover' + case 'left': + return 'w-1/2 h-auto max-h-96 object-contain rounded-lg shadow-lg' + case 'right': + return 'w-1/2 h-auto max-h-96 object-contain rounded-lg shadow-lg' + default: // centered + return 'max-w-4xl max-h-96 w-auto h-auto object-contain rounded-lg shadow-lg' + } + } + + const getContainerClasses = () => { + switch (slideData.layout) { + case 'full': + return 'relative w-full h-full' + case 'left': + return 'flex items-center space-x-16' + case 'right': + return 'flex items-center space-x-16 flex-row-reverse' + default: // centered + return 'flex flex-col items-center space-y-8' + } + } + + return ( +
+ {slideData.backgroundImage && ( +
+ )} + +
+ {/* Header - only show if not full layout */} + {slideData.layout !== 'full' && ( +
+

+ {slideData.title} +

+ {slideData.subtitle && ( +

+ {slideData.subtitle} +

+ )} +
+
+ )} + + {/* Content Area */} +
+ {slideData.layout === 'full' && ( +
+

+ {slideData.title} +

+ {slideData.subtitle && ( +

+ {slideData.subtitle} +

+ )} +
+ )} + + {slideData.title} + + {(slideData.layout === 'left' || slideData.layout === 'right') && ( +
+ {slideData.caption && ( +

+ {slideData.caption} +

+ )} +
+ )} +
+ + {/* Caption for centered and full layouts */} + {slideData.caption && (slideData.layout === 'centered' || slideData.layout === 'full') && ( +
+

+ {slideData.caption} +

+
+ )} +
+ + {/* Decorative accent */} +
+
+ ) +} + +export default ImageSlideLayout \ No newline at end of file diff --git a/servers/nextjs/components/layouts/TwoColumnSlideLayout.tsx b/servers/nextjs/components/layouts/TwoColumnSlideLayout.tsx new file mode 100644 index 00000000..ab8b0ae4 --- /dev/null +++ b/servers/nextjs/components/layouts/TwoColumnSlideLayout.tsx @@ -0,0 +1,96 @@ +import React from 'react' +import { z } from "zod"; + +const twoColumnSlideSchema = z.object({ + title: z.string().min(3).max(100).default('Two Column Layout'), + subtitle: z.string().min(3).max(150).optional().describe("Optional subtitle or section header"), + leftColumn: z.object({ + title: z.string().min(2).max(50).default('Left Column'), + content: z.string().min(10).max(1000).default('Content for the left column...').describe("Content for the left column") + }).default({ + title: 'Left Column', + content: 'Content for the left column...' + }), + rightColumn: z.object({ + title: z.string().min(2).max(50).default('Right Column'), + content: z.string().min(10).max(1000).default('Content for the right column...').describe("Content for the right column") + }).default({ + title: 'Right Column', + content: 'Content for the right column...' + }), + backgroundImage: z.string().url().default("https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4").optional().describe("URL to background image for the slide") +}) + +// Standardized schema export +export const Schema = twoColumnSlideSchema + +export type TwoColumnSlideData = z.infer + +interface TwoColumnSlideLayoutProps { + data?: Partial +} + +const TwoColumnSlideLayout: React.FC = ({ data }) => { + const slideData = twoColumnSlideSchema.parse(data || {}) + + return ( +
+ {slideData.backgroundImage && ( +
+ )} + +
+ {/* Header */} +
+

+ {slideData.title} +

+ {slideData.subtitle && ( +

+ {slideData.subtitle} +

+ )} +
+
+ + {/* Two Columns */} +
+ {/* Left Column */} +
+

+ {slideData.leftColumn.title} +

+
+ {slideData.leftColumn.content} +
+
+ + {/* Divider */} +
+ + {/* Right Column */} +
+

+ {slideData.rightColumn.title} +

+
+ {slideData.rightColumn.content} +
+
+
+
+ + {/* Decorative accent */} +
+
+ ) +} + +export default TwoColumnSlideLayout \ No newline at end of file diff --git a/servers/nextjs/package-lock.json b/servers/nextjs/package-lock.json index ffb80ae7..6b05f586 100644 --- a/servers/nextjs/package-lock.json +++ b/servers/nextjs/package-lock.json @@ -39,12 +39,11 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", - "html-to-image": "^1.11.13", "jsonrepair": "^3.12.0", "lucide-react": "^0.447.0", "marked": "^15.0.11", "next": "^14.2.14", - "puppeteer": "^24.8.2", + "puppeteer": "24.8.2", "react": "^18", "react-dom": "^18", "react-redux": "^9.1.2", @@ -52,7 +51,8 @@ "tailwind-merge": "^2.5.3", "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss-animate": "^1.0.7", - "tiptap-markdown": "^0.8.10" + "tiptap-markdown": "^0.8.10", + "zod": "^4.0.5" }, "devDependencies": { "@types/animejs": "^3.1.12", @@ -588,16 +588,16 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.10.5", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", - "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.4.tgz", + "integrity": "sha512-9DxbZx+XGMNdjBynIs4BRSz+M3iRDeB7qRcAr6UORFLphCIM2x3DXgOucvADiifcqCE4XePFUKcnaAMyGbrDlQ==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.1", + "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.7.2", + "semver": "^7.7.1", "tar-fs": "^3.0.8", "yargs": "^17.7.2" }, @@ -3013,6 +3013,15 @@ "devtools-protocol": "*" } }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/ci-info": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", @@ -3546,9 +3555,9 @@ "license": "MIT" }, "node_modules/devtools-protocol": { - "version": "0.0.1464554", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", - "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "version": "0.0.1439962", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1439962.tgz", + "integrity": "sha512-jJF48UdryzKiWhJ1bLKr7BFWUQCEIT5uCNbDLqkQJBtkFxYzILJH44WN0PDKMIlGDN7Utb8vyUY85C3w4R/t2g==", "license": "BSD-3-Clause" }, "node_modules/didyoumean": { @@ -4308,12 +4317,6 @@ "node": ">= 0.4" } }, - "node_modules/html-to-image": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", - "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", - "license": "MIT" - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5837,17 +5840,17 @@ } }, "node_modules/puppeteer": { - "version": "24.12.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz", - "integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==", + "version": "24.8.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.8.2.tgz", + "integrity": "sha512-Sn6SBPwJ6ASFvQ7knQkR+yG7pcmr4LfXzmoVp3NR0xXyBbPhJa8a8ybtb6fnw1g/DD/2t34//yirubVczko37w==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.5", + "@puppeteer/browsers": "2.10.4", "chromium-bidi": "5.1.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1464554", - "puppeteer-core": "24.12.1", + "devtools-protocol": "0.0.1439962", + "puppeteer-core": "24.8.2", "typed-query-selector": "^2.12.0" }, "bin": { @@ -5858,17 +5861,17 @@ } }, "node_modules/puppeteer-core": { - "version": "24.12.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz", - "integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==", + "version": "24.8.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.8.2.tgz", + "integrity": "sha512-wNw5cRZOHiFibWc0vdYCYO92QuKTbJ8frXiUfOq/UGJWMqhPoBThTKkV+dJ99YyWfzJ2CfQQ4T1nhhR0h8FlVw==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.5", + "@puppeteer/browsers": "2.10.4", "chromium-bidi": "5.1.0", - "debug": "^4.4.1", - "devtools-protocol": "0.0.1464554", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1439962", "typed-query-selector": "^2.12.0", - "ws": "^8.18.3" + "ws": "^8.18.2" }, "engines": { "node": ">=18" @@ -7362,9 +7365,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", + "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index 8131f50e..f8813264 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -54,7 +54,8 @@ "tailwind-merge": "^2.5.3", "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss-animate": "^1.0.7", - "tiptap-markdown": "^0.8.10" + "tiptap-markdown": "^0.8.10", + "zod": "^4.0.5" }, "devDependencies": { "@types/animejs": "^3.1.12",