Spaces:
Running
Running
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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
<AskAI
|
192 |
html={html}
|
193 |
-
setHtml={setHtml}
|
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 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
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;
|