Reubencf commited on
Commit
d764ce3
·
1 Parent(s): f5160dc

major changes

Browse files
.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 title
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  slide.addText(slideData.title, {
41
  x: 0.5,
42
- y: 0.5,
43
  w: 9,
44
- h: 1,
45
- fontSize: 32,
46
  bold: true,
47
- color: '363636',
48
- align: 'center',
 
 
 
 
 
 
 
 
 
49
  });
50
 
51
- // Add content points
52
  if (slideData.content && slideData.content.length > 0) {
53
- const contentText = slideData.content
54
- .map(point => `• ${point}`)
55
- .join('\n');
56
-
57
- slide.addText(contentText, {
58
- x: 1,
59
- y: 2,
60
- w: 8,
61
- h: 3,
62
- fontSize: 18,
63
- color: '363636',
64
- lineSpacing: 32,
65
- valign: 'top',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  });
67
  }
68
 
69
- // Add slide number
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  slide.addText(`${index + 1}`, {
71
- x: 9.2,
72
- y: 5,
73
- w: 0.5,
74
- h: 0.3,
75
- fontSize: 12,
76
- color: '999999',
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
- interface PresentationData {
7
- theme: string;
8
- slides: SlideSpec[];
9
- }
10
-
11
- export default function EditorPage() {
12
- const [presentation, setPresentation] = useState<PresentationData | null>(null);
13
- const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
14
-
15
- useEffect(() => {
16
- // Load presentation data from sessionStorage
17
- const savedPresentation = sessionStorage.getItem('currentPresentation');
18
- if (savedPresentation) {
19
- try {
20
- const data = JSON.parse(savedPresentation);
21
- setPresentation(data);
22
- } catch (error) {
23
- console.error('Error parsing presentation data:', error);
24
- }
25
- }
26
- }, []);
27
-
28
- if (!presentation || !presentation.slides.length) {
29
- return (
30
- <div className="min-h-dvh w-full bg-gradient-to-b from-white to-gray-50 text-gray-900 flex items-center justify-center">
31
- <div className="text-center">
32
- <h1 className="text-2xl font-semibold mb-4">No presentation data found</h1>
33
- <p className="text-gray-600">Please generate a presentation first.</p>
34
- <button onClick={() => window.location.href = '/'} className="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
35
- Go back to home
36
- </button>
37
- </div>
38
- </div>
39
- );
40
- }
41
-
42
- const currentSlide = presentation.slides[currentSlideIndex];
43
-
44
- return (
45
- <div className="min-h-dvh w-full bg-gradient-to-b from-white to-gray-50 text-gray-900">
46
- {/* Ribbon */}
47
- <div className="border-b border-gray-200 bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/80">
48
- <div className="mx-auto max-w-[1400px] px-4">
49
- <div className="flex items-center gap-3 py-2">
50
- <div className="text-sm font-semibold text-orange-600">FILE</div>
51
- <div className="flex items-center gap-5 text-xs text-gray-700">
52
- <span className="font-semibold text-blue-700">HOME</span>
53
- <span>INSERT</span>
54
- <span>DESIGN</span>
55
- <span>TRANSITIONS</span>
56
- <span>ANIMATIONS</span>
57
- <span>SLIDE SHOW</span>
58
- <span>REVIEW</span>
59
- <span>VIEW</span>
60
- </div>
61
- </div>
62
- <div className="grid grid-cols-12 gap-4 py-2 pb-3">
63
- <div className="col-span-2">
64
- <div className="text-xs font-medium text-gray-600 mb-1">Clipboard</div>
65
- <div className="h-10 rounded-md border border-gray-200 bg-white" />
66
- </div>
67
- <div className="col-span-2">
68
- <div className="text-xs font-medium text-gray-600 mb-1">Slides</div>
69
- <div className="h-10 rounded-md border border-gray-200 bg-white" />
70
- </div>
71
- <div className="col-span-3">
72
- <div className="text-xs font-medium text-gray-600 mb-1">Font</div>
73
- <div className="h-10 rounded-md border border-gray-200 bg-white" />
74
- </div>
75
- <div className="col-span-3">
76
- <div className="text-xs font-medium text-gray-600 mb-1">Paragraph</div>
77
- <div className="h-10 rounded-md border border-gray-200 bg-white" />
78
- </div>
79
- <div className="col-span-2">
80
- <div className="text-xs font-medium text-gray-600 mb-1">Drawing</div>
81
- <div className="h-10 rounded-md border border-gray-200 bg-white" />
82
- </div>
83
- </div>
84
- </div>
85
- </div>
86
-
87
- {/* Body */}
88
- <div className="mx-auto max-w-[1400px] px-4 py-6">
89
- <div className="flex gap-6">
90
- {/* Thumbnails */}
91
- <div className="w-[220px] shrink-0 border-r border-gray-200 pr-4">
92
- <div className="text-sm text-gray-600 mb-2">Slides ({presentation.slides.length})</div>
93
- <div className="space-y-3">
94
- {presentation.slides.map((slide, index) => (
95
- <div
96
- key={slide.id}
97
- onClick={() => setCurrentSlideIndex(index)}
98
- className={`relative h-[140px] w-full rounded cursor-pointer transition-all overflow-hidden ${
99
- index === currentSlideIndex
100
- ? 'border-2 border-orange-400 bg-white shadow-lg'
101
- : 'border border-gray-300 bg-gray-50 hover:border-gray-400'
102
- } shadow-[inset_0_0_0_1px_rgba(0,0,0,0.04)]`}
103
- >
104
- <div className="w-full h-full scale-[0.16] origin-top-left transform">
105
- <div className="w-[875px] h-[625px]">
106
- {renderSlide(slide, presentation.theme as 'dark' | 'light')}
107
- </div>
108
- </div>
109
- <div className="absolute bottom-1 left-1 text-xs text-gray-500 bg-white px-1 rounded">
110
- {index + 1}
111
- </div>
112
- </div>
113
- ))}
114
- </div>
115
- </div>
116
-
117
- {/* Canvas */}
118
- <div className="flex-1 overflow-auto">
119
- <div className="relative mx-auto aspect-[16/10] w-[1100px] rounded bg-white shadow-md border border-gray-200">
120
- {currentSlide && renderSlide(currentSlide, presentation.theme as 'dark' | 'light')}
121
- </div>
122
-
123
- {/* Navigation Controls */}
124
- <div className="flex justify-center gap-4 mt-6">
125
- <button
126
- onClick={() => setCurrentSlideIndex(Math.max(0, currentSlideIndex - 1))}
127
- disabled={currentSlideIndex === 0}
128
- className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400 disabled:cursor-not-allowed hover:bg-blue-700"
129
- >
130
- Previous
131
- </button>
132
- <span className="px-4 py-2 text-gray-600">
133
- {currentSlideIndex + 1} of {presentation.slides.length}
134
- </span>
135
- <button
136
- onClick={() => setCurrentSlideIndex(Math.min(presentation.slides.length - 1, currentSlideIndex + 1))}
137
- disabled={currentSlideIndex === presentation.slides.length - 1}
138
- className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400 disabled:cursor-not-allowed hover:bg-blue-700"
139
- >
140
- Next
141
- </button>
142
- </div>
143
- </div>
144
- </div>
145
- </div>
146
- </div>
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 handleLogin = async () => {
59
- const apiKey = prompt('Enter your HF Pro API Key:');
60
- if (!apiKey) return;
 
 
61
 
62
- try {
63
- const response = await fetch('/api/auth/hf', {
64
- method: 'POST',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- },
68
- body: JSON.stringify({ apiKey }),
69
- });
70
 
71
- if (response.ok) {
72
- setIsAuthenticated(true);
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
- export default function BulletsSlide({ title, body, theme = 'dark' }: BulletsSlideProps) {
10
- const isDark = theme === 'dark';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  return (
12
- <div className={`w-full h-full ${isDark ? 'bg-black text-white' : 'bg-white text-slate-900'} p-10`}>
13
- <h2 className="text-4xl font-semibold mb-8">{title}</h2>
14
- <div className="space-y-6">
15
- {body?.map((b, i) => (
16
- <div key={i} className="flex items-start gap-4">
17
- <div className="w-2 h-2 bg-current rounded-full mt-2 shrink-0" />
18
- <div>
19
- {b.heading && <div className="font-semibold text-lg mb-1">{b.heading}</div>}
20
- <p className="opacity-90 leading-relaxed text-lg">{b.text}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
- </div>
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
- export default function ChartSlide({ title, chart, theme = 'dark' }: ChartSlideProps) {
14
- const isDark = theme === 'dark';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  return (
17
- <div className={`w-full h-full ${isDark ? 'bg-black text-white' : 'bg-white text-slate-900'} p-10`}>
18
- <h2 className="text-4xl font-semibold mb-8 text-center">{title}</h2>
19
- <div className="flex-1 flex items-center justify-center">
20
- {chart ? (
21
- <div className="w-full max-w-2xl h-96 flex items-center justify-center border border-gray-300 rounded-lg">
22
- <div className="text-center">
23
- <div className="text-lg font-medium mb-2">Chart: {chart.type}</div>
24
- <div className="text-sm opacity-70">
25
- {chart.data?.labels?.join(', ') || 'Chart data'}
26
- </div>
27
- {chart.data?.datasets?.[0]?.data && (
28
- <div className="mt-4 flex gap-2 justify-center">
29
- {chart.data.datasets[0].data.map((value: number, i: number) => (
30
- <div key={i} className="text-center">
31
- <div className="w-8 bg-blue-500 rounded-t" style={{ height: `${Math.max(8, value)}px` }}></div>
32
- <div className="text-xs mt-1">{value}</div>
33
- </div>
34
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  </div>
36
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
38
- </div>
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
- export default function ContentImageSlide({ title, body, imageUrl, theme = 'dark' }: ContentImageSlideProps) {
11
- const isDark = theme === 'dark';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  return (
13
- <div className={`w-full h-full ${isDark ? 'bg-black text-white' : 'bg-white text-slate-900'} p-10 grid grid-cols-1 md:grid-cols-2 gap-8 items-center`}>
14
- <div>
15
- <h2 className="text-3xl font-semibold mb-4">{title}</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 className="w-full h-72 md:h-full rounded-xl overflow-hidden border border-white/10">
 
 
 
26
  {imageUrl ? (
27
- // eslint-disable-next-line @next/next/no-img-element
28
- <img src={imageUrl} alt="" className="w-full h-full object-cover" />
 
 
 
 
 
 
 
 
 
29
  ) : (
30
- <div className="w-full h-full grid place-items-center opacity-70">No image</div>
 
 
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(spec: SlideSpec, theme: 'dark' | 'light' = 'dark') {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  switch (spec.layout) {
23
  case 'title':
24
- return <TitleSlide title={spec.title || ''} subtitle={spec.subtitle} theme={theme} />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  case 'content-image':
26
- return <ContentImageSlide title={spec.title || ''} body={spec.body || []} imageUrl={spec.images?.[0]} theme={theme} />;
 
 
 
 
 
 
 
 
 
 
 
27
  case 'bullets':
28
- return <BulletsSlide title={spec.title || ''} body={spec.body || []} theme={theme} />;
 
 
 
 
 
 
 
 
 
29
  case 'chart':
30
- return <ChartSlide title={spec.title || ''} chart={spec.chart} theme={theme} />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  default:
32
- return <TitleSlide title={spec.title || 'Slide'} subtitle={spec.subtitle} theme={theme} />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- export default function TitleSlide({ title, subtitle, theme = 'dark' }: TitleSlideProps) {
10
- const isDark = theme === 'dark';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  return (
12
- <div className={`w-full h-full flex flex-col items-center justify-center ${isDark ? 'text-white bg-black' : 'text-slate-900 bg-white'} p-10 transition-colors`}>
13
- <h1 className="text-5xl font-serif mb-4 text-center">{title}</h1>
14
- {subtitle && <p className="text-lg opacity-80 max-w-3xl text-center">{subtitle}</p>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const fullPrompt = `You are a professional presentation content generator. Create clear, structured, and engaging slide content based on this prompt: "${prompt}". Format your response as clean text that can be used directly in presentation slides.`;
 
 
 
 
 
 
 
 
 
 
 
 
 
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.45",
1863
- "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.45.tgz",
1864
- "integrity": "sha512-lM3QOgbfkGZ5gAZOYWOmzMM6BbKcXOIHjgnUAoymTdZEcEcGSr0vy/LWGEiK+vBXC4vU+sCT+WNoA/JZ8TEWdA==",
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",