Merge branch 'feat/custom_layouts' into feat/custom_schema_and_layout
This commit is contained in:
commit
e4e6a8a308
23 changed files with 3021 additions and 916 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,9 @@ body {
|
|||
strong{
|
||||
@apply font-black ;
|
||||
}
|
||||
|
||||
|
||||
|
||||
::selection {
|
||||
background-color: hsl(var(--chart-1));
|
||||
color: white;
|
||||
|
|
|
|||
137
servers/nextjs/app/layout-preview/README.md
Normal file
137
servers/nextjs/app/layout-preview/README.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Layout Preview Studio
|
||||
|
||||
A modular, responsive layout preview system for viewing and testing presentation layout components with realistic sample data.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Dynamic Layout Discovery**: Automatically discovers and loads layout components
|
||||
- **Interactive Navigation**: Easy navigation with quick select grid
|
||||
- **Beautiful Presentation Display**: Mock browser frame with professional styling
|
||||
- **Detailed Information Panel**: Layout metadata and sample data inspection
|
||||
- **Responsive Design**: Mobile-friendly with collapsible sidebar
|
||||
- **Professional Loading States**: Skeleton loaders and error handling
|
||||
- **Type Safety**: Full TypeScript support with shared types
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
The system is built with a modular architecture for maintainability and reusability:
|
||||
|
||||
```
|
||||
layout-preview/
|
||||
├── components/ # Modular UI components
|
||||
│ ├── LayoutNavigation.tsx # Navigation controls & layout selector
|
||||
│ ├── LayoutDisplay.tsx # Main layout preview area
|
||||
│ ├── LayoutInfoPanel.tsx # Information and data structure display
|
||||
│ └── LoadingStates.tsx # Loading, error, and empty states
|
||||
├── hooks/ # Custom React hooks
|
||||
│ └── useLayoutLoader.ts # Layout loading logic and state management
|
||||
├── utils/ # Utility functions
|
||||
│ └── sampleDataGenerator.ts # Realistic sample data generation
|
||||
├── types/ # Shared TypeScript types
|
||||
│ └── index.ts # Common interfaces and types
|
||||
├── page.tsx # Main page component
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🧩 Components
|
||||
|
||||
### LayoutNavigation
|
||||
- Previous/Next navigation buttons
|
||||
- Layout counter and current layout info
|
||||
- Quick select grid for fast switching
|
||||
- Responsive design with mobile optimization
|
||||
|
||||
### LayoutDisplay
|
||||
- Mock browser frame presentation
|
||||
- Layout rendering with sample data
|
||||
- Professional shadow and styling effects
|
||||
- Empty state with helpful messaging
|
||||
|
||||
### LayoutInfoPanel
|
||||
- Layout metadata display
|
||||
- Collapsible sample data viewer
|
||||
- Copy to clipboard functionality
|
||||
- Position tracking in collection
|
||||
|
||||
### LoadingStates
|
||||
- Loading spinner with animated dots
|
||||
- Error state with retry functionality
|
||||
- Empty state with helpful instructions
|
||||
- Skeleton loading animations
|
||||
|
||||
## 🎯 Custom Hooks
|
||||
|
||||
### useLayoutLoader
|
||||
Handles all layout loading logic:
|
||||
- Fetches layout files from API
|
||||
- Imports and validates components
|
||||
- Generates realistic sample data
|
||||
- Provides retry functionality
|
||||
- Manages loading and error states
|
||||
|
||||
## 🛠️ Utilities
|
||||
|
||||
### sampleDataGenerator
|
||||
Intelligent sample data generation:
|
||||
- Context-aware field value generation
|
||||
- Support for images, emails, phones, URLs
|
||||
- Realistic business content
|
||||
- Zod schema parsing and validation
|
||||
- Array and object handling
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
The layout preview system is fully responsive:
|
||||
- **Desktop**: Sidebar navigation with main preview area
|
||||
- **Tablet**: Collapsible navigation panels
|
||||
- **Mobile**: Stacked layout with touch-friendly controls
|
||||
|
||||
## 🎨 Styling
|
||||
|
||||
Built with:
|
||||
- **Tailwind CSS**: Utility-first styling
|
||||
- **shadcn/ui**: Consistent component library
|
||||
- **Gradient Backgrounds**: Modern visual appeal
|
||||
- **Glass Morphism**: Backdrop blur effects
|
||||
- **Smooth Animations**: Hover and transition effects
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
The system automatically discovers layout components that export:
|
||||
```typescript
|
||||
// Layout component
|
||||
export default function MyLayout({ data }: { data: any }) {
|
||||
return <div>{/* Your layout */}</div>
|
||||
}
|
||||
|
||||
// Zod schema for type safety and sample data generation
|
||||
export const Schema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
// ... other fields
|
||||
})
|
||||
```
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
1. **Add Layout Components**: Place your layout files in the appropriate directory
|
||||
2. **Export Requirements**: Ensure each layout exports both a default component and Schema
|
||||
3. **Navigate**: Use the navigation controls or quick select grid
|
||||
4. **Inspect**: View layout information and sample data structure
|
||||
5. **Test**: See how your layouts render with realistic data
|
||||
|
||||
## 🎯 Benefits
|
||||
|
||||
- **Modular Architecture**: Easy to maintain and extend
|
||||
- **Type Safety**: Full TypeScript support prevents runtime errors
|
||||
- **Beautiful UI**: Professional design that's pleasant to use
|
||||
- **Developer Experience**: Quick feedback loop for layout development
|
||||
- **Responsive**: Works on all device sizes
|
||||
- **Accessible**: Keyboard navigation and screen reader support
|
||||
|
||||
## 📈 Performance
|
||||
|
||||
- **Lazy Loading**: Components are imported only when needed
|
||||
- **Optimized Rendering**: Efficient re-renders with React best practices
|
||||
- **Minimal Bundle**: Modular structure enables tree shaking
|
||||
- **Caching**: Sample data generation is memoized
|
||||
185
servers/nextjs/app/layout-preview/components/LoadingStates.tsx
Normal file
185
servers/nextjs/app/layout-preview/components/LoadingStates.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Loader2, AlertCircle, RefreshCw, FileX, Layers } from 'lucide-react'
|
||||
|
||||
interface LoadingStatesProps {
|
||||
type: 'loading' | 'error' | 'empty'
|
||||
message?: string
|
||||
onRetry?: () => void
|
||||
}
|
||||
|
||||
const LoadingStates: React.FC<LoadingStatesProps> = ({
|
||||
type,
|
||||
message,
|
||||
onRetry
|
||||
}) => {
|
||||
if (type === 'loading') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 flex items-center justify-center">
|
||||
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<CardContent className="space-y-6">
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 mx-auto mb-4 relative">
|
||||
<Loader2 className="w-16 h-16 text-blue-500 animate-spin" />
|
||||
<div className="absolute inset-0 w-16 h-16 border-4 border-blue-100 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
Loading Layouts
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
{message || 'Discovering and loading layout components...'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loading animation dots */}
|
||||
<div className="flex justify-center space-x-1">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 via-white to-orange-50 flex items-center justify-center">
|
||||
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm max-w-md">
|
||||
<CardContent className="space-y-6">
|
||||
<div className="w-16 h-16 mx-auto p-4 bg-red-100 rounded-full">
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
Something went wrong
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{message || 'Failed to load layouts. Please check your layout files and try again.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{onRetry && (
|
||||
<Button
|
||||
onClick={onRetry}
|
||||
className="mt-4 bg-red-500 hover:bg-red-600 text-white"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'empty') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-slate-50 flex items-center justify-center">
|
||||
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm max-w-md">
|
||||
<CardContent className="space-y-6">
|
||||
<div className="w-16 h-16 mx-auto p-4 bg-gray-100 rounded-full">
|
||||
<FileX className="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-semibold text-gray-700">
|
||||
No Layouts Found
|
||||
</h3>
|
||||
<p className="text-gray-500 text-sm leading-relaxed">
|
||||
No valid layout files were discovered. Make sure your layout components export both a default component and a Schema.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg text-left text-xs text-gray-600">
|
||||
<p className="font-medium mb-2">Expected structure:</p>
|
||||
<code className="block">
|
||||
export default MyLayout<br />
|
||||
export const Schema = z.object(...)
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{onRetry && (
|
||||
<Button
|
||||
onClick={onRetry}
|
||||
variant="outline"
|
||||
className="mt-4"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Component for layout grid skeleton while loading
|
||||
export const LayoutGridSkeleton: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
|
||||
{/* Header Skeleton */}
|
||||
<div className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-gray-200 rounded-lg animate-pulse"></div>
|
||||
<div className="w-32 h-6 bg-gray-200 rounded animate-pulse"></div>
|
||||
</div>
|
||||
<div className="w-16 h-6 bg-gray-200 rounded animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Skeleton */}
|
||||
<div className="max-w-7xl mx-auto p-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Sidebar Skeleton */}
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="w-24 h-4 bg-gray-200 rounded animate-pulse"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="w-full h-8 bg-gray-200 rounded animate-pulse"></div>
|
||||
<div className="w-full h-8 bg-gray-200 rounded animate-pulse"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div key={i} className="w-full h-12 bg-gray-200 rounded animate-pulse"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main Display Skeleton */}
|
||||
<div className="lg:col-span-3">
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="w-full h-96 bg-gray-200 rounded-lg animate-pulse"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="w-48 h-4 bg-gray-200 rounded animate-pulse"></div>
|
||||
<div className="w-32 h-3 bg-gray-200 rounded animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadingStates
|
||||
136
servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts
Normal file
136
servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import { LayoutInfo } from '../types'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
||||
interface UseLayoutLoaderReturn {
|
||||
layouts: LayoutInfo[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
||||
const [layouts, setLayouts] = useState<LayoutInfo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const loadAllLayouts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch('/api/layouts')
|
||||
if (!response.ok) {
|
||||
toast({
|
||||
title: 'Error loading layouts',
|
||||
description: response.statusText,
|
||||
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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',
|
||||
|
||||
|
||||
})
|
||||
console.error(`${layoutName} is missing required Schema export`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use empty object to let schema apply its default values
|
||||
// User will need to provide actual data when using the layouts
|
||||
const sampleData = module.Schema.parse({})
|
||||
|
||||
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) {
|
||||
// Use empty object to let schema apply its default values
|
||||
const sampleData = module.Schema.parse({})
|
||||
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) {
|
||||
toast({
|
||||
title: 'No valid layouts found',
|
||||
description: 'Make sure your layout files export both a default component and a Schema.',
|
||||
|
||||
})
|
||||
setError('No valid layouts found. Make sure your layout files export both a default component and a Schema.')
|
||||
} else {
|
||||
setLayouts(loadedLayouts)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading layouts:', error)
|
||||
setError(error instanceof Error ? error.message : 'Failed to load layouts')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const retry = () => {
|
||||
loadAllLayouts()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadAllLayouts()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
layouts,
|
||||
loading,
|
||||
error,
|
||||
retry
|
||||
}
|
||||
}
|
||||
86
servers/nextjs/app/layout-preview/page.tsx
Normal file
86
servers/nextjs/app/layout-preview/page.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import { useLayoutLoader } from './hooks/useLayoutLoader'
|
||||
import LoadingStates from './components/LoadingStates'
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
/**
|
||||
* Layout Preview Page
|
||||
*
|
||||
* Simple vertical display of all layout components with their sample data.
|
||||
*/
|
||||
const LayoutPreview = () => {
|
||||
const { layouts, loading, error, retry } = useLayoutLoader()
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
return <LoadingStates type="loading" />
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (error) {
|
||||
return <LoadingStates type="error" message={error} onRetry={retry} />
|
||||
}
|
||||
|
||||
// Handle empty state
|
||||
if (layouts.length === 0) {
|
||||
return <LoadingStates type="empty" onRetry={retry} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white z-20 backdrop-blur-sm border-b border-white/20 shadow-sm sticky top-0 ">
|
||||
<div className="max-w-4xl mx-auto px-6 py-4">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Layout Preview</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{layouts.length} layout{layouts.length !== 1 ? 's' : ''} found
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Layouts List */}
|
||||
<main className="max-w-7xl mx-auto space-y-8">
|
||||
{layouts.map((layout, index) => {
|
||||
const { component: LayoutComponent, sampleData, name, fileName } = layout
|
||||
|
||||
return (
|
||||
<Card key={index} className="overflow-hidden py-0 shadow-lg border-0 bg-white">
|
||||
{/* Layout Header */}
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">{name}</h2>
|
||||
<p className="text-sm text-gray-600 font-mono">{fileName}</p>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-blue-100 text-blue-700 rounded-md text-sm font-medium">
|
||||
#{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout Content */}
|
||||
|
||||
<LayoutComponent data={sampleData} />
|
||||
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</main>
|
||||
{/* Footer */}
|
||||
<footer className="mt-16 bg-white/50 backdrop-blur-sm border-t border-white/20">
|
||||
<div className="max-w-4xl mx-auto px-6 py-8">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
Layout Preview • {layouts.length} component{layouts.length !== 1 ? 's' : ''} rendered
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutPreview
|
||||
28
servers/nextjs/app/layout-preview/types/index.ts
Normal file
28
servers/nextjs/app/layout-preview/types/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Shared types for the Layout Preview system
|
||||
*/
|
||||
|
||||
export interface LayoutInfo {
|
||||
name: string
|
||||
component: React.ComponentType<any>
|
||||
schema: any
|
||||
sampleData: any
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export interface LoadingState {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface NavigationState {
|
||||
currentLayout: number
|
||||
totalLayouts: number
|
||||
}
|
||||
|
||||
export type LoadingStateType = 'loading' | 'error' | 'empty'
|
||||
|
||||
export interface ComponentProps {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
305
servers/nextjs/app/layout-preview/utils/sampleDataGenerator.ts
Normal file
305
servers/nextjs/app/layout-preview/utils/sampleDataGenerator.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
/**
|
||||
* Sample Data Generator Utility
|
||||
*
|
||||
* Generates realistic sample data from Zod schemas for layout previews.
|
||||
* Provides context-aware data generation based on field names and types.
|
||||
*/
|
||||
|
||||
export const generateSampleDataFromSchema = (schema: any, layoutName: string): any => {
|
||||
if (!schema) return {}
|
||||
|
||||
try {
|
||||
// Generate realistic sample data for all fields first
|
||||
const generatedData = generateRealisticData(schema._def?.shape || schema.shape, layoutName)
|
||||
|
||||
|
||||
// Merge generated data with defaults, giving priority to defaults
|
||||
return generatedData
|
||||
} 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
|
||||
|
||||
// Generate data for all fields (both required and optional)
|
||||
// We'll let the defaults override this later if they exist
|
||||
data[key] = generateFieldValue(key, field, layoutName)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// const arrayMock = (length:number,element:any) => {
|
||||
// return Array.from({length},()=>generateFieldValue(fieldName, element, layoutName))
|
||||
// }
|
||||
// const mockObject = (shapes:any) => {
|
||||
// let obj:any = {}
|
||||
// for(const [key,shape] of Object.entries(shapes)){
|
||||
// const defaultValue = shape.def.defaultValue
|
||||
// obj[key] = defaultValue ? defaultValue : generateFieldValue(key, shape, layoutName)
|
||||
// }
|
||||
// return obj
|
||||
// }
|
||||
|
||||
|
||||
// const generateMockValue = (fileType:string,format?:string)=>{
|
||||
// switch(fileType){
|
||||
// case 'number':
|
||||
// return Math.floor(Math.random() * 100) + 1
|
||||
// case 'string':
|
||||
// return generateStringValue(fieldName, fieldSchema, layoutName)
|
||||
// case 'boolean':
|
||||
// return Math.random() > 0.5
|
||||
// case 'object':
|
||||
// return mockObject(fieldSchema.def.shape)
|
||||
// case 'array':
|
||||
// return arrayMock(fieldSchema.def.length,fieldSchema.def.element)
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
const generateFieldValue = (fieldName: string, fieldSchema: any, layoutName: string): any => {
|
||||
console.log('BADU',fieldSchema,fieldName,layoutName)
|
||||
const defaultValue = fieldSchema.def.defaultValue;
|
||||
|
||||
if(defaultValue){
|
||||
console.log('DEFAULT VALUE',defaultValue)
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if(fieldSchema.def.type ==='optional'){
|
||||
return generateFieldValue(fieldName, fieldSchema.def.innerType, layoutName)
|
||||
}
|
||||
|
||||
// Get the actual field type - handle optional fields properly
|
||||
let actualFieldSchema = fieldSchema
|
||||
let fieldType = fieldSchema._def?.typeName
|
||||
|
||||
// If this is an optional field (ZodOptional), get the inner type
|
||||
if (fieldType === 'ZodOptional') {
|
||||
actualFieldSchema = fieldSchema._def?.innerType
|
||||
fieldType = actualFieldSchema?._def?.typeName
|
||||
}
|
||||
|
||||
// For preview purposes, always generate data for optional fields
|
||||
// (users want to see how the layout looks with content)
|
||||
|
||||
// Handle different field types
|
||||
switch (fieldType) {
|
||||
case 'ZodString':
|
||||
return generateStringValue(fieldName, actualFieldSchema, layoutName)
|
||||
case 'ZodArray':
|
||||
return generateArrayValue(fieldName, actualFieldSchema, layoutName)
|
||||
case 'ZodObject':
|
||||
return generateObjectValue(fieldName, actualFieldSchema, layoutName)
|
||||
case 'ZodEnum':
|
||||
const options = actualFieldSchema._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, actualFieldSchema, 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
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { Tooltip } from '@radix-ui/react-tooltip'
|
|||
import { TooltipProvider } from '@radix-ui/react-tooltip'
|
||||
import React from 'react'
|
||||
import { TooltipContent, TooltipTrigger, } from './ui/tooltip'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
const ToolTip = ({ children, content }: { children: React.ReactNode, content: string }) => {
|
||||
return (
|
||||
|
|
|
|||
139
servers/nextjs/components/layouts/BulletPointSlideLayout.tsx
Normal file
139
servers/nextjs/components/layouts/BulletPointSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import React from 'react'
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import * as z from "zod";
|
||||
|
||||
const bulletPointSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Key Points').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
icon: z.string().optional().describe('Icon to display in the slide'),
|
||||
bulletPoints: z.array(z.string().min(5).max(200)).min(2).max(8).default([
|
||||
'First key point that highlights important information',
|
||||
'Second bullet point with valuable insights',
|
||||
'Third point demonstrating clear benefits',
|
||||
'Fourth item showcasing key features'
|
||||
]).describe('List of bullet points (2-8 items)'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
console.log(zodToJsonSchema(bulletPointSlideSchema))
|
||||
|
||||
export const Schema = bulletPointSlideSchema
|
||||
|
||||
export type BulletPointSlideData = z.infer<typeof bulletPointSlideSchema>
|
||||
|
||||
interface BulletPointSlideLayoutProps {
|
||||
data?: Partial<BulletPointSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const BulletPointSlideLayout: React.FC<BulletPointSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = bulletPointSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const bulletColors = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Bullet Points */}
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-8 shadow-2xl border border-white/50 w-full max-w-5xl relative overflow-hidden">
|
||||
{/* Content background accent */}
|
||||
<div className={`absolute top-0 right-0 w-24 h-24 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
|
||||
<ul className={`space-y-${slideData.bulletPoints.length <= 4 ? '6' : slideData.bulletPoints.length <= 6 ? '4' : '3'} relative z-10`}>
|
||||
{slideData.bulletPoints.map((point, index) => (
|
||||
<li key={index} className="flex items-start group hover:transform hover:translateX-2 transition-all duration-200">
|
||||
{/* Enhanced bullet point icon */}
|
||||
<div className="relative mr-6 mt-1 flex-shrink-0">
|
||||
<div className={`${bulletColors[accentColor]} w-4 h-4 rounded-full shadow-lg group-hover:scale-125 transition-all duration-200 relative z-10`} />
|
||||
<div className={`absolute inset-0 ${bulletColors[accentColor]} w-4 h-4 rounded-full blur-sm opacity-50 group-hover:opacity-75 transition-opacity duration-200`} />
|
||||
</div>
|
||||
|
||||
{/* Enhanced bullet text */}
|
||||
<span className={`text-lg md:text-xl leading-relaxed break-words font-medium ${slideData.backgroundImage
|
||||
? 'text-slate-700'
|
||||
: 'text-slate-700'
|
||||
} group-hover:text-slate-900 transition-colors duration-200`}>
|
||||
{point}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BulletPointSlideLayout
|
||||
228
servers/nextjs/components/layouts/ConclusionSlideLayout.tsx
Normal file
228
servers/nextjs/components/layouts/ConclusionSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const conclusionSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Conclusion').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
keyTakeaways: z.array(z.string().min(5).max(200)).min(2).max(6).default([
|
||||
'Successfully achieved our primary objectives',
|
||||
'Demonstrated significant value and impact',
|
||||
'Established clear next steps for continued success',
|
||||
'Built strong foundation for future growth'
|
||||
]).describe('Key takeaways or summary points (2-6 items)'),
|
||||
callToAction: z.string().min(5).max(150).optional().describe('Optional call to action or next steps'),
|
||||
contactInfo: z.object({
|
||||
email: z.string().email().optional().describe('Contact email'),
|
||||
phone: z.string().min(5).max(50).optional().describe('Contact phone number'),
|
||||
website: z.string().url().optional().describe('Website URL')
|
||||
}).optional().describe('Optional contact information'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = conclusionSlideSchema
|
||||
|
||||
export type ConclusionSlideData = z.infer<typeof conclusionSlideSchema>
|
||||
|
||||
interface ConclusionSlideLayoutProps {
|
||||
data?: Partial<ConclusionSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const ConclusionSlideLayout: React.FC<ConclusionSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = conclusionSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const bulletColors = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Content Layout */}
|
||||
<main className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Key Takeaways - Takes up 2/3 of space */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-8 shadow-2xl border border-white/50 h-full relative overflow-hidden">
|
||||
{/* Content accent */}
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-6 relative z-10">Key Takeaways</h2>
|
||||
|
||||
<ul className={`space-y-4 relative z-10`}>
|
||||
{slideData.keyTakeaways.map((takeaway, index) => (
|
||||
<li key={index} className="flex items-start group hover:transform hover:translateX-2 transition-all duration-200">
|
||||
{/* Enhanced bullet point */}
|
||||
<div className="relative mr-4 mt-1.5 flex-shrink-0">
|
||||
<div className={`${bulletColors[accentColor]} w-3 h-3 rounded-full shadow-lg group-hover:scale-125 transition-all duration-200 relative z-10`} />
|
||||
<div className={`absolute inset-0 ${bulletColors[accentColor]} w-3 h-3 rounded-full blur-sm opacity-50 group-hover:opacity-75 transition-opacity duration-200`} />
|
||||
</div>
|
||||
|
||||
<span className="text-lg leading-relaxed break-words font-medium text-slate-700 group-hover:text-slate-900 transition-colors duration-200">
|
||||
{takeaway}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-16 h-16 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Call to Action & Contact Info - Takes up 1/3 of space */}
|
||||
<div className="space-y-6">
|
||||
{/* Call to Action */}
|
||||
{slideData.callToAction && (
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-6 shadow-2xl border border-white/50 text-center relative overflow-hidden">
|
||||
{/* CTA accent */}
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
<div className={`w-12 h-12 mx-auto mb-4 rounded-full ${accentSolids[accentColor]} flex items-center justify-center shadow-lg relative`}>
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
<div className={`absolute inset-0 rounded-full ${accentSolids[accentColor]} blur-md opacity-50`} />
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-3">Next Steps</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed break-words font-medium">
|
||||
{slideData.callToAction}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contact Information */}
|
||||
{slideData.contactInfo && Object.values(slideData.contactInfo).some(Boolean) && (
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-6 shadow-2xl border border-white/50 relative overflow-hidden">
|
||||
{/* Contact accent */}
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-4 text-center">Get in Touch</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{slideData.contactInfo.email && (
|
||||
<a
|
||||
href={`mailto:${slideData.contactInfo.email}`}
|
||||
className="flex items-center space-x-3 p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors duration-200 group"
|
||||
>
|
||||
<div className={`w-8 h-8 rounded-full ${accentSolids[accentColor]} flex items-center justify-center group-hover:scale-110 transition-transform duration-200`}>
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700 break-all">{slideData.contactInfo.email}</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{slideData.contactInfo.phone && (
|
||||
<a
|
||||
href={`tel:${slideData.contactInfo.phone}`}
|
||||
className="flex items-center space-x-3 p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors duration-200 group"
|
||||
>
|
||||
<div className={`w-8 h-8 rounded-full ${accentSolids[accentColor]} flex items-center justify-center group-hover:scale-110 transition-transform duration-200`}>
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700">{slideData.contactInfo.phone}</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{slideData.contactInfo.website && (
|
||||
<a
|
||||
href={slideData.contactInfo.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center space-x-3 p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors duration-200 group"
|
||||
>
|
||||
<div className={`w-8 h-8 rounded-full ${accentSolids[accentColor]} flex items-center justify-center group-hover:scale-110 transition-transform duration-200`}>
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.083 9h1.946c.089-1.546.383-2.97.837-4.118A6.004 6.004 0 004.083 9zM10 2a8 8 0 100 16 8 8 0 000-16zm0 2c-.076 0-.232.032-.465.262-.238.234-.497.623-.737 1.182-.389.907-.673 2.142-.766 3.556h3.936c-.093-1.414-.377-2.649-.766-3.556-.24-.559-.5-.948-.737-1.182C10.232 4.032 10.076 4 10 4zm3.971 5c-.089-1.546-.383-2.97-.837-4.118A6.004 6.004 0 0115.917 9h-1.946zm-2.003 2H8.032c.093 1.414.377 2.649.766 3.556.24.559.5.948.737 1.182.233.23.389.262.465.262.076 0 .232-.032.465-.262.238-.234.498-.623.737-1.182.389-.907.673-2.142.766-3.556zm1.166 4.118c.454-1.147.748-2.572.837-4.118h1.946a6.004 6.004 0 01-2.783 4.118zm-6.268 0C6.412 13.97 6.118 12.546 6.03 11H4.083a6.004 6.004 0 002.783 4.118z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700 break-all">{slideData.contactInfo.website.replace(/^https?:\/\//, '')}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-12 h-12 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConclusionSlideLayout
|
||||
122
servers/nextjs/components/layouts/ContentSlideLayout.tsx
Normal file
122
servers/nextjs/components/layouts/ContentSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const contentSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Slide Title').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
content: z.string().min(10).max(1000).default('Your slide content goes here. This is where you can add detailed information, explanations, or any other text content that supports your presentation.').describe('Main content text'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = contentSlideSchema
|
||||
|
||||
export type ContentSlideData = z.infer<typeof contentSlideSchema>
|
||||
|
||||
interface ContentSlideLayoutProps {
|
||||
data?: Partial<ContentSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const ContentSlideLayout: React.FC<ContentSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = contentSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
{/* Grid overlay for professional look */}
|
||||
<div className="absolute inset-0 opacity-[0.015]" style={{
|
||||
backgroundImage: `linear-gradient(0deg, rgba(0,0,0,0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px)`,
|
||||
backgroundSize: '40px 40px'
|
||||
}} />
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content with Enhanced Styling */}
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-8 shadow-2xl border border-white/50 max-w-5xl relative overflow-hidden">
|
||||
{/* Content background accent */}
|
||||
<div className={`absolute top-0 right-0 w-24 h-24 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
|
||||
<div className={`text-lg md:text-xl leading-relaxed break-words relative z-10 ${slideData.backgroundImage
|
||||
? 'text-slate-700'
|
||||
: 'text-slate-700'
|
||||
}`}>
|
||||
{slideData.content.split('\n').map((paragraph, index) => (
|
||||
paragraph.trim() && (
|
||||
<p key={index} className="mb-5 last:mb-0 font-medium leading-relaxed">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContentSlideLayout
|
||||
132
servers/nextjs/components/layouts/FirstSlideLayout.tsx
Normal file
132
servers/nextjs/components/layouts/FirstSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const firstSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Welcome to Our Presentation').describe('Main title of the presentation'),
|
||||
subtitle: z.string().min(10).max(200).default('Subtitle for the slide').optional().describe('Optional subtitle or tagline'),
|
||||
author: z.string().min(2).max(100).default('John Doe').optional().describe('Author or presenter name'),
|
||||
date: z.string().optional().describe('Presentation date'),
|
||||
company: z.string().min(2).max(100).default('Company Name').optional().describe('Company or organization name'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = firstSlideSchema
|
||||
|
||||
export type FirstSlideData = z.infer<typeof firstSlideSchema>
|
||||
|
||||
interface FirstSlideLayoutProps {
|
||||
data?: Partial<FirstSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const FirstSlideLayout: React.FC<FirstSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = firstSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
<div className={`absolute top-1/2 left-1/2 w-48 h-48 ${accentSolids[accentColor]} rounded-full transform -translate-x-1/2 -translate-y-1/2 blur-3xl opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Grid overlay for professional look */}
|
||||
<div className="absolute inset-0 opacity-[0.02]" style={{
|
||||
backgroundImage: `linear-gradient(0deg, rgba(0,0,0,0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px)`,
|
||||
backgroundSize: '60px 60px'
|
||||
}} />
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8 justify-center items-center text-center">
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col justify-center items-center max-w-5xl">
|
||||
{/* Title */}
|
||||
<h1 className={`text-5xl md:text-6xl font-black mb-6 tracking-tight leading-[0.9] break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-2xl'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl md:text-2xl font-light leading-relaxed mb-8 break-words max-w-4xl ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-lg'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Enhanced Accent Line */}
|
||||
<div className="relative mb-8">
|
||||
<div className={`w-32 h-1.5 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1.5 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional Metadata Container */}
|
||||
<div className="bg-white/90 backdrop-blur-md rounded-2xl px-8 py-6 shadow-xl border border-white/40">
|
||||
<div className="space-y-3">
|
||||
{slideData.author && (
|
||||
<p className={`text-lg font-semibold break-words text-slate-800`}>
|
||||
{slideData.author}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{slideData.company && (
|
||||
<p className={`text-base font-medium break-words ${accentColors[accentColor]} bg-gradient-to-r bg-clip-text text-transparent`}>
|
||||
{slideData.company}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{slideData.date && (
|
||||
<p className={`text-sm break-words text-slate-600 font-medium tracking-wide`}>
|
||||
{slideData.date}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent with glow effect */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
<div className={`absolute bottom-0 left-0 w-24 h-24 bg-gradient-to-tr ${accentColors[accentColor]} opacity-5 rounded-tr-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FirstSlideLayout
|
||||
|
||||
139
servers/nextjs/components/layouts/ImageSlideLayout.tsx
Normal file
139
servers/nextjs/components/layouts/ImageSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const imageSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Image Showcase').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).default('Subtitle for the slide').optional().describe('Optional subtitle or description'),
|
||||
image: z.string().default('https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop').describe('Main image URL'),
|
||||
imageCaption: z.string().min(5).max(200).default('Image caption').optional().describe('Optional image caption or description'),
|
||||
content: z.string().min(10).max(600).optional().describe('Optional supporting content text'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = imageSlideSchema
|
||||
|
||||
export type ImageSlideData = z.infer<typeof imageSlideSchema>
|
||||
|
||||
interface ImageSlideLayoutProps {
|
||||
data?: Partial<ImageSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const ImageSlideLayout: React.FC<ImageSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = imageSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Content Layout */}
|
||||
<main className="flex-1 flex items-center min-h-0">
|
||||
<div className=" ">
|
||||
|
||||
|
||||
<div className={`text-lg md:text-xl leading-relaxed break-words font-medium relative z-10 ${slideData.backgroundImage
|
||||
? 'text-slate-700'
|
||||
: 'text-slate-700'
|
||||
}`}>
|
||||
{slideData.content?.split('\n').map((paragraph, index) => (
|
||||
paragraph.trim() && (
|
||||
<p key={index} className="mb-5 last:mb-0">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-16 h-16 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="relative flex-1 group overflow-hidden rounded-2xl shadow-2xl">
|
||||
<img
|
||||
src={slideData.image}
|
||||
alt={slideData.imageCaption || slideData.title}
|
||||
className="w-full h-full object-cover transition-transform duration-300 "
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
</div>
|
||||
{slideData.imageCaption && (
|
||||
<p className={`text-sm mt-4 text-center italic break-words font-medium ${slideData.backgroundImage
|
||||
? 'text-slate-300'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.imageCaption}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageSlideLayout
|
||||
181
servers/nextjs/components/layouts/ProcessSlideLayout.tsx
Normal file
181
servers/nextjs/components/layouts/ProcessSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const processSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Our Process').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
processSteps: z.array(z.object({
|
||||
step: z.number().min(1).max(10).describe('Step number'),
|
||||
title: z.string().min(3).max(100).describe('Step title'),
|
||||
description: z.string().min(10).max(200).describe('Step description')
|
||||
})).min(2).max(6).default([
|
||||
{
|
||||
step: 1,
|
||||
title: 'Discovery',
|
||||
description: 'Understanding requirements and gathering initial insights'
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: 'Planning',
|
||||
description: 'Strategic planning and roadmap development'
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: 'Implementation',
|
||||
description: 'Executing the plan with precision and quality'
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: 'Delivery',
|
||||
description: 'Final delivery and ongoing support'
|
||||
}
|
||||
]).describe('Process steps (2-6 items)'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = processSlideSchema
|
||||
|
||||
export type ProcessSlideData = z.infer<typeof processSlideSchema>
|
||||
|
||||
interface ProcessSlideLayoutProps {
|
||||
data?: Partial<ProcessSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const ProcessSlideLayout: React.FC<ProcessSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = processSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const stepColors = {
|
||||
blue: 'bg-blue-600 text-white border-blue-600',
|
||||
green: 'bg-emerald-600 text-white border-emerald-600',
|
||||
purple: 'bg-violet-600 text-white border-violet-600',
|
||||
orange: 'bg-orange-600 text-white border-orange-600',
|
||||
red: 'bg-red-600 text-white border-red-600'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Process Steps */}
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className="w-full max-w-6xl">
|
||||
<div className="flex items-center justify-between">
|
||||
{slideData.processSteps.map((step, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{/* Process Step */}
|
||||
<div className="flex flex-col items-center text-center group" style={{ width: `${100 / slideData.processSteps.length}%` }}>
|
||||
{/* Step Number Circle */}
|
||||
<div className={`w-16 h-16 rounded-full ${stepColors[accentColor]} flex items-center justify-center text-2xl font-bold mb-4 shadow-2xl border-4 group-hover:scale-110 transition-all duration-300 relative`}>
|
||||
<span className="relative z-10">{step.step}</span>
|
||||
<div className={`absolute inset-0 rounded-full ${accentSolids[accentColor]} blur-md opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Step Content Card */}
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-6 shadow-2xl border border-white/50 w-full max-w-xs relative overflow-hidden group-hover:transform group-hover:scale-105 transition-all duration-300">
|
||||
{/* Card accent */}
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
{/* Step Title */}
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-3 break-words">
|
||||
{step.title}
|
||||
</h3>
|
||||
|
||||
{/* Step Description */}
|
||||
<p className="text-sm text-slate-600 leading-relaxed break-words font-medium">
|
||||
{step.description}
|
||||
</p>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-12 h-12 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow Between Steps */}
|
||||
{index < slideData.processSteps.length - 1 && (
|
||||
<div className="flex items-center justify-center mx-4">
|
||||
<div className={`w-8 h-1 bg-gradient-to-r ${accentColors[accentColor]} relative`}>
|
||||
<div className={`absolute -right-2 -top-1 w-0 h-0 border-l-4 border-t-2 border-b-2 ${accentSolids[accentColor]} border-t-transparent border-b-transparent`}
|
||||
style={{
|
||||
borderLeftColor: accentColor === 'blue' ? '#2563eb' :
|
||||
accentColor === 'green' ? '#059669' :
|
||||
accentColor === 'purple' ? '#7c3aed' :
|
||||
accentColor === 'orange' ? '#ea580c' : '#dc2626'
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProcessSlideLayout
|
||||
167
servers/nextjs/components/layouts/QuoteSlideLayout.tsx
Normal file
167
servers/nextjs/components/layouts/QuoteSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const quoteSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Testimonials').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
quote: z.string().min(10).max(500).default('This solution has transformed our business operations and exceeded all expectations.').describe('The main quote or testimonial'),
|
||||
author: z.string().min(2).max(100).default('John Smith').describe('Quote author name'),
|
||||
authorTitle: z.string().min(2).max(100).optional().describe('Author job title or position'),
|
||||
company: z.string().min(2).max(100).optional().describe('Author company or organization'),
|
||||
authorImage: z.string().optional().describe('URL to author photo'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = quoteSlideSchema
|
||||
|
||||
export type QuoteSlideData = z.infer<typeof quoteSlideSchema>
|
||||
|
||||
interface QuoteSlideLayoutProps {
|
||||
data?: Partial<QuoteSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = quoteSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Quote Content */}
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-3xl p-10 shadow-2xl border border-white/50 max-w-5xl text-center relative overflow-hidden">
|
||||
{/* Enhanced Background Quote Decoration */}
|
||||
<div className={`absolute top-4 left-6 text-8xl font-black opacity-10 bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent pointer-events-none select-none`}>
|
||||
"
|
||||
</div>
|
||||
<div className={`absolute bottom-4 right-6 text-8xl font-black opacity-10 bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent pointer-events-none select-none rotate-180`}>
|
||||
"
|
||||
</div>
|
||||
|
||||
{/* Quote Text */}
|
||||
<blockquote className={`text-2xl md:text-3xl leading-relaxed mb-8 italic break-words relative z-10 font-light ${slideData.backgroundImage
|
||||
? 'text-slate-700'
|
||||
: 'text-slate-700'
|
||||
}`}>
|
||||
"{slideData.quote}"
|
||||
</blockquote>
|
||||
|
||||
{/* Professional Author Attribution */}
|
||||
<div className="flex items-center justify-center space-x-4 relative z-10">
|
||||
{/* Author Avatar */}
|
||||
<div className="flex-shrink-0">
|
||||
{slideData.authorImage ? (
|
||||
<img
|
||||
src={slideData.authorImage}
|
||||
alt={slideData.author}
|
||||
className="w-16 h-16 rounded-full object-cover shadow-xl border-4 border-white"
|
||||
/>
|
||||
) : (
|
||||
<div className={`w-16 h-16 rounded-full ${accentSolids[accentColor]} flex items-center justify-center text-white font-bold text-xl shadow-xl border-4 border-white`}>
|
||||
{slideData.author.split(' ').map(n => n[0]).join('')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Author Details */}
|
||||
<div className="text-left">
|
||||
<p className="text-xl font-bold text-slate-900 break-words">
|
||||
{slideData.author}
|
||||
</p>
|
||||
|
||||
{slideData.authorTitle && (
|
||||
<p className={`text-base font-semibold bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent break-words`}>
|
||||
{slideData.authorTitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{slideData.company && (
|
||||
<p className="text-sm text-slate-600 font-medium break-words">
|
||||
{slideData.company}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Quote Accent Line */}
|
||||
<div className="flex justify-center mt-6">
|
||||
<div className="relative">
|
||||
<div className={`w-24 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-24 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-20 h-20 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuoteSlideLayout
|
||||
200
servers/nextjs/components/layouts/StatisticsSlideLayout.tsx
Normal file
200
servers/nextjs/components/layouts/StatisticsSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const statisticsSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Key Statistics').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
statistics: z.array(z.object({
|
||||
value: z.string().min(1).max(20).describe('Statistical value (e.g., "250%", "$1.2M", "99.9%")'),
|
||||
label: z.string().min(3).max(100).describe('Description of the statistic'),
|
||||
trend: z.enum(['up', 'down', 'neutral']).optional().describe('Trend direction indicator'),
|
||||
context: z.string().min(5).max(200).optional().describe('Additional context or time period')
|
||||
})).min(2).max(6).default([
|
||||
{
|
||||
value: '250%',
|
||||
label: 'Revenue Growth',
|
||||
trend: 'up',
|
||||
context: 'Year over year increase'
|
||||
},
|
||||
{
|
||||
value: '50M+',
|
||||
label: 'Active Users',
|
||||
trend: 'up',
|
||||
context: 'Global user base'
|
||||
},
|
||||
{
|
||||
value: '99.9%',
|
||||
label: 'Uptime',
|
||||
trend: 'neutral',
|
||||
context: 'Service reliability'
|
||||
},
|
||||
{
|
||||
value: '24/7',
|
||||
label: 'Support',
|
||||
trend: 'neutral',
|
||||
context: 'Customer service'
|
||||
}
|
||||
]).describe('List of statistics (2-6 items)'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = statisticsSlideSchema
|
||||
|
||||
export type StatisticsSlideData = z.infer<typeof statisticsSlideSchema>
|
||||
|
||||
interface StatisticsSlideLayoutProps {
|
||||
data?: Partial<StatisticsSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const StatisticsSlideLayout: React.FC<StatisticsSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = statisticsSlideSchema.parse(data || {})
|
||||
const statsCount = slideData.statistics.length
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
const trendColors = {
|
||||
up: {
|
||||
blue: 'text-emerald-600 bg-emerald-50 border-emerald-200',
|
||||
green: 'text-emerald-600 bg-emerald-50 border-emerald-200',
|
||||
purple: 'text-emerald-600 bg-emerald-50 border-emerald-200',
|
||||
orange: 'text-emerald-600 bg-emerald-50 border-emerald-200',
|
||||
red: 'text-emerald-600 bg-emerald-50 border-emerald-200'
|
||||
},
|
||||
down: 'text-red-600 bg-red-50 border-red-200',
|
||||
neutral: 'text-slate-600 bg-slate-50 border-slate-200'
|
||||
}
|
||||
|
||||
const getTrendIcon = (trend?: string) => {
|
||||
switch (trend) {
|
||||
case 'up':
|
||||
return '↗'
|
||||
case 'down':
|
||||
return '↘'
|
||||
case 'neutral':
|
||||
return '→'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const getGridCols = () => {
|
||||
if (statsCount <= 2) return 'grid-cols-2'
|
||||
if (statsCount <= 3) return 'grid-cols-3'
|
||||
if (statsCount <= 4) return 'grid-cols-2 lg:grid-cols-4'
|
||||
return 'grid-cols-2 lg:grid-cols-3'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Statistics Grid */}
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className={`grid ${getGridCols()} gap-6 w-full max-w-6xl`}>
|
||||
{slideData.statistics.map((stat, index) => (
|
||||
<div key={index} className="bg-white/95 backdrop-blur-lg rounded-2xl p-6 shadow-2xl border border-white/50 text-center relative overflow-hidden group hover:transform hover:scale-105 transition-all duration-300">
|
||||
{/* Card accent */}
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
{/* Statistic Value */}
|
||||
<div className={`text-4xl md:text-5xl font-black mb-2 bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{stat.value}
|
||||
</div>
|
||||
|
||||
{/* Statistic Label */}
|
||||
<h3 className="text-lg md:text-xl font-bold text-slate-900 mb-2 break-words">
|
||||
{stat.label}
|
||||
</h3>
|
||||
|
||||
{/* Trend Indicator */}
|
||||
{stat.trend && (
|
||||
<div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold border ${stat.trend === 'up' ? trendColors.up[accentColor] :
|
||||
stat.trend === 'down' ? trendColors.down :
|
||||
trendColors.neutral
|
||||
} mb-2`}>
|
||||
<span className="mr-1">{getTrendIcon(stat.trend)}</span>
|
||||
{stat.trend.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context */}
|
||||
{stat.context && (
|
||||
<p className="text-sm text-slate-600 break-words font-medium">
|
||||
{stat.context}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-16 h-16 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatisticsSlideLayout
|
||||
217
servers/nextjs/components/layouts/TeamSlideLayout.tsx
Normal file
217
servers/nextjs/components/layouts/TeamSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const teamSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Meet Our Team').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or team description'),
|
||||
teamMembers: z.array(z.object({
|
||||
name: z.string().min(2).max(100).describe('Team member name'),
|
||||
title: z.string().min(2).max(100).describe('Job title or role'),
|
||||
image: z.string().optional().describe('URL to team member photo'),
|
||||
bio: z.string().min(10).max(300).optional().describe('Brief biography or description'),
|
||||
email: z.string().email().optional().describe('Contact email'),
|
||||
linkedin: z.string().url().optional().describe('LinkedIn profile URL')
|
||||
})).min(1).max(6).default([
|
||||
{
|
||||
name: 'Sarah Johnson',
|
||||
title: 'Chief Executive Officer',
|
||||
image: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=300&h=300&fit=crop&crop=face',
|
||||
bio: 'Strategic leader with 15+ years experience driving innovation and growth in technology companies.',
|
||||
email: 'sarah@company.com',
|
||||
linkedin: 'https://linkedin.com/in/sarahjohnson'
|
||||
},
|
||||
{
|
||||
name: 'Michael Chen',
|
||||
title: 'Chief Technology Officer',
|
||||
image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=300&h=300&fit=crop&crop=face',
|
||||
bio: 'Technology visionary specializing in scalable architecture and emerging technologies.',
|
||||
email: 'michael@company.com',
|
||||
linkedin: 'https://linkedin.com/in/michaelchen'
|
||||
},
|
||||
{
|
||||
name: 'Emma Rodriguez',
|
||||
title: 'Head of Design',
|
||||
image: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=300&h=300&fit=crop&crop=face',
|
||||
bio: 'Creative director passionate about user-centered design and innovative digital experiences.',
|
||||
email: 'emma@company.com',
|
||||
linkedin: 'https://linkedin.com/in/emmarodriguez'
|
||||
}
|
||||
]).describe('Team members (1-6 people)'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = teamSlideSchema
|
||||
|
||||
export type TeamSlideData = z.infer<typeof teamSlideSchema>
|
||||
|
||||
interface TeamSlideLayoutProps {
|
||||
data?: Partial<TeamSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = teamSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
const iconColors = {
|
||||
blue: 'text-blue-600 hover:text-blue-700',
|
||||
green: 'text-emerald-600 hover:text-emerald-700',
|
||||
purple: 'text-violet-600 hover:text-violet-700',
|
||||
orange: 'text-orange-600 hover:text-orange-700',
|
||||
red: 'text-red-600 hover:text-red-700'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Team Grid */}
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className={`grid gap-6 w-full max-w-6xl ${slideData.teamMembers.length <= 2 ? 'grid-cols-2' :
|
||||
slideData.teamMembers.length <= 3 ? 'grid-cols-3' :
|
||||
slideData.teamMembers.length <= 4 ? 'grid-cols-2 lg:grid-cols-4' :
|
||||
'grid-cols-2 lg:grid-cols-3'
|
||||
}`}>
|
||||
{slideData.teamMembers.map((member, index) => (
|
||||
<div key={index} className="bg-white/95 backdrop-blur-lg rounded-2xl p-6 shadow-2xl border border-white/50 text-center relative overflow-hidden group hover:transform hover:scale-105 transition-all duration-300">
|
||||
{/* Card accent */}
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
{/* Professional Avatar */}
|
||||
<div className="relative mb-4 inline-block">
|
||||
<div className="w-20 h-20 mx-auto relative">
|
||||
{member.image ? (
|
||||
<img
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
className="w-full h-full object-cover rounded-full shadow-xl border-4 border-white group-hover:shadow-2xl transition-shadow duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className={`w-full h-full rounded-full ${accentSolids[accentColor]} flex items-center justify-center text-white font-bold text-xl shadow-xl border-4 border-white`}>
|
||||
{member.name.split(' ').map(n => n[0]).join('')}
|
||||
</div>
|
||||
)}
|
||||
<div className={`absolute inset-0 rounded-full bg-gradient-to-r ${accentColors[accentColor]} opacity-20 group-hover:opacity-30 transition-opacity duration-300`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Member Info */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-1 break-words">
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className={`text-sm font-semibold mb-3 bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{member.title}
|
||||
</p>
|
||||
|
||||
{member.bio && (
|
||||
<p className="text-xs text-slate-600 leading-relaxed break-words font-medium">
|
||||
{member.bio}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Professional Contact Links */}
|
||||
<div className="flex justify-center space-x-3">
|
||||
{member.email && (
|
||||
<a
|
||||
href={`mailto:${member.email}`}
|
||||
className={`p-2 rounded-full bg-slate-100 hover:bg-slate-200 ${iconColors[accentColor]} transition-all duration-200 hover:transform hover:scale-110`}
|
||||
title="Email"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{member.linkedin && (
|
||||
<a
|
||||
href={member.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`p-2 rounded-full bg-slate-100 hover:bg-slate-200 ${iconColors[accentColor]} transition-all duration-200 hover:transform hover:scale-110`}
|
||||
title="LinkedIn"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.338 16.338H13.67V12.16c0-.995-.017-2.277-1.387-2.277-1.39 0-1.601 1.086-1.601 2.207v4.248H8.014v-8.59h2.559v1.174h.037c.356-.675 1.227-1.387 2.526-1.387 2.703 0 3.203 1.778 3.203 4.092v4.711zM5.005 6.575a1.548 1.548 0 11-.003-3.096 1.548 1.548 0 01.003 3.096zm-1.337 9.763H6.34v-8.59H3.667v8.59zM17.668 1H2.328C1.595 1 1 1.581 1 2.298v15.403C1 18.418 1.595 19 2.328 19h15.34c.734 0 1.332-.582 1.332-1.299V2.298C19 1.581 18.402 1 17.668 1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-12 h-12 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TeamSlideLayout
|
||||
225
servers/nextjs/components/layouts/TimelineSlideLayout.tsx
Normal file
225
servers/nextjs/components/layouts/TimelineSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const timelineSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Project Timeline').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
timelineItems: z.array(z.object({
|
||||
date: z.string().min(2).max(50).describe('Date or time period'),
|
||||
title: z.string().min(3).max(100).describe('Event or milestone title'),
|
||||
description: z.string().min(10).max(300).describe('Event description'),
|
||||
status: z.enum(['completed', 'current', 'upcoming']).default('upcoming').describe('Timeline item status')
|
||||
})).min(2).max(6).default([
|
||||
{
|
||||
date: 'Q1 2024',
|
||||
title: 'Project Initiation',
|
||||
description: 'Project planning, team assembly, and initial requirements gathering',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
date: 'Q2 2024',
|
||||
title: 'Development Phase',
|
||||
description: 'Core development work, prototype creation, and testing implementation',
|
||||
status: 'current'
|
||||
},
|
||||
{
|
||||
date: 'Q3 2024',
|
||||
title: 'Testing & QA',
|
||||
description: 'Comprehensive testing, quality assurance, and user acceptance testing',
|
||||
status: 'upcoming'
|
||||
},
|
||||
{
|
||||
date: 'Q4 2024',
|
||||
title: 'Launch & Deployment',
|
||||
description: 'Final deployment, go-live activities, and post-launch monitoring',
|
||||
status: 'upcoming'
|
||||
}
|
||||
]).describe('Timeline events (2-6 items)'),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = timelineSlideSchema
|
||||
|
||||
export type TimelineSlideData = z.infer<typeof timelineSlideSchema>
|
||||
|
||||
interface TimelineSlideLayoutProps {
|
||||
data?: Partial<TimelineSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const TimelineSlideLayout: React.FC<TimelineSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
const slideData = timelineSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
completed: {
|
||||
blue: 'bg-emerald-600 border-emerald-600 shadow-emerald-200',
|
||||
green: 'bg-emerald-600 border-emerald-600 shadow-emerald-200',
|
||||
purple: 'bg-emerald-600 border-emerald-600 shadow-emerald-200',
|
||||
orange: 'bg-emerald-600 border-emerald-600 shadow-emerald-200',
|
||||
red: 'bg-emerald-600 border-emerald-600 shadow-emerald-200'
|
||||
},
|
||||
current: {
|
||||
blue: 'bg-blue-600 border-blue-600 ring-4 ring-blue-200 shadow-blue-300',
|
||||
green: 'bg-emerald-600 border-emerald-600 ring-4 ring-emerald-200 shadow-emerald-300',
|
||||
purple: 'bg-violet-600 border-violet-600 ring-4 ring-violet-200 shadow-violet-300',
|
||||
orange: 'bg-orange-600 border-orange-600 ring-4 ring-orange-200 shadow-orange-300',
|
||||
red: 'bg-red-600 border-red-600 ring-4 ring-red-200 shadow-red-300'
|
||||
},
|
||||
upcoming: {
|
||||
blue: 'bg-slate-300 border-slate-400 shadow-slate-200',
|
||||
green: 'bg-slate-300 border-slate-400 shadow-slate-200',
|
||||
purple: 'bg-slate-300 border-slate-400 shadow-slate-200',
|
||||
orange: 'bg-slate-300 border-slate-400 shadow-slate-200',
|
||||
red: 'bg-slate-300 border-slate-400 shadow-slate-200'
|
||||
}
|
||||
}
|
||||
|
||||
const lineColors = {
|
||||
blue: 'bg-blue-200',
|
||||
green: 'bg-emerald-200',
|
||||
purple: 'bg-violet-200',
|
||||
orange: 'bg-orange-200',
|
||||
red: 'bg-red-200'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Enhanced Timeline */}
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className="w-full max-w-6xl">
|
||||
<div className="relative flex justify-between items-start">
|
||||
{/* Timeline line */}
|
||||
<div className={`absolute top-8 left-0 right-0 h-1 ${lineColors[accentColor]} rounded-full`} />
|
||||
<div className={`absolute top-8 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full opacity-20`} />
|
||||
|
||||
{slideData.timelineItems.map((item, index) => (
|
||||
<div key={index} className="relative flex flex-col items-center" style={{ width: `${100 / slideData.timelineItems.length}%` }}>
|
||||
{/* Timeline node */}
|
||||
<div className={`w-6 h-6 rounded-full border-4 shadow-lg ${statusColors[item.status][accentColor]} relative z-10 mb-4 transition-all duration-300 hover:scale-110`}>
|
||||
{item.status === 'completed' && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{item.status === 'current' && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-white rounded-full animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Timeline content card */}
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-6 shadow-2xl border border-white/50 text-center min-h-[180px] flex flex-col justify-between relative overflow-hidden group hover:transform hover:scale-105 transition-all duration-300">
|
||||
{/* Card accent */}
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
<div>
|
||||
{/* Date */}
|
||||
<div className={`text-sm font-bold mb-2 px-3 py-1 rounded-full bg-gradient-to-r ${accentColors[accentColor]} text-white inline-block`}>
|
||||
{item.date}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-3 break-words">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-slate-600 leading-relaxed break-words font-medium">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="mt-4">
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${item.status === 'completed' ? 'bg-emerald-100 text-emerald-800' :
|
||||
item.status === 'current' ? `${accentColors[accentColor]} bg-opacity-10 text-${accentColor}-800` :
|
||||
'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{item.status === 'completed' && '✓ Completed'}
|
||||
{item.status === 'current' && '● In Progress'}
|
||||
{item.status === 'upcoming' && '○ Upcoming'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className={`absolute bottom-0 right-0 w-12 h-12 bg-gradient-to-tl ${accentColors[accentColor]} opacity-5 rounded-tl-full`} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineSlideLayout
|
||||
162
servers/nextjs/components/layouts/TwoColumnSlideLayout.tsx
Normal file
162
servers/nextjs/components/layouts/TwoColumnSlideLayout.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import React from 'react'
|
||||
import * as z from "zod";
|
||||
|
||||
const twoColumnSlideSchema = z.object({
|
||||
title: z.string().min(3).max(100).default('Two Column Layout').describe('Title of the slide'),
|
||||
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
|
||||
leftColumn: z.object({
|
||||
title: z.string().min(3).max(100).default('Left Column').describe('Left column title'),
|
||||
content: z.string().min(10).max(800).default('Content for the left column goes here. This can include detailed information, explanations, or supporting details.').describe('Left column content')
|
||||
}).default({
|
||||
title: 'Left Column',
|
||||
content: 'Content for the left column goes here. This can include detailed information, explanations, or supporting details.'
|
||||
}),
|
||||
rightColumn: z.object({
|
||||
title: z.string().min(3).max(100).default('Right Column').describe('Right column title'),
|
||||
content: z.string().min(10).max(800).default('Content for the right column goes here. This can include additional information, comparisons, or contrasting details.').describe('Right column content')
|
||||
}).default({
|
||||
title: 'Right Column',
|
||||
content: 'Content for the right column goes here. This can include additional information, comparisons, or contrasting details.'
|
||||
}),
|
||||
backgroundImage: z.string().optional().describe('URL to background image for the slide')
|
||||
})
|
||||
|
||||
export const Schema = twoColumnSlideSchema
|
||||
|
||||
|
||||
export type TwoColumnSlideData = z.infer<typeof twoColumnSlideSchema>
|
||||
|
||||
interface TwoColumnSlideLayoutProps {
|
||||
data?: Partial<TwoColumnSlideData>
|
||||
accentColor?: 'blue' | 'green' | 'purple' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
const TwoColumnSlideLayout: React.FC<TwoColumnSlideLayoutProps> = ({ data, accentColor = 'blue' }) => {
|
||||
|
||||
const slideData = twoColumnSlideSchema.parse(data || {})
|
||||
|
||||
const accentColors = {
|
||||
blue: 'from-blue-600 to-blue-800',
|
||||
green: 'from-emerald-600 to-emerald-800',
|
||||
purple: 'from-violet-600 to-violet-800',
|
||||
orange: 'from-orange-600 to-orange-800',
|
||||
red: 'from-red-600 to-red-800'
|
||||
}
|
||||
|
||||
const accentSolids = {
|
||||
blue: 'bg-blue-600',
|
||||
green: 'bg-emerald-600',
|
||||
purple: 'bg-violet-600',
|
||||
orange: 'bg-orange-600',
|
||||
red: 'bg-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
>
|
||||
{/* Enhanced geometric background decoration */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div className={`absolute top-0 right-0 w-96 h-96 ${accentSolids[accentColor]} rounded-full transform translate-x-32 -translate-y-32 blur-3xl`} />
|
||||
<div className={`absolute bottom-0 left-0 w-64 h-64 ${accentSolids[accentColor]} rounded-full transform -translate-x-16 translate-y-16 blur-2xl`} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData.backgroundImage
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
<span className={`bg-gradient-to-r ${accentColors[accentColor]} bg-clip-text text-transparent`}>
|
||||
{slideData.title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{slideData.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData.backgroundImage
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
{slideData.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="relative mt-4">
|
||||
<div className={`w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full shadow-lg`} />
|
||||
<div className={`absolute inset-0 w-32 h-1 bg-gradient-to-r ${accentColors[accentColor]} rounded-full blur-sm opacity-50`} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Two Column Content with Enhanced Styling */}
|
||||
<main className="flex-1 grid grid-cols-2 gap-6">
|
||||
{/* Left Column */}
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-8 shadow-2xl border border-white/50 flex flex-col relative overflow-hidden hover:shadow-3xl transition-shadow duration-300">
|
||||
{/* Column accent */}
|
||||
<div className={`absolute top-0 left-0 w-full h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
<h2 className={`text-2xl md:text-3xl font-bold mb-5 break-words ${slideData.backgroundImage
|
||||
? 'text-slate-900'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
{slideData.leftColumn.title}
|
||||
</h2>
|
||||
<div className={`text-base md:text-lg leading-relaxed break-words flex-1 ${slideData.backgroundImage
|
||||
? 'text-slate-700'
|
||||
: 'text-slate-700'
|
||||
}`}>
|
||||
{slideData.leftColumn.content.split('\n').map((paragraph, index) => (
|
||||
paragraph.trim() && (
|
||||
<p key={index} className="mb-4 last:mb-0 font-medium">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="bg-white/95 backdrop-blur-lg rounded-2xl p-8 shadow-2xl border border-white/50 flex flex-col relative overflow-hidden hover:shadow-3xl transition-shadow duration-300">
|
||||
{/* Column accent */}
|
||||
<div className={`absolute top-0 left-0 w-full h-1 bg-gradient-to-r ${accentColors[accentColor]}`} />
|
||||
|
||||
<h2 className={`text-2xl md:text-3xl font-bold mb-5 break-words ${slideData.backgroundImage
|
||||
? 'text-slate-900'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
{slideData.rightColumn.title}
|
||||
</h2>
|
||||
<div className={`text-base md:text-lg leading-relaxed break-words flex-1 ${slideData.backgroundImage
|
||||
? 'text-slate-700'
|
||||
: 'text-slate-700'
|
||||
}`}>
|
||||
{slideData.rightColumn.content.split('\n').map((paragraph, index) => (
|
||||
paragraph.trim() && (
|
||||
<p key={index} className="mb-4 last:mb-0 font-medium">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Enhanced decorative accent */}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-r ${accentColors[accentColor]} shadow-lg`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" />
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${accentColors[accentColor]} blur-sm opacity-50`} />
|
||||
</div>
|
||||
|
||||
{/* Professional corner accents */}
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${accentColors[accentColor]} opacity-5 rounded-bl-full`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TwoColumnSlideLayout
|
||||
31
servers/nextjs/components/ui/sonner.tsx
Normal file
31
servers/nextjs/components/ui/sonner.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
1075
servers/nextjs/package-lock.json
generated
1075
servers/nextjs/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "pfm",
|
||||
"name": "presenton",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
@ -46,15 +46,18 @@
|
|||
"lucide-react": "^0.447.0",
|
||||
"marked": "^15.0.11",
|
||||
"next": "^14.2.14",
|
||||
"puppeteer": "24.8.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-redux": "^9.1.2",
|
||||
"recharts": "^2.15.0",
|
||||
"sonner": "^2.0.6",
|
||||
"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": "^3.25.76",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/animejs": "^3.1.12",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue