add(Nextjs):Basic custom Layout and preview
This commit is contained in:
parent
9c0e7a37b1
commit
3da486707c
10 changed files with 1120 additions and 34 deletions
29
servers/nextjs/app/api/layouts/route.ts
Normal file
29
servers/nextjs/app/api/layouts/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
463
servers/nextjs/app/layout-preview/page.tsx
Normal file
463
servers/nextjs/app/layout-preview/page.tsx
Normal 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
|
||||
76
servers/nextjs/components/layouts/BulletPointSlideLayout.tsx
Normal file
76
servers/nextjs/components/layouts/BulletPointSlideLayout.tsx
Normal 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
|
||||
134
servers/nextjs/components/layouts/ConclusionSlideLayout.tsx
Normal file
134
servers/nextjs/components/layouts/ConclusionSlideLayout.tsx
Normal 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
|
||||
67
servers/nextjs/components/layouts/ContentSlideLayout.tsx
Normal file
67
servers/nextjs/components/layouts/ContentSlideLayout.tsx
Normal 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
|
||||
89
servers/nextjs/components/layouts/FirstSlideLayout.tsx
Normal file
89
servers/nextjs/components/layouts/FirstSlideLayout.tsx
Normal 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
|
||||
|
||||
128
servers/nextjs/components/layouts/ImageSlideLayout.tsx
Normal file
128
servers/nextjs/components/layouts/ImageSlideLayout.tsx
Normal 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
|
||||
96
servers/nextjs/components/layouts/TwoColumnSlideLayout.tsx
Normal file
96
servers/nextjs/components/layouts/TwoColumnSlideLayout.tsx
Normal 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
|
||||
69
servers/nextjs/package-lock.json
generated
69
servers/nextjs/package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue