samihalawa Claude commited on
Commit
5dcafa7
·
1 Parent(s): e2d8a68

feat: Enhance app with WYSIWYG editor and saved pages

Browse files

- Fixed AI response rendering issues with better HTML content detection
- Added WYSIWYG visual editor for modifying generated content without breaking structure
- Implemented a saved pages system to store and retrieve previously generated pages
- Improved error handling and user feedback throughout the application
- Added toggle to switch between code and visual editing modes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

src/components/App.tsx CHANGED
@@ -11,6 +11,8 @@ import Tabs from "./tabs/tabs";
11
  import AskAI from "./ask-ai/ask-ai";
12
  import { Auth } from "../utils/types";
13
  import Preview from "./preview/preview";
 
 
14
 
15
  function App() {
16
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
@@ -25,6 +27,7 @@ function App() {
25
  const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML);
26
  const [isAiWorking, setisAiWorking] = useState(false);
27
  const [auth, setAuth] = useState<Auth | undefined>(undefined);
 
28
 
29
  const fetchMe = async () => {
30
  const res = await fetch("/api/@me");
@@ -151,7 +154,22 @@ function App() {
151
  }
152
  }}
153
  >
154
- <DeployButton html={html} error={error} auth={auth} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  </Header>
156
  <main className="max-lg:flex-col flex w-full">
157
  <div
@@ -166,36 +184,45 @@ function App() {
166
  }}
167
  >
168
  <Tabs />
169
- <Editor
170
- language="html"
171
- theme="vs-dark"
172
- className={classNames(
173
- "h-[calc(30dvh-41px)] lg:h-[calc(100dvh-96px)]",
174
- {
175
- "pointer-events-none": isAiWorking,
176
- }
177
- )}
178
- value={html}
179
- onValidate={(markers) => {
180
- if (markers?.length > 0) {
181
- setError(true);
182
- }
183
- }}
184
- onChange={(value) => {
185
- const newValue = value ?? "";
186
- setHtml(newValue);
187
- setError(false);
188
- }}
189
- // Removed onMount for editorRef
190
- />
 
 
 
 
 
 
 
 
 
 
 
191
  <AskAI
192
  html={html}
193
- setHtml={setHtml} // Used for both full and diff updates now
194
- // Removed editorRef prop
195
  isAiWorking={isAiWorking}
196
  setisAiWorking={setisAiWorking}
197
  onScrollToBottom={() => {
198
- // Consider if scrolling is still needed here, maybe based on html length change?
199
  // For now, removing the direct editor scroll.
200
  }}
201
  />
 
11
  import AskAI from "./ask-ai/ask-ai";
12
  import { Auth } from "../utils/types";
13
  import Preview from "./preview/preview";
14
+ import WysiwygEditor from "./wysiwyg/wysiwyg-editor";
15
+ import SavedPages from "./saved-pages/saved-pages";
16
 
17
  function App() {
18
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
 
27
  const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML);
28
  const [isAiWorking, setisAiWorking] = useState(false);
29
  const [auth, setAuth] = useState<Auth | undefined>(undefined);
30
+ const [editMode, setEditMode] = useState<'code' | 'wysiwyg'>('code');
31
 
32
  const fetchMe = async () => {
33
  const res = await fetch("/api/@me");
 
154
  }
155
  }}
156
  >
157
+ <div className="flex space-x-2">
158
+ <SavedPages
159
+ currentHtml={html}
160
+ onLoadPage={(savedHtml) => {
161
+ setHtml(savedHtml);
162
+ setError(false);
163
+ }}
164
+ />
165
+ <button
166
+ className="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white rounded text-sm"
167
+ onClick={() => setEditMode(editMode === 'code' ? 'wysiwyg' : 'code')}
168
+ >
169
+ {editMode === 'code' ? 'Visual Editor' : 'Code Editor'}
170
+ </button>
171
+ <DeployButton html={html} error={error} auth={auth} />
172
+ </div>
173
  </Header>
174
  <main className="max-lg:flex-col flex w-full">
175
  <div
 
184
  }}
185
  >
186
  <Tabs />
187
+ {editMode === 'code' ? (
188
+ <Editor
189
+ language="html"
190
+ theme="vs-dark"
191
+ className={classNames(
192
+ "h-[calc(30dvh-41px)] lg:h-[calc(100dvh-96px)]",
193
+ {
194
+ "pointer-events-none": isAiWorking,
195
+ }
196
+ )}
197
+ value={html}
198
+ onValidate={(markers) => {
199
+ if (markers?.length > 0) {
200
+ setError(true);
201
+ }
202
+ }}
203
+ onChange={(value) => {
204
+ const newValue = value ?? "";
205
+ setHtml(newValue);
206
+ setError(false);
207
+ }}
208
+ />
209
+ ) : (
210
+ <div className="h-[calc(30dvh-41px)] lg:h-[calc(100dvh-96px)] overflow-hidden bg-gray-950">
211
+ <WysiwygEditor
212
+ html={html}
213
+ onSave={(updatedHtml) => {
214
+ setHtml(updatedHtml);
215
+ setError(false);
216
+ }}
217
+ />
218
+ </div>
219
+ )}
220
  <AskAI
221
  html={html}
222
+ setHtml={setHtml}
 
223
  isAiWorking={isAiWorking}
224
  setisAiWorking={setisAiWorking}
225
  onScrollToBottom={() => {
 
226
  // For now, removing the direct editor scroll.
227
  }}
228
  />
src/components/ask-ai/ask-ai.tsx CHANGED
@@ -117,14 +117,58 @@ function AskAI({
117
  } else {
118
  // Final update for full HTML mode
119
  const finalDoc = fullContentResponse.match(/<!DOCTYPE html>[\s\S]*<\/html>/)?.[0];
120
- if (finalDoc) {
121
- setHtml(finalDoc); // Ensure final complete HTML is set
122
- } else if (fullContentResponse.trim()) {
123
- // If we got content but it doesn't look like HTML, maybe it's an error message or explanation?
124
- console.warn("[AI Response] Final response doesn't look like HTML:", fullContentResponse);
125
- // Decide if we should show this to the user? Maybe a toast?
126
- // For now, let's assume the throttled updates were sufficient or it wasn't HTML.
127
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  }
129
 
130
  toast.success("AI processing complete");
 
117
  } else {
118
  // Final update for full HTML mode
119
  const finalDoc = fullContentResponse.match(/<!DOCTYPE html>[\s\S]*<\/html>/)?.[0];
120
+ if (finalDoc) {
121
+ console.log("[AI Response] Found complete HTML document");
122
+ setHtml(finalDoc); // Ensure final complete HTML is set
123
+ } else if (fullContentResponse.includes("<html") && fullContentResponse.includes("</html>")) {
124
+ // Try to match HTML without DOCTYPE
125
+ const htmlMatch = fullContentResponse.match(/<html[\s\S]*<\/html>/);
126
+ if (htmlMatch) {
127
+ console.log("[AI Response] Found HTML without DOCTYPE, adding DOCTYPE");
128
+ setHtml(`<!DOCTYPE html>\n${htmlMatch[0]}`);
129
+ }
130
+ } else if (fullContentResponse.trim()) {
131
+ console.warn("[AI Response] Final response doesn't contain proper HTML");
132
+
133
+ // Search for any HTML-like content
134
+ if (fullContentResponse.includes("<body") ||
135
+ (fullContentResponse.includes("<div") && fullContentResponse.includes("</div>"))) {
136
+
137
+ console.log("[AI Response] Found partial HTML content, wrapping it");
138
+ // Wrap the content in a basic HTML structure
139
+ const wrappedContent = `<!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <title>AI Generated Content</title>
143
+ <meta charset="utf-8">
144
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
145
+ </head>
146
+ <body>
147
+ ${fullContentResponse}
148
+ </body>
149
+ </html>`;
150
+ setHtml(wrappedContent);
151
+ } else {
152
+ // If it's just text, wrap it in pre tag
153
+ console.log("[AI Response] No HTML found, creating preview with text content");
154
+ const textContent = `<!DOCTYPE html>
155
+ <html>
156
+ <head>
157
+ <title>AI Generated Text</title>
158
+ <meta charset="utf-8">
159
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
160
+ <style>
161
+ body { font-family: Arial, sans-serif; padding: 20px; }
162
+ pre { white-space: pre-wrap; word-break: break-word; }
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <pre>${fullContentResponse}</pre>
167
+ </body>
168
+ </html>`;
169
+ setHtml(textContent);
170
+ }
171
+ }
172
  }
173
 
174
  toast.success("AI processing complete");
src/components/saved-pages/saved-pages.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { toast } from 'react-toastify';
3
+
4
+ interface SavedPage {
5
+ id: string;
6
+ title: string;
7
+ html: string;
8
+ createdAt: number;
9
+ }
10
+
11
+ interface SavedPagesProps {
12
+ currentHtml: string;
13
+ onLoadPage: (html: string) => void;
14
+ }
15
+
16
+ const SavedPages: React.FC<SavedPagesProps> = ({ currentHtml, onLoadPage }) => {
17
+ const [pages, setPages] = useState<SavedPage[]>([]);
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const [newPageTitle, setNewPageTitle] = useState('');
20
+ const [isAdding, setIsAdding] = useState(false);
21
+
22
+ // Load saved pages from localStorage
23
+ useEffect(() => {
24
+ const savedPagesJson = localStorage.getItem('autosite_saved_pages');
25
+ if (savedPagesJson) {
26
+ try {
27
+ const savedPages = JSON.parse(savedPagesJson);
28
+ setPages(savedPages);
29
+ } catch (error) {
30
+ console.error('Error loading saved pages:', error);
31
+ }
32
+ }
33
+ }, []);
34
+
35
+ // Save pages to localStorage when they change
36
+ useEffect(() => {
37
+ localStorage.setItem('autosite_saved_pages', JSON.stringify(pages));
38
+ }, [pages]);
39
+
40
+ // Save current page
41
+ const savePage = () => {
42
+ if (!currentHtml) {
43
+ toast.error('No content to save');
44
+ return;
45
+ }
46
+
47
+ if (!newPageTitle.trim()) {
48
+ toast.error('Please enter a title for your page');
49
+ return;
50
+ }
51
+
52
+ // Create a new page
53
+ const newPage: SavedPage = {
54
+ id: `page_${Date.now()}`,
55
+ title: newPageTitle,
56
+ html: currentHtml,
57
+ createdAt: Date.now()
58
+ };
59
+
60
+ setPages([newPage, ...pages]);
61
+ setNewPageTitle('');
62
+ setIsAdding(false);
63
+ toast.success('Page saved successfully');
64
+ };
65
+
66
+ // Load a page
67
+ const loadPage = (page: SavedPage) => {
68
+ if (window.confirm('Are you sure you want to load this page? Your current work will be replaced.')) {
69
+ onLoadPage(page.html);
70
+ setIsOpen(false);
71
+ toast.info(`Loaded page: ${page.title}`);
72
+ }
73
+ };
74
+
75
+ // Delete a page
76
+ const deletePage = (e: React.MouseEvent, pageId: string) => {
77
+ e.stopPropagation();
78
+ if (window.confirm('Are you sure you want to delete this page?')) {
79
+ setPages(pages.filter(page => page.id !== pageId));
80
+ toast.info('Page deleted');
81
+ }
82
+ };
83
+
84
+ return (
85
+ <div className="saved-pages-container">
86
+ <button
87
+ className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm"
88
+ onClick={() => setIsOpen(!isOpen)}
89
+ >
90
+ {isOpen ? 'Close Saved Pages' : 'Saved Pages'}
91
+ </button>
92
+
93
+ {isOpen && (
94
+ <div className="saved-pages-modal fixed inset-0 bg-black/50 flex items-center justify-center z-50">
95
+ <div className="bg-gray-900 rounded-lg w-full max-w-2xl p-5 max-h-[80vh] overflow-y-auto">
96
+ <div className="flex justify-between items-center mb-4">
97
+ <h2 className="text-xl text-white font-bold">Saved Pages</h2>
98
+ <button
99
+ className="text-gray-400 hover:text-white"
100
+ onClick={() => setIsOpen(false)}
101
+ >
102
+
103
+ </button>
104
+ </div>
105
+
106
+ {/* Add new page form */}
107
+ {isAdding ? (
108
+ <div className="mb-6 bg-gray-800 p-4 rounded-lg">
109
+ <input
110
+ type="text"
111
+ className="w-full bg-gray-700 border border-gray-600 text-white px-3 py-2 rounded mb-3"
112
+ placeholder="Page Title"
113
+ value={newPageTitle}
114
+ onChange={(e) => setNewPageTitle(e.target.value)}
115
+ />
116
+ <div className="flex space-x-2">
117
+ <button
118
+ className="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded"
119
+ onClick={savePage}
120
+ >
121
+ Save Page
122
+ </button>
123
+ <button
124
+ className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded"
125
+ onClick={() => {
126
+ setIsAdding(false);
127
+ setNewPageTitle('');
128
+ }}
129
+ >
130
+ Cancel
131
+ </button>
132
+ </div>
133
+ </div>
134
+ ) : (
135
+ <button
136
+ className="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded mb-6"
137
+ onClick={() => setIsAdding(true)}
138
+ >
139
+ Save Current Page
140
+ </button>
141
+ )}
142
+
143
+ {/* List of saved pages */}
144
+ {pages.length === 0 ? (
145
+ <p className="text-gray-400 text-center py-4">No saved pages yet</p>
146
+ ) : (
147
+ <div className="space-y-3">
148
+ {pages.map((page) => (
149
+ <div
150
+ key={page.id}
151
+ className="bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-700 flex justify-between items-center"
152
+ onClick={() => loadPage(page)}
153
+ >
154
+ <div>
155
+ <h3 className="text-white font-medium">{page.title}</h3>
156
+ <p className="text-gray-400 text-sm">
157
+ {new Date(page.createdAt).toLocaleString()}
158
+ </p>
159
+ </div>
160
+ <div className="flex space-x-2">
161
+ <button
162
+ className="text-red-500 hover:text-red-400"
163
+ onClick={(e) => deletePage(e, page.id)}
164
+ >
165
+ Delete
166
+ </button>
167
+ </div>
168
+ </div>
169
+ ))}
170
+ </div>
171
+ )}
172
+ </div>
173
+ </div>
174
+ )}
175
+ </div>
176
+ );
177
+ };
178
+
179
+ export default SavedPages;
src/components/wysiwyg/wysiwyg-editor.tsx ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { toast } from 'react-toastify';
3
+
4
+ // Define interface for editable elements
5
+ interface EditableElement {
6
+ id: string;
7
+ originalText: string;
8
+ currentText: string;
9
+ type: 'text' | 'heading' | 'paragraph' | 'image';
10
+ tag?: string;
11
+ src?: string;
12
+ alt?: string;
13
+ }
14
+
15
+ interface WysiwygEditorProps {
16
+ html: string;
17
+ onSave: (updatedHtml: string) => void;
18
+ }
19
+
20
+ const WysiwygEditor: React.FC<WysiwygEditorProps> = ({ html, onSave }) => {
21
+ const [isEditing, setIsEditing] = useState(false);
22
+ const [editableElements, setEditableElements] = useState<EditableElement[]>([]);
23
+ const iframeRef = useRef<HTMLIFrameElement>(null);
24
+
25
+ // Parse HTML to find editable elements when html changes
26
+ useEffect(() => {
27
+ if (!html) return;
28
+
29
+ // Reset editing state when HTML changes
30
+ setIsEditing(false);
31
+
32
+ // Schedule parsing to happen after render
33
+ setTimeout(() => {
34
+ extractEditableElements();
35
+ }, 500);
36
+ }, [html]);
37
+
38
+ // Extract editable elements from the iframe
39
+ const extractEditableElements = () => {
40
+ if (!iframeRef.current) return;
41
+
42
+ try {
43
+ const iframeDoc = iframeRef.current.contentDocument;
44
+ if (!iframeDoc) return;
45
+
46
+ const elements: EditableElement[] = [];
47
+
48
+ // Find text elements (headings, paragraphs, spans with text)
49
+ const textElements = iframeDoc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span:not(:empty)');
50
+ let elementId = 0;
51
+
52
+ textElements.forEach((el) => {
53
+ // Skip elements with no text content
54
+ if (!el.textContent?.trim()) return;
55
+
56
+ // Skip elements that are part of script, style tags or have specific classes
57
+ const closestNonEditable = el.closest('script, style, nav, footer');
58
+ if (closestNonEditable) return;
59
+
60
+ const id = `editable-${elementId++}`;
61
+ el.id = id; // Add ID to the element for easier access later
62
+
63
+ elements.push({
64
+ id,
65
+ originalText: el.textContent || '',
66
+ currentText: el.textContent || '',
67
+ type: el.tagName.toLowerCase().startsWith('h') ? 'heading' : 'paragraph',
68
+ tag: el.tagName.toLowerCase()
69
+ });
70
+ });
71
+
72
+ // Find image elements
73
+ const imageElements = iframeDoc.querySelectorAll('img');
74
+ imageElements.forEach((el) => {
75
+ const id = `editable-${elementId++}`;
76
+ el.id = id;
77
+
78
+ elements.push({
79
+ id,
80
+ originalText: el.alt || '',
81
+ currentText: el.alt || '',
82
+ type: 'image',
83
+ tag: 'img',
84
+ src: el.src,
85
+ alt: el.alt
86
+ });
87
+ });
88
+
89
+ setEditableElements(elements);
90
+ } catch (error) {
91
+ console.error('Error extracting editable elements:', error);
92
+ }
93
+ };
94
+
95
+ // Update HTML content with edited text
96
+ const applyChanges = () => {
97
+ if (!iframeRef.current) return;
98
+
99
+ try {
100
+ const iframeDoc = iframeRef.current.contentDocument;
101
+ if (!iframeDoc) return;
102
+
103
+ // Apply text changes
104
+ editableElements.forEach((element) => {
105
+ const el = iframeDoc.getElementById(element.id);
106
+ if (!el) return;
107
+
108
+ if (element.type === 'image') {
109
+ // Update image alt text
110
+ if (el instanceof HTMLImageElement) {
111
+ el.alt = element.currentText;
112
+ }
113
+ } else {
114
+ // Update text content
115
+ if (element.currentText !== element.originalText) {
116
+ el.textContent = element.currentText;
117
+ }
118
+ }
119
+ });
120
+
121
+ // Get the updated HTML
122
+ const updatedHtml = iframeDoc.documentElement.outerHTML;
123
+ onSave(updatedHtml);
124
+ toast.success('Changes saved successfully');
125
+ setIsEditing(false);
126
+ } catch (error) {
127
+ console.error('Error applying changes:', error);
128
+ toast.error('Failed to save changes');
129
+ }
130
+ };
131
+
132
+ // Handle text changes
133
+ const handleTextChange = (id: string, newText: string) => {
134
+ setEditableElements((prev) =>
135
+ prev.map((element) =>
136
+ element.id === id ? { ...element, currentText: newText } : element
137
+ )
138
+ );
139
+ };
140
+
141
+ return (
142
+ <div className="wysiwyg-container">
143
+ {/* Editor toolbar */}
144
+ <div className="wysiwyg-toolbar bg-gray-900 border-b border-gray-700 p-2">
145
+ {!isEditing ? (
146
+ <button
147
+ className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
148
+ onClick={() => setIsEditing(true)}
149
+ >
150
+ Edit Content
151
+ </button>
152
+ ) : (
153
+ <div className="flex gap-2">
154
+ <button
155
+ className="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700 text-sm"
156
+ onClick={applyChanges}
157
+ >
158
+ Save Changes
159
+ </button>
160
+ <button
161
+ className="px-3 py-1 bg-gray-600 text-white rounded hover:bg-gray-700 text-sm"
162
+ onClick={() => {
163
+ setIsEditing(false);
164
+ // Reset to original text
165
+ setEditableElements((prev) =>
166
+ prev.map((element) => ({ ...element, currentText: element.originalText }))
167
+ );
168
+ }}
169
+ >
170
+ Cancel
171
+ </button>
172
+ </div>
173
+ )}
174
+ </div>
175
+
176
+ {/* Editing interface */}
177
+ {isEditing && (
178
+ <div className="wysiwyg-editor bg-gray-800 p-4 overflow-y-auto max-h-[60vh]">
179
+ <h3 className="text-white text-lg mb-4">Edit Page Content</h3>
180
+ <div className="space-y-4">
181
+ {editableElements.map((element) => (
182
+ <div key={element.id} className="flex flex-col">
183
+ <label className="text-gray-300 text-sm mb-1">
184
+ {element.type === 'heading' ? `Heading (${element.tag})` :
185
+ element.type === 'image' ? 'Image Alt Text' : 'Text'}
186
+ </label>
187
+ {element.type === 'image' ? (
188
+ <div className="flex items-center gap-2">
189
+ <img
190
+ src={element.src}
191
+ alt={element.alt}
192
+ className="w-10 h-10 object-cover"
193
+ />
194
+ <input
195
+ type="text"
196
+ value={element.currentText}
197
+ onChange={(e) => handleTextChange(element.id, e.target.value)}
198
+ className="flex-1 bg-gray-700 border border-gray-600 text-white px-3 py-2 rounded"
199
+ placeholder="Alt text"
200
+ />
201
+ </div>
202
+ ) : (
203
+ <textarea
204
+ value={element.currentText}
205
+ onChange={(e) => handleTextChange(element.id, e.target.value)}
206
+ className="w-full bg-gray-700 border border-gray-600 text-white px-3 py-2 rounded min-h-[100px]"
207
+ placeholder={`Edit ${element.type}...`}
208
+ />
209
+ )}
210
+ </div>
211
+ ))}
212
+ </div>
213
+ </div>
214
+ )}
215
+
216
+ {/* Preview iframe */}
217
+ <div className="wysiwyg-preview">
218
+ <iframe
219
+ ref={iframeRef}
220
+ srcDoc={html}
221
+ title="WYSIWYG Preview"
222
+ className="w-full h-full border-0"
223
+ sandbox="allow-same-origin"
224
+ ></iframe>
225
+ </div>
226
+ </div>
227
+ );
228
+ };
229
+
230
+ export default WysiwygEditor;