add(Nextjs):Basic custom Layout and preview

This commit is contained in:
shiva raj badu 2025-07-14 13:32:47 +05:45
parent 9c0e7a37b1
commit 3da486707c
10 changed files with 1120 additions and 34 deletions

View file

@ -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 }
)
}
}

View file

@ -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<any>
schema: any
sampleData: any
fileName: string
}
const LayoutPreview = () => {
const [layouts, setLayouts] = useState<LayoutInfo[]>([])
const [loading, setLoading] = useState(true)
const [currentLayout, setCurrentLayout] = useState(0)
const [error, setError] = useState<string | null>(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 (
<div className="flex items-center justify-center min-h-screen">
<div className="text-xl">Loading layouts...</div>
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-xl text-red-500">Error: {error}</div>
</div>
)
}
if (layouts.length === 0) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-xl text-gray-500">No layouts found</div>
</div>
)
}
const CurrentLayoutComponent = layouts[currentLayout]?.component
return (
<div className="min-h-screen bg-gray-100">
{/* Navigation */}
<div className="bg-white shadow-md p-4">
<div className="max-w-6xl mx-auto flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-800">Layout Preview</h1>
<div className="flex items-center space-x-4">
<select
value={currentLayout}
onChange={(e) => setCurrentLayout(Number(e.target.value))}
className="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{layouts.map((layout, index) => (
<option key={index} value={index}>
{layout.name.replace('Layout', '').replace(/([A-Z])/g, ' $1').trim()}
</option>
))}
</select>
<div className="flex space-x-2">
<button
onClick={() => setCurrentLayout((prev) => Math.max(0, prev - 1))}
disabled={currentLayout === 0}
className="px-4 py-2 bg-blue-600 text-white rounded-md disabled:bg-gray-300 hover:bg-blue-700"
>
Previous
</button>
<button
onClick={() => setCurrentLayout((prev) => Math.min(layouts.length - 1, prev + 1))}
disabled={currentLayout === layouts.length - 1}
className="px-4 py-2 bg-blue-600 text-white rounded-md disabled:bg-gray-300 hover:bg-blue-700"
>
Next
</button>
</div>
</div>
</div>
</div>
{/* Layout Display */}
<div className="relative max-w-6xl mx-auto">
{CurrentLayoutComponent && (
<CurrentLayoutComponent data={layouts[currentLayout].sampleData} />
)}
</div>
{/* Layout Info */}
<div className="bg-white border-t p-4">
<div className="max-w-6xl mx-auto">
<h3 className="text-lg font-semibold mb-2">
Current Layout: {layouts[currentLayout]?.name}
</h3>
<p className="text-gray-600 mb-4">
{currentLayout + 1} of {layouts.length} layouts ({layouts[currentLayout]?.fileName})
</p>
{layouts[currentLayout]?.sampleData && (
<details className="bg-gray-50 p-4 rounded-md">
<summary className="cursor-pointer font-medium text-gray-700">
Sample Data Structure
</summary>
<pre className="mt-2 text-sm text-gray-600 overflow-x-auto">
{JSON.stringify(layouts[currentLayout].sampleData, null, 2)}
</pre>
</details>
)}
</div>
</div>
</div>
)
}
export default LayoutPreview

View file

@ -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<typeof Schema>
interface BulletPointSlideLayoutProps {
data?: Partial<BulletPointSlideData>
}
const BulletPointSlideLayout: React.FC<BulletPointSlideLayoutProps> = ({ data }) => {
const slideData = Schema.parse(data || {})
return (
<div
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-gray-50 via-white to-gray-100 overflow-hidden"
style={slideData.backgroundImage ? {
backgroundImage: `url(${slideData.backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} : {}}
>
{slideData.backgroundImage && (
<div className="absolute inset-0 bg-black bg-opacity-50" />
)}
<div className="relative z-10 flex flex-col h-full px-16 py-12">
{/* Header */}
<div className="mb-12">
<h1 className={`text-5xl font-bold mb-4 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.title}
</h1>
{slideData.subtitle && (
<p className={`text-2xl font-light ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-600'}`}>
{slideData.subtitle}
</p>
)}
<div className="w-24 h-1 bg-blue-600 mt-6" />
</div>
{/* Bullet Points */}
<div className="flex-1 flex items-center">
<div className="w-full max-w-4xl">
<ul className="space-y-8">
{slideData.bulletPoints.map((point, index) => (
<li key={index} className="flex items-start group">
<div className="flex-shrink-0 w-4 h-4 bg-blue-600 rounded-full mt-4 mr-8 group-hover:bg-blue-700 transition-colors" />
<span className={`text-2xl leading-relaxed ${slideData.backgroundImage ? 'text-white' : 'text-gray-800'}`}>
{point}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
{/* Decorative accent */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-r from-blue-500 to-blue-700" />
</div>
)
}
export default BulletPointSlideLayout

View file

@ -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<typeof conclusionSlideSchema>
interface ConclusionSlideLayoutProps {
data?: Partial<ConclusionSlideData>
}
const ConclusionSlideLayout: React.FC<ConclusionSlideLayoutProps> = ({ data }) => {
const slideData = conclusionSlideSchema.parse(data || {})
return (
<div
className="relative w-full aspect-[16/9] flex flex-col justify-center items-center bg-gradient-to-br from-blue-50 via-white to-blue-100 overflow-hidden"
style={slideData.backgroundImage ? {
backgroundImage: `url(${slideData.backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} : {}}
>
{slideData.backgroundImage && (
<div className="absolute inset-0 bg-black bg-opacity-50" />
)}
<div className="relative z-10 text-center max-w-6xl mx-auto px-16 py-12">
{/* Main Title */}
<h1 className={`text-6xl font-bold mb-6 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.title}
</h1>
{/* Subtitle */}
{slideData.subtitle && (
<p className={`text-3xl mb-12 font-light ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-600'}`}>
{slideData.subtitle}
</p>
)}
{/* Key Takeaways */}
<div className="mb-12">
<h2 className={`text-3xl font-semibold mb-8 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
Key Takeaways
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{slideData.keyTakeaways.map((takeaway, index) => (
<div key={index} className="bg-white bg-opacity-90 rounded-lg p-6 shadow-lg hover:shadow-xl transition-shadow">
<div className="flex items-center mb-4">
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold text-lg mr-4">
{index + 1}
</div>
</div>
<p className="text-lg text-gray-800 leading-relaxed">
{takeaway}
</p>
</div>
))}
</div>
</div>
{/* Call to Action */}
{slideData.callToAction && (
<div className="mb-12">
<div className="bg-blue-600 text-white rounded-lg p-8 inline-block shadow-lg hover:bg-blue-700 transition-colors">
<p className="text-2xl font-semibold">
{slideData.callToAction}
</p>
</div>
</div>
)}
{/* Contact Information */}
{slideData.contactInfo && (
<div className="mt-12">
<h3 className={`text-2xl font-semibold mb-6 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
Get in Touch
</h3>
<div className="flex flex-wrap justify-center gap-8 text-lg">
{slideData.contactInfo.email && (
<div className="flex items-center">
<span className={`font-semibold mr-2 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>Email:</span>
<span className={slideData.backgroundImage ? 'text-gray-200' : 'text-gray-700'}>{slideData.contactInfo.email}</span>
</div>
)}
{slideData.contactInfo.phone && (
<div className="flex items-center">
<span className={`font-semibold mr-2 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>Phone:</span>
<span className={slideData.backgroundImage ? 'text-gray-200' : 'text-gray-700'}>{slideData.contactInfo.phone}</span>
</div>
)}
{slideData.contactInfo.website && (
<div className="flex items-center">
<span className={`font-semibold mr-2 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>Website:</span>
<span className={slideData.backgroundImage ? 'text-gray-200' : 'text-gray-700'}>{slideData.contactInfo.website}</span>
</div>
)}
{slideData.contactInfo.linkedin && (
<div className="flex items-center">
<span className={`font-semibold mr-2 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>LinkedIn:</span>
<span className={slideData.backgroundImage ? 'text-gray-200' : 'text-gray-700'}>{slideData.contactInfo.linkedin}</span>
</div>
)}
</div>
</div>
)}
</div>
{/* Decorative accent */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-r from-blue-500 to-blue-700" />
</div>
)
}
export default ConclusionSlideLayout

View file

@ -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<typeof contentSlideSchema>
interface ContentSlideLayoutProps {
data?: Partial<ContentSlideData>
}
const ContentSlideLayout: React.FC<ContentSlideLayoutProps> = ({ data }) => {
const slideData = contentSlideSchema.parse(data || {})
console.log(slideData)
return (
<div
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-gray-50 via-white to-gray-100 overflow-hidden"
style={slideData.backgroundImage ? {
backgroundImage: `url(${slideData.backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} : {}}
>
{slideData.backgroundImage && (
<div className="absolute inset-0 bg-black bg-opacity-50" />
)}
<div className="relative z-10 flex flex-col h-full px-16 py-12">
{/* Header */}
<div className="mb-12">
<h1 className={`text-5xl font-bold mb-4 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.title}
</h1>
{slideData.subtitle && (
<p className={`text-2xl font-light ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-600'}`}>
{slideData.subtitle}
</p>
)}
<div className="w-24 h-1 bg-blue-600 mt-6" />
</div>
{/* Content Area */}
<div className="flex-1 flex items-center">
<div className="w-full max-w-5xl">
<div className={`text-xl leading-relaxed whitespace-pre-line ${slideData.backgroundImage ? 'text-white' : 'text-gray-800'}`}>
{slideData.content}
</div>
</div>
</div>
</div>
{/* Decorative accent */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-r from-blue-500 to-blue-700" />
</div>
)
}
export default ContentSlideLayout

View file

@ -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<typeof firstSlideSchema>
interface FirstSlideLayoutProps {
data?: Partial<FirstSlideData>
}
const FirstSlideLayout: React.FC<FirstSlideLayoutProps> = ({ data }) => {
const slideData = firstSlideSchema.parse(data || {})
return (
<div
className="relative w-full aspect-[16/9] flex flex-col justify-center items-center bg-gradient-to-br from-blue-50 via-white to-blue-100 overflow-hidden"
style={slideData.backgroundImage ? {
backgroundImage: `url(${slideData.backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} : {}}
>
{slideData.backgroundImage && (
<div className="absolute inset-0 bg-black bg-opacity-50" />
)}
<div className="relative z-10 text-center max-w-5xl mx-auto px-16 py-12">
{/* Logo */}
{slideData.logoUrl && (
<div className="mb-8">
<img
src={slideData.logoUrl}
alt="Logo"
className="h-20 w-auto mx-auto"
/>
</div>
)}
{/* Main Title */}
<h1 className={`text-6xl font-bold mb-8 leading-tight ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.title}
</h1>
{/* Subtitle */}
{slideData.subtitle && (
<p className={`text-3xl mb-16 font-light ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-600'}`}>
{slideData.subtitle}
</p>
)}
{/* Author and Organization */}
<div className={`text-2xl mb-6 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
<div className="font-semibold">{slideData.author}</div>
{slideData.organization && (
<div className={`mt-2 text-xl ${slideData.backgroundImage ? 'text-gray-300' : 'text-gray-600'}`}>
{slideData.organization}
</div>
)}
</div>
{/* Date */}
<div className={`text-lg ${slideData.backgroundImage ? 'text-blue-300' : 'text-blue-600'}`}>
{slideData.date}
</div>
</div>
{/* Decorative accent */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-r from-blue-500 to-blue-700" />
</div>
)
}
export default FirstSlideLayout

View file

@ -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<typeof imageSlideSchema>
interface ImageSlideLayoutProps {
data?: Partial<ImageSlideData>
}
const ImageSlideLayout: React.FC<ImageSlideLayoutProps> = ({ 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 (
<div
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-gray-50 via-white to-gray-100 overflow-hidden"
style={slideData.backgroundImage ? {
backgroundImage: `url(${slideData.backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} : {}}
>
{slideData.backgroundImage && (
<div className="absolute inset-0 bg-black bg-opacity-50" />
)}
<div className="relative z-10 flex flex-col h-full px-16 py-12">
{/* Header - only show if not full layout */}
{slideData.layout !== 'full' && (
<div className="mb-12">
<h1 className={`text-5xl font-bold mb-4 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.title}
</h1>
{slideData.subtitle && (
<p className={`text-2xl font-light ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-600'}`}>
{slideData.subtitle}
</p>
)}
<div className="w-24 h-1 bg-blue-600 mt-6" />
</div>
)}
{/* Content Area */}
<div className={`flex-1 ${getContainerClasses()}`}>
{slideData.layout === 'full' && (
<div className="absolute top-12 left-16 z-10">
<h1 className="text-5xl font-bold mb-4 text-white drop-shadow-lg">
{slideData.title}
</h1>
{slideData.subtitle && (
<p className="text-2xl text-white drop-shadow-lg font-light">
{slideData.subtitle}
</p>
)}
</div>
)}
<img
src={slideData.imageUrl}
alt={slideData.title}
className={getImageClasses()}
/>
{(slideData.layout === 'left' || slideData.layout === 'right') && (
<div className="w-1/2 flex flex-col justify-center">
{slideData.caption && (
<p className={`text-xl leading-relaxed ${slideData.backgroundImage ? 'text-white' : 'text-gray-700'}`}>
{slideData.caption}
</p>
)}
</div>
)}
</div>
{/* Caption for centered and full layouts */}
{slideData.caption && (slideData.layout === 'centered' || slideData.layout === 'full') && (
<div className={`mt-8 ${slideData.layout === 'full' ? 'absolute bottom-12 left-16 right-16' : ''}`}>
<p className={`text-xl leading-relaxed text-center ${slideData.layout === 'full' ? 'text-white drop-shadow-lg' : slideData.backgroundImage ? 'text-white' : 'text-gray-700'}`}>
{slideData.caption}
</p>
</div>
)}
</div>
{/* Decorative accent */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-r from-blue-500 to-blue-700" />
</div>
)
}
export default ImageSlideLayout

View file

@ -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<typeof twoColumnSlideSchema>
interface TwoColumnSlideLayoutProps {
data?: Partial<TwoColumnSlideData>
}
const TwoColumnSlideLayout: React.FC<TwoColumnSlideLayoutProps> = ({ data }) => {
const slideData = twoColumnSlideSchema.parse(data || {})
return (
<div
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-gray-50 via-white to-gray-100 overflow-hidden"
style={slideData.backgroundImage ? {
backgroundImage: `url(${slideData.backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} : {}}
>
{slideData.backgroundImage && (
<div className="absolute inset-0 bg-black bg-opacity-50" />
)}
<div className="relative z-10 flex flex-col h-full px-16 py-12">
{/* Header */}
<div className="mb-12">
<h1 className={`text-5xl font-bold mb-4 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.title}
</h1>
{slideData.subtitle && (
<p className={`text-2xl font-light ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-600'}`}>
{slideData.subtitle}
</p>
)}
<div className="w-24 h-1 bg-blue-600 mt-6" />
</div>
{/* Two Columns */}
<div className="flex-1 flex space-x-16">
{/* Left Column */}
<div className="w-1/2 flex flex-col">
<h2 className={`text-3xl font-semibold mb-8 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.leftColumn.title}
</h2>
<div className={`flex-1 text-xl leading-relaxed whitespace-pre-line ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-800'}`}>
{slideData.leftColumn.content}
</div>
</div>
{/* Divider */}
<div className={`w-px ${slideData.backgroundImage ? 'bg-gray-400' : 'bg-gray-300'}`} />
{/* Right Column */}
<div className="w-1/2 flex flex-col">
<h2 className={`text-3xl font-semibold mb-8 ${slideData.backgroundImage ? 'text-white' : 'text-gray-900'}`}>
{slideData.rightColumn.title}
</h2>
<div className={`flex-1 text-xl leading-relaxed whitespace-pre-line ${slideData.backgroundImage ? 'text-gray-200' : 'text-gray-800'}`}>
{slideData.rightColumn.content}
</div>
</div>
</div>
</div>
{/* Decorative accent */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-r from-blue-500 to-blue-700" />
</div>
)
}
export default TwoColumnSlideLayout

View file

@ -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"

View file

@ -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",