Spaces:
Running
Running
major changes
Browse files- .claude/settings.local.json +8 -1
- app/api/export-pptx/route.ts +107 -27
- app/api/unsplash-download/route.ts +28 -0
- app/editor/page.tsx +1363 -150
- app/layout.tsx +46 -2
- app/page.tsx +39 -21
- components/HFAuth.tsx +145 -0
- components/UnsplashImageSearch.tsx +210 -0
- components/slides/BulletsSlide.tsx +200 -15
- components/slides/ChartSlide.tsx +125 -28
- components/slides/ComparisonSlide.tsx +379 -0
- components/slides/ContentImageSlide.tsx +89 -12
- components/slides/HeroSlide.tsx +244 -0
- components/slides/ImageOnlySlide.tsx +277 -0
- components/slides/QuoteSlide.tsx +291 -0
- components/slides/SectionSlide.tsx +299 -0
- components/slides/SlideFactory.tsx +215 -6
- components/slides/StatsSlide.tsx +255 -0
- components/slides/TitleSlide.tsx +250 -7
- components/slides/TwoColumnSlide.tsx +284 -0
- data/templates.ts +391 -0
- lib/gemini-client.ts +15 -3
- package-lock.json +101 -4
- package.json +3 -1
.claude/settings.local.json
CHANGED
|
@@ -3,7 +3,14 @@
|
|
| 3 |
"allow": [
|
| 4 |
"Bash(npm install:*)",
|
| 5 |
"Bash(npm run build:*)",
|
| 6 |
-
"Bash(npm run dev:*)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
],
|
| 8 |
"deny": [],
|
| 9 |
"ask": []
|
|
|
|
| 3 |
"allow": [
|
| 4 |
"Bash(npm install:*)",
|
| 5 |
"Bash(npm run build:*)",
|
| 6 |
+
"Bash(npm run dev:*)",
|
| 7 |
+
"WebSearch",
|
| 8 |
+
"mcp__fetch__imageFetch",
|
| 9 |
+
"mcp__puppeteer__puppeteer_navigate",
|
| 10 |
+
"mcp__puppeteer__puppeteer_screenshot",
|
| 11 |
+
"mcp__puppeteer__puppeteer_fill",
|
| 12 |
+
"mcp__puppeteer__puppeteer_evaluate",
|
| 13 |
+
"mcp__puppeteer__puppeteer_click"
|
| 14 |
],
|
| 15 |
"deny": [],
|
| 16 |
"ask": []
|
app/api/export-pptx/route.ts
CHANGED
|
@@ -36,45 +36,125 @@ export async function POST(request: NextRequest) {
|
|
| 36 |
slides.forEach((slideData, index) => {
|
| 37 |
const slide = pres.addSlide();
|
| 38 |
|
| 39 |
-
// Add
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
slide.addText(slideData.title, {
|
| 41 |
x: 0.5,
|
| 42 |
-
y: 0.
|
| 43 |
w: 9,
|
| 44 |
-
h: 1,
|
| 45 |
-
fontSize:
|
| 46 |
bold: true,
|
| 47 |
-
color: '
|
| 48 |
-
align: '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
});
|
| 50 |
|
| 51 |
-
// Add content points
|
| 52 |
if (slideData.content && slideData.content.length > 0) {
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
.
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
});
|
| 67 |
}
|
| 68 |
|
| 69 |
-
// Add slide number
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
slide.addText(`${index + 1}`, {
|
| 71 |
-
x: 9.
|
| 72 |
-
y:
|
| 73 |
-
w: 0.
|
| 74 |
-
h: 0.
|
| 75 |
-
fontSize:
|
| 76 |
-
color: '
|
| 77 |
align: 'center',
|
|
|
|
|
|
|
|
|
|
| 78 |
});
|
| 79 |
|
| 80 |
// Add speaker notes if available
|
|
|
|
| 36 |
slides.forEach((slideData, index) => {
|
| 37 |
const slide = pres.addSlide();
|
| 38 |
|
| 39 |
+
// Add gradient background (Gamma AI style)
|
| 40 |
+
slide.background = {
|
| 41 |
+
fill: {
|
| 42 |
+
type: 'gradient',
|
| 43 |
+
dir: 45,
|
| 44 |
+
colors: [
|
| 45 |
+
{ position: 0, color: '667eea' }, // Purple-blue
|
| 46 |
+
{ position: 50, color: '764ba2' }, // Purple
|
| 47 |
+
{ position: 100, color: 'f093fb' } // Pink
|
| 48 |
+
]
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
// Add subtle pattern overlay
|
| 53 |
+
slide.addShape('rect', {
|
| 54 |
+
x: 0,
|
| 55 |
+
y: 0,
|
| 56 |
+
w: 10,
|
| 57 |
+
h: 5.625,
|
| 58 |
+
fill: {
|
| 59 |
+
type: 'pattern',
|
| 60 |
+
pattern: 'dots',
|
| 61 |
+
fgColor: 'FFFFFF',
|
| 62 |
+
bgColor: 'FFFFFF'
|
| 63 |
+
},
|
| 64 |
+
transparency: 95
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
// Add title with modern styling
|
| 68 |
slide.addText(slideData.title, {
|
| 69 |
x: 0.5,
|
| 70 |
+
y: 0.8,
|
| 71 |
w: 9,
|
| 72 |
+
h: 1.2,
|
| 73 |
+
fontSize: 36,
|
| 74 |
bold: true,
|
| 75 |
+
color: 'FFFFFF',
|
| 76 |
+
align: 'left',
|
| 77 |
+
fontFace: 'Segoe UI',
|
| 78 |
+
shadow: {
|
| 79 |
+
type: 'outer',
|
| 80 |
+
color: '000000',
|
| 81 |
+
blur: 8,
|
| 82 |
+
offset: 2,
|
| 83 |
+
angle: 45,
|
| 84 |
+
opacity: 30
|
| 85 |
+
}
|
| 86 |
});
|
| 87 |
|
| 88 |
+
// Add content points with modern styling
|
| 89 |
if (slideData.content && slideData.content.length > 0) {
|
| 90 |
+
slideData.content.forEach((point, pointIndex) => {
|
| 91 |
+
// Add bullet point with gradient accent
|
| 92 |
+
slide.addShape('circle', {
|
| 93 |
+
x: 0.8,
|
| 94 |
+
y: 2.2 + (pointIndex * 0.6),
|
| 95 |
+
w: 0.15,
|
| 96 |
+
h: 0.15,
|
| 97 |
+
fill: {
|
| 98 |
+
type: 'gradient',
|
| 99 |
+
dir: 45,
|
| 100 |
+
colors: [
|
| 101 |
+
{ position: 0, color: 'ff6b6b' }, // Coral
|
| 102 |
+
{ position: 100, color: 'feca57' } // Yellow
|
| 103 |
+
]
|
| 104 |
+
}
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
// Add content text
|
| 108 |
+
slide.addText(point, {
|
| 109 |
+
x: 1.2,
|
| 110 |
+
y: 2.1 + (pointIndex * 0.6),
|
| 111 |
+
w: 7.8,
|
| 112 |
+
h: 0.5,
|
| 113 |
+
fontSize: 18,
|
| 114 |
+
color: 'FFFFFF',
|
| 115 |
+
fontFace: 'Segoe UI',
|
| 116 |
+
valign: 'middle',
|
| 117 |
+
shadow: {
|
| 118 |
+
type: 'outer',
|
| 119 |
+
color: '000000',
|
| 120 |
+
blur: 4,
|
| 121 |
+
offset: 1,
|
| 122 |
+
angle: 45,
|
| 123 |
+
opacity: 20
|
| 124 |
+
}
|
| 125 |
+
});
|
| 126 |
});
|
| 127 |
}
|
| 128 |
|
| 129 |
+
// Add slide number with modern badge style
|
| 130 |
+
slide.addShape('roundRect', {
|
| 131 |
+
x: 9.0,
|
| 132 |
+
y: 4.9,
|
| 133 |
+
w: 0.8,
|
| 134 |
+
h: 0.4,
|
| 135 |
+
fill: {
|
| 136 |
+
type: 'gradient',
|
| 137 |
+
dir: 90,
|
| 138 |
+
colors: [
|
| 139 |
+
{ position: 0, color: '000000' },
|
| 140 |
+
{ position: 100, color: '333333' }
|
| 141 |
+
]
|
| 142 |
+
},
|
| 143 |
+
transparency: 30,
|
| 144 |
+
rectRadius: 0.2
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
slide.addText(`${index + 1}`, {
|
| 148 |
+
x: 9.0,
|
| 149 |
+
y: 4.9,
|
| 150 |
+
w: 0.8,
|
| 151 |
+
h: 0.4,
|
| 152 |
+
fontSize: 14,
|
| 153 |
+
color: 'FFFFFF',
|
| 154 |
align: 'center',
|
| 155 |
+
valign: 'middle',
|
| 156 |
+
bold: true,
|
| 157 |
+
fontFace: 'Segoe UI'
|
| 158 |
});
|
| 159 |
|
| 160 |
// Add speaker notes if available
|
app/api/unsplash-download/route.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
export async function GET(request: NextRequest) {
|
| 4 |
+
const { searchParams } = new URL(request.url);
|
| 5 |
+
const downloadUrl = searchParams.get('url');
|
| 6 |
+
|
| 7 |
+
if (!downloadUrl) {
|
| 8 |
+
return NextResponse.json({ error: 'Download URL is required' }, { status: 400 });
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
try {
|
| 12 |
+
// Trigger download tracking as required by Unsplash API
|
| 13 |
+
const response = await fetch(downloadUrl, {
|
| 14 |
+
headers: {
|
| 15 |
+
'Authorization': `Client-ID ${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`,
|
| 16 |
+
},
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
if (!response.ok) {
|
| 20 |
+
console.error('Failed to track download:', response.statusText);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
return NextResponse.json({ success: true });
|
| 24 |
+
} catch (error) {
|
| 25 |
+
console.error('Error tracking download:', error);
|
| 26 |
+
return NextResponse.json({ error: 'Failed to track download' }, { status: 500 });
|
| 27 |
+
}
|
| 28 |
+
}
|
app/editor/page.tsx
CHANGED
|
@@ -1,150 +1,1363 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import React, { useState, useEffect } from 'react';
|
| 4 |
-
import { renderSlide, type SlideSpec } from '@/components/slides/SlideFactory';
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from 'react';
|
| 4 |
+
import { renderSlide, type SlideSpec } from '@/components/slides/SlideFactory';
|
| 5 |
+
import UnsplashImageSearch from '@/components/UnsplashImageSearch';
|
| 6 |
+
import { professionalTemplates, createTemplateSlides, type Template } from '@/data/templates';
|
| 7 |
+
import {
|
| 8 |
+
Trash2, Palette, Plus, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight,
|
| 9 |
+
ChevronDown, Type, Paintbrush, Image, Upload, Download, Home, FileText, Presentation,
|
| 10 |
+
Play, X, ArrowLeft, ArrowRight, Pause
|
| 11 |
+
} from 'lucide-react';
|
| 12 |
+
|
| 13 |
+
interface PresentationData {
|
| 14 |
+
theme: string;
|
| 15 |
+
slides: SlideSpec[];
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type Theme = 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 19 |
+
|
| 20 |
+
const themes: { name: Theme; label: string; colors: string }[] = [
|
| 21 |
+
{ name: 'dark', label: 'Dark', colors: 'bg-gradient-to-br from-gray-800 to-gray-900 text-white' },
|
| 22 |
+
{ name: 'light', label: 'Light', colors: 'bg-white text-gray-900' },
|
| 23 |
+
{ name: 'blue', label: 'Ocean Blue', colors: 'bg-gradient-to-br from-blue-600 to-blue-800 text-white' },
|
| 24 |
+
{ name: 'purple', label: 'Royal Purple', colors: 'bg-gradient-to-br from-purple-600 to-purple-800 text-white' },
|
| 25 |
+
{ name: 'green', label: 'Forest Green', colors: 'bg-gradient-to-br from-green-600 to-green-800 text-white' },
|
| 26 |
+
{ name: 'orange', label: 'Sunset Orange', colors: 'bg-gradient-to-br from-orange-600 to-orange-800 text-white' },
|
| 27 |
+
{ name: 'teal', label: 'Ocean Teal', colors: 'bg-gradient-to-br from-teal-600 to-cyan-700 text-white' },
|
| 28 |
+
{ name: 'pink', label: 'Rose Pink', colors: 'bg-gradient-to-br from-pink-600 to-rose-700 text-white' },
|
| 29 |
+
{ name: 'indigo', label: 'Deep Indigo', colors: 'bg-gradient-to-br from-indigo-600 to-purple-700 text-white' },
|
| 30 |
+
{ name: 'amber', label: 'Golden Amber', colors: 'bg-gradient-to-br from-amber-500 to-orange-600 text-white' },
|
| 31 |
+
{ name: 'emerald', label: 'Emerald Mint', colors: 'bg-gradient-to-br from-emerald-500 to-teal-600 text-white' },
|
| 32 |
+
{ name: 'slate', label: 'Modern Slate', colors: 'bg-gradient-to-br from-slate-700 to-gray-800 text-white' },
|
| 33 |
+
{ name: 'midnight', label: 'Midnight Indigo', colors: 'bg-gradient-to-br from-indigo-900 to-slate-900 text-white' },
|
| 34 |
+
{ name: 'sunset', label: 'Sunset Orange', colors: 'bg-gradient-to-br from-orange-600 to-amber-500 text-white' },
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
const googleFonts = [
|
| 38 |
+
{ name: 'Inter', family: 'var(--font-inter)', label: 'Inter' },
|
| 39 |
+
{ name: 'Roboto', family: 'var(--font-roboto)', label: 'Roboto' },
|
| 40 |
+
{ name: 'Open Sans', family: 'var(--font-open-sans)', label: 'Open Sans' },
|
| 41 |
+
{ name: 'Lato', family: 'var(--font-lato)', label: 'Lato' },
|
| 42 |
+
{ name: 'Montserrat', family: 'var(--font-montserrat)', label: 'Montserrat' },
|
| 43 |
+
{ name: 'Poppins', family: 'var(--font-poppins)', label: 'Poppins' },
|
| 44 |
+
{ name: 'Playfair Display', family: 'var(--font-playfair)', label: 'Playfair Display' },
|
| 45 |
+
{ name: 'Merriweather', family: 'var(--font-merriweather)', label: 'Merriweather' },
|
| 46 |
+
{ name: 'Geist Sans', family: 'var(--font-geist-sans)', label: 'Geist Sans' },
|
| 47 |
+
];
|
| 48 |
+
|
| 49 |
+
const fontSizes = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 60, 72];
|
| 50 |
+
|
| 51 |
+
const colors = [
|
| 52 |
+
'#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
|
| 53 |
+
'#800000', '#008000', '#000080', '#808000', '#800080', '#008080', '#C0C0C0', '#808080',
|
| 54 |
+
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'
|
| 55 |
+
];
|
| 56 |
+
|
| 57 |
+
const layouts = [
|
| 58 |
+
{ id: 'title', name: 'Title Slide', description: 'Title and subtitle', icon: '📰' },
|
| 59 |
+
{ id: 'hero', name: 'Hero Slide', description: 'Large impactful title', icon: '🎯' },
|
| 60 |
+
{ id: 'content-image', name: 'Content + Image', description: 'Text with image', icon: '🖼️' },
|
| 61 |
+
{ id: 'bullets', name: 'Bullet Points', description: 'Bulleted list', icon: '📝' },
|
| 62 |
+
{ id: 'chart', name: 'Chart', description: 'Data visualization', icon: '📊' },
|
| 63 |
+
{ id: 'stats', name: 'Stats Display', description: 'Key metrics and numbers', icon: '📈' },
|
| 64 |
+
{ id: 'two-column', name: 'Two Column', description: 'Side-by-side content', icon: '📄' },
|
| 65 |
+
{ id: 'image-only', name: 'Image Focus', description: 'Large image with caption', icon: '🖼️' },
|
| 66 |
+
{ id: 'quote', name: 'Quote Slide', description: 'Large quote with author', icon: '💬' },
|
| 67 |
+
{ id: 'section', name: 'Section Header', description: 'Section divider', icon: '🏷️' },
|
| 68 |
+
{ id: 'comparison', name: 'Comparison', description: 'Compare two items', icon: '⚖️' }
|
| 69 |
+
];
|
| 70 |
+
|
| 71 |
+
export default function EditorPage() {
|
| 72 |
+
const [presentation, setPresentation] = useState<PresentationData | null>(null);
|
| 73 |
+
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
|
| 74 |
+
const [showThemeSelector, setShowThemeSelector] = useState(false);
|
| 75 |
+
const [editingField, setEditingField] = useState<{ slideId: string; field: string; index?: number } | null>(null);
|
| 76 |
+
|
| 77 |
+
// Formatting states
|
| 78 |
+
const [selectedFont, setSelectedFont] = useState('Inter');
|
| 79 |
+
const [selectedFontSize, setSelectedFontSize] = useState(16);
|
| 80 |
+
const [selectedColor, setSelectedColor] = useState('#000000');
|
| 81 |
+
const [isBold, setIsBold] = useState(false);
|
| 82 |
+
const [isItalic, setIsItalic] = useState(false);
|
| 83 |
+
const [isUnderline, setIsUnderline] = useState(false);
|
| 84 |
+
const [alignment, setAlignment] = useState<'left' | 'center' | 'right'>('left');
|
| 85 |
+
|
| 86 |
+
// UI states
|
| 87 |
+
const [showFontDropdown, setShowFontDropdown] = useState(false);
|
| 88 |
+
const [showFontSizeDropdown, setShowFontSizeDropdown] = useState(false);
|
| 89 |
+
const [showColorPicker, setShowColorPicker] = useState(false);
|
| 90 |
+
const [showImageUpload, setShowImageUpload] = useState(false);
|
| 91 |
+
const [showUnsplashSearch, setShowUnsplashSearch] = useState(false);
|
| 92 |
+
const [showHomeWarning, setShowHomeWarning] = useState(false);
|
| 93 |
+
const [selectedTextElement, setSelectedTextElement] = useState<any>(null);
|
| 94 |
+
const [isDragOver, setIsDragOver] = useState(false);
|
| 95 |
+
const [showLayoutSelector, setShowLayoutSelector] = useState(false);
|
| 96 |
+
const [showTemplateSelector, setShowTemplateSelector] = useState(false);
|
| 97 |
+
|
| 98 |
+
// Homepage-style background noise
|
| 99 |
+
const noiseSvg = "<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 60 60'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(#n)' opacity='0.35'/></svg>";
|
| 100 |
+
const noiseDataUrl = `url('data:image/svg+xml;utf8,${encodeURIComponent(noiseSvg)}')`;
|
| 101 |
+
|
| 102 |
+
// Handle text selection from slides
|
| 103 |
+
const handleTextSelection = (element: any) => {
|
| 104 |
+
setSelectedTextElement(element);
|
| 105 |
+
if (element) {
|
| 106 |
+
setSelectedFont(element.currentFont || 'Inter');
|
| 107 |
+
setSelectedFontSize(element.currentFontSize || 16);
|
| 108 |
+
setSelectedColor(element.currentColor || '#000000');
|
| 109 |
+
|
| 110 |
+
// Update formatting states based on current element formatting
|
| 111 |
+
const currentSlide = presentation?.slides.find(s => s.id === element.slideId);
|
| 112 |
+
if (currentSlide?.formatting?.[element.field]) {
|
| 113 |
+
const formatting = currentSlide.formatting[element.field];
|
| 114 |
+
setIsBold(formatting.bold || false);
|
| 115 |
+
setIsItalic(formatting.italic || false);
|
| 116 |
+
setIsUnderline(formatting.underline || false);
|
| 117 |
+
} else {
|
| 118 |
+
setIsBold(false);
|
| 119 |
+
setIsItalic(false);
|
| 120 |
+
setIsUnderline(false);
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
// Apply formatting to selected text
|
| 126 |
+
const applyTextFormatting = (property: string, value: any) => {
|
| 127 |
+
if (!selectedTextElement || !presentation) return;
|
| 128 |
+
|
| 129 |
+
const { slideId, field } = selectedTextElement;
|
| 130 |
+
const slideIndex = presentation.slides.findIndex(s => s.id === slideId);
|
| 131 |
+
|
| 132 |
+
if (slideIndex === -1) return;
|
| 133 |
+
|
| 134 |
+
const updatedSlides = [...presentation.slides];
|
| 135 |
+
const slide = { ...updatedSlides[slideIndex] };
|
| 136 |
+
|
| 137 |
+
// Store formatting data
|
| 138 |
+
if (!slide.formatting) {
|
| 139 |
+
slide.formatting = {};
|
| 140 |
+
}
|
| 141 |
+
if (!slide.formatting[field]) {
|
| 142 |
+
slide.formatting[field] = {};
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
slide.formatting[field][property] = value;
|
| 146 |
+
updatedSlides[slideIndex] = slide;
|
| 147 |
+
|
| 148 |
+
const updatedPresentation = { ...presentation, slides: updatedSlides };
|
| 149 |
+
setPresentation(updatedPresentation);
|
| 150 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 151 |
+
|
| 152 |
+
// Force update of the selected font states to trigger re-render
|
| 153 |
+
if (property === 'fontFamily') setSelectedFont(value);
|
| 154 |
+
if (property === 'fontSize') setSelectedFontSize(value);
|
| 155 |
+
if (property === 'color') setSelectedColor(value);
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
// Apply text styling
|
| 159 |
+
const applyTextStyling = (style: string) => {
|
| 160 |
+
if (!selectedTextElement) return;
|
| 161 |
+
|
| 162 |
+
const currentValue = selectedTextElement.currentStyles?.[style] || false;
|
| 163 |
+
applyTextFormatting(style, !currentValue);
|
| 164 |
+
|
| 165 |
+
// Update UI state
|
| 166 |
+
if (style === 'bold') setIsBold(!currentValue);
|
| 167 |
+
if (style === 'italic') setIsItalic(!currentValue);
|
| 168 |
+
if (style === 'underline') setIsUnderline(!currentValue);
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
// Slideshow states
|
| 172 |
+
const [isInSlideshow, setIsInSlideshow] = useState(false);
|
| 173 |
+
const [slideshowIndex, setSlideshowIndex] = useState(0);
|
| 174 |
+
const [isAutoPlay, setIsAutoPlay] = useState(false);
|
| 175 |
+
const [autoPlayInterval, setAutoPlayInterval] = useState<NodeJS.Timeout | null>(null);
|
| 176 |
+
|
| 177 |
+
useEffect(() => {
|
| 178 |
+
// Load presentation data from sessionStorage
|
| 179 |
+
const savedPresentation = sessionStorage.getItem('currentPresentation');
|
| 180 |
+
if (savedPresentation) {
|
| 181 |
+
try {
|
| 182 |
+
const data = JSON.parse(savedPresentation);
|
| 183 |
+
setPresentation(data);
|
| 184 |
+
} catch (error) {
|
| 185 |
+
console.error('Error parsing presentation data:', error);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
}, []);
|
| 189 |
+
|
| 190 |
+
const handleDeleteSlide = (index: number) => {
|
| 191 |
+
if (!presentation || presentation.slides.length <= 1) return;
|
| 192 |
+
|
| 193 |
+
const newSlides = presentation.slides.filter((_, i) => i !== index);
|
| 194 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 195 |
+
|
| 196 |
+
setPresentation(updatedPresentation);
|
| 197 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 198 |
+
|
| 199 |
+
// Adjust current slide index if necessary
|
| 200 |
+
if (currentSlideIndex >= newSlides.length) {
|
| 201 |
+
setCurrentSlideIndex(newSlides.length - 1);
|
| 202 |
+
} else if (index <= currentSlideIndex && currentSlideIndex > 0) {
|
| 203 |
+
setCurrentSlideIndex(currentSlideIndex - 1);
|
| 204 |
+
}
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
const handleFieldEdit = (slideId: string, field: string, index?: number) => {
|
| 208 |
+
setEditingField({ slideId, field, index });
|
| 209 |
+
};
|
| 210 |
+
|
| 211 |
+
const handleFieldUpdate = (slideId: string, field: string, value: any, index?: number) => {
|
| 212 |
+
if (!presentation) return;
|
| 213 |
+
|
| 214 |
+
const newSlides = presentation.slides.map(slide => {
|
| 215 |
+
if (slide.id !== slideId) return slide;
|
| 216 |
+
|
| 217 |
+
const updatedSlide = { ...slide };
|
| 218 |
+
|
| 219 |
+
if (field === 'title') {
|
| 220 |
+
updatedSlide.title = value;
|
| 221 |
+
} else if (field === 'subtitle') {
|
| 222 |
+
updatedSlide.subtitle = value;
|
| 223 |
+
} else if (field === 'body' && typeof index === 'number') {
|
| 224 |
+
if (!updatedSlide.body) updatedSlide.body = [];
|
| 225 |
+
updatedSlide.body[index] = { ...updatedSlide.body[index], text: value };
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
return updatedSlide;
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 232 |
+
setPresentation(updatedPresentation);
|
| 233 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 234 |
+
};
|
| 235 |
+
|
| 236 |
+
const handleFieldBlur = () => {
|
| 237 |
+
setEditingField(null);
|
| 238 |
+
};
|
| 239 |
+
|
| 240 |
+
const handleAddSlide = () => {
|
| 241 |
+
if (!presentation) return;
|
| 242 |
+
|
| 243 |
+
const newSlide: SlideSpec = {
|
| 244 |
+
id: `slide-${Date.now()}`,
|
| 245 |
+
layout: 'bullets',
|
| 246 |
+
title: 'New Slide',
|
| 247 |
+
body: [{ text: 'Add your content here' }]
|
| 248 |
+
};
|
| 249 |
+
|
| 250 |
+
const newSlides = [...presentation.slides, newSlide];
|
| 251 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 252 |
+
|
| 253 |
+
setPresentation(updatedPresentation);
|
| 254 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 255 |
+
setCurrentSlideIndex(newSlides.length - 1);
|
| 256 |
+
};
|
| 257 |
+
|
| 258 |
+
const handleThemeChange = (theme: Theme) => {
|
| 259 |
+
if (!presentation) return;
|
| 260 |
+
|
| 261 |
+
const updatedPresentation = { ...presentation, theme };
|
| 262 |
+
setPresentation(updatedPresentation);
|
| 263 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 264 |
+
setShowThemeSelector(false);
|
| 265 |
+
};
|
| 266 |
+
|
| 267 |
+
const handleTemplateSelect = (template: Template) => {
|
| 268 |
+
const templateSlides = createTemplateSlides(template);
|
| 269 |
+
const newPresentation: PresentationData = {
|
| 270 |
+
theme: template.theme,
|
| 271 |
+
slides: templateSlides
|
| 272 |
+
};
|
| 273 |
+
|
| 274 |
+
setPresentation(newPresentation);
|
| 275 |
+
setCurrentSlideIndex(0);
|
| 276 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(newPresentation));
|
| 277 |
+
setShowTemplateSelector(false);
|
| 278 |
+
};
|
| 279 |
+
|
| 280 |
+
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 281 |
+
const file = event.target.files?.[0];
|
| 282 |
+
if (!file || !presentation) return;
|
| 283 |
+
|
| 284 |
+
const reader = new FileReader();
|
| 285 |
+
reader.onload = (e) => {
|
| 286 |
+
const imageUrl = e.target?.result as string;
|
| 287 |
+
|
| 288 |
+
// Add image to current slide or create new slide with image
|
| 289 |
+
const currentSlide = presentation.slides[currentSlideIndex];
|
| 290 |
+
if (currentSlide && currentSlide.layout === 'content-image') {
|
| 291 |
+
// Update existing content-image slide
|
| 292 |
+
const newSlides = presentation.slides.map((slide, index) =>
|
| 293 |
+
index === currentSlideIndex
|
| 294 |
+
? { ...slide, images: [imageUrl] }
|
| 295 |
+
: slide
|
| 296 |
+
);
|
| 297 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 298 |
+
setPresentation(updatedPresentation);
|
| 299 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 300 |
+
} else {
|
| 301 |
+
// Create new content-image slide
|
| 302 |
+
const newSlide: SlideSpec = {
|
| 303 |
+
id: `slide-${Date.now()}`,
|
| 304 |
+
layout: 'content-image',
|
| 305 |
+
title: 'New Image Slide',
|
| 306 |
+
body: [{ text: 'Add your content here' }],
|
| 307 |
+
images: [imageUrl]
|
| 308 |
+
};
|
| 309 |
+
const newSlides = [...presentation.slides, newSlide];
|
| 310 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 311 |
+
setPresentation(updatedPresentation);
|
| 312 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 313 |
+
setCurrentSlideIndex(newSlides.length - 1);
|
| 314 |
+
}
|
| 315 |
+
};
|
| 316 |
+
reader.readAsDataURL(file);
|
| 317 |
+
setShowImageUpload(false);
|
| 318 |
+
};
|
| 319 |
+
|
| 320 |
+
const handleUnsplashImageSelect = (imageUrl: string) => {
|
| 321 |
+
if (!presentation) return;
|
| 322 |
+
|
| 323 |
+
// Add image to current slide or create new slide with image
|
| 324 |
+
const currentSlide = presentation.slides[currentSlideIndex];
|
| 325 |
+
if (currentSlide && currentSlide.layout === 'content-image') {
|
| 326 |
+
// Update existing content-image slide
|
| 327 |
+
const newSlides = presentation.slides.map((slide, index) =>
|
| 328 |
+
index === currentSlideIndex
|
| 329 |
+
? { ...slide, images: [imageUrl] }
|
| 330 |
+
: slide
|
| 331 |
+
);
|
| 332 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 333 |
+
setPresentation(updatedPresentation);
|
| 334 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 335 |
+
} else {
|
| 336 |
+
// Create new content-image slide
|
| 337 |
+
const newSlide: SlideSpec = {
|
| 338 |
+
id: `slide-${Date.now()}`,
|
| 339 |
+
layout: 'content-image',
|
| 340 |
+
title: 'New Image Slide',
|
| 341 |
+
body: [{ text: 'Add your content here' }],
|
| 342 |
+
images: [imageUrl]
|
| 343 |
+
};
|
| 344 |
+
const newSlides = [...presentation.slides, newSlide];
|
| 345 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 346 |
+
setPresentation(updatedPresentation);
|
| 347 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 348 |
+
setCurrentSlideIndex(newSlides.length - 1);
|
| 349 |
+
}
|
| 350 |
+
setShowUnsplashSearch(false);
|
| 351 |
+
};
|
| 352 |
+
|
| 353 |
+
// Drag and drop handlers
|
| 354 |
+
const handleDragOver = (e: React.DragEvent) => {
|
| 355 |
+
e.preventDefault();
|
| 356 |
+
e.stopPropagation();
|
| 357 |
+
setIsDragOver(true);
|
| 358 |
+
};
|
| 359 |
+
|
| 360 |
+
const handleDragLeave = (e: React.DragEvent) => {
|
| 361 |
+
e.preventDefault();
|
| 362 |
+
e.stopPropagation();
|
| 363 |
+
setIsDragOver(false);
|
| 364 |
+
};
|
| 365 |
+
|
| 366 |
+
const handleDrop = (e: React.DragEvent) => {
|
| 367 |
+
e.preventDefault();
|
| 368 |
+
e.stopPropagation();
|
| 369 |
+
setIsDragOver(false);
|
| 370 |
+
|
| 371 |
+
const files = Array.from(e.dataTransfer.files);
|
| 372 |
+
const imageFile = files.find(file => file.type.startsWith('image/'));
|
| 373 |
+
|
| 374 |
+
if (imageFile && presentation) {
|
| 375 |
+
const reader = new FileReader();
|
| 376 |
+
reader.onload = (event) => {
|
| 377 |
+
const imageUrl = event.target?.result as string;
|
| 378 |
+
|
| 379 |
+
// Add image to current slide or create new slide with image
|
| 380 |
+
const currentSlide = presentation.slides[currentSlideIndex];
|
| 381 |
+
if (currentSlide && currentSlide.layout === 'content-image') {
|
| 382 |
+
// Update existing content-image slide
|
| 383 |
+
const newSlides = presentation.slides.map((slide, index) =>
|
| 384 |
+
index === currentSlideIndex
|
| 385 |
+
? { ...slide, images: [imageUrl] }
|
| 386 |
+
: slide
|
| 387 |
+
);
|
| 388 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 389 |
+
setPresentation(updatedPresentation);
|
| 390 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 391 |
+
} else {
|
| 392 |
+
// Create new content-image slide
|
| 393 |
+
const newSlide: SlideSpec = {
|
| 394 |
+
id: `slide-${Date.now()}`,
|
| 395 |
+
layout: 'content-image',
|
| 396 |
+
title: 'New Image Slide',
|
| 397 |
+
body: [{ text: 'Add your content here' }],
|
| 398 |
+
images: [imageUrl]
|
| 399 |
+
};
|
| 400 |
+
const newSlides = [...presentation.slides, newSlide];
|
| 401 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 402 |
+
setPresentation(updatedPresentation);
|
| 403 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 404 |
+
setCurrentSlideIndex(newSlides.length - 1);
|
| 405 |
+
}
|
| 406 |
+
};
|
| 407 |
+
reader.readAsDataURL(imageFile);
|
| 408 |
+
}
|
| 409 |
+
};
|
| 410 |
+
|
| 411 |
+
// Handle layout change
|
| 412 |
+
const handleLayoutChange = (newLayout: string) => {
|
| 413 |
+
if (!presentation) return;
|
| 414 |
+
|
| 415 |
+
const currentSlide = presentation.slides[currentSlideIndex];
|
| 416 |
+
const updatedSlide: SlideSpec = {
|
| 417 |
+
...currentSlide,
|
| 418 |
+
layout: newLayout
|
| 419 |
+
};
|
| 420 |
+
|
| 421 |
+
// Ensure appropriate default content for each layout
|
| 422 |
+
switch (newLayout) {
|
| 423 |
+
case 'title':
|
| 424 |
+
if (!updatedSlide.subtitle) {
|
| 425 |
+
updatedSlide.subtitle = 'Subtitle';
|
| 426 |
+
}
|
| 427 |
+
break;
|
| 428 |
+
case 'content-image':
|
| 429 |
+
if (!updatedSlide.body || updatedSlide.body.length === 0) {
|
| 430 |
+
updatedSlide.body = [{ text: 'Add your content here' }];
|
| 431 |
+
}
|
| 432 |
+
if (!updatedSlide.images || updatedSlide.images.length === 0) {
|
| 433 |
+
updatedSlide.images = [];
|
| 434 |
+
}
|
| 435 |
+
break;
|
| 436 |
+
case 'bullets':
|
| 437 |
+
if (!updatedSlide.body || updatedSlide.body.length === 0) {
|
| 438 |
+
updatedSlide.body = [
|
| 439 |
+
{ text: 'First bullet point' },
|
| 440 |
+
{ text: 'Second bullet point' },
|
| 441 |
+
{ text: 'Third bullet point' }
|
| 442 |
+
];
|
| 443 |
+
}
|
| 444 |
+
break;
|
| 445 |
+
case 'chart':
|
| 446 |
+
if (!updatedSlide.chart) {
|
| 447 |
+
updatedSlide.chart = {
|
| 448 |
+
type: 'bar',
|
| 449 |
+
data: {
|
| 450 |
+
labels: ['A', 'B', 'C'],
|
| 451 |
+
datasets: [{ data: [10, 20, 15] }]
|
| 452 |
+
}
|
| 453 |
+
};
|
| 454 |
+
}
|
| 455 |
+
break;
|
| 456 |
+
case 'two-column':
|
| 457 |
+
if (!updatedSlide.body || updatedSlide.body.length < 2) {
|
| 458 |
+
updatedSlide.body = [
|
| 459 |
+
{ heading: 'Left Column', text: 'Content for the left side' },
|
| 460 |
+
{ heading: 'Right Column', text: 'Content for the right side' }
|
| 461 |
+
];
|
| 462 |
+
}
|
| 463 |
+
break;
|
| 464 |
+
case 'image-only':
|
| 465 |
+
if (!updatedSlide.images || updatedSlide.images.length === 0) {
|
| 466 |
+
updatedSlide.images = [];
|
| 467 |
+
}
|
| 468 |
+
if (!updatedSlide.subtitle) {
|
| 469 |
+
updatedSlide.subtitle = 'Image caption';
|
| 470 |
+
}
|
| 471 |
+
break;
|
| 472 |
+
case 'quote':
|
| 473 |
+
if (!updatedSlide.body || updatedSlide.body.length === 0) {
|
| 474 |
+
updatedSlide.body = [{ text: 'Your inspiring quote goes here' }];
|
| 475 |
+
}
|
| 476 |
+
if (!updatedSlide.subtitle) {
|
| 477 |
+
updatedSlide.subtitle = '— Author Name';
|
| 478 |
+
}
|
| 479 |
+
break;
|
| 480 |
+
case 'section':
|
| 481 |
+
if (!updatedSlide.subtitle) {
|
| 482 |
+
updatedSlide.subtitle = 'Section description';
|
| 483 |
+
}
|
| 484 |
+
break;
|
| 485 |
+
case 'comparison':
|
| 486 |
+
if (!updatedSlide.body || updatedSlide.body.length < 2) {
|
| 487 |
+
updatedSlide.body = [
|
| 488 |
+
{ heading: 'Option A', text: 'Benefits and features of option A' },
|
| 489 |
+
{ heading: 'Option B', text: 'Benefits and features of option B' }
|
| 490 |
+
];
|
| 491 |
+
}
|
| 492 |
+
break;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
const newSlides = presentation.slides.map((slide, index) =>
|
| 496 |
+
index === currentSlideIndex ? updatedSlide : slide
|
| 497 |
+
);
|
| 498 |
+
|
| 499 |
+
const updatedPresentation = { ...presentation, slides: newSlides };
|
| 500 |
+
setPresentation(updatedPresentation);
|
| 501 |
+
sessionStorage.setItem('currentPresentation', JSON.stringify(updatedPresentation));
|
| 502 |
+
setShowLayoutSelector(false);
|
| 503 |
+
};
|
| 504 |
+
|
| 505 |
+
const exportPresentation = async () => {
|
| 506 |
+
if (!presentation) return;
|
| 507 |
+
|
| 508 |
+
try {
|
| 509 |
+
const response = await fetch('/api/export-pptx', {
|
| 510 |
+
method: 'POST',
|
| 511 |
+
headers: {
|
| 512 |
+
'Content-Type': 'application/json',
|
| 513 |
+
},
|
| 514 |
+
body: JSON.stringify({ slides: presentation.slides }),
|
| 515 |
+
});
|
| 516 |
+
|
| 517 |
+
if (response.ok) {
|
| 518 |
+
const blob = await response.blob();
|
| 519 |
+
const url = window.URL.createObjectURL(blob);
|
| 520 |
+
const a = document.createElement('a');
|
| 521 |
+
a.href = url;
|
| 522 |
+
a.download = 'presentation.pptx';
|
| 523 |
+
document.body.appendChild(a);
|
| 524 |
+
a.click();
|
| 525 |
+
window.URL.revokeObjectURL(url);
|
| 526 |
+
document.body.removeChild(a);
|
| 527 |
+
}
|
| 528 |
+
} catch (error) {
|
| 529 |
+
console.error('Export failed:', error);
|
| 530 |
+
}
|
| 531 |
+
};
|
| 532 |
+
|
| 533 |
+
// Slideshow functions
|
| 534 |
+
const startSlideshow = () => {
|
| 535 |
+
setIsInSlideshow(true);
|
| 536 |
+
setSlideshowIndex(currentSlideIndex);
|
| 537 |
+
};
|
| 538 |
+
|
| 539 |
+
const exitSlideshow = () => {
|
| 540 |
+
setIsInSlideshow(false);
|
| 541 |
+
if (autoPlayInterval) {
|
| 542 |
+
clearInterval(autoPlayInterval);
|
| 543 |
+
setAutoPlayInterval(null);
|
| 544 |
+
}
|
| 545 |
+
setIsAutoPlay(false);
|
| 546 |
+
};
|
| 547 |
+
|
| 548 |
+
const nextSlide = () => {
|
| 549 |
+
if (!presentation) return;
|
| 550 |
+
setSlideshowIndex((prev) =>
|
| 551 |
+
prev < presentation.slides.length - 1 ? prev + 1 : 0
|
| 552 |
+
);
|
| 553 |
+
};
|
| 554 |
+
|
| 555 |
+
const previousSlide = () => {
|
| 556 |
+
if (!presentation) return;
|
| 557 |
+
setSlideshowIndex((prev) =>
|
| 558 |
+
prev > 0 ? prev - 1 : presentation.slides.length - 1
|
| 559 |
+
);
|
| 560 |
+
};
|
| 561 |
+
|
| 562 |
+
const toggleAutoPlay = () => {
|
| 563 |
+
if (isAutoPlay) {
|
| 564 |
+
if (autoPlayInterval) {
|
| 565 |
+
clearInterval(autoPlayInterval);
|
| 566 |
+
setAutoPlayInterval(null);
|
| 567 |
+
}
|
| 568 |
+
setIsAutoPlay(false);
|
| 569 |
+
} else {
|
| 570 |
+
const interval = setInterval(() => {
|
| 571 |
+
nextSlide();
|
| 572 |
+
}, 5000); // 5 seconds per slide
|
| 573 |
+
setAutoPlayInterval(interval);
|
| 574 |
+
setIsAutoPlay(true);
|
| 575 |
+
}
|
| 576 |
+
};
|
| 577 |
+
|
| 578 |
+
// Keyboard navigation for slideshow
|
| 579 |
+
useEffect(() => {
|
| 580 |
+
if (!isInSlideshow) return;
|
| 581 |
+
|
| 582 |
+
const handleKeyPress = (e: KeyboardEvent) => {
|
| 583 |
+
switch (e.key) {
|
| 584 |
+
case 'ArrowRight':
|
| 585 |
+
case ' ':
|
| 586 |
+
nextSlide();
|
| 587 |
+
break;
|
| 588 |
+
case 'ArrowLeft':
|
| 589 |
+
previousSlide();
|
| 590 |
+
break;
|
| 591 |
+
case 'Escape':
|
| 592 |
+
exitSlideshow();
|
| 593 |
+
break;
|
| 594 |
+
}
|
| 595 |
+
};
|
| 596 |
+
|
| 597 |
+
document.addEventListener('keydown', handleKeyPress);
|
| 598 |
+
return () => document.removeEventListener('keydown', handleKeyPress);
|
| 599 |
+
}, [isInSlideshow, presentation]);
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
if (!presentation || !presentation.slides.length) {
|
| 603 |
+
return (
|
| 604 |
+
<div className="min-h-dvh w-full bg-gradient-to-b from-white to-gray-50 text-gray-900 flex items-center justify-center">
|
| 605 |
+
<div className="text-center">
|
| 606 |
+
<h1 className="text-2xl font-semibold mb-4">No presentation data found</h1>
|
| 607 |
+
<p className="text-gray-600">Please generate a presentation first.</p>
|
| 608 |
+
<button onClick={() => window.location.href = '/'} className="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
|
| 609 |
+
Go back to home
|
| 610 |
+
</button>
|
| 611 |
+
</div>
|
| 612 |
+
</div>
|
| 613 |
+
);
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
const currentSlide = presentation.slides[currentSlideIndex];
|
| 617 |
+
|
| 618 |
+
return (
|
| 619 |
+
<div className="relative min-h-dvh w-full bg-background text-foreground flex flex-col">
|
| 620 |
+
<style>{`
|
| 621 |
+
@keyframes drift { 0% { transform: translate(-50%, -50%) scale(1); } 50% { transform: translate(-55%, -48%) scale(1.06); } 100% { transform: translate(-46%, -52%) scale(1.12); } }
|
| 622 |
+
@keyframes panBeam { 0% { transform: translateX(-12%) translateY(-50%); } 100% { transform: translateX(12%) translateY(-50%); } }
|
| 623 |
+
`}</style>
|
| 624 |
+
{/* Emerald gradient background to match homepage */}
|
| 625 |
+
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10"
|
| 626 |
+
style={{
|
| 627 |
+
background:
|
| 628 |
+
'radial-gradient(60% 14% at 50% 62%, rgba(163,255,206,0.58) 0%, rgba(163,255,206,0.25) 45%, rgba(163,255,206,0.06) 70%, transparent 80%), radial-gradient(85% 50% at 50% -10%, rgba(60,255,170,0.25) 0%, transparent 70%), radial-gradient(100% 60% at 50% 15%, rgba(16,185,129,0.22) 0%, transparent 50%), linear-gradient(180deg, #03140e 0%, #061d17 35%, #070c0a 100%)',
|
| 629 |
+
}}
|
| 630 |
+
/>
|
| 631 |
+
{/* animated glows & beam */}
|
| 632 |
+
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
|
| 633 |
+
<div className="absolute left-1/2 top-[38%] w-[900px] h-[900px] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[140px] opacity-35"
|
| 634 |
+
style={{ background: 'radial-gradient(closest-side, rgba(52,211,153,0.45), rgba(52,211,153,0.06))', animation: 'drift 18s ease-in-out infinite alternate' }} />
|
| 635 |
+
<div className="absolute left-[12%] top-[58%] w-[700px] h-[700px] rounded-full blur-[160px] opacity-25"
|
| 636 |
+
style={{ background: 'radial-gradient(closest-side, rgba(16,185,129,0.36), transparent 60%)', animation: 'drift 22s ease-in-out infinite alternate-reverse' }} />
|
| 637 |
+
<div className="absolute left-0 right-0 top-1/2 h-[220px] -translate-y-1/2 blur-[60px]"
|
| 638 |
+
style={{ background: 'linear-gradient(90deg, rgba(16,185,129,0) 0%, rgba(163,255,206,0.65) 50%, rgba(16,185,129,0) 100%)', opacity: 0.55, animation: 'panBeam 12s ease-in-out infinite alternate' }} />
|
| 639 |
+
</div>
|
| 640 |
+
{/* star field */}
|
| 641 |
+
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10"
|
| 642 |
+
style={{
|
| 643 |
+
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.22) 1px, rgba(255,255,255,0) 1.6px), radial-gradient(circle, rgba(255,255,255,0.16) 1px, rgba(255,255,255,0) 1.6px), radial-gradient(circle, rgba(255,255,255,0.12) 1px, rgba(255,255,255,0) 1.6px)',
|
| 644 |
+
backgroundSize: '120px 120px, 180px 180px, 240px 240px',
|
| 645 |
+
backgroundPosition: '0 0, 60px 40px, 100px 80px',
|
| 646 |
+
opacity: 0.5,
|
| 647 |
+
}}
|
| 648 |
+
/>
|
| 649 |
+
{/* noise overlay */}
|
| 650 |
+
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10"
|
| 651 |
+
style={{ backgroundImage: noiseDataUrl, backgroundRepeat: 'repeat', opacity: 0.08, mixBlendMode: 'overlay' }} />
|
| 652 |
+
{/* Title Bar */}
|
| 653 |
+
<div className="sticky top-0 z-30 border-b border-white/10 bg-slate-900/70 backdrop-blur px-4 py-2 text-white flex items-center justify-between">
|
| 654 |
+
<div className="flex items-center gap-4">
|
| 655 |
+
<div className="flex items-center gap-2">
|
| 656 |
+
<Presentation size={20} />
|
| 657 |
+
<span className="font-semibold">slidesAI</span>
|
| 658 |
+
</div>
|
| 659 |
+
</div>
|
| 660 |
+
<div className="flex items-center gap-2">
|
| 661 |
+
<button
|
| 662 |
+
onClick={() => setShowHomeWarning(true)}
|
| 663 |
+
className="h-8 px-3 rounded-md border border-white/10 bg-white/10 hover:bg-white/15 text-xs"
|
| 664 |
+
>
|
| 665 |
+
<Home size={14} />
|
| 666 |
+
</button>
|
| 667 |
+
<button
|
| 668 |
+
onClick={exportPresentation}
|
| 669 |
+
className="h-8 px-3 rounded-md border border-white/10 bg-white/10 hover:bg-white/15 text-xs flex items-center gap-1"
|
| 670 |
+
>
|
| 671 |
+
<Download size={14} />
|
| 672 |
+
Export
|
| 673 |
+
</button>
|
| 674 |
+
</div>
|
| 675 |
+
</div>
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
{/* slidesAI-style Ribbon */}
|
| 679 |
+
<div className="sticky top-[40px] z-20 bg-background/60 backdrop-blur border-b border-white/10 px-3 py-2">
|
| 680 |
+
<div className="flex items-center gap-3 overflow-x-auto">
|
| 681 |
+
{/* Clipboard */}
|
| 682 |
+
<div className="flex flex-col items-center rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 683 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Clipboard</div>
|
| 684 |
+
<div className="flex gap-1">
|
| 685 |
+
<button className="h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90">
|
| 686 |
+
<FileText size={14} />
|
| 687 |
+
</button>
|
| 688 |
+
</div>
|
| 689 |
+
</div>
|
| 690 |
+
|
| 691 |
+
<div className="w-px h-10 bg-white/10" />
|
| 692 |
+
|
| 693 |
+
{/* Templates */}
|
| 694 |
+
<div className="flex flex-col items-center rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 695 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Templates</div>
|
| 696 |
+
<div className="relative">
|
| 697 |
+
<button
|
| 698 |
+
onClick={() => setShowTemplateSelector(!showTemplateSelector)}
|
| 699 |
+
className="flex flex-col items-center h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90"
|
| 700 |
+
>
|
| 701 |
+
<Presentation size={14} />
|
| 702 |
+
<span className="text-xs mt-1">Browse</span>
|
| 703 |
+
</button>
|
| 704 |
+
{showTemplateSelector && (
|
| 705 |
+
<div className="absolute top-full left-0 mt-1 bg-slate-900 border border-white/10 rounded shadow-lg z-[200] min-w-[320px]">
|
| 706 |
+
<div className="p-4">
|
| 707 |
+
<div className="text-xs font-medium text-white/70 mb-3">Choose Template</div>
|
| 708 |
+
<div className="space-y-2">
|
| 709 |
+
{professionalTemplates.map((template) => (
|
| 710 |
+
<button
|
| 711 |
+
key={template.id}
|
| 712 |
+
onClick={() => handleTemplateSelect(template)}
|
| 713 |
+
className="w-full text-left p-3 rounded-lg hover:bg-white/10 transition-colors flex items-center gap-3 border border-transparent hover:border-white/20"
|
| 714 |
+
>
|
| 715 |
+
<span className="text-2xl">{template.thumbnail}</span>
|
| 716 |
+
<div>
|
| 717 |
+
<div className="font-medium text-sm text-white">{template.name}</div>
|
| 718 |
+
<div className="text-xs text-white/60">{template.description}</div>
|
| 719 |
+
<div className="text-xs text-emerald-400 mt-1">{template.slides.length} slides</div>
|
| 720 |
+
</div>
|
| 721 |
+
</button>
|
| 722 |
+
))}
|
| 723 |
+
</div>
|
| 724 |
+
</div>
|
| 725 |
+
</div>
|
| 726 |
+
)}
|
| 727 |
+
</div>
|
| 728 |
+
</div>
|
| 729 |
+
|
| 730 |
+
<div className="w-px h-10 bg-white/10" />
|
| 731 |
+
|
| 732 |
+
{/* Slides */}
|
| 733 |
+
<div className="flex flex-col items-center rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 734 |
+
<div className="text-xs text-white/80 mb-1">Slides</div>
|
| 735 |
+
<div className="flex gap-1">
|
| 736 |
+
<button
|
| 737 |
+
onClick={handleAddSlide}
|
| 738 |
+
className="flex flex-col items-center h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90"
|
| 739 |
+
>
|
| 740 |
+
<Plus size={14} />
|
| 741 |
+
<span className="text-xs mt-1">New Slide</span>
|
| 742 |
+
</button>
|
| 743 |
+
<button
|
| 744 |
+
onClick={() => handleDeleteSlide(currentSlideIndex)}
|
| 745 |
+
disabled={presentation?.slides.length === 1}
|
| 746 |
+
className="h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
| 747 |
+
>
|
| 748 |
+
<Trash2 size={14} />
|
| 749 |
+
</button>
|
| 750 |
+
</div>
|
| 751 |
+
</div>
|
| 752 |
+
|
| 753 |
+
<div className="w-px h-10 bg-white/10" />
|
| 754 |
+
|
| 755 |
+
{/* Layout */}
|
| 756 |
+
<div className="flex flex-col items-center rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 757 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Layout</div>
|
| 758 |
+
<div className="relative">
|
| 759 |
+
<button
|
| 760 |
+
onClick={() => setShowLayoutSelector(!showLayoutSelector)}
|
| 761 |
+
className="flex flex-col items-center h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90"
|
| 762 |
+
>
|
| 763 |
+
<span className="text-lg">{layouts.find(l => l.id === presentation?.slides[currentSlideIndex]?.layout)?.icon || '📰'}</span>
|
| 764 |
+
<span className="text-xs mt-1">Change</span>
|
| 765 |
+
</button>
|
| 766 |
+
{showLayoutSelector && (
|
| 767 |
+
<div className="absolute top-full left-0 mt-1 bg-slate-900 border border-white/10 rounded shadow-lg z-[200] min-w-[250px]">
|
| 768 |
+
<div className="p-3">
|
| 769 |
+
<div className="text-xs font-medium text-white/70 mb-2">Choose Layout</div>
|
| 770 |
+
<div className="space-y-1">
|
| 771 |
+
{layouts.map((layout) => (
|
| 772 |
+
<button
|
| 773 |
+
key={layout.id}
|
| 774 |
+
onClick={() => handleLayoutChange(layout.id)}
|
| 775 |
+
className={`w-full text-left p-3 rounded-lg hover:bg-white/10 transition-colors flex items-center gap-3 ${
|
| 776 |
+
presentation?.slides[currentSlideIndex]?.layout === layout.id
|
| 777 |
+
? 'bg-emerald-400/10 border border-emerald-400/40'
|
| 778 |
+
: 'border border-transparent'
|
| 779 |
+
}`}
|
| 780 |
+
>
|
| 781 |
+
<span className="text-xl">{layout.icon}</span>
|
| 782 |
+
<div>
|
| 783 |
+
<div className="font-medium text-sm text-white">{layout.name}</div>
|
| 784 |
+
<div className="text-xs text-white/60">{layout.description}</div>
|
| 785 |
+
</div>
|
| 786 |
+
</button>
|
| 787 |
+
))}
|
| 788 |
+
</div>
|
| 789 |
+
</div>
|
| 790 |
+
</div>
|
| 791 |
+
)}
|
| 792 |
+
</div>
|
| 793 |
+
</div>
|
| 794 |
+
|
| 795 |
+
<div className="w-px h-10 bg-white/10" />
|
| 796 |
+
|
| 797 |
+
{/* Font */}
|
| 798 |
+
<div className="flex flex-col rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 799 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Font</div>
|
| 800 |
+
<div className="flex items-center gap-1">
|
| 801 |
+
{/* Font Family Dropdown */}
|
| 802 |
+
<div className="relative">
|
| 803 |
+
<button
|
| 804 |
+
onClick={() => setShowFontDropdown(!showFontDropdown)}
|
| 805 |
+
className="flex items-center gap-1 px-2 py-1 border border-white/10 rounded bg-white/[0.06] hover:bg-white/[0.1] min-w-[120px] text-xs"
|
| 806 |
+
>
|
| 807 |
+
<span style={{ fontFamily: googleFonts.find(f => f.name === selectedFont)?.family }}>
|
| 808 |
+
{selectedFont}
|
| 809 |
+
</span>
|
| 810 |
+
<ChevronDown size={12} />
|
| 811 |
+
</button>
|
| 812 |
+
{showFontDropdown && (
|
| 813 |
+
<div className="absolute top-full left-0 mt-1 bg-slate-900 border border-white/10 rounded shadow-lg z-[200] min-w-[200px] max-h-60 overflow-y-auto">
|
| 814 |
+
{googleFonts.map((font) => (
|
| 815 |
+
<button
|
| 816 |
+
key={font.name}
|
| 817 |
+
onClick={(e) => {
|
| 818 |
+
e.preventDefault();
|
| 819 |
+
e.stopPropagation();
|
| 820 |
+
setSelectedFont(font.name);
|
| 821 |
+
setShowFontDropdown(false);
|
| 822 |
+
applyTextFormatting('fontFamily', font.name);
|
| 823 |
+
}}
|
| 824 |
+
className="w-full text-left px-3 py-2 text-sm hover:bg-white/10"
|
| 825 |
+
style={{ fontFamily: font.family }}
|
| 826 |
+
>
|
| 827 |
+
{font.label}
|
| 828 |
+
</button>
|
| 829 |
+
))}
|
| 830 |
+
</div>
|
| 831 |
+
)}
|
| 832 |
+
</div>
|
| 833 |
+
|
| 834 |
+
{/* Font Size Dropdown */}
|
| 835 |
+
<div className="relative">
|
| 836 |
+
<button
|
| 837 |
+
onClick={() => setShowFontSizeDropdown(!showFontSizeDropdown)}
|
| 838 |
+
className="flex items-center gap-1 px-1 py-1 border border-white/10 rounded bg-white/[0.06] hover:bg-white/[0.1] w-12 text-xs"
|
| 839 |
+
>
|
| 840 |
+
{selectedFontSize}
|
| 841 |
+
<ChevronDown size={12} />
|
| 842 |
+
</button>
|
| 843 |
+
{showFontSizeDropdown && (
|
| 844 |
+
<div className="absolute top-full left-0 mt-1 bg-slate-900 border border-white/10 rounded shadow-lg z-[200] max-h-60 overflow-y-auto">
|
| 845 |
+
{fontSizes.map((size) => (
|
| 846 |
+
<button
|
| 847 |
+
key={size}
|
| 848 |
+
onClick={(e) => {
|
| 849 |
+
e.preventDefault();
|
| 850 |
+
e.stopPropagation();
|
| 851 |
+
setSelectedFontSize(size);
|
| 852 |
+
setShowFontSizeDropdown(false);
|
| 853 |
+
applyTextFormatting('fontSize', size);
|
| 854 |
+
}}
|
| 855 |
+
className="w-full text-left px-3 py-2 text-sm hover:bg-white/10"
|
| 856 |
+
>
|
| 857 |
+
{size}
|
| 858 |
+
</button>
|
| 859 |
+
))}
|
| 860 |
+
</div>
|
| 861 |
+
)}
|
| 862 |
+
</div>
|
| 863 |
+
</div>
|
| 864 |
+
|
| 865 |
+
{/* Font Style Buttons */}
|
| 866 |
+
<div className="flex gap-1 mt-1">
|
| 867 |
+
<button
|
| 868 |
+
onClick={(e) => {
|
| 869 |
+
e.preventDefault();
|
| 870 |
+
e.stopPropagation();
|
| 871 |
+
applyTextStyling('bold');
|
| 872 |
+
}}
|
| 873 |
+
className={`p-1 border border-white/10 rounded hover:bg-white/10 ${isBold ? 'bg-emerald-400/10 border-emerald-400/40' : ''}`}
|
| 874 |
+
>
|
| 875 |
+
<Bold size={14} />
|
| 876 |
+
</button>
|
| 877 |
+
<button
|
| 878 |
+
onClick={(e) => {
|
| 879 |
+
e.preventDefault();
|
| 880 |
+
e.stopPropagation();
|
| 881 |
+
applyTextStyling('italic');
|
| 882 |
+
}}
|
| 883 |
+
className={`p-1 border border-white/10 rounded hover:bg-white/10 ${isItalic ? 'bg-emerald-400/10 border-emerald-400/40' : ''}`}
|
| 884 |
+
>
|
| 885 |
+
<Italic size={14} />
|
| 886 |
+
</button>
|
| 887 |
+
<button
|
| 888 |
+
onClick={(e) => {
|
| 889 |
+
e.preventDefault();
|
| 890 |
+
e.stopPropagation();
|
| 891 |
+
applyTextStyling('underline');
|
| 892 |
+
}}
|
| 893 |
+
className={`p-1 border border-white/10 rounded hover:bg-white/10 ${isUnderline ? 'bg-emerald-400/10 border-emerald-400/40' : ''}`}
|
| 894 |
+
>
|
| 895 |
+
<Underline size={14} />
|
| 896 |
+
</button>
|
| 897 |
+
<div className="relative">
|
| 898 |
+
<button
|
| 899 |
+
onClick={() => setShowColorPicker(!showColorPicker)}
|
| 900 |
+
className="p-1 border border-white/10 rounded hover:bg-white/10 flex items-center"
|
| 901 |
+
>
|
| 902 |
+
<Paintbrush size={14} />
|
| 903 |
+
<div className="w-3 h-1 mt-1" style={{ backgroundColor: selectedColor }} />
|
| 904 |
+
</button>
|
| 905 |
+
{showColorPicker && (
|
| 906 |
+
<div className="absolute top-full left-0 mt-1 bg-slate-900 border border-white/10 rounded shadow-lg z-[200] p-2">
|
| 907 |
+
<div className="grid grid-cols-8 gap-1">
|
| 908 |
+
{colors.map((color) => (
|
| 909 |
+
<button
|
| 910 |
+
key={color}
|
| 911 |
+
onClick={(e) => {
|
| 912 |
+
e.preventDefault();
|
| 913 |
+
e.stopPropagation();
|
| 914 |
+
setSelectedColor(color);
|
| 915 |
+
setShowColorPicker(false);
|
| 916 |
+
applyTextFormatting('color', color);
|
| 917 |
+
}}
|
| 918 |
+
className="w-6 h-6 rounded border border-white/10 hover:scale-110 transition-transform"
|
| 919 |
+
style={{ backgroundColor: color }}
|
| 920 |
+
/>
|
| 921 |
+
))}
|
| 922 |
+
</div>
|
| 923 |
+
</div>
|
| 924 |
+
)}
|
| 925 |
+
</div>
|
| 926 |
+
</div>
|
| 927 |
+
</div>
|
| 928 |
+
|
| 929 |
+
<div className="w-px h-10 bg-white/10" />
|
| 930 |
+
|
| 931 |
+
{/* Paragraph */}
|
| 932 |
+
<div className="flex flex-col rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 933 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Paragraph</div>
|
| 934 |
+
<div className="flex gap-1">
|
| 935 |
+
<button
|
| 936 |
+
onClick={() => setAlignment('left')}
|
| 937 |
+
className={`p-1 border border-white/10 rounded hover:bg-white/10 ${alignment === 'left' ? 'bg-emerald-400/10 border-emerald-400/40' : ''}`}
|
| 938 |
+
>
|
| 939 |
+
<AlignLeft size={14} />
|
| 940 |
+
</button>
|
| 941 |
+
<button
|
| 942 |
+
onClick={() => setAlignment('center')}
|
| 943 |
+
className={`p-1 border border-white/10 rounded hover:bg-white/10 ${alignment === 'center' ? 'bg-emerald-400/10 border-emerald-400/40' : ''}`}
|
| 944 |
+
>
|
| 945 |
+
<AlignCenter size={14} />
|
| 946 |
+
</button>
|
| 947 |
+
<button
|
| 948 |
+
onClick={() => setAlignment('right')}
|
| 949 |
+
className={`p-1 border border-white/10 rounded hover:bg-white/10 ${alignment === 'right' ? 'bg-emerald-400/10 border-emerald-400/40' : ''}`}
|
| 950 |
+
>
|
| 951 |
+
<AlignRight size={14} />
|
| 952 |
+
</button>
|
| 953 |
+
</div>
|
| 954 |
+
</div>
|
| 955 |
+
|
| 956 |
+
<div className="w-px h-10 bg-white/10" />
|
| 957 |
+
|
| 958 |
+
{/* Insert */}
|
| 959 |
+
<div className="flex flex-col items-center rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 960 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Insert</div>
|
| 961 |
+
<div className="flex gap-1">
|
| 962 |
+
<button
|
| 963 |
+
onClick={() => setShowImageUpload(true)}
|
| 964 |
+
className="flex flex-col items-center h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90"
|
| 965 |
+
>
|
| 966 |
+
<Image size={14} />
|
| 967 |
+
<span className="text-xs mt-1">Images</span>
|
| 968 |
+
</button>
|
| 969 |
+
</div>
|
| 970 |
+
</div>
|
| 971 |
+
|
| 972 |
+
<div className="w-px h-10 bg-white/10" />
|
| 973 |
+
|
| 974 |
+
{/* Slideshow */}
|
| 975 |
+
<div className="flex flex-col items-center rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 976 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Slideshow</div>
|
| 977 |
+
<div className="flex gap-1">
|
| 978 |
+
<button
|
| 979 |
+
onClick={startSlideshow}
|
| 980 |
+
className="flex flex-col items-center h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90"
|
| 981 |
+
>
|
| 982 |
+
<Play size={14} />
|
| 983 |
+
<span className="text-xs mt-1">Start</span>
|
| 984 |
+
</button>
|
| 985 |
+
</div>
|
| 986 |
+
</div>
|
| 987 |
+
|
| 988 |
+
<div className="w-px h-10 bg-white/10" />
|
| 989 |
+
|
| 990 |
+
{/* Design */}
|
| 991 |
+
<div className="flex flex-col items-center rounded-lg border border-white/10 bg-white/[0.04] px-2 py-1.5 shrink-0">
|
| 992 |
+
<div className="text-xs text-white/80 mb-1 font-medium">Design</div>
|
| 993 |
+
<div className="relative">
|
| 994 |
+
<button
|
| 995 |
+
onClick={() => setShowThemeSelector(!showThemeSelector)}
|
| 996 |
+
className="flex flex-col items-center h-7 px-2 rounded border border-white/10 bg-white/[0.06] hover:bg-white/[0.1] text-white/90"
|
| 997 |
+
>
|
| 998 |
+
<Palette size={14} />
|
| 999 |
+
<span className="text-xs mt-1">Themes</span>
|
| 1000 |
+
</button>
|
| 1001 |
+
{showThemeSelector && (
|
| 1002 |
+
<div className="absolute top-full left-0 mt-1 bg-slate-900 border border-white/10 rounded shadow-lg z-[200] min-w-[200px]">
|
| 1003 |
+
<div className="grid grid-cols-2 gap-2 p-3">
|
| 1004 |
+
{themes.map((theme) => (
|
| 1005 |
+
<button
|
| 1006 |
+
key={theme.name}
|
| 1007 |
+
onClick={() => handleThemeChange(theme.name)}
|
| 1008 |
+
className={`p-3 rounded border-2 transition-all ${
|
| 1009 |
+
presentation?.theme === theme.name
|
| 1010 |
+
? 'border-emerald-400/60 bg-emerald-400/10'
|
| 1011 |
+
: 'border-white/10 hover:border-white/20'
|
| 1012 |
+
}`}
|
| 1013 |
+
>
|
| 1014 |
+
<div className={`w-full h-8 rounded mb-1 ${theme.colors}`} />
|
| 1015 |
+
<div className="text-xs font-medium">{theme.label}</div>
|
| 1016 |
+
</button>
|
| 1017 |
+
))}
|
| 1018 |
+
</div>
|
| 1019 |
+
</div>
|
| 1020 |
+
)}
|
| 1021 |
+
</div>
|
| 1022 |
+
</div>
|
| 1023 |
+
</div>
|
| 1024 |
+
</div>
|
| 1025 |
+
|
| 1026 |
+
{/* Image Upload Modal */}
|
| 1027 |
+
{showImageUpload && (
|
| 1028 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 1029 |
+
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
| 1030 |
+
<h3 className="text-lg font-semibold mb-4">Add Image</h3>
|
| 1031 |
+
|
| 1032 |
+
<div className="space-y-4">
|
| 1033 |
+
{/* Upload from device */}
|
| 1034 |
+
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
| 1035 |
+
<Upload size={40} className="mx-auto text-gray-400 mb-3" />
|
| 1036 |
+
<p className="text-gray-600 mb-3">Upload from your device</p>
|
| 1037 |
+
<input
|
| 1038 |
+
type="file"
|
| 1039 |
+
accept="image/*"
|
| 1040 |
+
onChange={handleImageUpload}
|
| 1041 |
+
className="hidden"
|
| 1042 |
+
id="image-upload"
|
| 1043 |
+
/>
|
| 1044 |
+
<label
|
| 1045 |
+
htmlFor="image-upload"
|
| 1046 |
+
className="inline-block bg-blue-600 text-white px-4 py-2 rounded cursor-pointer hover:bg-blue-700"
|
| 1047 |
+
>
|
| 1048 |
+
Choose File
|
| 1049 |
+
</label>
|
| 1050 |
+
</div>
|
| 1051 |
+
|
| 1052 |
+
{/* Divider */}
|
| 1053 |
+
<div className="flex items-center gap-3">
|
| 1054 |
+
<div className="flex-1 h-px bg-gray-300"></div>
|
| 1055 |
+
<span className="text-gray-500 text-sm">or</span>
|
| 1056 |
+
<div className="flex-1 h-px bg-gray-300"></div>
|
| 1057 |
+
</div>
|
| 1058 |
+
|
| 1059 |
+
{/* Search Unsplash */}
|
| 1060 |
+
<div className="border border-gray-300 rounded-lg p-6 text-center">
|
| 1061 |
+
<Image size={40} className="mx-auto text-gray-400 mb-3" />
|
| 1062 |
+
<p className="text-gray-600 mb-3">Search free stock photos</p>
|
| 1063 |
+
<button
|
| 1064 |
+
onClick={() => {
|
| 1065 |
+
setShowImageUpload(false);
|
| 1066 |
+
setShowUnsplashSearch(true);
|
| 1067 |
+
}}
|
| 1068 |
+
className="inline-block bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700"
|
| 1069 |
+
>
|
| 1070 |
+
Search Unsplash
|
| 1071 |
+
</button>
|
| 1072 |
+
</div>
|
| 1073 |
+
</div>
|
| 1074 |
+
|
| 1075 |
+
<div className="flex gap-3 mt-6">
|
| 1076 |
+
<button
|
| 1077 |
+
onClick={() => setShowImageUpload(false)}
|
| 1078 |
+
className="flex-1 px-4 py-2 border border-gray-300 rounded hover:bg-gray-50"
|
| 1079 |
+
>
|
| 1080 |
+
Cancel
|
| 1081 |
+
</button>
|
| 1082 |
+
</div>
|
| 1083 |
+
</div>
|
| 1084 |
+
</div>
|
| 1085 |
+
)}
|
| 1086 |
+
|
| 1087 |
+
{/* Main Content Area */}
|
| 1088 |
+
<div className="flex-1 flex overflow-hidden">
|
| 1089 |
+
{/* Slide Thumbnails Panel */}
|
| 1090 |
+
<div className="w-[220px] bg-sidebar text-sidebar-foreground border-r border-white/10 flex flex-col shrink-0">
|
| 1091 |
+
<div className="p-2 border-b border-white/10 bg-white/[0.04]">
|
| 1092 |
+
<h3 className="font-medium text-xs">Slides</h3>
|
| 1093 |
+
</div>
|
| 1094 |
+
<div className="flex-1 overflow-y-auto p-1.5">
|
| 1095 |
+
<div className="space-y-2">
|
| 1096 |
+
{presentation.slides.map((slide, index) => (
|
| 1097 |
+
<div
|
| 1098 |
+
key={slide.id}
|
| 1099 |
+
className={`relative group cursor-pointer rounded-lg overflow-hidden transition-all ${
|
| 1100 |
+
index === currentSlideIndex
|
| 1101 |
+
? 'ring-2 ring-emerald-400/70 bg-emerald-400/5 border-emerald-400/30'
|
| 1102 |
+
: 'hover:ring-1 hover:ring-white/20 bg-white/[0.03]'
|
| 1103 |
+
} shadow-sm border border-white/10`}
|
| 1104 |
+
onClick={() => setCurrentSlideIndex(index)}
|
| 1105 |
+
>
|
| 1106 |
+
<div className="aspect-video p-1.5">
|
| 1107 |
+
<div className="w-full h-full scale-[0.1] origin-top-left transform">
|
| 1108 |
+
<div key={`${slide.id}-${presentation.theme}`} className="w-[1000px] h-[562px]">
|
| 1109 |
+
{renderSlide(slide, presentation.theme as Theme)}
|
| 1110 |
+
</div>
|
| 1111 |
+
</div>
|
| 1112 |
+
</div>
|
| 1113 |
+
|
| 1114 |
+
{/* Slide Number */}
|
| 1115 |
+
<div className="absolute top-1 left-1 bg-slate-900/80 text-white text-xs px-1.5 py-0.5 rounded">
|
| 1116 |
+
{index + 1}
|
| 1117 |
+
</div>
|
| 1118 |
+
|
| 1119 |
+
{/* Delete Button */}
|
| 1120 |
+
{presentation.slides.length > 1 && (
|
| 1121 |
+
<button
|
| 1122 |
+
onClick={(e) => {
|
| 1123 |
+
e.stopPropagation();
|
| 1124 |
+
handleDeleteSlide(index);
|
| 1125 |
+
}}
|
| 1126 |
+
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity p-1 bg-red-500 text-white rounded hover:bg-red-600"
|
| 1127 |
+
title="Delete slide"
|
| 1128 |
+
>
|
| 1129 |
+
<Trash2 size={12} />
|
| 1130 |
+
</button>
|
| 1131 |
+
)}
|
| 1132 |
+
|
| 1133 |
+
{/* Slide Title */}
|
| 1134 |
+
<div className="px-2 py-1 bg-white/[0.04] border-t border-white/10">
|
| 1135 |
+
<p className="text-xs truncate">
|
| 1136 |
+
{slide.title || `Slide ${index + 1}`}
|
| 1137 |
+
</p>
|
| 1138 |
+
</div>
|
| 1139 |
+
</div>
|
| 1140 |
+
))}
|
| 1141 |
+
</div>
|
| 1142 |
+
</div>
|
| 1143 |
+
</div>
|
| 1144 |
+
|
| 1145 |
+
{/* Main Editing Area */}
|
| 1146 |
+
<div className="flex-1 flex flex-col">
|
| 1147 |
+
{/* Slide Canvas */}
|
| 1148 |
+
<div className="flex-1 flex items-center justify-center p-4">
|
| 1149 |
+
<div className="relative rounded-2xl ring-1 ring-white/10 shadow-[0_10px_40px_rgba(0,0,0,0.45)] overflow-hidden w-full max-w-4xl aspect-video" style={{ background: '#0e131b' }}>
|
| 1150 |
+
<div aria-hidden className="absolute inset-0" style={{ backgroundImage: 'radial-gradient(rgba(148,163,184,0.15) 1px, transparent 1px)', backgroundSize: '18px 18px' }} />
|
| 1151 |
+
{/* Ruler/Guide Indicators */}
|
| 1152 |
+
<div className="absolute -top-6 left-0 right-0 h-4 bg-white/[0.04] border-b border-white/10 text-xs text-white/60 flex items-center px-2">
|
| 1153 |
+
<div className="flex items-center gap-4">
|
| 1154 |
+
<span>0"</span>
|
| 1155 |
+
<span>1"</span>
|
| 1156 |
+
<span>2"</span>
|
| 1157 |
+
<span>3"</span>
|
| 1158 |
+
<span>4"</span>
|
| 1159 |
+
<span>5"</span>
|
| 1160 |
+
<span>6"</span>
|
| 1161 |
+
<span>7"</span>
|
| 1162 |
+
<span>8"</span>
|
| 1163 |
+
<span>9"</span>
|
| 1164 |
+
<span>10"</span>
|
| 1165 |
+
</div>
|
| 1166 |
+
</div>
|
| 1167 |
+
<div className="absolute -left-6 top-0 bottom-0 w-4 bg-white/[0.04] border-r border-white/10 text-xs text-white/60 flex flex-col items-center justify-start pt-2 gap-4">
|
| 1168 |
+
<span>0"</span>
|
| 1169 |
+
<span>1"</span>
|
| 1170 |
+
<span>2"</span>
|
| 1171 |
+
<span>3"</span>
|
| 1172 |
+
<span>4"</span>
|
| 1173 |
+
<span>5"</span>
|
| 1174 |
+
<span>6"</span>
|
| 1175 |
+
</div>
|
| 1176 |
+
|
| 1177 |
+
{/* Slide Content */}
|
| 1178 |
+
<div
|
| 1179 |
+
className={`relative w-full h-full transition-all ${isDragOver ? 'ring-2 ring-emerald-400 bg-emerald-400/10' : ''}`}
|
| 1180 |
+
onDragOver={handleDragOver}
|
| 1181 |
+
onDragLeave={handleDragLeave}
|
| 1182 |
+
onDrop={handleDrop}
|
| 1183 |
+
>
|
| 1184 |
+
{currentSlide && <div key={`${currentSlide.id}-${presentation.theme}`}>{renderSlide(currentSlide, presentation.theme as Theme, {
|
| 1185 |
+
isEditable: true,
|
| 1186 |
+
onFieldUpdate: handleFieldUpdate,
|
| 1187 |
+
onTextSelect: handleTextSelection,
|
| 1188 |
+
selectedFont: selectedFont,
|
| 1189 |
+
selectedFontSize: selectedFontSize,
|
| 1190 |
+
selectedColor: selectedColor,
|
| 1191 |
+
onImageEdit: () => setShowImageUpload(true)
|
| 1192 |
+
})}</div>}
|
| 1193 |
+
|
| 1194 |
+
{/* Drag Overlay */}
|
| 1195 |
+
{isDragOver && (
|
| 1196 |
+
<div className="absolute inset-0 bg-emerald-400/20 flex items-center justify-center pointer-events-none z-50">
|
| 1197 |
+
<div className="bg-emerald-500 text-white px-4 py-2 rounded-lg font-medium">
|
| 1198 |
+
Drop image here to add to slide
|
| 1199 |
+
</div>
|
| 1200 |
+
</div>
|
| 1201 |
+
)}
|
| 1202 |
+
</div>
|
| 1203 |
+
</div>
|
| 1204 |
+
</div>
|
| 1205 |
+
|
| 1206 |
+
{/* Bottom Status Bar */}
|
| 1207 |
+
<div className="bg-white/[0.04] border-t border-white/10 px-4 py-2 flex items-center justify-between">
|
| 1208 |
+
<div className="flex items-center gap-4">
|
| 1209 |
+
<span className="text-sm text-white/70">
|
| 1210 |
+
Slide {currentSlideIndex + 1} of {presentation.slides.length}
|
| 1211 |
+
</span>
|
| 1212 |
+
<div className="flex items-center gap-2">
|
| 1213 |
+
<button
|
| 1214 |
+
onClick={() => setCurrentSlideIndex(Math.max(0, currentSlideIndex - 1))}
|
| 1215 |
+
disabled={currentSlideIndex === 0}
|
| 1216 |
+
className="p-1 hover:bg-white/10 rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
| 1217 |
+
title="Previous slide"
|
| 1218 |
+
>
|
| 1219 |
+
<ChevronDown size={16} className="rotate-90" />
|
| 1220 |
+
</button>
|
| 1221 |
+
<button
|
| 1222 |
+
onClick={() => setCurrentSlideIndex(Math.min(presentation.slides.length - 1, currentSlideIndex + 1))}
|
| 1223 |
+
disabled={currentSlideIndex === presentation.slides.length - 1}
|
| 1224 |
+
className="p-1 hover:bg-white/10 rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
| 1225 |
+
title="Next slide"
|
| 1226 |
+
>
|
| 1227 |
+
<ChevronDown size={16} className="-rotate-90" />
|
| 1228 |
+
</button>
|
| 1229 |
+
</div>
|
| 1230 |
+
</div>
|
| 1231 |
+
<div className="flex items-center gap-4">
|
| 1232 |
+
<div className="flex items-center gap-2">
|
| 1233 |
+
<span className="text-sm text-white/70">Zoom:</span>
|
| 1234 |
+
<select className="text-sm border border-white/10 bg-white/[0.06] rounded px-2 py-1">
|
| 1235 |
+
<option value="100">100%</option>
|
| 1236 |
+
<option value="75">75%</option>
|
| 1237 |
+
<option value="50">50%</option>
|
| 1238 |
+
<option value="25">25%</option>
|
| 1239 |
+
</select>
|
| 1240 |
+
</div>
|
| 1241 |
+
<div className="text-sm text-white/70">Ready</div>
|
| 1242 |
+
</div>
|
| 1243 |
+
</div>
|
| 1244 |
+
</div>
|
| 1245 |
+
</div>
|
| 1246 |
+
|
| 1247 |
+
{/* Slideshow Modal */}
|
| 1248 |
+
{isInSlideshow && presentation && (
|
| 1249 |
+
<div className="fixed inset-0 bg-black z-50 flex flex-col">
|
| 1250 |
+
{/* Slideshow Controls */}
|
| 1251 |
+
<div className="absolute top-4 right-4 z-60 flex items-center gap-2">
|
| 1252 |
+
<button
|
| 1253 |
+
onClick={toggleAutoPlay}
|
| 1254 |
+
className="p-2 bg-black/50 text-white rounded-full hover:bg-black/70 backdrop-blur-sm"
|
| 1255 |
+
title={isAutoPlay ? 'Pause Auto Play' : 'Start Auto Play'}
|
| 1256 |
+
>
|
| 1257 |
+
{isAutoPlay ? <Pause size={20} /> : <Play size={20} />}
|
| 1258 |
+
</button>
|
| 1259 |
+
<button
|
| 1260 |
+
onClick={exitSlideshow}
|
| 1261 |
+
className="p-2 bg-black/50 text-white rounded-full hover:bg-black/70 backdrop-blur-sm"
|
| 1262 |
+
title="Exit Slideshow (ESC)"
|
| 1263 |
+
>
|
| 1264 |
+
<X size={20} />
|
| 1265 |
+
</button>
|
| 1266 |
+
</div>
|
| 1267 |
+
|
| 1268 |
+
{/* Slide Counter */}
|
| 1269 |
+
<div className="absolute top-4 left-4 z-60 text-white bg-black/50 px-3 py-1 rounded-full backdrop-blur-sm">
|
| 1270 |
+
{slideshowIndex + 1} of {presentation.slides.length}
|
| 1271 |
+
</div>
|
| 1272 |
+
|
| 1273 |
+
{/* Main Slide Area */}
|
| 1274 |
+
<div className="flex-1 flex items-center justify-center p-8">
|
| 1275 |
+
<div key={`slideshow-${presentation.slides[slideshowIndex]?.id}-${presentation.theme}`} className="w-full h-full max-w-6xl max-h-[80vh] bg-white rounded-lg shadow-2xl overflow-hidden">
|
| 1276 |
+
{renderSlide(presentation.slides[slideshowIndex], presentation.theme as Theme)}
|
| 1277 |
+
</div>
|
| 1278 |
+
</div>
|
| 1279 |
+
|
| 1280 |
+
{/* Navigation Controls */}
|
| 1281 |
+
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 z-60 flex items-center gap-4">
|
| 1282 |
+
<button
|
| 1283 |
+
onClick={previousSlide}
|
| 1284 |
+
className="p-3 bg-black/50 text-white rounded-full hover:bg-black/70 backdrop-blur-sm"
|
| 1285 |
+
title="Previous Slide (←)"
|
| 1286 |
+
>
|
| 1287 |
+
<ArrowLeft size={24} />
|
| 1288 |
+
</button>
|
| 1289 |
+
<div className="flex items-center gap-2">
|
| 1290 |
+
{presentation.slides.map((_, index) => (
|
| 1291 |
+
<button
|
| 1292 |
+
key={index}
|
| 1293 |
+
onClick={() => setSlideshowIndex(index)}
|
| 1294 |
+
className={`w-3 h-3 rounded-full transition-all ${
|
| 1295 |
+
index === slideshowIndex
|
| 1296 |
+
? 'bg-white'
|
| 1297 |
+
: 'bg-white/50 hover:bg-white/70'
|
| 1298 |
+
}`}
|
| 1299 |
+
/>
|
| 1300 |
+
))}
|
| 1301 |
+
</div>
|
| 1302 |
+
<button
|
| 1303 |
+
onClick={nextSlide}
|
| 1304 |
+
className="p-3 bg-black/50 text-white rounded-full hover:bg-black/70 backdrop-blur-sm"
|
| 1305 |
+
title="Next Slide (→ or Space)"
|
| 1306 |
+
>
|
| 1307 |
+
<ArrowRight size={24} />
|
| 1308 |
+
</button>
|
| 1309 |
+
</div>
|
| 1310 |
+
|
| 1311 |
+
{/* Click Areas for Navigation */}
|
| 1312 |
+
<button
|
| 1313 |
+
onClick={previousSlide}
|
| 1314 |
+
className="absolute left-0 top-0 w-1/3 h-full bg-transparent z-50"
|
| 1315 |
+
title="Previous Slide"
|
| 1316 |
+
/>
|
| 1317 |
+
<button
|
| 1318 |
+
onClick={nextSlide}
|
| 1319 |
+
className="absolute right-0 top-0 w-1/3 h-full bg-transparent z-50"
|
| 1320 |
+
title="Next Slide"
|
| 1321 |
+
/>
|
| 1322 |
+
</div>
|
| 1323 |
+
)}
|
| 1324 |
+
|
| 1325 |
+
{/* Unsplash Image Search */}
|
| 1326 |
+
{showUnsplashSearch && (
|
| 1327 |
+
<UnsplashImageSearch
|
| 1328 |
+
onImageSelect={handleUnsplashImageSelect}
|
| 1329 |
+
onClose={() => setShowUnsplashSearch(false)}
|
| 1330 |
+
/>
|
| 1331 |
+
)}
|
| 1332 |
+
|
| 1333 |
+
{/* Home Warning Dialog */}
|
| 1334 |
+
{showHomeWarning && (
|
| 1335 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 1336 |
+
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
| 1337 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-3">⚠️ Warning</h3>
|
| 1338 |
+
<p className="text-gray-700 mb-6">
|
| 1339 |
+
Going back to home will cause you to lose your current presentation. Make sure to export your slides before leaving.
|
| 1340 |
+
</p>
|
| 1341 |
+
<div className="flex gap-3">
|
| 1342 |
+
<button
|
| 1343 |
+
onClick={() => setShowHomeWarning(false)}
|
| 1344 |
+
className="flex-1 px-4 py-2 border border-gray-300 rounded hover:bg-gray-50 text-gray-700"
|
| 1345 |
+
>
|
| 1346 |
+
Cancel
|
| 1347 |
+
</button>
|
| 1348 |
+
<button
|
| 1349 |
+
onClick={() => window.location.href = '/'}
|
| 1350 |
+
className="flex-1 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
| 1351 |
+
>
|
| 1352 |
+
Continue to Home
|
| 1353 |
+
</button>
|
| 1354 |
+
</div>
|
| 1355 |
+
</div>
|
| 1356 |
+
</div>
|
| 1357 |
+
)}
|
| 1358 |
+
|
| 1359 |
+
</div>
|
| 1360 |
+
);
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
|
app/layout.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
-
import { Geist, Geist_Mono, Sora } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
const geistSans = Geist({
|
|
@@ -17,6 +17,50 @@ const sora = Sora({
|
|
| 17 |
subsets: ["latin"],
|
| 18 |
});
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
export const metadata: Metadata = {
|
| 21 |
title: "PowerPoint Generator",
|
| 22 |
description: "Generate PowerPoint presentations",
|
|
@@ -30,7 +74,7 @@ export default function RootLayout({
|
|
| 30 |
return (
|
| 31 |
<html lang="en" className="dark">
|
| 32 |
<body
|
| 33 |
-
className={`${geistSans.variable} ${geistMono.variable} ${sora.variable} antialiased`}
|
| 34 |
>
|
| 35 |
{children}
|
| 36 |
</body>
|
|
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono, Sora, Inter, Roboto, Open_Sans, Lato, Montserrat, Poppins, Playfair_Display, Merriweather } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
const geistSans = Geist({
|
|
|
|
| 17 |
subsets: ["latin"],
|
| 18 |
});
|
| 19 |
|
| 20 |
+
const inter = Inter({
|
| 21 |
+
variable: "--font-inter",
|
| 22 |
+
subsets: ["latin"],
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
const roboto = Roboto({
|
| 26 |
+
variable: "--font-roboto",
|
| 27 |
+
subsets: ["latin"],
|
| 28 |
+
weight: ["300", "400", "500", "700"],
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
const openSans = Open_Sans({
|
| 32 |
+
variable: "--font-open-sans",
|
| 33 |
+
subsets: ["latin"],
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
const lato = Lato({
|
| 37 |
+
variable: "--font-lato",
|
| 38 |
+
subsets: ["latin"],
|
| 39 |
+
weight: ["300", "400", "700"],
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
const montserrat = Montserrat({
|
| 43 |
+
variable: "--font-montserrat",
|
| 44 |
+
subsets: ["latin"],
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
const poppins = Poppins({
|
| 48 |
+
variable: "--font-poppins",
|
| 49 |
+
subsets: ["latin"],
|
| 50 |
+
weight: ["300", "400", "500", "600", "700"],
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
const playfair = Playfair_Display({
|
| 54 |
+
variable: "--font-playfair",
|
| 55 |
+
subsets: ["latin"],
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
const merriweather = Merriweather({
|
| 59 |
+
variable: "--font-merriweather",
|
| 60 |
+
subsets: ["latin"],
|
| 61 |
+
weight: ["300", "400", "700"],
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
export const metadata: Metadata = {
|
| 65 |
title: "PowerPoint Generator",
|
| 66 |
description: "Generate PowerPoint presentations",
|
|
|
|
| 74 |
return (
|
| 75 |
<html lang="en" className="dark">
|
| 76 |
<body
|
| 77 |
+
className={`${geistSans.variable} ${geistMono.variable} ${sora.variable} ${inter.variable} ${roboto.variable} ${openSans.variable} ${lato.variable} ${montserrat.variable} ${poppins.variable} ${playfair.variable} ${merriweather.variable} antialiased`}
|
| 78 |
>
|
| 79 |
{children}
|
| 80 |
</body>
|
app/page.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|
| 5 |
import { ArrowUp, ChevronDown, Check } from 'lucide-react';
|
| 6 |
import { DeepSeek, OpenAI, Kimi, Ollama } from '@lobehub/icons';
|
| 7 |
import * as Select from '@radix-ui/react-select';
|
|
|
|
| 8 |
|
| 9 |
export default function Home() {
|
| 10 |
const router = useRouter();
|
|
@@ -12,6 +13,7 @@ export default function Home() {
|
|
| 12 |
const [selectedModel, setSelectedModel] = useState('gemini-2.5-flash-lite');
|
| 13 |
const [isLoading, setIsLoading] = useState(false);
|
| 14 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
|
|
| 15 |
|
| 16 |
const noiseSvg = "<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 60 60'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(#n)' opacity='0.35'/></svg>";
|
| 17 |
const noiseDataUrl = `url('data:image/svg+xml;utf8,${encodeURIComponent(noiseSvg)}')`;
|
|
@@ -21,6 +23,13 @@ export default function Home() {
|
|
| 21 |
|
| 22 |
if (!prompt.trim()) return;
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
setIsLoading(true);
|
| 25 |
|
| 26 |
try {
|
|
@@ -55,29 +64,19 @@ export default function Home() {
|
|
| 55 |
}
|
| 56 |
};
|
| 57 |
|
| 58 |
-
const
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
'Content-Type': 'application/json',
|
| 67 |
-
},
|
| 68 |
-
body: JSON.stringify({ apiKey }),
|
| 69 |
-
});
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
alert('Successfully logged in with HF Pro!');
|
| 74 |
-
} else {
|
| 75 |
-
alert('Invalid API key. Please try again.');
|
| 76 |
-
}
|
| 77 |
-
} catch (error) {
|
| 78 |
-
console.error('Login error:', error);
|
| 79 |
-
alert('Login failed. Please try again.');
|
| 80 |
-
}
|
| 81 |
};
|
| 82 |
return (
|
| 83 |
<div className="relative min-h-dvh flex items-center justify-center px-4">
|
|
@@ -220,6 +219,25 @@ export default function Home() {
|
|
| 220 |
</form>
|
| 221 |
</div>
|
| 222 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
</div>
|
| 224 |
);
|
| 225 |
}
|
|
|
|
| 5 |
import { ArrowUp, ChevronDown, Check } from 'lucide-react';
|
| 6 |
import { DeepSeek, OpenAI, Kimi, Ollama } from '@lobehub/icons';
|
| 7 |
import * as Select from '@radix-ui/react-select';
|
| 8 |
+
import HFAuth from '@/components/HFAuth';
|
| 9 |
|
| 10 |
export default function Home() {
|
| 11 |
const router = useRouter();
|
|
|
|
| 13 |
const [selectedModel, setSelectedModel] = useState('gemini-2.5-flash-lite');
|
| 14 |
const [isLoading, setIsLoading] = useState(false);
|
| 15 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
| 16 |
+
const [showAuthModal, setShowAuthModal] = useState(false);
|
| 17 |
|
| 18 |
const noiseSvg = "<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 60 60'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(#n)' opacity='0.35'/></svg>";
|
| 19 |
const noiseDataUrl = `url('data:image/svg+xml;utf8,${encodeURIComponent(noiseSvg)}')`;
|
|
|
|
| 23 |
|
| 24 |
if (!prompt.trim()) return;
|
| 25 |
|
| 26 |
+
// Check if authentication is required for the selected model
|
| 27 |
+
const requiresAuth = selectedModel.includes('huggingface') || selectedModel.includes('meta') || selectedModel.includes('mistral');
|
| 28 |
+
if (requiresAuth && !isAuthenticated) {
|
| 29 |
+
alert('This model requires Hugging Face authentication. Please sign in first.');
|
| 30 |
+
return;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
setIsLoading(true);
|
| 34 |
|
| 35 |
try {
|
|
|
|
| 64 |
}
|
| 65 |
};
|
| 66 |
|
| 67 |
+
const handleAuthSuccess = (result: any) => {
|
| 68 |
+
setIsAuthenticated(true);
|
| 69 |
+
setShowAuthModal(false);
|
| 70 |
+
console.log('Authentication successful:', result);
|
| 71 |
+
};
|
| 72 |
|
| 73 |
+
const handleAuthError = (error: string) => {
|
| 74 |
+
console.error('Authentication error:', error);
|
| 75 |
+
alert('Authentication failed: ' + error);
|
| 76 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
const handleLogin = () => {
|
| 79 |
+
setShowAuthModal(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
};
|
| 81 |
return (
|
| 82 |
<div className="relative min-h-dvh flex items-center justify-center px-4">
|
|
|
|
| 219 |
</form>
|
| 220 |
</div>
|
| 221 |
</div>
|
| 222 |
+
|
| 223 |
+
{/* Authentication Modal */}
|
| 224 |
+
{showAuthModal && (
|
| 225 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 226 |
+
<div className="bg-white rounded-lg max-w-md w-full mx-4 relative">
|
| 227 |
+
<button
|
| 228 |
+
onClick={() => setShowAuthModal(false)}
|
| 229 |
+
className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 text-xl"
|
| 230 |
+
>
|
| 231 |
+
×
|
| 232 |
+
</button>
|
| 233 |
+
<HFAuth
|
| 234 |
+
onAuthSuccess={handleAuthSuccess}
|
| 235 |
+
onAuthError={handleAuthError}
|
| 236 |
+
/>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
)}
|
| 240 |
+
|
| 241 |
</div>
|
| 242 |
);
|
| 243 |
}
|
components/HFAuth.tsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from 'react';
|
| 4 |
+
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
|
| 5 |
+
|
| 6 |
+
interface OAuthResult {
|
| 7 |
+
accessToken: string;
|
| 8 |
+
userInfo: {
|
| 9 |
+
id: string;
|
| 10 |
+
name: string;
|
| 11 |
+
fullname: string;
|
| 12 |
+
email: string;
|
| 13 |
+
avatarUrl: string;
|
| 14 |
+
};
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface HFAuthProps {
|
| 18 |
+
onAuthSuccess: (result: OAuthResult) => void;
|
| 19 |
+
onAuthError: (error: string) => void;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default function HFAuth({ onAuthSuccess, onAuthError }: HFAuthProps) {
|
| 23 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 24 |
+
const [oauthResult, setOauthResult] = useState<OAuthResult | null>(null);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
const initializeAuth = async () => {
|
| 28 |
+
try {
|
| 29 |
+
// Check for existing OAuth result in localStorage
|
| 30 |
+
let stored = localStorage.getItem('hf_oauth');
|
| 31 |
+
if (stored) {
|
| 32 |
+
try {
|
| 33 |
+
const parsedResult = JSON.parse(stored);
|
| 34 |
+
setOauthResult(parsedResult);
|
| 35 |
+
onAuthSuccess(parsedResult);
|
| 36 |
+
setIsLoading(false);
|
| 37 |
+
return;
|
| 38 |
+
} catch {
|
| 39 |
+
localStorage.removeItem('hf_oauth');
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Handle OAuth redirect if present
|
| 44 |
+
const result = await oauthHandleRedirectIfPresent();
|
| 45 |
+
if (result) {
|
| 46 |
+
const oauthData: OAuthResult = {
|
| 47 |
+
accessToken: result.accessToken,
|
| 48 |
+
userInfo: result.userInfo
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
setOauthResult(oauthData);
|
| 52 |
+
localStorage.setItem('hf_oauth', JSON.stringify(oauthData));
|
| 53 |
+
onAuthSuccess(oauthData);
|
| 54 |
+
}
|
| 55 |
+
} catch (error) {
|
| 56 |
+
console.error('OAuth initialization error:', error);
|
| 57 |
+
onAuthError('Failed to initialize authentication');
|
| 58 |
+
} finally {
|
| 59 |
+
setIsLoading(false);
|
| 60 |
+
}
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
initializeAuth();
|
| 64 |
+
}, [onAuthSuccess, onAuthError]);
|
| 65 |
+
|
| 66 |
+
const handleSignIn = async () => {
|
| 67 |
+
try {
|
| 68 |
+
setIsLoading(true);
|
| 69 |
+
// Default scopes for reading user info
|
| 70 |
+
const scopes = 'read-repos read-billing';
|
| 71 |
+
const loginUrl = await oauthLoginUrl({ scopes });
|
| 72 |
+
window.location.href = loginUrl + '&prompt=consent';
|
| 73 |
+
} catch (error) {
|
| 74 |
+
console.error('Sign in error:', error);
|
| 75 |
+
onAuthError('Failed to initiate sign in');
|
| 76 |
+
setIsLoading(false);
|
| 77 |
+
}
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
const handleSignOut = () => {
|
| 81 |
+
localStorage.removeItem('hf_oauth');
|
| 82 |
+
setOauthResult(null);
|
| 83 |
+
// Redirect to clean URL without OAuth parameters
|
| 84 |
+
const cleanUrl = window.location.href.replace(/[?&]code=[^&]*/, '').replace(/[?&]state=[^&]*/, '');
|
| 85 |
+
window.location.href = cleanUrl;
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
if (isLoading) {
|
| 89 |
+
return (
|
| 90 |
+
<div className="flex items-center justify-center p-4">
|
| 91 |
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-600"></div>
|
| 92 |
+
<span className="ml-2 text-sm text-gray-600">Checking authentication...</span>
|
| 93 |
+
</div>
|
| 94 |
+
);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
if (oauthResult) {
|
| 98 |
+
return (
|
| 99 |
+
<div className="flex items-center gap-3 p-3 bg-green-50 border border-green-200 rounded-lg">
|
| 100 |
+
<img
|
| 101 |
+
src={oauthResult.userInfo.avatarUrl}
|
| 102 |
+
alt={oauthResult.userInfo.name}
|
| 103 |
+
className="w-8 h-8 rounded-full"
|
| 104 |
+
/>
|
| 105 |
+
<div className="flex-1">
|
| 106 |
+
<div className="text-sm font-medium text-green-800">
|
| 107 |
+
Welcome, {oauthResult.userInfo.fullname || oauthResult.userInfo.name}!
|
| 108 |
+
</div>
|
| 109 |
+
<div className="text-xs text-green-600">Signed in with Hugging Face</div>
|
| 110 |
+
</div>
|
| 111 |
+
<button
|
| 112 |
+
onClick={handleSignOut}
|
| 113 |
+
className="px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
|
| 114 |
+
>
|
| 115 |
+
Sign Out
|
| 116 |
+
</button>
|
| 117 |
+
</div>
|
| 118 |
+
);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<div className="text-center p-6">
|
| 123 |
+
<div className="mb-4">
|
| 124 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">Sign in with Hugging Face</h3>
|
| 125 |
+
<p className="text-sm text-gray-600 mb-4">
|
| 126 |
+
Connect your Hugging Face account to access AI-powered presentation generation
|
| 127 |
+
</p>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<button
|
| 131 |
+
onClick={handleSignIn}
|
| 132 |
+
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-yellow-400 to-orange-500 text-white font-medium rounded-lg hover:from-yellow-500 hover:to-orange-600 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105"
|
| 133 |
+
>
|
| 134 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
| 135 |
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
| 136 |
+
</svg>
|
| 137 |
+
Sign in with Hugging Face
|
| 138 |
+
</button>
|
| 139 |
+
|
| 140 |
+
<div className="mt-4 text-xs text-gray-500">
|
| 141 |
+
Secure OAuth authentication • Your data stays private
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
);
|
| 145 |
+
}
|
components/UnsplashImageSearch.tsx
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from 'react';
|
| 4 |
+
import { createApi } from 'unsplash-js';
|
| 5 |
+
import { Search, Download, X } from 'lucide-react';
|
| 6 |
+
|
| 7 |
+
interface UnsplashImage {
|
| 8 |
+
id: string;
|
| 9 |
+
urls: {
|
| 10 |
+
small: string;
|
| 11 |
+
regular: string;
|
| 12 |
+
full: string;
|
| 13 |
+
};
|
| 14 |
+
alt_description: string;
|
| 15 |
+
user: {
|
| 16 |
+
name: string;
|
| 17 |
+
username: string;
|
| 18 |
+
};
|
| 19 |
+
links: {
|
| 20 |
+
download_location: string;
|
| 21 |
+
};
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
interface UnsplashImageSearchProps {
|
| 25 |
+
onImageSelect: (imageUrl: string) => void;
|
| 26 |
+
onClose: () => void;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export default function UnsplashImageSearch({ onImageSelect, onClose }: UnsplashImageSearchProps) {
|
| 30 |
+
const [query, setQuery] = useState('');
|
| 31 |
+
const [images, setImages] = useState<UnsplashImage[]>([]);
|
| 32 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 33 |
+
const [error, setError] = useState<string | null>(null);
|
| 34 |
+
const [page, setPage] = useState(1);
|
| 35 |
+
const [hasMore, setHasMore] = useState(true);
|
| 36 |
+
|
| 37 |
+
const unsplash = createApi({
|
| 38 |
+
accessKey: process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY || '',
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
const searchImages = async (searchQuery: string, pageNum: number = 1) => {
|
| 42 |
+
if (!process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY) {
|
| 43 |
+
setError('Unsplash API key not configured. Please add NEXT_PUBLIC_UNSPLASH_ACCESS_KEY to your environment variables.');
|
| 44 |
+
return;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
setIsLoading(true);
|
| 48 |
+
setError(null);
|
| 49 |
+
|
| 50 |
+
try {
|
| 51 |
+
const result = await unsplash.search.getPhotos({
|
| 52 |
+
query: searchQuery,
|
| 53 |
+
page: pageNum,
|
| 54 |
+
perPage: 20,
|
| 55 |
+
orientation: 'landscape',
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
if (result.errors) {
|
| 59 |
+
setError('Failed to search images');
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const newImages = result.response?.results || [];
|
| 64 |
+
|
| 65 |
+
if (pageNum === 1) {
|
| 66 |
+
setImages(newImages);
|
| 67 |
+
} else {
|
| 68 |
+
setImages(prev => [...prev, ...newImages]);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
setHasMore(newImages.length === 20);
|
| 72 |
+
setPage(pageNum);
|
| 73 |
+
} catch (err) {
|
| 74 |
+
setError('Failed to search images');
|
| 75 |
+
console.error('Unsplash search error:', err);
|
| 76 |
+
} finally {
|
| 77 |
+
setIsLoading(false);
|
| 78 |
+
}
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const handleSearch = (e: React.FormEvent) => {
|
| 82 |
+
e.preventDefault();
|
| 83 |
+
if (query.trim()) {
|
| 84 |
+
searchImages(query.trim(), 1);
|
| 85 |
+
}
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
const loadMore = () => {
|
| 89 |
+
if (!isLoading && hasMore && query) {
|
| 90 |
+
searchImages(query, page + 1);
|
| 91 |
+
}
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
const handleImageSelect = (image: UnsplashImage) => {
|
| 95 |
+
// Trigger download tracking as required by Unsplash API
|
| 96 |
+
if (image.links.download_location) {
|
| 97 |
+
fetch(`/api/unsplash-download?url=${encodeURIComponent(image.links.download_location)}`);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
onImageSelect(image.urls.regular);
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
useEffect(() => {
|
| 104 |
+
// Load popular images on component mount
|
| 105 |
+
searchImages('presentation', 1);
|
| 106 |
+
}, []);
|
| 107 |
+
|
| 108 |
+
return (
|
| 109 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 110 |
+
<div className="bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[90vh] flex flex-col">
|
| 111 |
+
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
| 112 |
+
<h3 className="text-lg font-semibold text-gray-900">Search Unsplash Images</h3>
|
| 113 |
+
<button
|
| 114 |
+
onClick={onClose}
|
| 115 |
+
className="p-1 text-gray-400 hover:text-gray-600"
|
| 116 |
+
>
|
| 117 |
+
<X size={20} />
|
| 118 |
+
</button>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div className="p-4 border-b border-gray-200">
|
| 122 |
+
<form onSubmit={handleSearch} className="flex gap-2">
|
| 123 |
+
<div className="relative flex-1">
|
| 124 |
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={16} />
|
| 125 |
+
<input
|
| 126 |
+
type="text"
|
| 127 |
+
value={query}
|
| 128 |
+
onChange={(e) => setQuery(e.target.value)}
|
| 129 |
+
placeholder="Search for images..."
|
| 130 |
+
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 131 |
+
/>
|
| 132 |
+
</div>
|
| 133 |
+
<button
|
| 134 |
+
type="submit"
|
| 135 |
+
disabled={isLoading || !query.trim()}
|
| 136 |
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
| 137 |
+
>
|
| 138 |
+
Search
|
| 139 |
+
</button>
|
| 140 |
+
</form>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<div className="flex-1 overflow-y-auto p-4">
|
| 144 |
+
{error && (
|
| 145 |
+
<div className="text-center py-8">
|
| 146 |
+
<div className="text-red-600 mb-2">{error}</div>
|
| 147 |
+
{!process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY && (
|
| 148 |
+
<div className="text-sm text-gray-600">
|
| 149 |
+
Please add your Unsplash access key to .env.local file
|
| 150 |
+
</div>
|
| 151 |
+
)}
|
| 152 |
+
</div>
|
| 153 |
+
)}
|
| 154 |
+
|
| 155 |
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
| 156 |
+
{images.map((image) => (
|
| 157 |
+
<div
|
| 158 |
+
key={image.id}
|
| 159 |
+
className="relative group cursor-pointer rounded-lg overflow-hidden bg-gray-100 aspect-video"
|
| 160 |
+
onClick={() => handleImageSelect(image)}
|
| 161 |
+
>
|
| 162 |
+
<img
|
| 163 |
+
src={image.urls.small}
|
| 164 |
+
alt={image.alt_description || 'Unsplash image'}
|
| 165 |
+
className="w-full h-full object-cover transition-transform group-hover:scale-105"
|
| 166 |
+
/>
|
| 167 |
+
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-opacity flex items-center justify-center">
|
| 168 |
+
<Download className="text-white opacity-0 group-hover:opacity-100 transition-opacity" size={24} />
|
| 169 |
+
</div>
|
| 170 |
+
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-2">
|
| 171 |
+
<div className="text-white text-xs truncate">
|
| 172 |
+
Photo by {image.user.name}
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
))}
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
{isLoading && (
|
| 180 |
+
<div className="text-center py-8">
|
| 181 |
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
| 182 |
+
<div className="text-gray-600 mt-2">Searching images...</div>
|
| 183 |
+
</div>
|
| 184 |
+
)}
|
| 185 |
+
|
| 186 |
+
{hasMore && images.length > 0 && !isLoading && (
|
| 187 |
+
<div className="text-center py-4">
|
| 188 |
+
<button
|
| 189 |
+
onClick={loadMore}
|
| 190 |
+
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
| 191 |
+
>
|
| 192 |
+
Load More
|
| 193 |
+
</button>
|
| 194 |
+
</div>
|
| 195 |
+
)}
|
| 196 |
+
|
| 197 |
+
{!hasMore && images.length > 0 && (
|
| 198 |
+
<div className="text-center py-4 text-gray-500">
|
| 199 |
+
No more images to load
|
| 200 |
+
</div>
|
| 201 |
+
)}
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<div className="p-4 border-t border-gray-200 text-xs text-gray-500 text-center">
|
| 205 |
+
Images provided by <a href="https://unsplash.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">Unsplash</a>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
);
|
| 210 |
+
}
|
components/slides/BulletsSlide.tsx
CHANGED
|
@@ -1,26 +1,211 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
|
| 3 |
export interface BulletsSlideProps {
|
| 4 |
title: string;
|
| 5 |
body: Array<{ heading?: string; text: string }>;
|
| 6 |
-
theme?: 'dark' | 'light';
|
|
|
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
return (
|
| 12 |
-
<div className={`w-full h-full ${
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</div>
|
| 22 |
-
|
| 23 |
-
|
| 24 |
</div>
|
| 25 |
</div>
|
| 26 |
);
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
|
| 3 |
export interface BulletsSlideProps {
|
| 4 |
title: string;
|
| 5 |
body: Array<{ heading?: string; text: string }>;
|
| 6 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'sunset' | 'midnight';
|
| 7 |
+
slideId?: string;
|
| 8 |
+
onFieldUpdate?: (slideId: string, field: string, value: string, index?: number) => void;
|
| 9 |
+
isEditable?: boolean;
|
| 10 |
}
|
| 11 |
|
| 12 |
+
const getThemeClasses = (theme: string) => {
|
| 13 |
+
switch (theme) {
|
| 14 |
+
case 'light':
|
| 15 |
+
return 'text-slate-900 bg-white';
|
| 16 |
+
case 'blue':
|
| 17 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 18 |
+
case 'purple':
|
| 19 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 20 |
+
case 'green':
|
| 21 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 22 |
+
case 'orange':
|
| 23 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 24 |
+
case 'teal':
|
| 25 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 26 |
+
case 'pink':
|
| 27 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 28 |
+
case 'indigo':
|
| 29 |
+
return 'text-white bg-[#0b0c14]';
|
| 30 |
+
case 'midnight':
|
| 31 |
+
return 'text-white bg-[#0b0c14]';
|
| 32 |
+
case 'sunset':
|
| 33 |
+
return 'text-white bg-[#220b03]';
|
| 34 |
+
case 'amber':
|
| 35 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 36 |
+
case 'emerald':
|
| 37 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 38 |
+
case 'slate':
|
| 39 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 40 |
+
default: // dark
|
| 41 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const getBulletColor = (theme: string) => {
|
| 46 |
+
switch (theme) {
|
| 47 |
+
case 'light':
|
| 48 |
+
return 'bg-blue-600';
|
| 49 |
+
case 'blue':
|
| 50 |
+
return 'bg-blue-200';
|
| 51 |
+
case 'purple':
|
| 52 |
+
return 'bg-purple-200';
|
| 53 |
+
case 'green':
|
| 54 |
+
return 'bg-green-200';
|
| 55 |
+
case 'orange':
|
| 56 |
+
return 'bg-orange-200';
|
| 57 |
+
case 'teal':
|
| 58 |
+
return 'bg-teal-200';
|
| 59 |
+
case 'pink':
|
| 60 |
+
return 'bg-pink-200';
|
| 61 |
+
case 'indigo':
|
| 62 |
+
return 'bg-indigo-200';
|
| 63 |
+
case 'midnight':
|
| 64 |
+
return 'bg-indigo-200';
|
| 65 |
+
case 'sunset':
|
| 66 |
+
return 'bg-orange-200';
|
| 67 |
+
case 'amber':
|
| 68 |
+
return 'bg-amber-200';
|
| 69 |
+
case 'emerald':
|
| 70 |
+
return 'bg-emerald-200';
|
| 71 |
+
case 'slate':
|
| 72 |
+
return 'bg-slate-200';
|
| 73 |
+
default: // dark
|
| 74 |
+
return 'bg-white';
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
export default function BulletsSlide({
|
| 79 |
+
title,
|
| 80 |
+
body,
|
| 81 |
+
theme = 'dark',
|
| 82 |
+
slideId,
|
| 83 |
+
onFieldUpdate,
|
| 84 |
+
isEditable = false
|
| 85 |
+
}: BulletsSlideProps) {
|
| 86 |
+
const [editingField, setEditingField] = useState<{ field: string; index?: number } | null>(null);
|
| 87 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 88 |
+
const [tempBody, setTempBody] = useState<string[]>(body?.map(b => b.text) || []);
|
| 89 |
+
|
| 90 |
+
const themeClasses = getThemeClasses(theme);
|
| 91 |
+
const bulletColor = getBulletColor(theme);
|
| 92 |
+
|
| 93 |
+
const handleFieldClick = (field: string, index?: number) => {
|
| 94 |
+
if (!isEditable) return;
|
| 95 |
+
setEditingField({ field, index });
|
| 96 |
+
if (field === 'title') setTempTitle(title);
|
| 97 |
+
if (field === 'body' && typeof index === 'number') {
|
| 98 |
+
setTempBody(body?.map(b => b.text) || []);
|
| 99 |
+
}
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
const handleFieldBlur = (field: string, index?: number) => {
|
| 103 |
+
if (!slideId || !onFieldUpdate) return;
|
| 104 |
+
|
| 105 |
+
if (field === 'title' && tempTitle !== title) {
|
| 106 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 107 |
+
}
|
| 108 |
+
if (field === 'body' && typeof index === 'number' && tempBody[index] !== body?.[index]?.text) {
|
| 109 |
+
onFieldUpdate(slideId, 'body', tempBody[index], index);
|
| 110 |
+
}
|
| 111 |
+
setEditingField(null);
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string, index?: number) => {
|
| 115 |
+
if (e.key === 'Enter') {
|
| 116 |
+
e.preventDefault();
|
| 117 |
+
handleFieldBlur(field, index);
|
| 118 |
+
}
|
| 119 |
+
if (e.key === 'Escape') {
|
| 120 |
+
setEditingField(null);
|
| 121 |
+
if (field === 'title') setTempTitle(title);
|
| 122 |
+
if (field === 'body') setTempBody(body?.map(b => b.text) || []);
|
| 123 |
+
}
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
const updateTempBodyItem = (index: number, value: string) => {
|
| 127 |
+
const newTempBody = [...tempBody];
|
| 128 |
+
newTempBody[index] = value;
|
| 129 |
+
setTempBody(newTempBody);
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
return (
|
| 133 |
+
<div className={`w-full h-full ${themeClasses} p-10 relative overflow-hidden`}>
|
| 134 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 135 |
+
<div
|
| 136 |
+
aria-hidden
|
| 137 |
+
className="absolute inset-0"
|
| 138 |
+
style={{
|
| 139 |
+
background:
|
| 140 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%)',
|
| 141 |
+
}}
|
| 142 |
+
/>
|
| 143 |
+
)}
|
| 144 |
+
{theme === 'sunset' && (
|
| 145 |
+
<div
|
| 146 |
+
aria-hidden
|
| 147 |
+
className="absolute inset-0"
|
| 148 |
+
style={{
|
| 149 |
+
background:
|
| 150 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%)',
|
| 151 |
+
}}
|
| 152 |
+
/>
|
| 153 |
+
)}
|
| 154 |
+
{/* Background pattern */}
|
| 155 |
+
<div className="absolute inset-0 opacity-5">
|
| 156 |
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,_white_1px,_transparent_0)] bg-[length:20px_20px]" />
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<div className="relative z-10">
|
| 160 |
+
{editingField?.field === 'title' ? (
|
| 161 |
+
<input
|
| 162 |
+
type="text"
|
| 163 |
+
value={tempTitle}
|
| 164 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 165 |
+
onBlur={() => handleFieldBlur('title')}
|
| 166 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 167 |
+
className="text-4xl font-semibold mb-8 drop-shadow-lg bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70 w-full"
|
| 168 |
+
placeholder="Enter title"
|
| 169 |
+
autoFocus
|
| 170 |
+
/>
|
| 171 |
+
) : (
|
| 172 |
+
<h2
|
| 173 |
+
className={`text-4xl font-semibold mb-8 drop-shadow-lg ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1 transition-colors' : ''}`}
|
| 174 |
+
onClick={() => handleFieldClick('title')}
|
| 175 |
+
>
|
| 176 |
+
{title}
|
| 177 |
+
</h2>
|
| 178 |
+
)}
|
| 179 |
+
|
| 180 |
+
<div className="space-y-6">
|
| 181 |
+
{body?.map((b, i) => (
|
| 182 |
+
<div key={i} className="flex items-start gap-4">
|
| 183 |
+
<div className={`w-3 h-3 ${bulletColor} rounded-full mt-2 shrink-0 shadow-lg`} />
|
| 184 |
+
<div className="flex-1">
|
| 185 |
+
{b.heading && <div className="font-semibold text-lg mb-1 drop-shadow-md">{b.heading}</div>}
|
| 186 |
+
{editingField?.field === 'body' && editingField?.index === i ? (
|
| 187 |
+
<textarea
|
| 188 |
+
value={tempBody[i] || ''}
|
| 189 |
+
onChange={(e) => updateTempBodyItem(i, e.target.value)}
|
| 190 |
+
onBlur={() => handleFieldBlur('body', i)}
|
| 191 |
+
onKeyDown={(e) => handleKeyDown(e, 'body', i)}
|
| 192 |
+
className="opacity-90 leading-relaxed text-lg drop-shadow-sm bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70 w-full resize-none"
|
| 193 |
+
placeholder="Enter content"
|
| 194 |
+
rows={2}
|
| 195 |
+
autoFocus
|
| 196 |
+
/>
|
| 197 |
+
) : (
|
| 198 |
+
<p
|
| 199 |
+
className={`opacity-90 leading-relaxed text-lg drop-shadow-sm ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1 transition-colors' : ''}`}
|
| 200 |
+
onClick={() => handleFieldClick('body', i)}
|
| 201 |
+
>
|
| 202 |
+
{b.text}
|
| 203 |
+
</p>
|
| 204 |
+
)}
|
| 205 |
+
</div>
|
| 206 |
</div>
|
| 207 |
+
))}
|
| 208 |
+
</div>
|
| 209 |
</div>
|
| 210 |
</div>
|
| 211 |
);
|
components/slides/ChartSlide.tsx
CHANGED
|
@@ -7,40 +7,137 @@ export interface ChartSlideProps {
|
|
| 7 |
data: Record<string, unknown>;
|
| 8 |
options?: Record<string, unknown>;
|
| 9 |
};
|
| 10 |
-
theme?: 'dark' | 'light';
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
return (
|
| 17 |
-
<div className={`w-full h-full ${
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
</div>
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
</div>
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
<div className="w-full max-w-2xl h-96 flex items-center justify-center border border-gray-300 rounded-lg opacity-70">
|
| 41 |
-
No chart data available
|
| 42 |
-
</div>
|
| 43 |
-
)}
|
| 44 |
</div>
|
| 45 |
</div>
|
| 46 |
);
|
|
|
|
| 7 |
data: Record<string, unknown>;
|
| 8 |
options?: Record<string, unknown>;
|
| 9 |
};
|
| 10 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'sunset' | 'midnight';
|
| 11 |
+
slideId?: string;
|
| 12 |
+
onFieldUpdate?: (slideId: string, field: string, value: string, index?: number) => void;
|
| 13 |
+
isEditable?: boolean;
|
| 14 |
}
|
| 15 |
|
| 16 |
+
const getThemeClasses = (theme: string) => {
|
| 17 |
+
switch (theme) {
|
| 18 |
+
case 'light':
|
| 19 |
+
return 'text-slate-900 bg-white';
|
| 20 |
+
case 'blue':
|
| 21 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 22 |
+
case 'purple':
|
| 23 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 24 |
+
case 'green':
|
| 25 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 26 |
+
case 'orange':
|
| 27 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 28 |
+
case 'teal':
|
| 29 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 30 |
+
case 'pink':
|
| 31 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 32 |
+
case 'indigo':
|
| 33 |
+
return 'text-white bg-[#0b0c14]';
|
| 34 |
+
case 'midnight':
|
| 35 |
+
return 'text-white bg-[#0b0c14]';
|
| 36 |
+
case 'sunset':
|
| 37 |
+
return 'text-white bg-[#220b03]';
|
| 38 |
+
case 'amber':
|
| 39 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 40 |
+
case 'emerald':
|
| 41 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 42 |
+
case 'slate':
|
| 43 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 44 |
+
default: // dark
|
| 45 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const getChartBarColor = (theme: string) => {
|
| 50 |
+
switch (theme) {
|
| 51 |
+
case 'light':
|
| 52 |
+
return 'bg-blue-600';
|
| 53 |
+
case 'blue':
|
| 54 |
+
return 'bg-blue-300';
|
| 55 |
+
case 'purple':
|
| 56 |
+
return 'bg-purple-300';
|
| 57 |
+
case 'green':
|
| 58 |
+
return 'bg-green-300';
|
| 59 |
+
case 'orange':
|
| 60 |
+
return 'bg-orange-300';
|
| 61 |
+
case 'teal':
|
| 62 |
+
return 'bg-teal-300';
|
| 63 |
+
case 'pink':
|
| 64 |
+
return 'bg-pink-300';
|
| 65 |
+
case 'indigo':
|
| 66 |
+
return 'bg-indigo-300';
|
| 67 |
+
case 'midnight':
|
| 68 |
+
return 'bg-indigo-300';
|
| 69 |
+
case 'sunset':
|
| 70 |
+
return 'bg-orange-300';
|
| 71 |
+
case 'amber':
|
| 72 |
+
return 'bg-amber-300';
|
| 73 |
+
case 'emerald':
|
| 74 |
+
return 'bg-emerald-300';
|
| 75 |
+
case 'slate':
|
| 76 |
+
return 'bg-slate-300';
|
| 77 |
+
default: // dark
|
| 78 |
+
return 'bg-white';
|
| 79 |
+
}
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
export default function ChartSlide({ title, chart, theme = 'dark', slideId, onFieldUpdate, isEditable = false }: ChartSlideProps) {
|
| 83 |
+
const themeClasses = getThemeClasses(theme);
|
| 84 |
+
const barColor = getChartBarColor(theme);
|
| 85 |
|
| 86 |
return (
|
| 87 |
+
<div className={`w-full h-full ${themeClasses} p-10 relative overflow-hidden`}>
|
| 88 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 89 |
+
<div
|
| 90 |
+
aria-hidden
|
| 91 |
+
className="absolute inset-0"
|
| 92 |
+
style={{
|
| 93 |
+
background:
|
| 94 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%)',
|
| 95 |
+
}}
|
| 96 |
+
/>
|
| 97 |
+
)}
|
| 98 |
+
{theme === 'sunset' && (
|
| 99 |
+
<div
|
| 100 |
+
aria-hidden
|
| 101 |
+
className="absolute inset-0"
|
| 102 |
+
style={{
|
| 103 |
+
background:
|
| 104 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%)',
|
| 105 |
+
}}
|
| 106 |
+
/>
|
| 107 |
+
)}
|
| 108 |
+
{/* Background pattern */}
|
| 109 |
+
<div className="absolute inset-0 opacity-5">
|
| 110 |
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,_white_1px,_transparent_0)] bg-[length:20px_20px]" />
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div className="relative z-10">
|
| 114 |
+
<h2 className="text-4xl font-semibold mb-8 text-center drop-shadow-lg">{title}</h2>
|
| 115 |
+
<div className="flex-1 flex items-center justify-center">
|
| 116 |
+
{chart ? (
|
| 117 |
+
<div className="w-full max-w-2xl h-96 flex items-center justify-center border border-white/20 rounded-lg bg-white/10 backdrop-blur-sm shadow-2xl">
|
| 118 |
+
<div className="text-center">
|
| 119 |
+
<div className="text-lg font-medium mb-2 drop-shadow-md">Chart: {chart.type}</div>
|
| 120 |
+
<div className="text-sm opacity-70 drop-shadow-sm">
|
| 121 |
+
{Array.isArray((chart.data as any)?.labels) ? ((chart.data as any).labels as string[]).join(', ') : 'Chart data'}
|
| 122 |
</div>
|
| 123 |
+
{(chart as any).data?.datasets?.[0]?.data && (
|
| 124 |
+
<div className="mt-4 flex gap-2 justify-center">
|
| 125 |
+
{((chart as any).data.datasets[0].data as number[]).map((value: number, i: number) => (
|
| 126 |
+
<div key={i} className="text-center">
|
| 127 |
+
<div className={`w-8 ${barColor} rounded-t shadow-lg`} style={{ height: `${Math.max(8, value * 2)}px` }}></div>
|
| 128 |
+
<div className="text-xs mt-1 drop-shadow-sm">{value}</div>
|
| 129 |
+
</div>
|
| 130 |
+
))}
|
| 131 |
+
</div>
|
| 132 |
+
)}
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
) : (
|
| 136 |
+
<div className="w-full max-w-2xl h-96 flex items-center justify-center border border-white/20 rounded-lg opacity-70 bg-white/10 backdrop-blur-sm">
|
| 137 |
+
No chart data available
|
| 138 |
</div>
|
| 139 |
+
)}
|
| 140 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
</div>
|
| 142 |
</div>
|
| 143 |
);
|
components/slides/ComparisonSlide.tsx
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface ComparisonSlideProps {
|
| 4 |
+
title: string;
|
| 5 |
+
leftTitle: string;
|
| 6 |
+
rightTitle: string;
|
| 7 |
+
leftContent: Array<string>;
|
| 8 |
+
rightContent: Array<string>;
|
| 9 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 10 |
+
slideId?: string;
|
| 11 |
+
onFieldUpdate?: (slideId: string, field: string, value: string, index?: number) => void;
|
| 12 |
+
isEditable?: boolean;
|
| 13 |
+
onTextSelect?: (element: any) => void;
|
| 14 |
+
selectedFont?: string;
|
| 15 |
+
selectedFontSize?: number;
|
| 16 |
+
selectedColor?: string;
|
| 17 |
+
formatting?: Record<string, Record<string, any>>;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const getThemeClasses = (theme: string) => {
|
| 21 |
+
switch (theme) {
|
| 22 |
+
case 'light':
|
| 23 |
+
return 'text-slate-900 bg-white';
|
| 24 |
+
case 'blue':
|
| 25 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 26 |
+
case 'purple':
|
| 27 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 28 |
+
case 'green':
|
| 29 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 30 |
+
case 'orange':
|
| 31 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 32 |
+
case 'teal':
|
| 33 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 34 |
+
case 'pink':
|
| 35 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 36 |
+
case 'indigo':
|
| 37 |
+
return 'text-white bg-[#0b0c14]';
|
| 38 |
+
case 'midnight':
|
| 39 |
+
return 'text-white bg-[#0b0c14]';
|
| 40 |
+
case 'sunset':
|
| 41 |
+
return 'text-white bg-[#220b03]';
|
| 42 |
+
case 'amber':
|
| 43 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 44 |
+
case 'emerald':
|
| 45 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 46 |
+
case 'slate':
|
| 47 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 48 |
+
default: // dark
|
| 49 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
export default function ComparisonSlide({
|
| 54 |
+
title,
|
| 55 |
+
leftTitle,
|
| 56 |
+
rightTitle,
|
| 57 |
+
leftContent = ['Feature 1', 'Feature 2', 'Feature 3'],
|
| 58 |
+
rightContent = ['Feature A', 'Feature B', 'Feature C'],
|
| 59 |
+
theme = 'dark',
|
| 60 |
+
slideId,
|
| 61 |
+
onFieldUpdate,
|
| 62 |
+
isEditable = false,
|
| 63 |
+
onTextSelect,
|
| 64 |
+
selectedFont = 'serif',
|
| 65 |
+
selectedFontSize = 16,
|
| 66 |
+
selectedColor,
|
| 67 |
+
formatting = {}
|
| 68 |
+
}: ComparisonSlideProps) {
|
| 69 |
+
const [editingField, setEditingField] = useState<{ field: string; index?: number; side?: 'left' | 'right' } | null>(null);
|
| 70 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 71 |
+
const [tempLeftTitle, setTempLeftTitle] = useState(leftTitle);
|
| 72 |
+
const [tempRightTitle, setTempRightTitle] = useState(rightTitle);
|
| 73 |
+
const [tempLeftContent, setTempLeftContent] = useState(leftContent);
|
| 74 |
+
const [tempRightContent, setTempRightContent] = useState(rightContent);
|
| 75 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 76 |
+
|
| 77 |
+
const themeClasses = getThemeClasses(theme);
|
| 78 |
+
|
| 79 |
+
const getFieldFormatting = (field: string) => {
|
| 80 |
+
const fieldFormatting = formatting[field] || {};
|
| 81 |
+
return {
|
| 82 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 83 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'title' ? 32 : field.includes('Title') ? 20 : 16)),
|
| 84 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 85 |
+
fontWeight: fieldFormatting.bold ? 'bold' : (field.includes('Title') ? '600' : 'normal'),
|
| 86 |
+
fontStyle: fieldFormatting.italic ? 'italic' : 'normal',
|
| 87 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 88 |
+
};
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
const handleFieldClick = (field: string, index?: number, side?: 'left' | 'right') => {
|
| 92 |
+
if (!isEditable) return;
|
| 93 |
+
setEditingField({ field, index, side });
|
| 94 |
+
setSelectedElement(field);
|
| 95 |
+
|
| 96 |
+
if (field === 'title') setTempTitle(title);
|
| 97 |
+
if (field === 'leftTitle') setTempLeftTitle(leftTitle);
|
| 98 |
+
if (field === 'rightTitle') setTempRightTitle(rightTitle);
|
| 99 |
+
|
| 100 |
+
if (onTextSelect) {
|
| 101 |
+
onTextSelect({
|
| 102 |
+
field,
|
| 103 |
+
slideId,
|
| 104 |
+
currentFont: selectedFont,
|
| 105 |
+
currentFontSize: field === 'title' ? 32 : field.includes('Title') ? 20 : 16,
|
| 106 |
+
currentColor: selectedColor
|
| 107 |
+
});
|
| 108 |
+
}
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
const handleFieldBlur = (field: string, index?: number, side?: 'left' | 'right') => {
|
| 112 |
+
if (!slideId || !onFieldUpdate) return;
|
| 113 |
+
|
| 114 |
+
if (field === 'title' && tempTitle !== title) {
|
| 115 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 116 |
+
}
|
| 117 |
+
if (field === 'leftTitle' && tempLeftTitle !== leftTitle) {
|
| 118 |
+
onFieldUpdate(slideId, 'leftTitle', tempLeftTitle);
|
| 119 |
+
}
|
| 120 |
+
if (field === 'rightTitle' && tempRightTitle !== rightTitle) {
|
| 121 |
+
onFieldUpdate(slideId, 'rightTitle', tempRightTitle);
|
| 122 |
+
}
|
| 123 |
+
if (field === 'content' && typeof index === 'number' && side) {
|
| 124 |
+
const content = side === 'left' ? tempLeftContent : tempRightContent;
|
| 125 |
+
onFieldUpdate(slideId, `${side}Content`, JSON.stringify(content), index);
|
| 126 |
+
}
|
| 127 |
+
setEditingField(null);
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string, index?: number, side?: 'left' | 'right') => {
|
| 131 |
+
if (e.key === 'Enter') {
|
| 132 |
+
e.preventDefault();
|
| 133 |
+
handleFieldBlur(field, index, side);
|
| 134 |
+
}
|
| 135 |
+
if (e.key === 'Escape') {
|
| 136 |
+
setEditingField(null);
|
| 137 |
+
if (field === 'title') setTempTitle(title);
|
| 138 |
+
if (field === 'leftTitle') setTempLeftTitle(leftTitle);
|
| 139 |
+
if (field === 'rightTitle') setTempRightTitle(rightTitle);
|
| 140 |
+
if (field === 'content') {
|
| 141 |
+
setTempLeftContent([...leftContent]);
|
| 142 |
+
setTempRightContent([...rightContent]);
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
const updateTempContent = (side: 'left' | 'right', index: number, value: string) => {
|
| 148 |
+
if (side === 'left') {
|
| 149 |
+
const newContent = [...tempLeftContent];
|
| 150 |
+
newContent[index] = value;
|
| 151 |
+
setTempLeftContent(newContent);
|
| 152 |
+
} else {
|
| 153 |
+
const newContent = [...tempRightContent];
|
| 154 |
+
newContent[index] = value;
|
| 155 |
+
setTempRightContent(newContent);
|
| 156 |
+
}
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
+
return (
|
| 160 |
+
<div className={`w-full h-full ${themeClasses} p-12 relative overflow-hidden`}>
|
| 161 |
+
{/* Special backgrounds */}
|
| 162 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 163 |
+
<>
|
| 164 |
+
<div
|
| 165 |
+
aria-hidden
|
| 166 |
+
className="absolute inset-0 -z-10"
|
| 167 |
+
style={{
|
| 168 |
+
background:
|
| 169 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 170 |
+
}}
|
| 171 |
+
/>
|
| 172 |
+
<div
|
| 173 |
+
aria-hidden
|
| 174 |
+
className="absolute inset-0 -z-10"
|
| 175 |
+
style={{
|
| 176 |
+
backgroundImage:
|
| 177 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 178 |
+
backgroundSize: '160px 160px',
|
| 179 |
+
opacity: 0.25,
|
| 180 |
+
}}
|
| 181 |
+
/>
|
| 182 |
+
</>
|
| 183 |
+
)}
|
| 184 |
+
{theme === 'sunset' && (
|
| 185 |
+
<div
|
| 186 |
+
aria-hidden
|
| 187 |
+
className="absolute inset-0 -z-10"
|
| 188 |
+
style={{
|
| 189 |
+
background:
|
| 190 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 191 |
+
}}
|
| 192 |
+
/>
|
| 193 |
+
)}
|
| 194 |
+
|
| 195 |
+
<div className="relative z-10 h-full flex flex-col">
|
| 196 |
+
{/* Title */}
|
| 197 |
+
{editingField?.field === 'title' ? (
|
| 198 |
+
<input
|
| 199 |
+
type="text"
|
| 200 |
+
value={tempTitle}
|
| 201 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 202 |
+
onBlur={() => handleFieldBlur('title')}
|
| 203 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 204 |
+
className="mb-12 text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 205 |
+
style={{
|
| 206 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 207 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 208 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 209 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 210 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 211 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 212 |
+
}}
|
| 213 |
+
placeholder="Comparison Title"
|
| 214 |
+
autoFocus
|
| 215 |
+
/>
|
| 216 |
+
) : (
|
| 217 |
+
<h2
|
| 218 |
+
className={`mb-12 text-center font-semibold ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${selectedElement === 'title' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 219 |
+
style={{
|
| 220 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 221 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 222 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 223 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 224 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 225 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 226 |
+
}}
|
| 227 |
+
onClick={() => handleFieldClick('title')}
|
| 228 |
+
>
|
| 229 |
+
{title}
|
| 230 |
+
</h2>
|
| 231 |
+
)}
|
| 232 |
+
|
| 233 |
+
{/* Comparison Grid */}
|
| 234 |
+
<div className="flex-1 grid md:grid-cols-2 gap-8">
|
| 235 |
+
{/* Left Side */}
|
| 236 |
+
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-6 border border-white/20 relative">
|
| 237 |
+
{/* VS indicator */}
|
| 238 |
+
<div className="absolute -right-4 top-1/2 transform -translate-y-1/2 bg-white/20 rounded-full w-8 h-8 flex items-center justify-center text-sm font-bold border-2 border-white/30">
|
| 239 |
+
VS
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
{/* Left Title */}
|
| 243 |
+
{editingField?.field === 'leftTitle' ? (
|
| 244 |
+
<input
|
| 245 |
+
type="text"
|
| 246 |
+
value={tempLeftTitle}
|
| 247 |
+
onChange={(e) => setTempLeftTitle(e.target.value)}
|
| 248 |
+
onBlur={() => handleFieldBlur('leftTitle')}
|
| 249 |
+
onKeyDown={(e) => handleKeyDown(e, 'leftTitle')}
|
| 250 |
+
className="w-full mb-6 text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 251 |
+
style={{
|
| 252 |
+
fontFamily: getFieldFormatting('leftTitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('leftTitle').fontFamily})`,
|
| 253 |
+
fontSize: `${getFieldFormatting('leftTitle').fontSize}px`,
|
| 254 |
+
color: getFieldFormatting('leftTitle').color || 'inherit',
|
| 255 |
+
fontWeight: getFieldFormatting('leftTitle').fontWeight,
|
| 256 |
+
fontStyle: getFieldFormatting('leftTitle').fontStyle,
|
| 257 |
+
textDecoration: getFieldFormatting('leftTitle').textDecoration
|
| 258 |
+
}}
|
| 259 |
+
placeholder="Option A"
|
| 260 |
+
autoFocus
|
| 261 |
+
/>
|
| 262 |
+
) : (
|
| 263 |
+
<h3
|
| 264 |
+
className={`mb-6 text-center font-semibold ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${selectedElement === 'leftTitle' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 265 |
+
style={{
|
| 266 |
+
fontFamily: getFieldFormatting('leftTitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('leftTitle').fontFamily})`,
|
| 267 |
+
fontSize: `${getFieldFormatting('leftTitle').fontSize}px`,
|
| 268 |
+
color: getFieldFormatting('leftTitle').color || 'inherit',
|
| 269 |
+
fontWeight: getFieldFormatting('leftTitle').fontWeight,
|
| 270 |
+
fontStyle: getFieldFormatting('leftTitle').fontStyle,
|
| 271 |
+
textDecoration: getFieldFormatting('leftTitle').textDecoration
|
| 272 |
+
}}
|
| 273 |
+
onClick={() => handleFieldClick('leftTitle')}
|
| 274 |
+
>
|
| 275 |
+
{leftTitle}
|
| 276 |
+
</h3>
|
| 277 |
+
)}
|
| 278 |
+
|
| 279 |
+
{/* Left Content */}
|
| 280 |
+
<ul className="space-y-3">
|
| 281 |
+
{leftContent.map((item, index) => (
|
| 282 |
+
<li key={index} className="flex items-start">
|
| 283 |
+
<span className="text-green-400 mr-3 mt-1">✓</span>
|
| 284 |
+
{editingField?.field === 'content' && editingField.index === index && editingField.side === 'left' ? (
|
| 285 |
+
<input
|
| 286 |
+
type="text"
|
| 287 |
+
value={tempLeftContent[index] || ''}
|
| 288 |
+
onChange={(e) => updateTempContent('left', index, e.target.value)}
|
| 289 |
+
onBlur={() => handleFieldBlur('content', index, 'left')}
|
| 290 |
+
onKeyDown={(e) => handleKeyDown(e, 'content', index, 'left')}
|
| 291 |
+
className="flex-1 bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70"
|
| 292 |
+
placeholder="Feature"
|
| 293 |
+
autoFocus
|
| 294 |
+
/>
|
| 295 |
+
) : (
|
| 296 |
+
<span
|
| 297 |
+
className={`flex-1 ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''}`}
|
| 298 |
+
onClick={() => handleFieldClick('content', index, 'left')}
|
| 299 |
+
>
|
| 300 |
+
{item}
|
| 301 |
+
</span>
|
| 302 |
+
)}
|
| 303 |
+
</li>
|
| 304 |
+
))}
|
| 305 |
+
</ul>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
{/* Right Side */}
|
| 309 |
+
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-6 border border-white/20">
|
| 310 |
+
{/* Right Title */}
|
| 311 |
+
{editingField?.field === 'rightTitle' ? (
|
| 312 |
+
<input
|
| 313 |
+
type="text"
|
| 314 |
+
value={tempRightTitle}
|
| 315 |
+
onChange={(e) => setTempRightTitle(e.target.value)}
|
| 316 |
+
onBlur={() => handleFieldBlur('rightTitle')}
|
| 317 |
+
onKeyDown={(e) => handleKeyDown(e, 'rightTitle')}
|
| 318 |
+
className="w-full mb-6 text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 319 |
+
style={{
|
| 320 |
+
fontFamily: getFieldFormatting('rightTitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('rightTitle').fontFamily})`,
|
| 321 |
+
fontSize: `${getFieldFormatting('rightTitle').fontSize}px`,
|
| 322 |
+
color: getFieldFormatting('rightTitle').color || 'inherit',
|
| 323 |
+
fontWeight: getFieldFormatting('rightTitle').fontWeight,
|
| 324 |
+
fontStyle: getFieldFormatting('rightTitle').fontStyle,
|
| 325 |
+
textDecoration: getFieldFormatting('rightTitle').textDecoration
|
| 326 |
+
}}
|
| 327 |
+
placeholder="Option B"
|
| 328 |
+
autoFocus
|
| 329 |
+
/>
|
| 330 |
+
) : (
|
| 331 |
+
<h3
|
| 332 |
+
className={`mb-6 text-center font-semibold ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${selectedElement === 'rightTitle' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 333 |
+
style={{
|
| 334 |
+
fontFamily: getFieldFormatting('rightTitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('rightTitle').fontFamily})`,
|
| 335 |
+
fontSize: `${getFieldFormatting('rightTitle').fontSize}px`,
|
| 336 |
+
color: getFieldFormatting('rightTitle').color || 'inherit',
|
| 337 |
+
fontWeight: getFieldFormatting('rightTitle').fontWeight,
|
| 338 |
+
fontStyle: getFieldFormatting('rightTitle').fontStyle,
|
| 339 |
+
textDecoration: getFieldFormatting('rightTitle').textDecoration
|
| 340 |
+
}}
|
| 341 |
+
onClick={() => handleFieldClick('rightTitle')}
|
| 342 |
+
>
|
| 343 |
+
{rightTitle}
|
| 344 |
+
</h3>
|
| 345 |
+
)}
|
| 346 |
+
|
| 347 |
+
{/* Right Content */}
|
| 348 |
+
<ul className="space-y-3">
|
| 349 |
+
{rightContent.map((item, index) => (
|
| 350 |
+
<li key={index} className="flex items-start">
|
| 351 |
+
<span className="text-blue-400 mr-3 mt-1">✓</span>
|
| 352 |
+
{editingField?.field === 'content' && editingField.index === index && editingField.side === 'right' ? (
|
| 353 |
+
<input
|
| 354 |
+
type="text"
|
| 355 |
+
value={tempRightContent[index] || ''}
|
| 356 |
+
onChange={(e) => updateTempContent('right', index, e.target.value)}
|
| 357 |
+
onBlur={() => handleFieldBlur('content', index, 'right')}
|
| 358 |
+
onKeyDown={(e) => handleKeyDown(e, 'content', index, 'right')}
|
| 359 |
+
className="flex-1 bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70"
|
| 360 |
+
placeholder="Feature"
|
| 361 |
+
autoFocus
|
| 362 |
+
/>
|
| 363 |
+
) : (
|
| 364 |
+
<span
|
| 365 |
+
className={`flex-1 ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''}`}
|
| 366 |
+
onClick={() => handleFieldClick('content', index, 'right')}
|
| 367 |
+
>
|
| 368 |
+
{item}
|
| 369 |
+
</span>
|
| 370 |
+
)}
|
| 371 |
+
</li>
|
| 372 |
+
))}
|
| 373 |
+
</ul>
|
| 374 |
+
</div>
|
| 375 |
+
</div>
|
| 376 |
+
</div>
|
| 377 |
+
</div>
|
| 378 |
+
);
|
| 379 |
+
}
|
components/slides/ContentImageSlide.tsx
CHANGED
|
@@ -4,30 +4,107 @@ export interface ContentImageSlideProps {
|
|
| 4 |
title: string;
|
| 5 |
body: Array<{ heading?: string; text: string }>;
|
| 6 |
imageUrl?: string;
|
| 7 |
-
theme?: 'dark' | 'light';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
return (
|
| 13 |
-
<div className={`w-full h-full ${
|
| 14 |
-
|
| 15 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
<div className="space-y-4">
|
| 17 |
{body?.map((b, i) => (
|
| 18 |
<div key={i}>
|
| 19 |
-
{b.heading && <div className="font-semibold">{b.heading}</div>}
|
| 20 |
-
<p className="opacity-90 leading-relaxed">{b.text}</p>
|
| 21 |
</div>
|
| 22 |
))}
|
| 23 |
</div>
|
| 24 |
</div>
|
| 25 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 26 |
{imageUrl ? (
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
) : (
|
| 30 |
-
<div className=
|
|
|
|
|
|
|
| 31 |
)}
|
| 32 |
</div>
|
| 33 |
</div>
|
|
|
|
| 4 |
title: string;
|
| 5 |
body: Array<{ heading?: string; text: string }>;
|
| 6 |
imageUrl?: string;
|
| 7 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'sunset' | 'midnight';
|
| 8 |
+
slideId?: string;
|
| 9 |
+
onFieldUpdate?: (slideId: string, field: string, value: string, index?: number) => void;
|
| 10 |
+
isEditable?: boolean;
|
| 11 |
+
onImageEdit?: () => void;
|
| 12 |
}
|
| 13 |
|
| 14 |
+
const getThemeClasses = (theme: string) => {
|
| 15 |
+
switch (theme) {
|
| 16 |
+
case 'light':
|
| 17 |
+
return 'text-slate-900 bg-white';
|
| 18 |
+
case 'blue':
|
| 19 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 20 |
+
case 'purple':
|
| 21 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 22 |
+
case 'green':
|
| 23 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 24 |
+
case 'orange':
|
| 25 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 26 |
+
case 'teal':
|
| 27 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 28 |
+
case 'pink':
|
| 29 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 30 |
+
case 'indigo':
|
| 31 |
+
return 'text-white bg-[#0b0c14]';
|
| 32 |
+
case 'midnight':
|
| 33 |
+
return 'text-white bg-[#0b0c14]';
|
| 34 |
+
case 'sunset':
|
| 35 |
+
return 'text-white bg-[#220b03]';
|
| 36 |
+
case 'amber':
|
| 37 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 38 |
+
case 'emerald':
|
| 39 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 40 |
+
case 'slate':
|
| 41 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 42 |
+
default: // dark
|
| 43 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
export default function ContentImageSlide({ title, body, imageUrl, theme = 'dark', slideId, onFieldUpdate, isEditable = false, onImageEdit }: ContentImageSlideProps) {
|
| 48 |
+
const themeClasses = getThemeClasses(theme);
|
| 49 |
+
|
| 50 |
return (
|
| 51 |
+
<div className={`w-full h-full ${themeClasses} p-10 grid grid-cols-1 md:grid-cols-2 gap-8 items-center relative overflow-hidden`}>
|
| 52 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 53 |
+
<div
|
| 54 |
+
aria-hidden
|
| 55 |
+
className="absolute inset-0"
|
| 56 |
+
style={{
|
| 57 |
+
background:
|
| 58 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%)',
|
| 59 |
+
}}
|
| 60 |
+
/>
|
| 61 |
+
)}
|
| 62 |
+
{theme === 'sunset' && (
|
| 63 |
+
<div
|
| 64 |
+
aria-hidden
|
| 65 |
+
className="absolute inset-0"
|
| 66 |
+
style={{
|
| 67 |
+
background:
|
| 68 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%)',
|
| 69 |
+
}}
|
| 70 |
+
/>
|
| 71 |
+
)}
|
| 72 |
+
{/* Background pattern */}
|
| 73 |
+
<div className="absolute inset-0 opacity-5">
|
| 74 |
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,_white_1px,_transparent_0)] bg-[length:20px_20px]" />
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<div className="relative z-10">
|
| 78 |
+
<h2 className="text-3xl font-semibold mb-4 drop-shadow-lg">{title}</h2>
|
| 79 |
<div className="space-y-4">
|
| 80 |
{body?.map((b, i) => (
|
| 81 |
<div key={i}>
|
| 82 |
+
{b.heading && <div className="font-semibold drop-shadow-md">{b.heading}</div>}
|
| 83 |
+
<p className="opacity-90 leading-relaxed drop-shadow-sm">{b.text}</p>
|
| 84 |
</div>
|
| 85 |
))}
|
| 86 |
</div>
|
| 87 |
</div>
|
| 88 |
+
<div
|
| 89 |
+
className={`w-full h-72 md:h-full rounded-xl overflow-hidden border border-white/20 shadow-2xl relative z-10 ${isEditable ? 'cursor-pointer group' : ''}`}
|
| 90 |
+
onClick={isEditable ? onImageEdit : undefined}
|
| 91 |
+
>
|
| 92 |
{imageUrl ? (
|
| 93 |
+
<>
|
| 94 |
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
| 95 |
+
<img src={imageUrl} alt="" className="w-full h-full object-cover" />
|
| 96 |
+
{isEditable && (
|
| 97 |
+
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
| 98 |
+
<div className="bg-white text-black px-3 py-1 rounded text-sm font-medium">
|
| 99 |
+
Click to change image
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
)}
|
| 103 |
+
</>
|
| 104 |
) : (
|
| 105 |
+
<div className={`w-full h-full grid place-items-center opacity-70 bg-black/20 ${isEditable ? 'hover:bg-black/30 transition-colors' : ''}`}>
|
| 106 |
+
{isEditable ? 'Click to add image' : 'No image'}
|
| 107 |
+
</div>
|
| 108 |
)}
|
| 109 |
</div>
|
| 110 |
</div>
|
components/slides/HeroSlide.tsx
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface HeroSlideProps {
|
| 4 |
+
title: string;
|
| 5 |
+
subtitle?: string;
|
| 6 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 7 |
+
slideId?: string;
|
| 8 |
+
onFieldUpdate?: (slideId: string, field: string, value: string) => void;
|
| 9 |
+
isEditable?: boolean;
|
| 10 |
+
onTextSelect?: (element: any) => void;
|
| 11 |
+
selectedFont?: string;
|
| 12 |
+
selectedFontSize?: number;
|
| 13 |
+
selectedColor?: string;
|
| 14 |
+
formatting?: Record<string, Record<string, any>>;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const getThemeClasses = (theme: string) => {
|
| 18 |
+
switch (theme) {
|
| 19 |
+
case 'light':
|
| 20 |
+
return 'text-slate-900 bg-white';
|
| 21 |
+
case 'blue':
|
| 22 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 23 |
+
case 'purple':
|
| 24 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 25 |
+
case 'green':
|
| 26 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 27 |
+
case 'orange':
|
| 28 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 29 |
+
case 'teal':
|
| 30 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 31 |
+
case 'pink':
|
| 32 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 33 |
+
case 'indigo':
|
| 34 |
+
return 'text-white bg-[#0b0c14]';
|
| 35 |
+
case 'midnight':
|
| 36 |
+
return 'text-white bg-[#0b0c14]';
|
| 37 |
+
case 'sunset':
|
| 38 |
+
return 'text-white bg-[#220b03]';
|
| 39 |
+
case 'amber':
|
| 40 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 41 |
+
case 'emerald':
|
| 42 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 43 |
+
case 'slate':
|
| 44 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 45 |
+
default: // dark
|
| 46 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
export default function HeroSlide({
|
| 51 |
+
title,
|
| 52 |
+
subtitle,
|
| 53 |
+
theme = 'dark',
|
| 54 |
+
slideId,
|
| 55 |
+
onFieldUpdate,
|
| 56 |
+
isEditable = false,
|
| 57 |
+
onTextSelect,
|
| 58 |
+
selectedFont = 'serif',
|
| 59 |
+
selectedFontSize = 72,
|
| 60 |
+
selectedColor,
|
| 61 |
+
formatting = {}
|
| 62 |
+
}: HeroSlideProps) {
|
| 63 |
+
const [editingField, setEditingField] = useState<string | null>(null);
|
| 64 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 65 |
+
const [tempSubtitle, setTempSubtitle] = useState(subtitle || '');
|
| 66 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 67 |
+
|
| 68 |
+
const themeClasses = getThemeClasses(theme);
|
| 69 |
+
|
| 70 |
+
const getFieldFormatting = (field: string) => {
|
| 71 |
+
const fieldFormatting = formatting[field] || {};
|
| 72 |
+
return {
|
| 73 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 74 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'title' ? 72 : 24)),
|
| 75 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 76 |
+
fontWeight: fieldFormatting.bold ? 'bold' : (field === 'title' ? '800' : 'normal'),
|
| 77 |
+
fontStyle: fieldFormatting.italic ? 'italic' : 'normal',
|
| 78 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 79 |
+
};
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const handleFieldClick = (field: string) => {
|
| 83 |
+
if (!isEditable) return;
|
| 84 |
+
setEditingField(field);
|
| 85 |
+
setSelectedElement(field);
|
| 86 |
+
if (field === 'title') setTempTitle(title);
|
| 87 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 88 |
+
|
| 89 |
+
if (onTextSelect) {
|
| 90 |
+
onTextSelect({
|
| 91 |
+
field,
|
| 92 |
+
slideId,
|
| 93 |
+
currentFont: selectedFont,
|
| 94 |
+
currentFontSize: field === 'title' ? 72 : 24,
|
| 95 |
+
currentColor: selectedColor
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
const handleFieldBlur = (field: string) => {
|
| 101 |
+
if (!slideId || !onFieldUpdate) return;
|
| 102 |
+
|
| 103 |
+
if (field === 'title' && tempTitle !== title) {
|
| 104 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 105 |
+
}
|
| 106 |
+
if (field === 'subtitle' && tempSubtitle !== subtitle) {
|
| 107 |
+
onFieldUpdate(slideId, 'subtitle', tempSubtitle);
|
| 108 |
+
}
|
| 109 |
+
setEditingField(null);
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
| 113 |
+
if (e.key === 'Enter') {
|
| 114 |
+
e.preventDefault();
|
| 115 |
+
handleFieldBlur(field);
|
| 116 |
+
}
|
| 117 |
+
if (e.key === 'Escape') {
|
| 118 |
+
setEditingField(null);
|
| 119 |
+
if (field === 'title') setTempTitle(title);
|
| 120 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 121 |
+
}
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
return (
|
| 125 |
+
<div className={`w-full h-full flex flex-col items-center justify-center ${themeClasses} relative overflow-hidden`}>
|
| 126 |
+
{/* Special backgrounds for themed slides */}
|
| 127 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 128 |
+
<>
|
| 129 |
+
<div
|
| 130 |
+
aria-hidden
|
| 131 |
+
className="absolute inset-0 -z-10"
|
| 132 |
+
style={{
|
| 133 |
+
background:
|
| 134 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 135 |
+
}}
|
| 136 |
+
/>
|
| 137 |
+
<div
|
| 138 |
+
aria-hidden
|
| 139 |
+
className="absolute inset-0 -z-10"
|
| 140 |
+
style={{
|
| 141 |
+
backgroundImage:
|
| 142 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 143 |
+
backgroundSize: '160px 160px',
|
| 144 |
+
opacity: 0.25,
|
| 145 |
+
}}
|
| 146 |
+
/>
|
| 147 |
+
</>
|
| 148 |
+
)}
|
| 149 |
+
{theme === 'sunset' && (
|
| 150 |
+
<div
|
| 151 |
+
aria-hidden
|
| 152 |
+
className="absolute inset-0 -z-10"
|
| 153 |
+
style={{
|
| 154 |
+
background:
|
| 155 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 156 |
+
}}
|
| 157 |
+
/>
|
| 158 |
+
)}
|
| 159 |
+
|
| 160 |
+
{/* Minimal grid pattern */}
|
| 161 |
+
<div className="absolute inset-0 opacity-5">
|
| 162 |
+
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[length:60px_60px]" />
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<div className="relative z-10 text-center max-w-6xl px-8">
|
| 166 |
+
{/* Hero Title - Extra large and bold */}
|
| 167 |
+
{editingField === 'title' ? (
|
| 168 |
+
<input
|
| 169 |
+
type="text"
|
| 170 |
+
value={tempTitle}
|
| 171 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 172 |
+
onBlur={() => handleFieldBlur('title')}
|
| 173 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 174 |
+
className="w-full mb-8 text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 175 |
+
style={{
|
| 176 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 177 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 178 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 179 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 180 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 181 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 182 |
+
}}
|
| 183 |
+
placeholder="Enter hero title"
|
| 184 |
+
autoFocus
|
| 185 |
+
/>
|
| 186 |
+
) : (
|
| 187 |
+
<h1
|
| 188 |
+
className={`mb-8 text-center leading-tight tracking-tight ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${selectedElement === 'title' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 189 |
+
style={{
|
| 190 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 191 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 192 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 193 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 194 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 195 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 196 |
+
}}
|
| 197 |
+
onClick={() => handleFieldClick('title')}
|
| 198 |
+
>
|
| 199 |
+
{title}
|
| 200 |
+
</h1>
|
| 201 |
+
)}
|
| 202 |
+
|
| 203 |
+
{/* Hero Subtitle */}
|
| 204 |
+
{(subtitle || isEditable) && (
|
| 205 |
+
editingField === 'subtitle' ? (
|
| 206 |
+
<input
|
| 207 |
+
type="text"
|
| 208 |
+
value={tempSubtitle}
|
| 209 |
+
onChange={(e) => setTempSubtitle(e.target.value)}
|
| 210 |
+
onBlur={() => handleFieldBlur('subtitle')}
|
| 211 |
+
onKeyDown={(e) => handleKeyDown(e, 'subtitle')}
|
| 212 |
+
className="w-full opacity-80 text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 213 |
+
style={{
|
| 214 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 215 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 216 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 217 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 218 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 219 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 220 |
+
}}
|
| 221 |
+
placeholder="Enter subtitle"
|
| 222 |
+
autoFocus
|
| 223 |
+
/>
|
| 224 |
+
) : (
|
| 225 |
+
<p
|
| 226 |
+
className={`opacity-80 max-w-4xl mx-auto text-center leading-relaxed ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${!subtitle && isEditable ? 'text-white/50' : ''} ${selectedElement === 'subtitle' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 227 |
+
style={{
|
| 228 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 229 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 230 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 231 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 232 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 233 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 234 |
+
}}
|
| 235 |
+
onClick={() => handleFieldClick('subtitle')}
|
| 236 |
+
>
|
| 237 |
+
{subtitle || (isEditable ? 'Click to add subtitle' : '')}
|
| 238 |
+
</p>
|
| 239 |
+
)
|
| 240 |
+
)}
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
);
|
| 244 |
+
}
|
components/slides/ImageOnlySlide.tsx
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface ImageOnlySlideProps {
|
| 4 |
+
title: string;
|
| 5 |
+
subtitle?: string;
|
| 6 |
+
imageUrl?: string;
|
| 7 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 8 |
+
slideId?: string;
|
| 9 |
+
onFieldUpdate?: (slideId: string, field: string, value: string) => void;
|
| 10 |
+
isEditable?: boolean;
|
| 11 |
+
onImageEdit?: () => void;
|
| 12 |
+
onTextSelect?: (element: any) => void;
|
| 13 |
+
selectedFont?: string;
|
| 14 |
+
selectedFontSize?: number;
|
| 15 |
+
selectedColor?: string;
|
| 16 |
+
formatting?: Record<string, Record<string, any>>;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const getThemeClasses = (theme: string) => {
|
| 20 |
+
switch (theme) {
|
| 21 |
+
case 'light':
|
| 22 |
+
return 'text-slate-900 bg-white';
|
| 23 |
+
case 'blue':
|
| 24 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 25 |
+
case 'purple':
|
| 26 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 27 |
+
case 'green':
|
| 28 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 29 |
+
case 'orange':
|
| 30 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 31 |
+
case 'teal':
|
| 32 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 33 |
+
case 'pink':
|
| 34 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 35 |
+
case 'indigo':
|
| 36 |
+
return 'text-white bg-[#0b0c14]';
|
| 37 |
+
case 'midnight':
|
| 38 |
+
return 'text-white bg-[#0b0c14]';
|
| 39 |
+
case 'sunset':
|
| 40 |
+
return 'text-white bg-[#220b03]';
|
| 41 |
+
case 'amber':
|
| 42 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 43 |
+
case 'emerald':
|
| 44 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 45 |
+
case 'slate':
|
| 46 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 47 |
+
default: // dark
|
| 48 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
export default function ImageOnlySlide({
|
| 53 |
+
title,
|
| 54 |
+
subtitle,
|
| 55 |
+
imageUrl,
|
| 56 |
+
theme = 'dark',
|
| 57 |
+
slideId,
|
| 58 |
+
onFieldUpdate,
|
| 59 |
+
isEditable = false,
|
| 60 |
+
onImageEdit,
|
| 61 |
+
onTextSelect,
|
| 62 |
+
selectedFont = 'serif',
|
| 63 |
+
selectedFontSize = 24,
|
| 64 |
+
selectedColor,
|
| 65 |
+
formatting = {}
|
| 66 |
+
}: ImageOnlySlideProps) {
|
| 67 |
+
const [editingField, setEditingField] = useState<string | null>(null);
|
| 68 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 69 |
+
const [tempSubtitle, setTempSubtitle] = useState(subtitle || '');
|
| 70 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 71 |
+
|
| 72 |
+
const themeClasses = getThemeClasses(theme);
|
| 73 |
+
|
| 74 |
+
// Get formatting for a specific field
|
| 75 |
+
const getFieldFormatting = (field: string) => {
|
| 76 |
+
const fieldFormatting = formatting[field] || {};
|
| 77 |
+
return {
|
| 78 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 79 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'title' ? 24 : 16)),
|
| 80 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 81 |
+
fontWeight: fieldFormatting.bold ? 'bold' : 'normal',
|
| 82 |
+
fontStyle: fieldFormatting.italic ? 'italic' : 'normal',
|
| 83 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 84 |
+
};
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
const handleFieldClick = (field: string) => {
|
| 88 |
+
if (!isEditable) return;
|
| 89 |
+
setEditingField(field);
|
| 90 |
+
setSelectedElement(field);
|
| 91 |
+
if (field === 'title') setTempTitle(title);
|
| 92 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 93 |
+
|
| 94 |
+
// Notify parent about text selection
|
| 95 |
+
if (onTextSelect) {
|
| 96 |
+
onTextSelect({
|
| 97 |
+
field,
|
| 98 |
+
slideId,
|
| 99 |
+
currentFont: selectedFont,
|
| 100 |
+
currentFontSize: field === 'title' ? 24 : 16,
|
| 101 |
+
currentColor: selectedColor
|
| 102 |
+
});
|
| 103 |
+
}
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
const handleFieldBlur = (field: string) => {
|
| 107 |
+
if (!slideId || !onFieldUpdate) return;
|
| 108 |
+
|
| 109 |
+
if (field === 'title' && tempTitle !== title) {
|
| 110 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 111 |
+
}
|
| 112 |
+
if (field === 'subtitle' && tempSubtitle !== subtitle) {
|
| 113 |
+
onFieldUpdate(slideId, 'subtitle', tempSubtitle);
|
| 114 |
+
}
|
| 115 |
+
setEditingField(null);
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
| 119 |
+
if (e.key === 'Enter') {
|
| 120 |
+
e.preventDefault();
|
| 121 |
+
handleFieldBlur(field);
|
| 122 |
+
}
|
| 123 |
+
if (e.key === 'Escape') {
|
| 124 |
+
setEditingField(null);
|
| 125 |
+
if (field === 'title') setTempTitle(title);
|
| 126 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 127 |
+
}
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
return (
|
| 131 |
+
<div className={`w-full h-full ${themeClasses} relative overflow-hidden`}>
|
| 132 |
+
{/* Special backgrounds for themed slides */}
|
| 133 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 134 |
+
<>
|
| 135 |
+
<div
|
| 136 |
+
aria-hidden
|
| 137 |
+
className="absolute inset-0 -z-10"
|
| 138 |
+
style={{
|
| 139 |
+
background:
|
| 140 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 141 |
+
}}
|
| 142 |
+
/>
|
| 143 |
+
<div
|
| 144 |
+
aria-hidden
|
| 145 |
+
className="absolute inset-0 -z-10"
|
| 146 |
+
style={{
|
| 147 |
+
backgroundImage:
|
| 148 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 149 |
+
backgroundSize: '160px 160px',
|
| 150 |
+
opacity: 0.25,
|
| 151 |
+
}}
|
| 152 |
+
/>
|
| 153 |
+
</>
|
| 154 |
+
)}
|
| 155 |
+
{theme === 'sunset' && (
|
| 156 |
+
<div
|
| 157 |
+
aria-hidden
|
| 158 |
+
className="absolute inset-0 -z-10"
|
| 159 |
+
style={{
|
| 160 |
+
background:
|
| 161 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 162 |
+
}}
|
| 163 |
+
/>
|
| 164 |
+
)}
|
| 165 |
+
|
| 166 |
+
{/* Main Image */}
|
| 167 |
+
<div className="w-full h-full flex flex-col">
|
| 168 |
+
<div
|
| 169 |
+
className={`flex-1 ${isEditable ? 'cursor-pointer group' : ''}`}
|
| 170 |
+
onClick={isEditable ? onImageEdit : undefined}
|
| 171 |
+
>
|
| 172 |
+
{imageUrl ? (
|
| 173 |
+
<div className="relative w-full h-full">
|
| 174 |
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
| 175 |
+
<img src={imageUrl} alt={title} className="w-full h-full object-cover" />
|
| 176 |
+
{isEditable && (
|
| 177 |
+
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
| 178 |
+
<div className="bg-white text-black px-4 py-2 rounded text-sm font-medium">
|
| 179 |
+
Click to change image
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
)}
|
| 183 |
+
</div>
|
| 184 |
+
) : (
|
| 185 |
+
<div className={`w-full h-full flex items-center justify-center bg-black/20 ${isEditable ? 'hover:bg-black/30 transition-colors' : ''}`}>
|
| 186 |
+
<div className="text-center">
|
| 187 |
+
<div className="text-6xl mb-4 opacity-50">🖼️</div>
|
| 188 |
+
<div className="text-white/70">
|
| 189 |
+
{isEditable ? 'Click to add image' : 'No image'}
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
)}
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
{/* Caption Area */}
|
| 197 |
+
<div className="bg-black/30 backdrop-blur-sm p-6 text-center">
|
| 198 |
+
{/* Title */}
|
| 199 |
+
{editingField === 'title' ? (
|
| 200 |
+
<input
|
| 201 |
+
type="text"
|
| 202 |
+
value={tempTitle}
|
| 203 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 204 |
+
onBlur={() => handleFieldBlur('title')}
|
| 205 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 206 |
+
className="w-full mb-2 text-center drop-shadow-lg bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70"
|
| 207 |
+
style={{
|
| 208 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 209 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 210 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 211 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 212 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 213 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 214 |
+
}}
|
| 215 |
+
placeholder="Enter title"
|
| 216 |
+
autoFocus
|
| 217 |
+
/>
|
| 218 |
+
) : (
|
| 219 |
+
<h2
|
| 220 |
+
className={`mb-2 text-center drop-shadow-lg transition-all font-semibold ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''} ${selectedElement === 'title' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 221 |
+
style={{
|
| 222 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 223 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 224 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 225 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 226 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 227 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 228 |
+
}}
|
| 229 |
+
onClick={() => handleFieldClick('title')}
|
| 230 |
+
>
|
| 231 |
+
{title}
|
| 232 |
+
</h2>
|
| 233 |
+
)}
|
| 234 |
+
|
| 235 |
+
{/* Subtitle/Caption */}
|
| 236 |
+
{(subtitle || isEditable) && (
|
| 237 |
+
editingField === 'subtitle' ? (
|
| 238 |
+
<input
|
| 239 |
+
type="text"
|
| 240 |
+
value={tempSubtitle}
|
| 241 |
+
onChange={(e) => setTempSubtitle(e.target.value)}
|
| 242 |
+
onBlur={() => handleFieldBlur('subtitle')}
|
| 243 |
+
onKeyDown={(e) => handleKeyDown(e, 'subtitle')}
|
| 244 |
+
className="w-full text-center drop-shadow-md bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70"
|
| 245 |
+
style={{
|
| 246 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 247 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 248 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 249 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 250 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 251 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 252 |
+
}}
|
| 253 |
+
placeholder="Enter caption"
|
| 254 |
+
autoFocus
|
| 255 |
+
/>
|
| 256 |
+
) : (
|
| 257 |
+
<p
|
| 258 |
+
className={`text-center drop-shadow-md opacity-90 transition-all ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''} ${selectedElement === 'subtitle' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''} ${!subtitle && isEditable ? 'text-white/50' : ''}`}
|
| 259 |
+
style={{
|
| 260 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 261 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 262 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 263 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 264 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 265 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 266 |
+
}}
|
| 267 |
+
onClick={() => handleFieldClick('subtitle')}
|
| 268 |
+
>
|
| 269 |
+
{subtitle || (isEditable ? 'Click to add caption' : '')}
|
| 270 |
+
</p>
|
| 271 |
+
)
|
| 272 |
+
)}
|
| 273 |
+
</div>
|
| 274 |
+
</div>
|
| 275 |
+
</div>
|
| 276 |
+
);
|
| 277 |
+
}
|
components/slides/QuoteSlide.tsx
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface QuoteSlideProps {
|
| 4 |
+
quote: string;
|
| 5 |
+
author?: string;
|
| 6 |
+
company?: string;
|
| 7 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 8 |
+
slideId?: string;
|
| 9 |
+
onFieldUpdate?: (slideId: string, field: string, value: string) => void;
|
| 10 |
+
isEditable?: boolean;
|
| 11 |
+
onTextSelect?: (element: any) => void;
|
| 12 |
+
selectedFont?: string;
|
| 13 |
+
selectedFontSize?: number;
|
| 14 |
+
selectedColor?: string;
|
| 15 |
+
formatting?: Record<string, Record<string, any>>;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const getThemeClasses = (theme: string) => {
|
| 19 |
+
switch (theme) {
|
| 20 |
+
case 'light':
|
| 21 |
+
return 'text-slate-900 bg-white';
|
| 22 |
+
case 'blue':
|
| 23 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 24 |
+
case 'purple':
|
| 25 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 26 |
+
case 'green':
|
| 27 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 28 |
+
case 'orange':
|
| 29 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 30 |
+
case 'teal':
|
| 31 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 32 |
+
case 'pink':
|
| 33 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 34 |
+
case 'indigo':
|
| 35 |
+
return 'text-white bg-[#0b0c14]';
|
| 36 |
+
case 'midnight':
|
| 37 |
+
return 'text-white bg-[#0b0c14]';
|
| 38 |
+
case 'sunset':
|
| 39 |
+
return 'text-white bg-[#220b03]';
|
| 40 |
+
case 'amber':
|
| 41 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 42 |
+
case 'emerald':
|
| 43 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 44 |
+
case 'slate':
|
| 45 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 46 |
+
default: // dark
|
| 47 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
export default function QuoteSlide({
|
| 52 |
+
quote,
|
| 53 |
+
author,
|
| 54 |
+
company,
|
| 55 |
+
theme = 'dark',
|
| 56 |
+
slideId,
|
| 57 |
+
onFieldUpdate,
|
| 58 |
+
isEditable = false,
|
| 59 |
+
onTextSelect,
|
| 60 |
+
selectedFont = 'serif',
|
| 61 |
+
selectedFontSize = 32,
|
| 62 |
+
selectedColor,
|
| 63 |
+
formatting = {}
|
| 64 |
+
}: QuoteSlideProps) {
|
| 65 |
+
const [editingField, setEditingField] = useState<string | null>(null);
|
| 66 |
+
const [tempQuote, setTempQuote] = useState(quote);
|
| 67 |
+
const [tempAuthor, setTempAuthor] = useState(author || '');
|
| 68 |
+
const [tempCompany, setTempCompany] = useState(company || '');
|
| 69 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 70 |
+
|
| 71 |
+
const themeClasses = getThemeClasses(theme);
|
| 72 |
+
|
| 73 |
+
const getFieldFormatting = (field: string) => {
|
| 74 |
+
const fieldFormatting = formatting[field] || {};
|
| 75 |
+
return {
|
| 76 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 77 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'quote' ? 32 : 18)),
|
| 78 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 79 |
+
fontWeight: fieldFormatting.bold ? 'bold' : 'normal',
|
| 80 |
+
fontStyle: fieldFormatting.italic ? 'italic' : (field === 'quote' ? 'italic' : 'normal'),
|
| 81 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 82 |
+
};
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const handleFieldClick = (field: string) => {
|
| 86 |
+
if (!isEditable) return;
|
| 87 |
+
setEditingField(field);
|
| 88 |
+
setSelectedElement(field);
|
| 89 |
+
if (field === 'quote') setTempQuote(quote);
|
| 90 |
+
if (field === 'author') setTempAuthor(author || '');
|
| 91 |
+
if (field === 'company') setTempCompany(company || '');
|
| 92 |
+
|
| 93 |
+
if (onTextSelect) {
|
| 94 |
+
onTextSelect({
|
| 95 |
+
field,
|
| 96 |
+
slideId,
|
| 97 |
+
currentFont: selectedFont,
|
| 98 |
+
currentFontSize: field === 'quote' ? 32 : 18,
|
| 99 |
+
currentColor: selectedColor
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
const handleFieldBlur = (field: string) => {
|
| 105 |
+
if (!slideId || !onFieldUpdate) return;
|
| 106 |
+
|
| 107 |
+
if (field === 'quote' && tempQuote !== quote) {
|
| 108 |
+
onFieldUpdate(slideId, 'quote', tempQuote);
|
| 109 |
+
}
|
| 110 |
+
if (field === 'author' && tempAuthor !== author) {
|
| 111 |
+
onFieldUpdate(slideId, 'author', tempAuthor);
|
| 112 |
+
}
|
| 113 |
+
if (field === 'company' && tempCompany !== company) {
|
| 114 |
+
onFieldUpdate(slideId, 'company', tempCompany);
|
| 115 |
+
}
|
| 116 |
+
setEditingField(null);
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
| 120 |
+
if (e.key === 'Enter' && field !== 'quote') {
|
| 121 |
+
e.preventDefault();
|
| 122 |
+
handleFieldBlur(field);
|
| 123 |
+
}
|
| 124 |
+
if (e.key === 'Escape') {
|
| 125 |
+
setEditingField(null);
|
| 126 |
+
if (field === 'quote') setTempQuote(quote);
|
| 127 |
+
if (field === 'author') setTempAuthor(author || '');
|
| 128 |
+
if (field === 'company') setTempCompany(company || '');
|
| 129 |
+
}
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
return (
|
| 133 |
+
<div className={`w-full h-full flex flex-col items-center justify-center ${themeClasses} p-12 relative overflow-hidden`}>
|
| 134 |
+
{/* Special backgrounds */}
|
| 135 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 136 |
+
<>
|
| 137 |
+
<div
|
| 138 |
+
aria-hidden
|
| 139 |
+
className="absolute inset-0 -z-10"
|
| 140 |
+
style={{
|
| 141 |
+
background:
|
| 142 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 143 |
+
}}
|
| 144 |
+
/>
|
| 145 |
+
<div
|
| 146 |
+
aria-hidden
|
| 147 |
+
className="absolute inset-0 -z-10"
|
| 148 |
+
style={{
|
| 149 |
+
backgroundImage:
|
| 150 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 151 |
+
backgroundSize: '160px 160px',
|
| 152 |
+
opacity: 0.25,
|
| 153 |
+
}}
|
| 154 |
+
/>
|
| 155 |
+
</>
|
| 156 |
+
)}
|
| 157 |
+
{theme === 'sunset' && (
|
| 158 |
+
<div
|
| 159 |
+
aria-hidden
|
| 160 |
+
className="absolute inset-0 -z-10"
|
| 161 |
+
style={{
|
| 162 |
+
background:
|
| 163 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 164 |
+
}}
|
| 165 |
+
/>
|
| 166 |
+
)}
|
| 167 |
+
|
| 168 |
+
<div className="relative z-10 text-center max-w-4xl">
|
| 169 |
+
{/* Quote Mark */}
|
| 170 |
+
<div className="text-8xl opacity-20 mb-8">"</div>
|
| 171 |
+
|
| 172 |
+
{/* Quote Text */}
|
| 173 |
+
{editingField === 'quote' ? (
|
| 174 |
+
<textarea
|
| 175 |
+
value={tempQuote}
|
| 176 |
+
onChange={(e) => setTempQuote(e.target.value)}
|
| 177 |
+
onBlur={() => handleFieldBlur('quote')}
|
| 178 |
+
onKeyDown={(e) => handleKeyDown(e, 'quote')}
|
| 179 |
+
className="w-full h-32 mb-12 text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70 resize-none"
|
| 180 |
+
style={{
|
| 181 |
+
fontFamily: getFieldFormatting('quote').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('quote').fontFamily})`,
|
| 182 |
+
fontSize: `${getFieldFormatting('quote').fontSize}px`,
|
| 183 |
+
color: getFieldFormatting('quote').color || 'inherit',
|
| 184 |
+
fontWeight: getFieldFormatting('quote').fontWeight,
|
| 185 |
+
fontStyle: getFieldFormatting('quote').fontStyle,
|
| 186 |
+
textDecoration: getFieldFormatting('quote').textDecoration
|
| 187 |
+
}}
|
| 188 |
+
placeholder="Enter quote"
|
| 189 |
+
autoFocus
|
| 190 |
+
/>
|
| 191 |
+
) : (
|
| 192 |
+
<blockquote
|
| 193 |
+
className={`mb-12 leading-relaxed ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-4' : ''} ${selectedElement === 'quote' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 194 |
+
style={{
|
| 195 |
+
fontFamily: getFieldFormatting('quote').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('quote').fontFamily})`,
|
| 196 |
+
fontSize: `${getFieldFormatting('quote').fontSize}px`,
|
| 197 |
+
color: getFieldFormatting('quote').color || 'inherit',
|
| 198 |
+
fontWeight: getFieldFormatting('quote').fontWeight,
|
| 199 |
+
fontStyle: getFieldFormatting('quote').fontStyle,
|
| 200 |
+
textDecoration: getFieldFormatting('quote').textDecoration
|
| 201 |
+
}}
|
| 202 |
+
onClick={() => handleFieldClick('quote')}
|
| 203 |
+
>
|
| 204 |
+
{quote}
|
| 205 |
+
</blockquote>
|
| 206 |
+
)}
|
| 207 |
+
|
| 208 |
+
{/* Attribution */}
|
| 209 |
+
<div className="space-y-2">
|
| 210 |
+
{/* Author */}
|
| 211 |
+
{(author || isEditable) && (
|
| 212 |
+
editingField === 'author' ? (
|
| 213 |
+
<input
|
| 214 |
+
type="text"
|
| 215 |
+
value={tempAuthor}
|
| 216 |
+
onChange={(e) => setTempAuthor(e.target.value)}
|
| 217 |
+
onBlur={() => handleFieldBlur('author')}
|
| 218 |
+
onKeyDown={(e) => handleKeyDown(e, 'author')}
|
| 219 |
+
className="w-full text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 220 |
+
style={{
|
| 221 |
+
fontFamily: getFieldFormatting('author').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('author').fontFamily})`,
|
| 222 |
+
fontSize: `${getFieldFormatting('author').fontSize}px`,
|
| 223 |
+
color: getFieldFormatting('author').color || 'inherit',
|
| 224 |
+
fontWeight: getFieldFormatting('author').fontWeight,
|
| 225 |
+
fontStyle: getFieldFormatting('author').fontStyle,
|
| 226 |
+
textDecoration: getFieldFormatting('author').textDecoration
|
| 227 |
+
}}
|
| 228 |
+
placeholder="Author name"
|
| 229 |
+
autoFocus
|
| 230 |
+
/>
|
| 231 |
+
) : (
|
| 232 |
+
<div
|
| 233 |
+
className={`font-semibold ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${!author && isEditable ? 'text-white/50' : ''} ${selectedElement === 'author' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 234 |
+
style={{
|
| 235 |
+
fontFamily: getFieldFormatting('author').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('author').fontFamily})`,
|
| 236 |
+
fontSize: `${getFieldFormatting('author').fontSize}px`,
|
| 237 |
+
color: getFieldFormatting('author').color || 'inherit',
|
| 238 |
+
fontWeight: getFieldFormatting('author').fontWeight,
|
| 239 |
+
fontStyle: getFieldFormatting('author').fontStyle,
|
| 240 |
+
textDecoration: getFieldFormatting('author').textDecoration
|
| 241 |
+
}}
|
| 242 |
+
onClick={() => handleFieldClick('author')}
|
| 243 |
+
>
|
| 244 |
+
{author || (isEditable ? 'Click to add author' : '')}
|
| 245 |
+
</div>
|
| 246 |
+
)
|
| 247 |
+
)}
|
| 248 |
+
|
| 249 |
+
{/* Company */}
|
| 250 |
+
{(company || isEditable) && (
|
| 251 |
+
editingField === 'company' ? (
|
| 252 |
+
<input
|
| 253 |
+
type="text"
|
| 254 |
+
value={tempCompany}
|
| 255 |
+
onChange={(e) => setTempCompany(e.target.value)}
|
| 256 |
+
onBlur={() => handleFieldBlur('company')}
|
| 257 |
+
onKeyDown={(e) => handleKeyDown(e, 'company')}
|
| 258 |
+
className="w-full text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 259 |
+
style={{
|
| 260 |
+
fontFamily: getFieldFormatting('company').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('company').fontFamily})`,
|
| 261 |
+
fontSize: `${getFieldFormatting('company').fontSize}px`,
|
| 262 |
+
color: getFieldFormatting('company').color || 'inherit',
|
| 263 |
+
fontWeight: getFieldFormatting('company').fontWeight,
|
| 264 |
+
fontStyle: getFieldFormatting('company').fontStyle,
|
| 265 |
+
textDecoration: getFieldFormatting('company').textDecoration
|
| 266 |
+
}}
|
| 267 |
+
placeholder="Company name"
|
| 268 |
+
autoFocus
|
| 269 |
+
/>
|
| 270 |
+
) : (
|
| 271 |
+
<div
|
| 272 |
+
className={`opacity-80 ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${!company && isEditable ? 'text-white/50' : ''} ${selectedElement === 'company' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 273 |
+
style={{
|
| 274 |
+
fontFamily: getFieldFormatting('company').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('company').fontFamily})`,
|
| 275 |
+
fontSize: `${getFieldFormatting('company').fontSize}px`,
|
| 276 |
+
color: getFieldFormatting('company').color || 'inherit',
|
| 277 |
+
fontWeight: getFieldFormatting('company').fontWeight,
|
| 278 |
+
fontStyle: getFieldFormatting('company').fontStyle,
|
| 279 |
+
textDecoration: getFieldFormatting('company').textDecoration
|
| 280 |
+
}}
|
| 281 |
+
onClick={() => handleFieldClick('company')}
|
| 282 |
+
>
|
| 283 |
+
{company || (isEditable ? 'Click to add company' : '')}
|
| 284 |
+
</div>
|
| 285 |
+
)
|
| 286 |
+
)}
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
);
|
| 291 |
+
}
|
components/slides/SectionSlide.tsx
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface SectionSlideProps {
|
| 4 |
+
title: string;
|
| 5 |
+
subtitle?: string;
|
| 6 |
+
sectionNumber?: string;
|
| 7 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 8 |
+
slideId?: string;
|
| 9 |
+
onFieldUpdate?: (slideId: string, field: string, value: string) => void;
|
| 10 |
+
isEditable?: boolean;
|
| 11 |
+
onTextSelect?: (element: any) => void;
|
| 12 |
+
selectedFont?: string;
|
| 13 |
+
selectedFontSize?: number;
|
| 14 |
+
selectedColor?: string;
|
| 15 |
+
formatting?: Record<string, Record<string, any>>;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const getThemeClasses = (theme: string) => {
|
| 19 |
+
switch (theme) {
|
| 20 |
+
case 'light':
|
| 21 |
+
return 'text-slate-900 bg-white';
|
| 22 |
+
case 'blue':
|
| 23 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 24 |
+
case 'purple':
|
| 25 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 26 |
+
case 'green':
|
| 27 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 28 |
+
case 'orange':
|
| 29 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 30 |
+
case 'teal':
|
| 31 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 32 |
+
case 'pink':
|
| 33 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 34 |
+
case 'indigo':
|
| 35 |
+
return 'text-white bg-[#0b0c14]';
|
| 36 |
+
case 'midnight':
|
| 37 |
+
return 'text-white bg-[#0b0c14]';
|
| 38 |
+
case 'sunset':
|
| 39 |
+
return 'text-white bg-[#220b03]';
|
| 40 |
+
case 'amber':
|
| 41 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 42 |
+
case 'emerald':
|
| 43 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 44 |
+
case 'slate':
|
| 45 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 46 |
+
default: // dark
|
| 47 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
export default function SectionSlide({
|
| 52 |
+
title,
|
| 53 |
+
subtitle,
|
| 54 |
+
sectionNumber,
|
| 55 |
+
theme = 'dark',
|
| 56 |
+
slideId,
|
| 57 |
+
onFieldUpdate,
|
| 58 |
+
isEditable = false,
|
| 59 |
+
onTextSelect,
|
| 60 |
+
selectedFont = 'serif',
|
| 61 |
+
selectedFontSize = 48,
|
| 62 |
+
selectedColor,
|
| 63 |
+
formatting = {}
|
| 64 |
+
}: SectionSlideProps) {
|
| 65 |
+
const [editingField, setEditingField] = useState<string | null>(null);
|
| 66 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 67 |
+
const [tempSubtitle, setTempSubtitle] = useState(subtitle || '');
|
| 68 |
+
const [tempSectionNumber, setTempSectionNumber] = useState(sectionNumber || '');
|
| 69 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 70 |
+
|
| 71 |
+
const themeClasses = getThemeClasses(theme);
|
| 72 |
+
|
| 73 |
+
const getFieldFormatting = (field: string) => {
|
| 74 |
+
const fieldFormatting = formatting[field] || {};
|
| 75 |
+
return {
|
| 76 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 77 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'title' ? 48 : field === 'sectionNumber' ? 120 : 24)),
|
| 78 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 79 |
+
fontWeight: fieldFormatting.bold ? 'bold' : (field === 'title' ? '700' : field === 'sectionNumber' ? '900' : 'normal'),
|
| 80 |
+
fontStyle: fieldFormatting.italic ? 'italic' : 'normal',
|
| 81 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 82 |
+
};
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const handleFieldClick = (field: string) => {
|
| 86 |
+
if (!isEditable) return;
|
| 87 |
+
setEditingField(field);
|
| 88 |
+
setSelectedElement(field);
|
| 89 |
+
if (field === 'title') setTempTitle(title);
|
| 90 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 91 |
+
if (field === 'sectionNumber') setTempSectionNumber(sectionNumber || '');
|
| 92 |
+
|
| 93 |
+
if (onTextSelect) {
|
| 94 |
+
onTextSelect({
|
| 95 |
+
field,
|
| 96 |
+
slideId,
|
| 97 |
+
currentFont: selectedFont,
|
| 98 |
+
currentFontSize: field === 'title' ? 48 : field === 'sectionNumber' ? 120 : 24,
|
| 99 |
+
currentColor: selectedColor
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
const handleFieldBlur = (field: string) => {
|
| 105 |
+
if (!slideId || !onFieldUpdate) return;
|
| 106 |
+
|
| 107 |
+
if (field === 'title' && tempTitle !== title) {
|
| 108 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 109 |
+
}
|
| 110 |
+
if (field === 'subtitle' && tempSubtitle !== subtitle) {
|
| 111 |
+
onFieldUpdate(slideId, 'subtitle', tempSubtitle);
|
| 112 |
+
}
|
| 113 |
+
if (field === 'sectionNumber' && tempSectionNumber !== sectionNumber) {
|
| 114 |
+
onFieldUpdate(slideId, 'sectionNumber', tempSectionNumber);
|
| 115 |
+
}
|
| 116 |
+
setEditingField(null);
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
| 120 |
+
if (e.key === 'Enter') {
|
| 121 |
+
e.preventDefault();
|
| 122 |
+
handleFieldBlur(field);
|
| 123 |
+
}
|
| 124 |
+
if (e.key === 'Escape') {
|
| 125 |
+
setEditingField(null);
|
| 126 |
+
if (field === 'title') setTempTitle(title);
|
| 127 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 128 |
+
if (field === 'sectionNumber') setTempSectionNumber(sectionNumber || '');
|
| 129 |
+
}
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
return (
|
| 133 |
+
<div className={`w-full h-full ${themeClasses} relative overflow-hidden`}>
|
| 134 |
+
{/* Special backgrounds */}
|
| 135 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 136 |
+
<>
|
| 137 |
+
<div
|
| 138 |
+
aria-hidden
|
| 139 |
+
className="absolute inset-0 -z-10"
|
| 140 |
+
style={{
|
| 141 |
+
background:
|
| 142 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 143 |
+
}}
|
| 144 |
+
/>
|
| 145 |
+
<div
|
| 146 |
+
aria-hidden
|
| 147 |
+
className="absolute inset-0 -z-10"
|
| 148 |
+
style={{
|
| 149 |
+
backgroundImage:
|
| 150 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 151 |
+
backgroundSize: '160px 160px',
|
| 152 |
+
opacity: 0.25,
|
| 153 |
+
}}
|
| 154 |
+
/>
|
| 155 |
+
</>
|
| 156 |
+
)}
|
| 157 |
+
{theme === 'sunset' && (
|
| 158 |
+
<div
|
| 159 |
+
aria-hidden
|
| 160 |
+
className="absolute inset-0 -z-10"
|
| 161 |
+
style={{
|
| 162 |
+
background:
|
| 163 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 164 |
+
}}
|
| 165 |
+
/>
|
| 166 |
+
)}
|
| 167 |
+
|
| 168 |
+
{/* Decorative elements */}
|
| 169 |
+
<div className="absolute inset-0 opacity-10">
|
| 170 |
+
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-white to-transparent" />
|
| 171 |
+
<div className="absolute bottom-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-white to-transparent" />
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<div className="relative z-10 h-full flex items-center">
|
| 175 |
+
<div className="w-full px-16">
|
| 176 |
+
<div className="grid grid-cols-12 gap-8 items-center">
|
| 177 |
+
{/* Large Section Number */}
|
| 178 |
+
<div className="col-span-4">
|
| 179 |
+
{editingField === 'sectionNumber' ? (
|
| 180 |
+
<input
|
| 181 |
+
type="text"
|
| 182 |
+
value={tempSectionNumber}
|
| 183 |
+
onChange={(e) => setTempSectionNumber(e.target.value)}
|
| 184 |
+
onBlur={() => handleFieldBlur('sectionNumber')}
|
| 185 |
+
onKeyDown={(e) => handleKeyDown(e, 'sectionNumber')}
|
| 186 |
+
className="w-full text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 187 |
+
style={{
|
| 188 |
+
fontFamily: getFieldFormatting('sectionNumber').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('sectionNumber').fontFamily})`,
|
| 189 |
+
fontSize: `${getFieldFormatting('sectionNumber').fontSize}px`,
|
| 190 |
+
color: getFieldFormatting('sectionNumber').color || 'inherit',
|
| 191 |
+
fontWeight: getFieldFormatting('sectionNumber').fontWeight,
|
| 192 |
+
fontStyle: getFieldFormatting('sectionNumber').fontStyle,
|
| 193 |
+
textDecoration: getFieldFormatting('sectionNumber').textDecoration
|
| 194 |
+
}}
|
| 195 |
+
placeholder="01"
|
| 196 |
+
autoFocus
|
| 197 |
+
/>
|
| 198 |
+
) : (
|
| 199 |
+
<div
|
| 200 |
+
className={`text-center opacity-30 leading-none ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-4' : ''} ${selectedElement === 'sectionNumber' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 201 |
+
style={{
|
| 202 |
+
fontFamily: getFieldFormatting('sectionNumber').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('sectionNumber').fontFamily})`,
|
| 203 |
+
fontSize: `${getFieldFormatting('sectionNumber').fontSize}px`,
|
| 204 |
+
color: getFieldFormatting('sectionNumber').color || 'inherit',
|
| 205 |
+
fontWeight: getFieldFormatting('sectionNumber').fontWeight,
|
| 206 |
+
fontStyle: getFieldFormatting('sectionNumber').fontStyle,
|
| 207 |
+
textDecoration: getFieldFormatting('sectionNumber').textDecoration
|
| 208 |
+
}}
|
| 209 |
+
onClick={() => handleFieldClick('sectionNumber')}
|
| 210 |
+
>
|
| 211 |
+
{sectionNumber || (isEditable ? '01' : '')}
|
| 212 |
+
</div>
|
| 213 |
+
)}
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
{/* Section Content */}
|
| 217 |
+
<div className="col-span-8">
|
| 218 |
+
{/* Section Title */}
|
| 219 |
+
{editingField === 'title' ? (
|
| 220 |
+
<input
|
| 221 |
+
type="text"
|
| 222 |
+
value={tempTitle}
|
| 223 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 224 |
+
onBlur={() => handleFieldBlur('title')}
|
| 225 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 226 |
+
className="w-full mb-4 bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 227 |
+
style={{
|
| 228 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 229 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 230 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 231 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 232 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 233 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 234 |
+
}}
|
| 235 |
+
placeholder="Section Title"
|
| 236 |
+
autoFocus
|
| 237 |
+
/>
|
| 238 |
+
) : (
|
| 239 |
+
<h2
|
| 240 |
+
className={`mb-4 leading-tight ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${selectedElement === 'title' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 241 |
+
style={{
|
| 242 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 243 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 244 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 245 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 246 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 247 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 248 |
+
}}
|
| 249 |
+
onClick={() => handleFieldClick('title')}
|
| 250 |
+
>
|
| 251 |
+
{title}
|
| 252 |
+
</h2>
|
| 253 |
+
)}
|
| 254 |
+
|
| 255 |
+
{/* Section Subtitle */}
|
| 256 |
+
{(subtitle || isEditable) && (
|
| 257 |
+
editingField === 'subtitle' ? (
|
| 258 |
+
<input
|
| 259 |
+
type="text"
|
| 260 |
+
value={tempSubtitle}
|
| 261 |
+
onChange={(e) => setTempSubtitle(e.target.value)}
|
| 262 |
+
onBlur={() => handleFieldBlur('subtitle')}
|
| 263 |
+
onKeyDown={(e) => handleKeyDown(e, 'subtitle')}
|
| 264 |
+
className="w-full bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 265 |
+
style={{
|
| 266 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 267 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 268 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 269 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 270 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 271 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 272 |
+
}}
|
| 273 |
+
placeholder="Section description"
|
| 274 |
+
autoFocus
|
| 275 |
+
/>
|
| 276 |
+
) : (
|
| 277 |
+
<p
|
| 278 |
+
className={`opacity-80 leading-relaxed ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${!subtitle && isEditable ? 'text-white/50' : ''} ${selectedElement === 'subtitle' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 279 |
+
style={{
|
| 280 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 281 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 282 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 283 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 284 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 285 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 286 |
+
}}
|
| 287 |
+
onClick={() => handleFieldClick('subtitle')}
|
| 288 |
+
>
|
| 289 |
+
{subtitle || (isEditable ? 'Click to add description' : '')}
|
| 290 |
+
</p>
|
| 291 |
+
)
|
| 292 |
+
)}
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
);
|
| 299 |
+
}
|
components/slides/SlideFactory.tsx
CHANGED
|
@@ -3,6 +3,13 @@ import TitleSlide from './TitleSlide';
|
|
| 3 |
import ContentImageSlide from './ContentImageSlide';
|
| 4 |
import BulletsSlide from './BulletsSlide';
|
| 5 |
import ChartSlide from './ChartSlide';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
export type SlideSpec = {
|
| 8 |
id: string;
|
|
@@ -16,20 +23,222 @@ export type SlideSpec = {
|
|
| 16 |
data: Record<string, unknown>;
|
| 17 |
options?: Record<string, unknown>;
|
| 18 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
};
|
| 20 |
|
| 21 |
-
export function renderSlide(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
switch (spec.layout) {
|
| 23 |
case 'title':
|
| 24 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
case 'content-image':
|
| 26 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
case 'bullets':
|
| 28 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
case 'chart':
|
| 30 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
default:
|
| 32 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
}
|
| 35 |
|
|
|
|
| 3 |
import ContentImageSlide from './ContentImageSlide';
|
| 4 |
import BulletsSlide from './BulletsSlide';
|
| 5 |
import ChartSlide from './ChartSlide';
|
| 6 |
+
import TwoColumnSlide from './TwoColumnSlide';
|
| 7 |
+
import ImageOnlySlide from './ImageOnlySlide';
|
| 8 |
+
import HeroSlide from './HeroSlide';
|
| 9 |
+
import StatsSlide from './StatsSlide';
|
| 10 |
+
import QuoteSlide from './QuoteSlide';
|
| 11 |
+
import SectionSlide from './SectionSlide';
|
| 12 |
+
import ComparisonSlide from './ComparisonSlide';
|
| 13 |
|
| 14 |
export type SlideSpec = {
|
| 15 |
id: string;
|
|
|
|
| 23 |
data: Record<string, unknown>;
|
| 24 |
options?: Record<string, unknown>;
|
| 25 |
};
|
| 26 |
+
// New properties for modern slide types
|
| 27 |
+
quote?: string;
|
| 28 |
+
author?: string;
|
| 29 |
+
company?: string;
|
| 30 |
+
stats?: Array<{ number: string; label: string }>;
|
| 31 |
+
sectionNumber?: string;
|
| 32 |
+
leftTitle?: string;
|
| 33 |
+
rightTitle?: string;
|
| 34 |
+
leftContent?: Array<string>;
|
| 35 |
+
rightContent?: Array<string>;
|
| 36 |
+
// Optional per-field formatting captured by the editor (not required by renderers)
|
| 37 |
+
formatting?: Record<string, Record<string, unknown>>;
|
| 38 |
};
|
| 39 |
|
| 40 |
+
export function renderSlide(
|
| 41 |
+
spec: SlideSpec,
|
| 42 |
+
theme: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset' = 'dark',
|
| 43 |
+
options?: {
|
| 44 |
+
isEditable?: boolean;
|
| 45 |
+
onFieldUpdate?: (slideId: string, field: string, value: string, index?: number) => void;
|
| 46 |
+
onTextSelect?: (element: any) => void;
|
| 47 |
+
selectedFont?: string;
|
| 48 |
+
selectedFontSize?: number;
|
| 49 |
+
selectedColor?: string;
|
| 50 |
+
onImageEdit?: () => void;
|
| 51 |
+
}
|
| 52 |
+
) {
|
| 53 |
+
const { isEditable = false, onFieldUpdate, onTextSelect, selectedFont, selectedFontSize, selectedColor, onImageEdit } = options || {};
|
| 54 |
+
|
| 55 |
switch (spec.layout) {
|
| 56 |
case 'title':
|
| 57 |
+
return (
|
| 58 |
+
<TitleSlide
|
| 59 |
+
title={spec.title || ''}
|
| 60 |
+
subtitle={spec.subtitle}
|
| 61 |
+
theme={theme}
|
| 62 |
+
slideId={spec.id}
|
| 63 |
+
onFieldUpdate={onFieldUpdate}
|
| 64 |
+
isEditable={isEditable}
|
| 65 |
+
onTextSelect={onTextSelect}
|
| 66 |
+
selectedFont={selectedFont}
|
| 67 |
+
selectedFontSize={selectedFontSize}
|
| 68 |
+
selectedColor={selectedColor}
|
| 69 |
+
formatting={spec.formatting}
|
| 70 |
+
/>
|
| 71 |
+
);
|
| 72 |
case 'content-image':
|
| 73 |
+
return (
|
| 74 |
+
<ContentImageSlide
|
| 75 |
+
title={spec.title || ''}
|
| 76 |
+
body={spec.body || []}
|
| 77 |
+
imageUrl={spec.images?.[0]}
|
| 78 |
+
theme={theme}
|
| 79 |
+
slideId={spec.id}
|
| 80 |
+
onFieldUpdate={onFieldUpdate}
|
| 81 |
+
isEditable={isEditable}
|
| 82 |
+
onImageEdit={onImageEdit}
|
| 83 |
+
/>
|
| 84 |
+
);
|
| 85 |
case 'bullets':
|
| 86 |
+
return (
|
| 87 |
+
<BulletsSlide
|
| 88 |
+
title={spec.title || ''}
|
| 89 |
+
body={spec.body || []}
|
| 90 |
+
theme={theme}
|
| 91 |
+
slideId={spec.id}
|
| 92 |
+
onFieldUpdate={onFieldUpdate}
|
| 93 |
+
isEditable={isEditable}
|
| 94 |
+
/>
|
| 95 |
+
);
|
| 96 |
case 'chart':
|
| 97 |
+
return (
|
| 98 |
+
<ChartSlide
|
| 99 |
+
title={spec.title || ''}
|
| 100 |
+
chart={spec.chart}
|
| 101 |
+
theme={theme}
|
| 102 |
+
slideId={spec.id}
|
| 103 |
+
onFieldUpdate={onFieldUpdate}
|
| 104 |
+
isEditable={isEditable}
|
| 105 |
+
/>
|
| 106 |
+
);
|
| 107 |
+
case 'two-column':
|
| 108 |
+
return (
|
| 109 |
+
<TwoColumnSlide
|
| 110 |
+
title={spec.title || ''}
|
| 111 |
+
body={spec.body || []}
|
| 112 |
+
theme={theme}
|
| 113 |
+
slideId={spec.id}
|
| 114 |
+
onFieldUpdate={onFieldUpdate}
|
| 115 |
+
isEditable={isEditable}
|
| 116 |
+
onTextSelect={onTextSelect}
|
| 117 |
+
selectedFont={selectedFont}
|
| 118 |
+
selectedFontSize={selectedFontSize}
|
| 119 |
+
selectedColor={selectedColor}
|
| 120 |
+
formatting={spec.formatting}
|
| 121 |
+
/>
|
| 122 |
+
);
|
| 123 |
+
case 'image-only':
|
| 124 |
+
return (
|
| 125 |
+
<ImageOnlySlide
|
| 126 |
+
title={spec.title || ''}
|
| 127 |
+
subtitle={spec.subtitle}
|
| 128 |
+
imageUrl={spec.images?.[0]}
|
| 129 |
+
theme={theme}
|
| 130 |
+
slideId={spec.id}
|
| 131 |
+
onFieldUpdate={onFieldUpdate}
|
| 132 |
+
isEditable={isEditable}
|
| 133 |
+
onImageEdit={onImageEdit}
|
| 134 |
+
onTextSelect={onTextSelect}
|
| 135 |
+
selectedFont={selectedFont}
|
| 136 |
+
selectedFontSize={selectedFontSize}
|
| 137 |
+
selectedColor={selectedColor}
|
| 138 |
+
formatting={spec.formatting}
|
| 139 |
+
/>
|
| 140 |
+
);
|
| 141 |
+
case 'hero':
|
| 142 |
+
return (
|
| 143 |
+
<HeroSlide
|
| 144 |
+
title={spec.title || ''}
|
| 145 |
+
subtitle={spec.subtitle}
|
| 146 |
+
theme={theme}
|
| 147 |
+
slideId={spec.id}
|
| 148 |
+
onFieldUpdate={onFieldUpdate}
|
| 149 |
+
isEditable={isEditable}
|
| 150 |
+
onTextSelect={onTextSelect}
|
| 151 |
+
selectedFont={selectedFont}
|
| 152 |
+
selectedFontSize={selectedFontSize}
|
| 153 |
+
selectedColor={selectedColor}
|
| 154 |
+
formatting={spec.formatting}
|
| 155 |
+
/>
|
| 156 |
+
);
|
| 157 |
+
case 'stats':
|
| 158 |
+
return (
|
| 159 |
+
<StatsSlide
|
| 160 |
+
title={spec.title || ''}
|
| 161 |
+
stats={spec.stats || []}
|
| 162 |
+
theme={theme}
|
| 163 |
+
slideId={spec.id}
|
| 164 |
+
onFieldUpdate={onFieldUpdate}
|
| 165 |
+
isEditable={isEditable}
|
| 166 |
+
onTextSelect={onTextSelect}
|
| 167 |
+
selectedFont={selectedFont}
|
| 168 |
+
selectedFontSize={selectedFontSize}
|
| 169 |
+
selectedColor={selectedColor}
|
| 170 |
+
formatting={spec.formatting}
|
| 171 |
+
/>
|
| 172 |
+
);
|
| 173 |
+
case 'quote':
|
| 174 |
+
return (
|
| 175 |
+
<QuoteSlide
|
| 176 |
+
quote={spec.quote || ''}
|
| 177 |
+
author={spec.author}
|
| 178 |
+
company={spec.company}
|
| 179 |
+
theme={theme}
|
| 180 |
+
slideId={spec.id}
|
| 181 |
+
onFieldUpdate={onFieldUpdate}
|
| 182 |
+
isEditable={isEditable}
|
| 183 |
+
onTextSelect={onTextSelect}
|
| 184 |
+
selectedFont={selectedFont}
|
| 185 |
+
selectedFontSize={selectedFontSize}
|
| 186 |
+
selectedColor={selectedColor}
|
| 187 |
+
formatting={spec.formatting}
|
| 188 |
+
/>
|
| 189 |
+
);
|
| 190 |
+
case 'section':
|
| 191 |
+
return (
|
| 192 |
+
<SectionSlide
|
| 193 |
+
title={spec.title || ''}
|
| 194 |
+
subtitle={spec.subtitle}
|
| 195 |
+
sectionNumber={spec.sectionNumber}
|
| 196 |
+
theme={theme}
|
| 197 |
+
slideId={spec.id}
|
| 198 |
+
onFieldUpdate={onFieldUpdate}
|
| 199 |
+
isEditable={isEditable}
|
| 200 |
+
onTextSelect={onTextSelect}
|
| 201 |
+
selectedFont={selectedFont}
|
| 202 |
+
selectedFontSize={selectedFontSize}
|
| 203 |
+
selectedColor={selectedColor}
|
| 204 |
+
formatting={spec.formatting}
|
| 205 |
+
/>
|
| 206 |
+
);
|
| 207 |
+
case 'comparison':
|
| 208 |
+
return (
|
| 209 |
+
<ComparisonSlide
|
| 210 |
+
title={spec.title || ''}
|
| 211 |
+
leftTitle={spec.leftTitle || 'Option A'}
|
| 212 |
+
rightTitle={spec.rightTitle || 'Option B'}
|
| 213 |
+
leftContent={spec.leftContent || []}
|
| 214 |
+
rightContent={spec.rightContent || []}
|
| 215 |
+
theme={theme}
|
| 216 |
+
slideId={spec.id}
|
| 217 |
+
onFieldUpdate={onFieldUpdate}
|
| 218 |
+
isEditable={isEditable}
|
| 219 |
+
onTextSelect={onTextSelect}
|
| 220 |
+
selectedFont={selectedFont}
|
| 221 |
+
selectedFontSize={selectedFontSize}
|
| 222 |
+
selectedColor={selectedColor}
|
| 223 |
+
formatting={spec.formatting}
|
| 224 |
+
/>
|
| 225 |
+
);
|
| 226 |
default:
|
| 227 |
+
return (
|
| 228 |
+
<TitleSlide
|
| 229 |
+
title={spec.title || 'Slide'}
|
| 230 |
+
subtitle={spec.subtitle}
|
| 231 |
+
theme={theme}
|
| 232 |
+
slideId={spec.id}
|
| 233 |
+
onFieldUpdate={onFieldUpdate}
|
| 234 |
+
isEditable={isEditable}
|
| 235 |
+
onTextSelect={onTextSelect}
|
| 236 |
+
selectedFont={selectedFont}
|
| 237 |
+
selectedFontSize={selectedFontSize}
|
| 238 |
+
selectedColor={selectedColor}
|
| 239 |
+
formatting={spec.formatting}
|
| 240 |
+
/>
|
| 241 |
+
);
|
| 242 |
}
|
| 243 |
}
|
| 244 |
|
components/slides/StatsSlide.tsx
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface StatsSlideProps {
|
| 4 |
+
title: string;
|
| 5 |
+
stats: Array<{ number: string; label: string }>;
|
| 6 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 7 |
+
slideId?: string;
|
| 8 |
+
onFieldUpdate?: (slideId: string, field: string, value: string, index?: number) => void;
|
| 9 |
+
isEditable?: boolean;
|
| 10 |
+
onTextSelect?: (element: any) => void;
|
| 11 |
+
selectedFont?: string;
|
| 12 |
+
selectedFontSize?: number;
|
| 13 |
+
selectedColor?: string;
|
| 14 |
+
formatting?: Record<string, Record<string, any>>;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const getThemeClasses = (theme: string) => {
|
| 18 |
+
switch (theme) {
|
| 19 |
+
case 'light':
|
| 20 |
+
return 'text-slate-900 bg-white';
|
| 21 |
+
case 'blue':
|
| 22 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 23 |
+
case 'purple':
|
| 24 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 25 |
+
case 'green':
|
| 26 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 27 |
+
case 'orange':
|
| 28 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 29 |
+
case 'teal':
|
| 30 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 31 |
+
case 'pink':
|
| 32 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 33 |
+
case 'indigo':
|
| 34 |
+
return 'text-white bg-[#0b0c14]';
|
| 35 |
+
case 'midnight':
|
| 36 |
+
return 'text-white bg-[#0b0c14]';
|
| 37 |
+
case 'sunset':
|
| 38 |
+
return 'text-white bg-[#220b03]';
|
| 39 |
+
case 'amber':
|
| 40 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 41 |
+
case 'emerald':
|
| 42 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 43 |
+
case 'slate':
|
| 44 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 45 |
+
default: // dark
|
| 46 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
export default function StatsSlide({
|
| 51 |
+
title,
|
| 52 |
+
stats = [
|
| 53 |
+
{ number: '100+', label: 'Customers' },
|
| 54 |
+
{ number: '50M', label: 'Revenue' },
|
| 55 |
+
{ number: '99%', label: 'Satisfaction' }
|
| 56 |
+
],
|
| 57 |
+
theme = 'dark',
|
| 58 |
+
slideId,
|
| 59 |
+
onFieldUpdate,
|
| 60 |
+
isEditable = false,
|
| 61 |
+
onTextSelect,
|
| 62 |
+
selectedFont = 'serif',
|
| 63 |
+
selectedFontSize = 16,
|
| 64 |
+
selectedColor,
|
| 65 |
+
formatting = {}
|
| 66 |
+
}: StatsSlideProps) {
|
| 67 |
+
const [editingField, setEditingField] = useState<{ field: string; index?: number } | null>(null);
|
| 68 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 69 |
+
const [tempStats, setTempStats] = useState(stats);
|
| 70 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 71 |
+
|
| 72 |
+
const themeClasses = getThemeClasses(theme);
|
| 73 |
+
|
| 74 |
+
const getFieldFormatting = (field: string) => {
|
| 75 |
+
const fieldFormatting = formatting[field] || {};
|
| 76 |
+
return {
|
| 77 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 78 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'title' ? 32 : 16)),
|
| 79 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 80 |
+
fontWeight: fieldFormatting.bold ? 'bold' : 'normal',
|
| 81 |
+
fontStyle: fieldFormatting.italic ? 'italic' : 'normal',
|
| 82 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 83 |
+
};
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
const handleFieldClick = (field: string, index?: number) => {
|
| 87 |
+
if (!isEditable) return;
|
| 88 |
+
setEditingField({ field, index });
|
| 89 |
+
setSelectedElement(field);
|
| 90 |
+
if (field === 'title') setTempTitle(title);
|
| 91 |
+
|
| 92 |
+
if (onTextSelect) {
|
| 93 |
+
onTextSelect({
|
| 94 |
+
field,
|
| 95 |
+
slideId,
|
| 96 |
+
currentFont: selectedFont,
|
| 97 |
+
currentFontSize: field === 'title' ? 32 : 16,
|
| 98 |
+
currentColor: selectedColor
|
| 99 |
+
});
|
| 100 |
+
}
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
const handleFieldBlur = (field: string, index?: number) => {
|
| 104 |
+
if (!slideId || !onFieldUpdate) return;
|
| 105 |
+
|
| 106 |
+
if (field === 'title' && tempTitle !== title) {
|
| 107 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 108 |
+
}
|
| 109 |
+
if (field === 'stat' && typeof index === 'number') {
|
| 110 |
+
// Handle stat updates
|
| 111 |
+
const statKey = `stat-${index}`;
|
| 112 |
+
onFieldUpdate(slideId, statKey, JSON.stringify(tempStats[index]));
|
| 113 |
+
}
|
| 114 |
+
setEditingField(null);
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string, index?: number) => {
|
| 118 |
+
if (e.key === 'Enter') {
|
| 119 |
+
e.preventDefault();
|
| 120 |
+
handleFieldBlur(field, index);
|
| 121 |
+
}
|
| 122 |
+
if (e.key === 'Escape') {
|
| 123 |
+
setEditingField(null);
|
| 124 |
+
if (field === 'title') setTempTitle(title);
|
| 125 |
+
if (field === 'stat') setTempStats([...stats]);
|
| 126 |
+
}
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
const updateTempStat = (index: number, key: 'number' | 'label', value: string) => {
|
| 130 |
+
const newStats = [...tempStats];
|
| 131 |
+
newStats[index] = { ...newStats[index], [key]: value };
|
| 132 |
+
setTempStats(newStats);
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
return (
|
| 136 |
+
<div className={`w-full h-full ${themeClasses} p-12 relative overflow-hidden`}>
|
| 137 |
+
{/* Special backgrounds */}
|
| 138 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 139 |
+
<>
|
| 140 |
+
<div
|
| 141 |
+
aria-hidden
|
| 142 |
+
className="absolute inset-0 -z-10"
|
| 143 |
+
style={{
|
| 144 |
+
background:
|
| 145 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 146 |
+
}}
|
| 147 |
+
/>
|
| 148 |
+
<div
|
| 149 |
+
aria-hidden
|
| 150 |
+
className="absolute inset-0 -z-10"
|
| 151 |
+
style={{
|
| 152 |
+
backgroundImage:
|
| 153 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 154 |
+
backgroundSize: '160px 160px',
|
| 155 |
+
opacity: 0.25,
|
| 156 |
+
}}
|
| 157 |
+
/>
|
| 158 |
+
</>
|
| 159 |
+
)}
|
| 160 |
+
{theme === 'sunset' && (
|
| 161 |
+
<div
|
| 162 |
+
aria-hidden
|
| 163 |
+
className="absolute inset-0 -z-10"
|
| 164 |
+
style={{
|
| 165 |
+
background:
|
| 166 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 167 |
+
}}
|
| 168 |
+
/>
|
| 169 |
+
)}
|
| 170 |
+
|
| 171 |
+
<div className="relative z-10 h-full flex flex-col">
|
| 172 |
+
{/* Title */}
|
| 173 |
+
{editingField?.field === 'title' ? (
|
| 174 |
+
<input
|
| 175 |
+
type="text"
|
| 176 |
+
value={tempTitle}
|
| 177 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 178 |
+
onBlur={() => handleFieldBlur('title')}
|
| 179 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 180 |
+
className="mb-16 text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 181 |
+
style={{
|
| 182 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 183 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 184 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 185 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 186 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 187 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 188 |
+
}}
|
| 189 |
+
placeholder="Enter title"
|
| 190 |
+
autoFocus
|
| 191 |
+
/>
|
| 192 |
+
) : (
|
| 193 |
+
<h2
|
| 194 |
+
className={`mb-16 text-center font-semibold ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-2' : ''} ${selectedElement === 'title' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 195 |
+
style={{
|
| 196 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 197 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 198 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 199 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 200 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 201 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 202 |
+
}}
|
| 203 |
+
onClick={() => handleFieldClick('title')}
|
| 204 |
+
>
|
| 205 |
+
{title}
|
| 206 |
+
</h2>
|
| 207 |
+
)}
|
| 208 |
+
|
| 209 |
+
{/* Stats Grid */}
|
| 210 |
+
<div className="flex-1 flex items-center justify-center">
|
| 211 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-16 w-full max-w-4xl">
|
| 212 |
+
{stats.map((stat, index) => (
|
| 213 |
+
<div key={index} className="text-center">
|
| 214 |
+
{/* Stat Number */}
|
| 215 |
+
{editingField?.field === 'stat' && editingField.index === index ? (
|
| 216 |
+
<div className="space-y-4">
|
| 217 |
+
<input
|
| 218 |
+
type="text"
|
| 219 |
+
value={tempStats[index]?.number || ''}
|
| 220 |
+
onChange={(e) => updateTempStat(index, 'number', e.target.value)}
|
| 221 |
+
onBlur={() => handleFieldBlur('stat', index)}
|
| 222 |
+
onKeyDown={(e) => handleKeyDown(e, 'stat', index)}
|
| 223 |
+
className="w-full text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70 text-4xl font-bold"
|
| 224 |
+
placeholder="Number"
|
| 225 |
+
autoFocus
|
| 226 |
+
/>
|
| 227 |
+
<input
|
| 228 |
+
type="text"
|
| 229 |
+
value={tempStats[index]?.label || ''}
|
| 230 |
+
onChange={(e) => updateTempStat(index, 'label', e.target.value)}
|
| 231 |
+
className="w-full text-center bg-transparent border-2 border-white/50 rounded px-4 py-2 outline-none text-white placeholder-white/70"
|
| 232 |
+
placeholder="Label"
|
| 233 |
+
/>
|
| 234 |
+
</div>
|
| 235 |
+
) : (
|
| 236 |
+
<div
|
| 237 |
+
className={`${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-4 py-6' : ''}`}
|
| 238 |
+
onClick={() => handleFieldClick('stat', index)}
|
| 239 |
+
>
|
| 240 |
+
<div className="text-6xl font-bold mb-4 bg-gradient-to-r from-white to-white/80 bg-clip-text text-transparent">
|
| 241 |
+
{stat.number}
|
| 242 |
+
</div>
|
| 243 |
+
<div className="text-lg opacity-80 font-medium uppercase tracking-wide">
|
| 244 |
+
{stat.label}
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
)}
|
| 248 |
+
</div>
|
| 249 |
+
))}
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
);
|
| 255 |
+
}
|
components/slides/TitleSlide.tsx
CHANGED
|
@@ -1,17 +1,260 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
|
| 3 |
export interface TitleSlideProps {
|
| 4 |
title: string;
|
| 5 |
subtitle?: string;
|
| 6 |
-
theme?: 'dark' | 'light';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
return (
|
| 12 |
-
<div className={`w-full h-full flex flex-col items-center justify-center ${
|
| 13 |
-
|
| 14 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
</div>
|
| 16 |
);
|
| 17 |
}
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
|
| 3 |
export interface TitleSlideProps {
|
| 4 |
title: string;
|
| 5 |
subtitle?: string;
|
| 6 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 7 |
+
slideId?: string;
|
| 8 |
+
onFieldUpdate?: (slideId: string, field: string, value: string) => void;
|
| 9 |
+
isEditable?: boolean;
|
| 10 |
+
onTextSelect?: (element: any) => void;
|
| 11 |
+
selectedFont?: string;
|
| 12 |
+
selectedFontSize?: number;
|
| 13 |
+
selectedColor?: string;
|
| 14 |
+
formatting?: Record<string, Record<string, any>>;
|
| 15 |
}
|
| 16 |
|
| 17 |
+
const getThemeClasses = (theme: string) => {
|
| 18 |
+
switch (theme) {
|
| 19 |
+
case 'light':
|
| 20 |
+
return 'text-slate-900 bg-white';
|
| 21 |
+
case 'blue':
|
| 22 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 23 |
+
case 'purple':
|
| 24 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 25 |
+
case 'green':
|
| 26 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 27 |
+
case 'orange':
|
| 28 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 29 |
+
case 'teal':
|
| 30 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 31 |
+
case 'pink':
|
| 32 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 33 |
+
case 'indigo':
|
| 34 |
+
return 'text-white bg-[#0b0c14]';
|
| 35 |
+
case 'midnight':
|
| 36 |
+
return 'text-white bg-[#0b0c14]';
|
| 37 |
+
case 'sunset':
|
| 38 |
+
return 'text-white bg-[#220b03]';
|
| 39 |
+
case 'amber':
|
| 40 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 41 |
+
case 'emerald':
|
| 42 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 43 |
+
case 'slate':
|
| 44 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 45 |
+
default: // dark
|
| 46 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
export default function TitleSlide({
|
| 51 |
+
title,
|
| 52 |
+
subtitle,
|
| 53 |
+
theme = 'dark',
|
| 54 |
+
slideId,
|
| 55 |
+
onFieldUpdate,
|
| 56 |
+
isEditable = false,
|
| 57 |
+
onTextSelect,
|
| 58 |
+
selectedFont = 'serif',
|
| 59 |
+
selectedFontSize = 48,
|
| 60 |
+
selectedColor,
|
| 61 |
+
formatting = {}
|
| 62 |
+
}: TitleSlideProps) {
|
| 63 |
+
const [editingField, setEditingField] = useState<string | null>(null);
|
| 64 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 65 |
+
const [tempSubtitle, setTempSubtitle] = useState(subtitle || '');
|
| 66 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 67 |
+
|
| 68 |
+
const themeClasses = getThemeClasses(theme);
|
| 69 |
+
|
| 70 |
+
// Get formatting for a specific field
|
| 71 |
+
const getFieldFormatting = (field: string) => {
|
| 72 |
+
const fieldFormatting = formatting[field] || {};
|
| 73 |
+
return {
|
| 74 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 75 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'title' ? 48 : 18)),
|
| 76 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 77 |
+
fontWeight: fieldFormatting.bold ? 'bold' : 'normal',
|
| 78 |
+
fontStyle: fieldFormatting.italic ? 'italic' : 'normal',
|
| 79 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 80 |
+
};
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
const handleFieldClick = (field: string) => {
|
| 84 |
+
if (!isEditable) return;
|
| 85 |
+
setEditingField(field);
|
| 86 |
+
setSelectedElement(field);
|
| 87 |
+
if (field === 'title') setTempTitle(title);
|
| 88 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 89 |
+
|
| 90 |
+
// Notify parent about text selection
|
| 91 |
+
if (onTextSelect) {
|
| 92 |
+
onTextSelect({
|
| 93 |
+
field,
|
| 94 |
+
slideId,
|
| 95 |
+
currentFont: selectedFont,
|
| 96 |
+
currentFontSize: field === 'title' ? 48 : 18,
|
| 97 |
+
currentColor: selectedColor
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
const handleTextSelect = (field: string) => {
|
| 103 |
+
if (!isEditable) return;
|
| 104 |
+
setSelectedElement(field);
|
| 105 |
+
|
| 106 |
+
if (onTextSelect) {
|
| 107 |
+
onTextSelect({
|
| 108 |
+
field,
|
| 109 |
+
slideId,
|
| 110 |
+
currentFont: selectedFont,
|
| 111 |
+
currentFontSize: field === 'title' ? 48 : 18,
|
| 112 |
+
currentColor: selectedColor
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
const handleFieldBlur = (field: string) => {
|
| 118 |
+
if (!slideId || !onFieldUpdate) return;
|
| 119 |
+
|
| 120 |
+
if (field === 'title' && tempTitle !== title) {
|
| 121 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 122 |
+
}
|
| 123 |
+
if (field === 'subtitle' && tempSubtitle !== subtitle) {
|
| 124 |
+
onFieldUpdate(slideId, 'subtitle', tempSubtitle);
|
| 125 |
+
}
|
| 126 |
+
setEditingField(null);
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
| 130 |
+
if (e.key === 'Enter') {
|
| 131 |
+
e.preventDefault();
|
| 132 |
+
handleFieldBlur(field);
|
| 133 |
+
}
|
| 134 |
+
if (e.key === 'Escape') {
|
| 135 |
+
setEditingField(null);
|
| 136 |
+
if (field === 'title') setTempTitle(title);
|
| 137 |
+
if (field === 'subtitle') setTempSubtitle(subtitle || '');
|
| 138 |
+
}
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
return (
|
| 142 |
+
<div className={`w-full h-full flex flex-col items-center justify-center ${themeClasses} p-10 transition-colors relative overflow-hidden`}>
|
| 143 |
+
{/* Aurora background to match reference (purple/indigo on black) */}
|
| 144 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 145 |
+
<>
|
| 146 |
+
<div
|
| 147 |
+
aria-hidden
|
| 148 |
+
className="absolute inset-0 -z-10"
|
| 149 |
+
style={{
|
| 150 |
+
background:
|
| 151 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 152 |
+
}}
|
| 153 |
+
/>
|
| 154 |
+
<div
|
| 155 |
+
aria-hidden
|
| 156 |
+
className="absolute inset-0 -z-10"
|
| 157 |
+
style={{
|
| 158 |
+
backgroundImage:
|
| 159 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 160 |
+
backgroundSize: '160px 160px',
|
| 161 |
+
opacity: 0.25,
|
| 162 |
+
}}
|
| 163 |
+
/>
|
| 164 |
+
</>
|
| 165 |
+
)}
|
| 166 |
+
{theme === 'sunset' && (
|
| 167 |
+
<div
|
| 168 |
+
aria-hidden
|
| 169 |
+
className="absolute inset-0 -z-10"
|
| 170 |
+
style={{
|
| 171 |
+
background:
|
| 172 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 173 |
+
}}
|
| 174 |
+
/>
|
| 175 |
+
)}
|
| 176 |
+
{/* Background pattern */}
|
| 177 |
+
<div className="absolute inset-0 opacity-10">
|
| 178 |
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,_white_1px,_transparent_0)] bg-[length:20px_20px]" />
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
<div className="relative z-10 text-center">
|
| 182 |
+
{editingField === 'title' ? (
|
| 183 |
+
<input
|
| 184 |
+
type="text"
|
| 185 |
+
value={tempTitle}
|
| 186 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 187 |
+
onBlur={() => handleFieldBlur('title')}
|
| 188 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 189 |
+
className="mb-4 text-center drop-shadow-lg bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70"
|
| 190 |
+
style={{
|
| 191 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 192 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 193 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 194 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 195 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 196 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 197 |
+
}}
|
| 198 |
+
placeholder="Enter title"
|
| 199 |
+
autoFocus
|
| 200 |
+
/>
|
| 201 |
+
) : (
|
| 202 |
+
<h1
|
| 203 |
+
className={`mb-4 text-center drop-shadow-lg transition-all ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''} ${selectedElement === 'title' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 204 |
+
style={{
|
| 205 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 206 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 207 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 208 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 209 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 210 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 211 |
+
}}
|
| 212 |
+
onClick={() => handleFieldClick('title')}
|
| 213 |
+
onMouseDown={() => handleTextSelect('title')}
|
| 214 |
+
>
|
| 215 |
+
{title}
|
| 216 |
+
</h1>
|
| 217 |
+
)}
|
| 218 |
+
|
| 219 |
+
{(subtitle || isEditable) && (
|
| 220 |
+
editingField === 'subtitle' ? (
|
| 221 |
+
<input
|
| 222 |
+
type="text"
|
| 223 |
+
value={tempSubtitle}
|
| 224 |
+
onChange={(e) => setTempSubtitle(e.target.value)}
|
| 225 |
+
onBlur={() => handleFieldBlur('subtitle')}
|
| 226 |
+
onKeyDown={(e) => handleKeyDown(e, 'subtitle')}
|
| 227 |
+
className="opacity-90 max-w-3xl text-center drop-shadow-md bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70"
|
| 228 |
+
style={{
|
| 229 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 230 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 231 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 232 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 233 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 234 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 235 |
+
}}
|
| 236 |
+
placeholder="Enter subtitle"
|
| 237 |
+
autoFocus
|
| 238 |
+
/>
|
| 239 |
+
) : (
|
| 240 |
+
<p
|
| 241 |
+
className={`opacity-90 max-w-3xl text-center drop-shadow-md transition-all ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''} ${!subtitle && isEditable ? 'text-white/50' : ''} ${selectedElement === 'subtitle' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 242 |
+
style={{
|
| 243 |
+
fontFamily: getFieldFormatting('subtitle').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('subtitle').fontFamily})`,
|
| 244 |
+
fontSize: `${getFieldFormatting('subtitle').fontSize}px`,
|
| 245 |
+
color: getFieldFormatting('subtitle').color || 'inherit',
|
| 246 |
+
fontWeight: getFieldFormatting('subtitle').fontWeight,
|
| 247 |
+
fontStyle: getFieldFormatting('subtitle').fontStyle,
|
| 248 |
+
textDecoration: getFieldFormatting('subtitle').textDecoration
|
| 249 |
+
}}
|
| 250 |
+
onClick={() => handleFieldClick('subtitle')}
|
| 251 |
+
onMouseDown={() => handleTextSelect('subtitle')}
|
| 252 |
+
>
|
| 253 |
+
{subtitle || (isEditable ? 'Click to add subtitle' : '')}
|
| 254 |
+
</p>
|
| 255 |
+
)
|
| 256 |
+
)}
|
| 257 |
+
</div>
|
| 258 |
</div>
|
| 259 |
);
|
| 260 |
}
|
components/slides/TwoColumnSlide.tsx
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface TwoColumnSlideProps {
|
| 4 |
+
title: string;
|
| 5 |
+
body: Array<{ heading?: string; text: string }>;
|
| 6 |
+
theme?: 'dark' | 'light' | 'blue' | 'purple' | 'green' | 'orange' | 'teal' | 'pink' | 'indigo' | 'amber' | 'emerald' | 'slate' | 'midnight' | 'sunset';
|
| 7 |
+
slideId?: string;
|
| 8 |
+
onFieldUpdate?: (slideId: string, field: string, value: string, index?: number) => void;
|
| 9 |
+
isEditable?: boolean;
|
| 10 |
+
onTextSelect?: (element: any) => void;
|
| 11 |
+
selectedFont?: string;
|
| 12 |
+
selectedFontSize?: number;
|
| 13 |
+
selectedColor?: string;
|
| 14 |
+
formatting?: Record<string, Record<string, any>>;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const getThemeClasses = (theme: string) => {
|
| 18 |
+
switch (theme) {
|
| 19 |
+
case 'light':
|
| 20 |
+
return 'text-slate-900 bg-white';
|
| 21 |
+
case 'blue':
|
| 22 |
+
return 'text-white bg-gradient-to-br from-blue-600 to-blue-800';
|
| 23 |
+
case 'purple':
|
| 24 |
+
return 'text-white bg-gradient-to-br from-purple-600 to-purple-800';
|
| 25 |
+
case 'green':
|
| 26 |
+
return 'text-white bg-gradient-to-br from-green-600 to-green-800';
|
| 27 |
+
case 'orange':
|
| 28 |
+
return 'text-white bg-gradient-to-br from-orange-600 to-orange-800';
|
| 29 |
+
case 'teal':
|
| 30 |
+
return 'text-white bg-gradient-to-br from-teal-600 to-cyan-700';
|
| 31 |
+
case 'pink':
|
| 32 |
+
return 'text-white bg-gradient-to-br from-pink-600 to-rose-700';
|
| 33 |
+
case 'indigo':
|
| 34 |
+
return 'text-white bg-[#0b0c14]';
|
| 35 |
+
case 'midnight':
|
| 36 |
+
return 'text-white bg-[#0b0c14]';
|
| 37 |
+
case 'sunset':
|
| 38 |
+
return 'text-white bg-[#220b03]';
|
| 39 |
+
case 'amber':
|
| 40 |
+
return 'text-white bg-gradient-to-br from-amber-500 to-orange-600';
|
| 41 |
+
case 'emerald':
|
| 42 |
+
return 'text-white bg-gradient-to-br from-emerald-500 to-teal-600';
|
| 43 |
+
case 'slate':
|
| 44 |
+
return 'text-white bg-gradient-to-br from-slate-700 to-gray-800';
|
| 45 |
+
default: // dark
|
| 46 |
+
return 'text-white bg-gradient-to-br from-gray-800 to-gray-900';
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
export default function TwoColumnSlide({
|
| 51 |
+
title,
|
| 52 |
+
body,
|
| 53 |
+
theme = 'dark',
|
| 54 |
+
slideId,
|
| 55 |
+
onFieldUpdate,
|
| 56 |
+
isEditable = false,
|
| 57 |
+
onTextSelect,
|
| 58 |
+
selectedFont = 'serif',
|
| 59 |
+
selectedFontSize = 16,
|
| 60 |
+
selectedColor,
|
| 61 |
+
formatting = {}
|
| 62 |
+
}: TwoColumnSlideProps) {
|
| 63 |
+
const [editingField, setEditingField] = useState<{ field: string; index?: number } | null>(null);
|
| 64 |
+
const [tempTitle, setTempTitle] = useState(title);
|
| 65 |
+
const [tempBody, setTempBody] = useState<string[]>(body?.map(b => b.text) || []);
|
| 66 |
+
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
| 67 |
+
|
| 68 |
+
const themeClasses = getThemeClasses(theme);
|
| 69 |
+
|
| 70 |
+
// Get formatting for a specific field
|
| 71 |
+
const getFieldFormatting = (field: string) => {
|
| 72 |
+
const fieldFormatting = formatting[field] || {};
|
| 73 |
+
return {
|
| 74 |
+
fontFamily: fieldFormatting.fontFamily || (selectedElement === field ? selectedFont : 'serif'),
|
| 75 |
+
fontSize: fieldFormatting.fontSize || (selectedElement === field ? selectedFontSize : (field === 'title' ? 36 : 16)),
|
| 76 |
+
color: fieldFormatting.color || (selectedElement === field ? selectedColor : undefined),
|
| 77 |
+
fontWeight: fieldFormatting.bold ? 'bold' : 'normal',
|
| 78 |
+
fontStyle: fieldFormatting.italic ? 'italic' : 'normal',
|
| 79 |
+
textDecoration: fieldFormatting.underline ? 'underline' : 'none'
|
| 80 |
+
};
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
const handleFieldClick = (field: string, index?: number) => {
|
| 84 |
+
if (!isEditable) return;
|
| 85 |
+
setEditingField({ field, index });
|
| 86 |
+
setSelectedElement(field);
|
| 87 |
+
if (field === 'title') setTempTitle(title);
|
| 88 |
+
if (field === 'body' && typeof index === 'number') {
|
| 89 |
+
setTempBody(body?.map(b => b.text) || []);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Notify parent about text selection
|
| 93 |
+
if (onTextSelect) {
|
| 94 |
+
onTextSelect({
|
| 95 |
+
field,
|
| 96 |
+
slideId,
|
| 97 |
+
currentFont: selectedFont,
|
| 98 |
+
currentFontSize: field === 'title' ? 36 : 16,
|
| 99 |
+
currentColor: selectedColor
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
const handleTextSelect = (field: string) => {
|
| 105 |
+
if (!isEditable) return;
|
| 106 |
+
setSelectedElement(field);
|
| 107 |
+
|
| 108 |
+
if (onTextSelect) {
|
| 109 |
+
onTextSelect({
|
| 110 |
+
field,
|
| 111 |
+
slideId,
|
| 112 |
+
currentFont: selectedFont,
|
| 113 |
+
currentFontSize: field === 'title' ? 36 : 16,
|
| 114 |
+
currentColor: selectedColor
|
| 115 |
+
});
|
| 116 |
+
}
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const handleFieldBlur = (field: string, index?: number) => {
|
| 120 |
+
if (!slideId || !onFieldUpdate) return;
|
| 121 |
+
|
| 122 |
+
if (field === 'title' && tempTitle !== title) {
|
| 123 |
+
onFieldUpdate(slideId, 'title', tempTitle);
|
| 124 |
+
}
|
| 125 |
+
if (field === 'body' && typeof index === 'number' && tempBody[index] !== body?.[index]?.text) {
|
| 126 |
+
onFieldUpdate(slideId, 'body', tempBody[index], index);
|
| 127 |
+
}
|
| 128 |
+
setEditingField(null);
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
const handleKeyDown = (e: React.KeyboardEvent, field: string, index?: number) => {
|
| 132 |
+
if (e.key === 'Enter') {
|
| 133 |
+
e.preventDefault();
|
| 134 |
+
handleFieldBlur(field, index);
|
| 135 |
+
}
|
| 136 |
+
if (e.key === 'Escape') {
|
| 137 |
+
setEditingField(null);
|
| 138 |
+
if (field === 'title') setTempTitle(title);
|
| 139 |
+
if (field === 'body') setTempBody(body?.map(b => b.text) || []);
|
| 140 |
+
}
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
const updateTempBodyItem = (index: number, value: string) => {
|
| 144 |
+
const newTempBody = [...tempBody];
|
| 145 |
+
newTempBody[index] = value;
|
| 146 |
+
setTempBody(newTempBody);
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
return (
|
| 150 |
+
<div className={`w-full h-full ${themeClasses} p-10 relative overflow-hidden`}>
|
| 151 |
+
{/* Special backgrounds for themed slides */}
|
| 152 |
+
{(theme === 'indigo' || theme === 'midnight') && (
|
| 153 |
+
<>
|
| 154 |
+
<div
|
| 155 |
+
aria-hidden
|
| 156 |
+
className="absolute inset-0 -z-10"
|
| 157 |
+
style={{
|
| 158 |
+
background:
|
| 159 |
+
'radial-gradient(55% 65% at 68% 12%, rgba(99,102,241,0.75) 0%, rgba(99,102,241,0.25) 45%, transparent 70%), radial-gradient(50% 60% at 32% 70%, rgba(168,85,247,0.7) 0%, rgba(168,85,247,0.25) 45%, transparent 70%), linear-gradient(180deg, #05050a 0%, #0b0c14 100%)',
|
| 160 |
+
}}
|
| 161 |
+
/>
|
| 162 |
+
<div
|
| 163 |
+
aria-hidden
|
| 164 |
+
className="absolute inset-0 -z-10"
|
| 165 |
+
style={{
|
| 166 |
+
backgroundImage:
|
| 167 |
+
'radial-gradient(circle, rgba(255,255,255,0.2) 1px, rgba(255,255,255,0) 1.6px)',
|
| 168 |
+
backgroundSize: '160px 160px',
|
| 169 |
+
opacity: 0.25,
|
| 170 |
+
}}
|
| 171 |
+
/>
|
| 172 |
+
</>
|
| 173 |
+
)}
|
| 174 |
+
{theme === 'sunset' && (
|
| 175 |
+
<div
|
| 176 |
+
aria-hidden
|
| 177 |
+
className="absolute inset-0 -z-10"
|
| 178 |
+
style={{
|
| 179 |
+
background:
|
| 180 |
+
'radial-gradient(60% 60% at 60% 0%, rgba(255,115,80,0.8) 0%, rgba(255,115,80,0.25) 40%, transparent 70%), radial-gradient(70% 50% at 0% 90%, rgba(255,191,0,0.6) 0%, rgba(255,191,0,0.2) 45%, transparent 70%), linear-gradient(180deg, #260a03 0%, #3a0d05 100%)',
|
| 181 |
+
}}
|
| 182 |
+
/>
|
| 183 |
+
)}
|
| 184 |
+
|
| 185 |
+
{/* Background pattern */}
|
| 186 |
+
<div className="absolute inset-0 opacity-5">
|
| 187 |
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,_white_1px,_transparent_0)] bg-[length:20px_20px]" />
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div className="relative z-10">
|
| 191 |
+
{/* Title */}
|
| 192 |
+
{editingField?.field === 'title' ? (
|
| 193 |
+
<input
|
| 194 |
+
type="text"
|
| 195 |
+
value={tempTitle}
|
| 196 |
+
onChange={(e) => setTempTitle(e.target.value)}
|
| 197 |
+
onBlur={() => handleFieldBlur('title')}
|
| 198 |
+
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
| 199 |
+
className="w-full mb-8 text-center drop-shadow-lg bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70"
|
| 200 |
+
style={{
|
| 201 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 202 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 203 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 204 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 205 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 206 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 207 |
+
}}
|
| 208 |
+
placeholder="Enter title"
|
| 209 |
+
autoFocus
|
| 210 |
+
/>
|
| 211 |
+
) : (
|
| 212 |
+
<h2
|
| 213 |
+
className={`mb-8 text-center drop-shadow-lg transition-all ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''} ${selectedElement === 'title' ? 'ring-2 ring-blue-400 bg-blue-100/20' : ''}`}
|
| 214 |
+
style={{
|
| 215 |
+
fontFamily: getFieldFormatting('title').fontFamily === 'serif' ? 'serif' : `var(--font-${getFieldFormatting('title').fontFamily})`,
|
| 216 |
+
fontSize: `${getFieldFormatting('title').fontSize}px`,
|
| 217 |
+
color: getFieldFormatting('title').color || 'inherit',
|
| 218 |
+
fontWeight: getFieldFormatting('title').fontWeight,
|
| 219 |
+
fontStyle: getFieldFormatting('title').fontStyle,
|
| 220 |
+
textDecoration: getFieldFormatting('title').textDecoration
|
| 221 |
+
}}
|
| 222 |
+
onClick={() => handleFieldClick('title')}
|
| 223 |
+
onMouseDown={() => handleTextSelect('title')}
|
| 224 |
+
>
|
| 225 |
+
{title}
|
| 226 |
+
</h2>
|
| 227 |
+
)}
|
| 228 |
+
|
| 229 |
+
{/* Two Columns */}
|
| 230 |
+
<div className="grid md:grid-cols-2 gap-8 h-96">
|
| 231 |
+
{/* Left Column */}
|
| 232 |
+
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-6 border border-white/20">
|
| 233 |
+
{body?.[0]?.heading && (
|
| 234 |
+
<h3 className="text-xl font-semibold mb-4 drop-shadow-md">{body[0].heading}</h3>
|
| 235 |
+
)}
|
| 236 |
+
{editingField?.field === 'body' && editingField.index === 0 ? (
|
| 237 |
+
<textarea
|
| 238 |
+
value={tempBody[0] || ''}
|
| 239 |
+
onChange={(e) => updateTempBodyItem(0, e.target.value)}
|
| 240 |
+
onBlur={() => handleFieldBlur('body', 0)}
|
| 241 |
+
onKeyDown={(e) => handleKeyDown(e, 'body', 0)}
|
| 242 |
+
className="w-full h-32 bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70 resize-none"
|
| 243 |
+
placeholder="Left column content"
|
| 244 |
+
autoFocus
|
| 245 |
+
/>
|
| 246 |
+
) : (
|
| 247 |
+
<p
|
| 248 |
+
className={`opacity-90 leading-relaxed drop-shadow-sm ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''}`}
|
| 249 |
+
onClick={() => handleFieldClick('body', 0)}
|
| 250 |
+
>
|
| 251 |
+
{body?.[0]?.text || 'Left column content'}
|
| 252 |
+
</p>
|
| 253 |
+
)}
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
{/* Right Column */}
|
| 257 |
+
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-6 border border-white/20">
|
| 258 |
+
{body?.[1]?.heading && (
|
| 259 |
+
<h3 className="text-xl font-semibold mb-4 drop-shadow-md">{body[1].heading}</h3>
|
| 260 |
+
)}
|
| 261 |
+
{editingField?.field === 'body' && editingField.index === 1 ? (
|
| 262 |
+
<textarea
|
| 263 |
+
value={tempBody[1] || ''}
|
| 264 |
+
onChange={(e) => updateTempBodyItem(1, e.target.value)}
|
| 265 |
+
onBlur={() => handleFieldBlur('body', 1)}
|
| 266 |
+
onKeyDown={(e) => handleKeyDown(e, 'body', 1)}
|
| 267 |
+
className="w-full h-32 bg-transparent border-2 border-white/50 rounded px-2 py-1 outline-none text-white placeholder-white/70 resize-none"
|
| 268 |
+
placeholder="Right column content"
|
| 269 |
+
autoFocus
|
| 270 |
+
/>
|
| 271 |
+
) : (
|
| 272 |
+
<p
|
| 273 |
+
className={`opacity-90 leading-relaxed drop-shadow-sm ${isEditable ? 'cursor-pointer hover:bg-white/10 rounded px-2 py-1' : ''}`}
|
| 274 |
+
onClick={() => handleFieldClick('body', 1)}
|
| 275 |
+
>
|
| 276 |
+
{body?.[1]?.text || 'Right column content'}
|
| 277 |
+
</p>
|
| 278 |
+
)}
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
);
|
| 284 |
+
}
|
data/templates.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { SlideSpec } from '@/components/slides/SlideFactory';
|
| 2 |
+
|
| 3 |
+
export interface Template {
|
| 4 |
+
id: string;
|
| 5 |
+
name: string;
|
| 6 |
+
description: string;
|
| 7 |
+
thumbnail: string;
|
| 8 |
+
theme: string;
|
| 9 |
+
slides: Omit<SlideSpec, 'id'>[];
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const professionalTemplates: Template[] = [
|
| 13 |
+
{
|
| 14 |
+
id: 'business-pitch',
|
| 15 |
+
name: 'Business Pitch',
|
| 16 |
+
description: 'Professional presentation for business proposals',
|
| 17 |
+
thumbnail: '💼',
|
| 18 |
+
theme: 'midnight',
|
| 19 |
+
slides: [
|
| 20 |
+
{
|
| 21 |
+
layout: 'hero',
|
| 22 |
+
title: 'Your Company Name',
|
| 23 |
+
subtitle: 'Innovative Solutions for Tomorrow'
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
layout: 'section',
|
| 27 |
+
title: 'Problem & Opportunity',
|
| 28 |
+
subtitle: 'Understanding the market challenge',
|
| 29 |
+
sectionNumber: '01'
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
layout: 'bullets',
|
| 33 |
+
title: 'Market Problem',
|
| 34 |
+
body: [
|
| 35 |
+
{ text: 'Current solutions are outdated and inefficient' },
|
| 36 |
+
{ text: '$2.5B market opportunity waiting to be captured' },
|
| 37 |
+
{ text: 'Customers struggling with legacy systems' },
|
| 38 |
+
{ text: '45% increase in demand for modern solutions' }
|
| 39 |
+
]
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
layout: 'section',
|
| 43 |
+
title: 'Our Solution',
|
| 44 |
+
subtitle: 'Revolutionary approach to solving industry challenges',
|
| 45 |
+
sectionNumber: '02'
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
layout: 'two-column',
|
| 49 |
+
title: 'How We Solve It',
|
| 50 |
+
body: [
|
| 51 |
+
{ heading: 'Traditional Approach', text: 'Manual processes\nHigh error rates\nSlow implementation\nLimited scalability' },
|
| 52 |
+
{ heading: 'Our Innovation', text: 'Automated workflows\n99.9% accuracy\nRapid deployment\nInfinite scalability' }
|
| 53 |
+
]
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
layout: 'stats',
|
| 57 |
+
title: 'Proven Results',
|
| 58 |
+
stats: [
|
| 59 |
+
{ number: '300%', label: 'ROI Increase' },
|
| 60 |
+
{ number: '50K+', label: 'Users' },
|
| 61 |
+
{ number: '99.9%', label: 'Uptime' }
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
layout: 'quote',
|
| 66 |
+
quote: 'This solution transformed our entire operation. We\'ve never seen results like this before.',
|
| 67 |
+
author: 'Jane Smith',
|
| 68 |
+
company: 'Fortune 500 Company'
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
layout: 'section',
|
| 72 |
+
title: 'Financial Projections',
|
| 73 |
+
subtitle: 'Expected growth and revenue targets',
|
| 74 |
+
sectionNumber: '03'
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
layout: 'chart',
|
| 78 |
+
title: 'Revenue Growth Forecast',
|
| 79 |
+
chart: {
|
| 80 |
+
type: 'bar',
|
| 81 |
+
data: {
|
| 82 |
+
labels: ['Year 1', 'Year 2', 'Year 3', 'Year 4', 'Year 5'],
|
| 83 |
+
datasets: [{
|
| 84 |
+
data: [250, 500, 1200, 2800, 5000]
|
| 85 |
+
}]
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
layout: 'comparison',
|
| 91 |
+
title: 'Investment Options',
|
| 92 |
+
leftTitle: 'Basic Package',
|
| 93 |
+
rightTitle: 'Premium Package',
|
| 94 |
+
leftContent: [
|
| 95 |
+
'Core features',
|
| 96 |
+
'Email support',
|
| 97 |
+
'1 year license',
|
| 98 |
+
'Basic training'
|
| 99 |
+
],
|
| 100 |
+
rightContent: [
|
| 101 |
+
'All premium features',
|
| 102 |
+
'24/7 phone support',
|
| 103 |
+
'Lifetime license',
|
| 104 |
+
'Comprehensive training'
|
| 105 |
+
]
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
layout: 'hero',
|
| 109 |
+
title: 'Let\'s Build the Future Together',
|
| 110 |
+
subtitle: 'Ready to transform your business?'
|
| 111 |
+
}
|
| 112 |
+
]
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
id: 'startup-deck',
|
| 116 |
+
name: 'Startup Pitch Deck',
|
| 117 |
+
description: 'Complete investor pitch deck template',
|
| 118 |
+
thumbnail: '🚀',
|
| 119 |
+
theme: 'blue',
|
| 120 |
+
slides: [
|
| 121 |
+
{
|
| 122 |
+
layout: 'hero',
|
| 123 |
+
title: 'Startup Name',
|
| 124 |
+
subtitle: 'Disrupting the Industry'
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
layout: 'bullets',
|
| 128 |
+
title: 'The Problem',
|
| 129 |
+
body: [
|
| 130 |
+
{ text: 'Market inefficiency costs billions annually' },
|
| 131 |
+
{ text: 'Current solutions are fragmented and complex' },
|
| 132 |
+
{ text: 'Users demand better, faster alternatives' }
|
| 133 |
+
]
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
layout: 'image-only',
|
| 137 |
+
title: 'Our Solution',
|
| 138 |
+
subtitle: 'Revolutionary platform that changes everything'
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
layout: 'stats',
|
| 142 |
+
title: 'Market Opportunity',
|
| 143 |
+
stats: [
|
| 144 |
+
{ number: '$50B', label: 'Total Market' },
|
| 145 |
+
{ number: '12M', label: 'Target Users' },
|
| 146 |
+
{ number: '23%', label: 'Annual Growth' }
|
| 147 |
+
]
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
layout: 'bullets',
|
| 151 |
+
title: 'Business Model',
|
| 152 |
+
body: [
|
| 153 |
+
{ text: 'SaaS subscription with tiered pricing' },
|
| 154 |
+
{ text: 'Premium features for enterprise clients' },
|
| 155 |
+
{ text: 'Marketplace revenue sharing' },
|
| 156 |
+
{ text: 'Data analytics and insights' }
|
| 157 |
+
]
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
layout: 'chart',
|
| 161 |
+
title: 'Financial Projections',
|
| 162 |
+
chart: {
|
| 163 |
+
type: 'line',
|
| 164 |
+
data: {
|
| 165 |
+
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
|
| 166 |
+
datasets: [{
|
| 167 |
+
data: [100, 250, 600, 1200]
|
| 168 |
+
}]
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
layout: 'two-column',
|
| 174 |
+
title: 'Team & Advisors',
|
| 175 |
+
body: [
|
| 176 |
+
{ heading: 'Core Team', text: 'Experienced founders\nTop-tier engineers\nIndustry veterans\nProven track record' },
|
| 177 |
+
{ heading: 'Advisory Board', text: 'Former C-suite executives\nSerial entrepreneurs\nIndustry thought leaders\nTechnical experts' }
|
| 178 |
+
]
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
layout: 'stats',
|
| 182 |
+
title: 'Funding Request',
|
| 183 |
+
stats: [
|
| 184 |
+
{ number: '$2M', label: 'Series A' },
|
| 185 |
+
{ number: '18', label: 'Months Runway' },
|
| 186 |
+
{ number: '10x', label: 'Expected Return' }
|
| 187 |
+
]
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
layout: 'hero',
|
| 191 |
+
title: 'Join Us in Changing the World',
|
| 192 |
+
subtitle: 'Together, we can build something extraordinary'
|
| 193 |
+
}
|
| 194 |
+
]
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
id: 'product-launch',
|
| 198 |
+
name: 'Product Launch',
|
| 199 |
+
description: 'Comprehensive product introduction presentation',
|
| 200 |
+
thumbnail: '📱',
|
| 201 |
+
theme: 'purple',
|
| 202 |
+
slides: [
|
| 203 |
+
{
|
| 204 |
+
layout: 'hero',
|
| 205 |
+
title: 'Introducing [Product Name]',
|
| 206 |
+
subtitle: 'The future of [industry] is here'
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
layout: 'section',
|
| 210 |
+
title: 'Market Research',
|
| 211 |
+
subtitle: 'Understanding our customers\' needs',
|
| 212 |
+
sectionNumber: '01'
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
layout: 'stats',
|
| 216 |
+
title: 'Market Insights',
|
| 217 |
+
stats: [
|
| 218 |
+
{ number: '78%', label: 'Want Better Solutions' },
|
| 219 |
+
{ number: '2.3M', label: 'Potential Customers' },
|
| 220 |
+
{ number: '$45', label: 'Avg. Monthly Spend' }
|
| 221 |
+
]
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
layout: 'section',
|
| 225 |
+
title: 'Product Features',
|
| 226 |
+
subtitle: 'What makes our product special',
|
| 227 |
+
sectionNumber: '02'
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
layout: 'image-only',
|
| 231 |
+
title: 'Product Demo',
|
| 232 |
+
subtitle: 'See it in action'
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
layout: 'two-column',
|
| 236 |
+
title: 'Key Benefits',
|
| 237 |
+
body: [
|
| 238 |
+
{ heading: 'For Users', text: 'Intuitive interface\nTime-saving features\nSeamless integration\nMobile-first design' },
|
| 239 |
+
{ heading: 'For Business', text: 'Increased productivity\nCost reduction\nScalable solution\nReal-time analytics' }
|
| 240 |
+
]
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
layout: 'comparison',
|
| 244 |
+
title: 'Competitive Advantage',
|
| 245 |
+
leftTitle: 'Competitors',
|
| 246 |
+
rightTitle: 'Our Product',
|
| 247 |
+
leftContent: [
|
| 248 |
+
'Complex setup process',
|
| 249 |
+
'Limited customization',
|
| 250 |
+
'Poor mobile experience',
|
| 251 |
+
'High learning curve'
|
| 252 |
+
],
|
| 253 |
+
rightContent: [
|
| 254 |
+
'One-click setup',
|
| 255 |
+
'Fully customizable',
|
| 256 |
+
'Mobile-optimized',
|
| 257 |
+
'Intuitive from day one'
|
| 258 |
+
]
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
layout: 'section',
|
| 262 |
+
title: 'Go-to-Market Strategy',
|
| 263 |
+
subtitle: 'How we\'ll reach our customers',
|
| 264 |
+
sectionNumber: '03'
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
layout: 'bullets',
|
| 268 |
+
title: 'Launch Strategy',
|
| 269 |
+
body: [
|
| 270 |
+
{ text: 'Soft launch with beta customers' },
|
| 271 |
+
{ text: 'Influencer partnerships and PR campaign' },
|
| 272 |
+
{ text: 'Product Hunt and tech media coverage' },
|
| 273 |
+
{ text: 'Direct sales to enterprise customers' },
|
| 274 |
+
{ text: 'Content marketing and SEO strategy' }
|
| 275 |
+
]
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
layout: 'stats',
|
| 279 |
+
title: 'Launch Targets',
|
| 280 |
+
stats: [
|
| 281 |
+
{ number: '1K', label: 'Beta Users' },
|
| 282 |
+
{ number: '10K', label: 'Month 1 Users' },
|
| 283 |
+
{ number: '$100K', label: 'Month 3 Revenue' }
|
| 284 |
+
]
|
| 285 |
+
},
|
| 286 |
+
{
|
| 287 |
+
layout: 'hero',
|
| 288 |
+
title: 'Ready to Launch',
|
| 289 |
+
subtitle: 'The future starts now'
|
| 290 |
+
}
|
| 291 |
+
]
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
id: 'company-overview',
|
| 295 |
+
name: 'Company Overview',
|
| 296 |
+
description: 'Professional company introduction',
|
| 297 |
+
thumbnail: '🏢',
|
| 298 |
+
theme: 'emerald',
|
| 299 |
+
slides: [
|
| 300 |
+
{
|
| 301 |
+
layout: 'hero',
|
| 302 |
+
title: 'Company Name',
|
| 303 |
+
subtitle: 'Leading the industry since 2020'
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
layout: 'section',
|
| 307 |
+
title: 'Our Mission',
|
| 308 |
+
subtitle: 'What drives us every day',
|
| 309 |
+
sectionNumber: '01'
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
layout: 'quote',
|
| 313 |
+
quote: 'To empower businesses with innovative technology solutions that drive growth and create lasting value.',
|
| 314 |
+
author: 'CEO Name',
|
| 315 |
+
company: 'Company Name'
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
layout: 'stats',
|
| 319 |
+
title: 'Company at a Glance',
|
| 320 |
+
stats: [
|
| 321 |
+
{ number: '500+', label: 'Employees' },
|
| 322 |
+
{ number: '$50M', label: 'Annual Revenue' },
|
| 323 |
+
{ number: '15+', label: 'Countries' }
|
| 324 |
+
]
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
layout: 'section',
|
| 328 |
+
title: 'Our Services',
|
| 329 |
+
subtitle: 'Comprehensive solutions for modern businesses',
|
| 330 |
+
sectionNumber: '02'
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
layout: 'two-column',
|
| 334 |
+
title: 'What We Offer',
|
| 335 |
+
body: [
|
| 336 |
+
{ heading: 'Technology Solutions', text: 'Custom software development\nCloud infrastructure\nCybersecurity services\nData analytics platforms' },
|
| 337 |
+
{ heading: 'Consulting Services', text: 'Digital transformation\nProcess optimization\nStrategic planning\nChange management' }
|
| 338 |
+
]
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
layout: 'bullets',
|
| 342 |
+
title: 'Industry Expertise',
|
| 343 |
+
body: [
|
| 344 |
+
{ text: 'Financial Services - Banking, insurance, fintech' },
|
| 345 |
+
{ text: 'Healthcare - Electronic records, telemedicine' },
|
| 346 |
+
{ text: 'Retail - E-commerce, inventory management' },
|
| 347 |
+
{ text: 'Manufacturing - Supply chain, automation' },
|
| 348 |
+
{ text: 'Education - Learning platforms, student systems' }
|
| 349 |
+
]
|
| 350 |
+
},
|
| 351 |
+
{
|
| 352 |
+
layout: 'section',
|
| 353 |
+
title: 'Our Success Stories',
|
| 354 |
+
subtitle: 'Real results for real clients',
|
| 355 |
+
sectionNumber: '03'
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
layout: 'quote',
|
| 359 |
+
quote: 'Working with this company transformed our operations. We achieved 200% growth in just one year.',
|
| 360 |
+
author: 'Client Name',
|
| 361 |
+
company: 'Fortune 500 Company'
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
layout: 'stats',
|
| 365 |
+
title: 'Client Success Metrics',
|
| 366 |
+
stats: [
|
| 367 |
+
{ number: '200%', label: 'Avg. Growth' },
|
| 368 |
+
{ number: '98%', label: 'Client Retention' },
|
| 369 |
+
{ number: '4.9/5', label: 'Satisfaction' }
|
| 370 |
+
]
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
layout: 'hero',
|
| 374 |
+
title: 'Partner With Us',
|
| 375 |
+
subtitle: 'Let\'s build something amazing together'
|
| 376 |
+
}
|
| 377 |
+
]
|
| 378 |
+
}
|
| 379 |
+
];
|
| 380 |
+
|
| 381 |
+
// Helper function to create slides with unique IDs
|
| 382 |
+
export function createTemplateSlides(template: Template): SlideSpec[] {
|
| 383 |
+
return template.slides.map((slide, index) => ({
|
| 384 |
+
...slide,
|
| 385 |
+
id: `${template.id}-slide-${index + 1}`
|
| 386 |
+
}));
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
export function getTemplateById(id: string): Template | undefined {
|
| 390 |
+
return professionalTemplates.find(template => template.id === id);
|
| 391 |
+
}
|
lib/gemini-client.ts
CHANGED
|
@@ -17,11 +17,23 @@ export class GeminiClient {
|
|
| 17 |
|
| 18 |
async generateSlideContent(prompt: string): Promise<string> {
|
| 19 |
try {
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
const result = await this.model.generateContent(fullPrompt);
|
| 23 |
const response = await result.response;
|
| 24 |
-
return response.text() || '';
|
| 25 |
} catch (error) {
|
| 26 |
console.error('Error generating slide content with Gemini:', error);
|
| 27 |
throw new Error('Failed to generate slide content');
|
|
|
|
| 17 |
|
| 18 |
async generateSlideContent(prompt: string): Promise<string> {
|
| 19 |
try {
|
| 20 |
+
// The prompt already contains the JSON schema instructions from orchestrator
|
| 21 |
+
const result = await this.model.generateContent({
|
| 22 |
+
contents: [{
|
| 23 |
+
role: 'user',
|
| 24 |
+
parts: [{ text: prompt }]
|
| 25 |
+
}],
|
| 26 |
+
generationConfig: {
|
| 27 |
+
temperature: 0.4,
|
| 28 |
+
topK: 1,
|
| 29 |
+
topP: 1,
|
| 30 |
+
maxOutputTokens: 4096,
|
| 31 |
+
responseMimeType: 'application/json'
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
|
|
|
|
| 35 |
const response = await result.response;
|
| 36 |
+
return response.text() || '{}';
|
| 37 |
} catch (error) {
|
| 38 |
console.error('Error generating slide content with Gemini:', error);
|
| 39 |
throw new Error('Failed to generate slide content');
|
package-lock.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
"dependencies": {
|
| 11 |
"@auth/core": "^0.34.2",
|
| 12 |
"@google/generative-ai": "^0.24.1",
|
|
|
|
| 13 |
"@huggingface/inference": "^4.8.0",
|
| 14 |
"@lobehub/icons": "^2.33.0",
|
| 15 |
"@radix-ui/react-select": "^2.2.6",
|
|
@@ -30,7 +31,8 @@
|
|
| 30 |
"react-chartjs-2": "^5.3.0",
|
| 31 |
"react-dom": "19.1.0",
|
| 32 |
"swiper": "^11.2.10",
|
| 33 |
-
"tailwind-merge": "^3.3.1"
|
|
|
|
| 34 |
},
|
| 35 |
"devDependencies": {
|
| 36 |
"@eslint/eslintrc": "^3",
|
|
@@ -1836,6 +1838,24 @@
|
|
| 1836 |
"node": ">=18.0.0"
|
| 1837 |
}
|
| 1838 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1839 |
"node_modules/@huggingface/inference": {
|
| 1840 |
"version": "4.8.0",
|
| 1841 |
"resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.8.0.tgz",
|
|
@@ -1859,9 +1879,9 @@
|
|
| 1859 |
}
|
| 1860 |
},
|
| 1861 |
"node_modules/@huggingface/tasks": {
|
| 1862 |
-
"version": "0.19.
|
| 1863 |
-
"resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.
|
| 1864 |
-
"integrity": "sha512-
|
| 1865 |
"license": "MIT"
|
| 1866 |
},
|
| 1867 |
"node_modules/@humanfs/core": {
|
|
@@ -5722,6 +5742,16 @@
|
|
| 5722 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 5723 |
}
|
| 5724 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5725 |
"node_modules/ansi-styles": {
|
| 5726 |
"version": "4.3.0",
|
| 5727 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
@@ -6568,6 +6598,19 @@
|
|
| 6568 |
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
| 6569 |
"license": "MIT"
|
| 6570 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6571 |
"node_modules/client-only": {
|
| 6572 |
"version": "0.0.1",
|
| 6573 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
@@ -9796,6 +9839,16 @@
|
|
| 9796 |
"url": "https://github.com/sponsors/ljharb"
|
| 9797 |
}
|
| 9798 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9799 |
"node_modules/is-generator-function": {
|
| 9800 |
"version": "1.1.0",
|
| 9801 |
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
|
|
@@ -15384,6 +15437,28 @@
|
|
| 15384 |
"license": "MIT",
|
| 15385 |
"peer": true
|
| 15386 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15387 |
"node_modules/string.prototype.includes": {
|
| 15388 |
"version": "2.0.1",
|
| 15389 |
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
|
@@ -15511,6 +15586,19 @@
|
|
| 15511 |
"url": "https://github.com/sponsors/wooorm"
|
| 15512 |
}
|
| 15513 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15514 |
"node_modules/strip-bom": {
|
| 15515 |
"version": "3.0.0",
|
| 15516 |
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
|
@@ -16206,6 +16294,15 @@
|
|
| 16206 |
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
|
| 16207 |
}
|
| 16208 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16209 |
"node_modules/update-browserslist-db": {
|
| 16210 |
"version": "1.1.3",
|
| 16211 |
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"@auth/core": "^0.34.2",
|
| 12 |
"@google/generative-ai": "^0.24.1",
|
| 13 |
+
"@huggingface/hub": "^2.6.5",
|
| 14 |
"@huggingface/inference": "^4.8.0",
|
| 15 |
"@lobehub/icons": "^2.33.0",
|
| 16 |
"@radix-ui/react-select": "^2.2.6",
|
|
|
|
| 31 |
"react-chartjs-2": "^5.3.0",
|
| 32 |
"react-dom": "19.1.0",
|
| 33 |
"swiper": "^11.2.10",
|
| 34 |
+
"tailwind-merge": "^3.3.1",
|
| 35 |
+
"unsplash-js": "^7.0.20"
|
| 36 |
},
|
| 37 |
"devDependencies": {
|
| 38 |
"@eslint/eslintrc": "^3",
|
|
|
|
| 1838 |
"node": ">=18.0.0"
|
| 1839 |
}
|
| 1840 |
},
|
| 1841 |
+
"node_modules/@huggingface/hub": {
|
| 1842 |
+
"version": "2.6.5",
|
| 1843 |
+
"resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.6.5.tgz",
|
| 1844 |
+
"integrity": "sha512-3O20Pe9bSXQ/4LxUNnf75jZAZaYvF/3lVdIU0GrJOL3oTlneuxdRHZ9AXuu25hYY05fyng0G83EIS5Xn9uJtGg==",
|
| 1845 |
+
"license": "MIT",
|
| 1846 |
+
"dependencies": {
|
| 1847 |
+
"@huggingface/tasks": "^0.19.46"
|
| 1848 |
+
},
|
| 1849 |
+
"bin": {
|
| 1850 |
+
"hfjs": "dist/cli.js"
|
| 1851 |
+
},
|
| 1852 |
+
"engines": {
|
| 1853 |
+
"node": ">=18"
|
| 1854 |
+
},
|
| 1855 |
+
"optionalDependencies": {
|
| 1856 |
+
"cli-progress": "^3.12.0"
|
| 1857 |
+
}
|
| 1858 |
+
},
|
| 1859 |
"node_modules/@huggingface/inference": {
|
| 1860 |
"version": "4.8.0",
|
| 1861 |
"resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.8.0.tgz",
|
|
|
|
| 1879 |
}
|
| 1880 |
},
|
| 1881 |
"node_modules/@huggingface/tasks": {
|
| 1882 |
+
"version": "0.19.46",
|
| 1883 |
+
"resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.46.tgz",
|
| 1884 |
+
"integrity": "sha512-c6F/r7zRQjmyo6Ji8c2TUbdeeu6WAdZxYLRd+G7Xxvfbadi6iDwk2szt/oinC5v5Ljyc2sjzesaqGB6hLWy/DA==",
|
| 1885 |
"license": "MIT"
|
| 1886 |
},
|
| 1887 |
"node_modules/@humanfs/core": {
|
|
|
|
| 5742 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 5743 |
}
|
| 5744 |
},
|
| 5745 |
+
"node_modules/ansi-regex": {
|
| 5746 |
+
"version": "5.0.1",
|
| 5747 |
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
| 5748 |
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
| 5749 |
+
"license": "MIT",
|
| 5750 |
+
"optional": true,
|
| 5751 |
+
"engines": {
|
| 5752 |
+
"node": ">=8"
|
| 5753 |
+
}
|
| 5754 |
+
},
|
| 5755 |
"node_modules/ansi-styles": {
|
| 5756 |
"version": "4.3.0",
|
| 5757 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
|
|
| 6598 |
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
| 6599 |
"license": "MIT"
|
| 6600 |
},
|
| 6601 |
+
"node_modules/cli-progress": {
|
| 6602 |
+
"version": "3.12.0",
|
| 6603 |
+
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
| 6604 |
+
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
|
| 6605 |
+
"license": "MIT",
|
| 6606 |
+
"optional": true,
|
| 6607 |
+
"dependencies": {
|
| 6608 |
+
"string-width": "^4.2.3"
|
| 6609 |
+
},
|
| 6610 |
+
"engines": {
|
| 6611 |
+
"node": ">=4"
|
| 6612 |
+
}
|
| 6613 |
+
},
|
| 6614 |
"node_modules/client-only": {
|
| 6615 |
"version": "0.0.1",
|
| 6616 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
|
|
| 9839 |
"url": "https://github.com/sponsors/ljharb"
|
| 9840 |
}
|
| 9841 |
},
|
| 9842 |
+
"node_modules/is-fullwidth-code-point": {
|
| 9843 |
+
"version": "3.0.0",
|
| 9844 |
+
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
| 9845 |
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
| 9846 |
+
"license": "MIT",
|
| 9847 |
+
"optional": true,
|
| 9848 |
+
"engines": {
|
| 9849 |
+
"node": ">=8"
|
| 9850 |
+
}
|
| 9851 |
+
},
|
| 9852 |
"node_modules/is-generator-function": {
|
| 9853 |
"version": "1.1.0",
|
| 9854 |
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
|
|
|
|
| 15437 |
"license": "MIT",
|
| 15438 |
"peer": true
|
| 15439 |
},
|
| 15440 |
+
"node_modules/string-width": {
|
| 15441 |
+
"version": "4.2.3",
|
| 15442 |
+
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
| 15443 |
+
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
| 15444 |
+
"license": "MIT",
|
| 15445 |
+
"optional": true,
|
| 15446 |
+
"dependencies": {
|
| 15447 |
+
"emoji-regex": "^8.0.0",
|
| 15448 |
+
"is-fullwidth-code-point": "^3.0.0",
|
| 15449 |
+
"strip-ansi": "^6.0.1"
|
| 15450 |
+
},
|
| 15451 |
+
"engines": {
|
| 15452 |
+
"node": ">=8"
|
| 15453 |
+
}
|
| 15454 |
+
},
|
| 15455 |
+
"node_modules/string-width/node_modules/emoji-regex": {
|
| 15456 |
+
"version": "8.0.0",
|
| 15457 |
+
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
| 15458 |
+
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
| 15459 |
+
"license": "MIT",
|
| 15460 |
+
"optional": true
|
| 15461 |
+
},
|
| 15462 |
"node_modules/string.prototype.includes": {
|
| 15463 |
"version": "2.0.1",
|
| 15464 |
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
|
|
|
| 15586 |
"url": "https://github.com/sponsors/wooorm"
|
| 15587 |
}
|
| 15588 |
},
|
| 15589 |
+
"node_modules/strip-ansi": {
|
| 15590 |
+
"version": "6.0.1",
|
| 15591 |
+
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
| 15592 |
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
| 15593 |
+
"license": "MIT",
|
| 15594 |
+
"optional": true,
|
| 15595 |
+
"dependencies": {
|
| 15596 |
+
"ansi-regex": "^5.0.1"
|
| 15597 |
+
},
|
| 15598 |
+
"engines": {
|
| 15599 |
+
"node": ">=8"
|
| 15600 |
+
}
|
| 15601 |
+
},
|
| 15602 |
"node_modules/strip-bom": {
|
| 15603 |
"version": "3.0.0",
|
| 15604 |
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
|
|
|
| 16294 |
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
|
| 16295 |
}
|
| 16296 |
},
|
| 16297 |
+
"node_modules/unsplash-js": {
|
| 16298 |
+
"version": "7.0.20",
|
| 16299 |
+
"resolved": "https://registry.npmjs.org/unsplash-js/-/unsplash-js-7.0.20.tgz",
|
| 16300 |
+
"integrity": "sha512-0IzJGKbD6qHm0wg5sNndcCjhNDrtAr08/mySr9TGJ6biD7N03ZldidqXopkOvhAABaeFtRGYBrX1YkgMhKrFOQ==",
|
| 16301 |
+
"license": "MIT",
|
| 16302 |
+
"engines": {
|
| 16303 |
+
"node": ">=10"
|
| 16304 |
+
}
|
| 16305 |
+
},
|
| 16306 |
"node_modules/update-browserslist-db": {
|
| 16307 |
"version": "1.1.3",
|
| 16308 |
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
| 11 |
"dependencies": {
|
| 12 |
"@auth/core": "^0.34.2",
|
| 13 |
"@google/generative-ai": "^0.24.1",
|
|
|
|
| 14 |
"@huggingface/inference": "^4.8.0",
|
| 15 |
"@lobehub/icons": "^2.33.0",
|
| 16 |
"@radix-ui/react-select": "^2.2.6",
|
|
@@ -31,7 +32,8 @@
|
|
| 31 |
"react-chartjs-2": "^5.3.0",
|
| 32 |
"react-dom": "19.1.0",
|
| 33 |
"swiper": "^11.2.10",
|
| 34 |
-
"tailwind-merge": "^3.3.1"
|
|
|
|
| 35 |
},
|
| 36 |
"devDependencies": {
|
| 37 |
"@eslint/eslintrc": "^3",
|
|
|
|
| 11 |
"dependencies": {
|
| 12 |
"@auth/core": "^0.34.2",
|
| 13 |
"@google/generative-ai": "^0.24.1",
|
| 14 |
+
"@huggingface/hub": "^2.6.5",
|
| 15 |
"@huggingface/inference": "^4.8.0",
|
| 16 |
"@lobehub/icons": "^2.33.0",
|
| 17 |
"@radix-ui/react-select": "^2.2.6",
|
|
|
|
| 32 |
"react-chartjs-2": "^5.3.0",
|
| 33 |
"react-dom": "19.1.0",
|
| 34 |
"swiper": "^11.2.10",
|
| 35 |
+
"tailwind-merge": "^3.3.1",
|
| 36 |
+
"unsplash-js": "^7.0.20"
|
| 37 |
},
|
| 38 |
"devDependencies": {
|
| 39 |
"@eslint/eslintrc": "^3",
|