feat: Adsterra integration, code splitting, cleanup, Onidel affiliate

This commit is contained in:
dwindown
2026-02-18 16:50:45 +07:00
parent 7ba289be5c
commit 9dc3285adb
40 changed files with 352 additions and 2933 deletions

View File

@@ -1,28 +1,30 @@
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import React, { useEffect, Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import ErrorBoundary from './components/ErrorBoundary';
import Home from './pages/Home';
import UrlTool from './pages/UrlTool';
import Base64Tool from './pages/Base64Tool';
import BeautifierTool from './pages/BeautifierTool';
import DiffTool from './pages/DiffTool';
import TextLengthTool from './pages/TextLengthTool';
import ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor';
import InvoiceEditor from './pages/InvoiceEditor';
import MarkdownEditor from './pages/MarkdownEditor';
import InvoicePreview from './pages/InvoicePreview';
import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal';
import ReleaseNotes from './pages/ReleaseNotes';
import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy';
import NotFound from './pages/NotFound';
import Loading from './components/Loading';
import { initGA } from './utils/analytics';
import './index.css';
const Home = lazy(() => import('./pages/Home'));
const UrlTool = lazy(() => import('./pages/UrlTool'));
const Base64Tool = lazy(() => import('./pages/Base64Tool'));
const BeautifierTool = lazy(() => import('./pages/BeautifierTool'));
const DiffTool = lazy(() => import('./pages/DiffTool'));
const TextLengthTool = lazy(() => import('./pages/TextLengthTool'));
const ObjectEditor = lazy(() => import('./pages/ObjectEditor'));
const TableEditor = lazy(() => import('./pages/TableEditor'));
const InvoiceEditor = lazy(() => import('./pages/InvoiceEditor'));
const MarkdownEditor = lazy(() => import('./pages/MarkdownEditor'));
const InvoicePreview = lazy(() => import('./pages/InvoicePreview'));
const InvoicePreviewMinimal = lazy(() => import('./pages/InvoicePreviewMinimal'));
const ReleaseNotes = lazy(() => import('./pages/ReleaseNotes'));
const TermsOfService = lazy(() => import('./pages/TermsOfService'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
const NotFound = lazy(() => import('./pages/NotFound'));
function App() {
// Initialize Google Analytics on app startup
useEffect(() => {
@@ -34,6 +36,7 @@ function App() {
<ErrorBoundary>
<Router>
<Layout>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/url" element={<UrlTool />} />
@@ -47,12 +50,13 @@ function App() {
<Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
<Route path="/whats-new" element={<ReleaseNotes />} />
<Route path="/whats-new" element={<Navigate to="/release-notes" replace />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</Layout>
</Router>
</ErrorBoundary>

View File

@@ -1,35 +1,48 @@
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
const AdBlock = ({ className = '', adKey = 'e0ca7c61c83457f093bbc2e261b43d31' }) => {
const iframeRef = useRef(null);
/**
* AdBlock Component - Individual ad unit
* Displays a single Google AdSense ad
*/
const AdBlock = ({ slot, size = '300x250', className = '' }) => {
useEffect(() => {
try {
// Push ad to AdSense queue
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}, []);
if (!iframeRef.current) return;
const [width, height] = size.split('x');
const iframe = iframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '${adKey}',
'format' : 'iframe',
'height' : 250,
'width' : 300,
'params' : {}
};
</script>
<script type="text/javascript" src="https://bustleplaguereed.com/${adKey}/invoke.js"></script>
</body>
</html>
`);
doc.close();
}, [adKey]);
return (
<div className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}>
<ins
className="adsbygoogle"
style={{
display: 'block',
width: `${width}px`,
height: `${height}px`
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
/>
</div>
<iframe
ref={iframeRef}
className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}
style={{ width: '300px', height: '250px', border: 'none' }}
title="Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
);
};

View File

@@ -1,29 +1,15 @@
import React from 'react';
import AdBlock from './AdBlock';
import OfferBlock from './OfferBlock';
import AffiliateBlock from './AffiliateBlock';
/**
* AdColumn Component - Desktop sidebar with 3 ad units
* Hidden on mobile/tablet, visible on desktop (1200px+)
* All ads are 300x250 (Medium Rectangle) to comply with Google AdSense policies
* - Ads must be fully viewable without scrolling
* - No scrollable containers allowed
*/
const AdColumn = ({
slot1 = 'REPLACE_WITH_SLOT_1',
slot2 = 'REPLACE_WITH_SLOT_2',
slot3 = 'REPLACE_WITH_SLOT_3'
}) => {
const AdColumn = () => {
return (
<aside className="hidden xl:block w-[300px] flex-shrink-0">
<div className="fixed top-20 right-8 w-[300px] space-y-5">
{/* Ad 1: Medium Rectangle */}
<AdBlock slot={slot1} size="300x250" />
{/* Ad 2: Medium Rectangle */}
<AdBlock slot={slot2} size="300x250" />
{/* Ad 3: Medium Rectangle */}
<AdBlock slot={slot3} size="300x250" />
<div className="sticky top-20 space-y-5">
<AdBlock />
<OfferBlock />
<AffiliateBlock />
</div>
</aside>
);

View File

@@ -0,0 +1,21 @@
import React from 'react';
const AffiliateBlock = () => {
return (
<a
href="https://onidel.com/?referral=1568841"
target="_blank"
rel="noopener noreferrer sponsored"
className="block w-[300px] h-[250px] rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"
>
<img
src="/images/onidel-banner.webp"
alt="Onidel - Professional Development Services"
className="w-full h-full object-cover"
loading="lazy"
/>
</a>
);
};
export default AffiliateBlock;

18
src/components/Loading.js Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Loader2 } from 'lucide-react';
const Loading = () => {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] w-full">
<div className="relative">
<div className="absolute inset-0 bg-blue-500/20 blur-xl rounded-full animate-pulse"></div>
<Loader2 className="h-12 w-12 text-blue-600 dark:text-blue-400 animate-spin relative z-10" />
</div>
<p className="mt-4 text-slate-500 dark:text-slate-400 font-medium animate-pulse">
Loading...
</p>
</div>
);
};
export default Loading;

View File

@@ -1,17 +1,12 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { X } from 'lucide-react';
/**
* MobileAdBanner Component - Sticky bottom banner for mobile
* Visible only on mobile/tablet, hidden on desktop
* Includes close button for better UX
*/
const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
const MobileAdBanner = () => {
const [visible, setVisible] = useState(true);
const [closed, setClosed] = useState(false);
const iframeRef = useRef(null);
useEffect(() => {
// Check if user previously closed the banner (session storage)
const wasClosed = sessionStorage.getItem('mobileAdClosed');
if (wasClosed === 'true') {
setClosed(true);
@@ -20,13 +15,41 @@ const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
}, []);
useEffect(() => {
if (visible && !closed) {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}
if (!visible || closed || !iframeRef.current) return;
const timer = setTimeout(() => {
if (!iframeRef.current) return;
const iframe = iframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '2965bcf877388cafa84160592c550f5a',
'format' : 'iframe',
'height' : 50,
'width' : 320,
'params' : {}
};
</script>
<script type="text/javascript" src="https://bustleplaguereed.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
</body>
</html>
`);
doc.close();
}, 500);
return () => clearTimeout(timer);
}, [visible, closed]);
const handleClose = () => {
@@ -41,22 +64,17 @@ const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
<div className="xl:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleClose}
className="absolute top-1 right-1 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm"
className="absolute -top-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm z-10"
aria-label="Close ad"
>
<X className="h-4 w-4" />
</button>
<div className="flex justify-center py-2">
<ins
className="adsbygoogle"
style={{
display: 'inline-block',
width: '320px',
height: '50px'
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
<div className="flex justify-center items-center py-2">
<iframe
ref={iframeRef}
style={{ width: '320px', height: '50px', border: 'none' }}
title="Mobile Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div>

View File

@@ -0,0 +1,22 @@
import React from 'react';
const OfferBlock = () => {
return (
<div className="w-[300px] h-[250px] bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex flex-col items-center justify-center text-white p-6 text-center shadow-lg hover:shadow-xl transition-shadow duration-300">
<span className="bg-white/20 text-xs font-bold px-2 py-1 rounded mb-4 backdrop-blur-sm">
SPECIAL OFFER
</span>
<h3 className="text-2xl font-bold mb-2">
Upgrade to PRO
</h3>
<p className="text-indigo-100 text-sm mb-6">
Get unlimited access to all developer tools and features.
</p>
<button className="bg-white text-indigo-600 font-bold py-2 px-6 rounded-full hover:bg-indigo-50 transition-colors shadow-md">
Learn More
</button>
</div>
);
};
export default OfferBlock;

View File

@@ -111,7 +111,6 @@ const InvoiceEditor = () => {
const currencyData = await response.json();
setCurrencies(currencyData);
} catch (error) {
console.error('Failed to load currencies:', error);
// Fallback to basic currencies
setCurrencies([
{ code: 'IDR', name: 'Indonesian Rupiah', symbol: 'Rp' },
@@ -165,7 +164,7 @@ const InvoiceEditor = () => {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load saved invoice:', error);
// Failed to load saved invoice
}
}, [searchParams]);
@@ -176,7 +175,7 @@ const InvoiceEditor = () => {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice:', error);
// Failed to save invoice
}
}
}, [invoiceData, createNewCompleted]);
@@ -227,7 +226,7 @@ const InvoiceEditor = () => {
try {
localStorage.setItem('pdfPageSize', pdfPageSize);
} catch (error) {
console.error('Failed to save PDF page size:', error);
// Failed to save PDF page size
}
}, [pdfPageSize]);
@@ -619,7 +618,6 @@ const InvoiceEditor = () => {
// Navigate to preview page
navigate('/invoice-preview');
} catch (error) {
console.error('Failed to save invoice data:', error);
alert('Failed to save invoice data. Please try again.');
}
};

View File

@@ -94,7 +94,6 @@ const InvoicePreview = () => {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
@@ -262,7 +261,6 @@ const InvoicePreview = () => {
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay
@@ -281,7 +279,7 @@ const InvoicePreview = () => {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice data before edit:', error);
// Failed to save invoice data before edit
}
}
// Add a parameter to indicate we're editing existing data

View File

@@ -29,7 +29,6 @@ const InvoicePreviewMinimal = () => {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
@@ -164,7 +163,6 @@ const InvoicePreviewMinimal = () => {
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay

View File

@@ -58,8 +58,6 @@ const MarkdownEditor = () => {
const language = token.lang || '';
const normalizedLang = language ? language.toLowerCase().trim() : '';
console.log('Code block detected:', { language, normalizedLang, codeLength: codeString.length });
let highlightedCode = codeString;
// Apply syntax highlighting
@@ -67,18 +65,14 @@ const MarkdownEditor = () => {
try {
const result = hljs.highlight(codeString, { language: normalizedLang });
highlightedCode = result.value;
console.log('Highlighted with language:', normalizedLang);
} catch (e) {
console.error(`Highlight error for ${normalizedLang}:`, e);
highlightedCode = codeString;
}
} else {
try {
const result = hljs.highlightAuto(codeString);
highlightedCode = result.value;
console.log('Auto-highlighted, detected:', result.language);
} catch (e) {
console.error('Auto-highlight error:', e);
highlightedCode = codeString;
}
}
@@ -136,7 +130,6 @@ const MarkdownEditor = () => {
ADD_ATTR: ['data-code-id', 'title', 'id', 'type', 'checked', 'disabled']
});
} catch (e) {
console.error('Markdown parse error:', e);
return '<p>Error parsing markdown</p>';
}
};
@@ -173,7 +166,7 @@ const MarkdownEditor = () => {
button.textContent = originalText;
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
// Failed to copy
});
}
return;
@@ -818,7 +811,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
response = await fetch(urlToFetch);
} catch (corsError) {
// If CORS error, try with CORS proxy
console.log('CORS error, trying with proxy...');
response = await fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(urlToFetch)}`);
}
@@ -855,7 +847,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
});
} catch (err) {
console.error('Fetch error:', err);
setError(`Failed to fetch from URL: ${err.message}`);
} finally {
setFetching(false);
@@ -1494,7 +1485,6 @@ ${html}
html2pdf().set(opt).from(wrapper).save().then(() => {
setError('');
}).catch((err) => {
console.error('PDF generation error:', err);
setError('Failed to generate PDF');
});
};

View File

@@ -385,9 +385,6 @@ const ObjectEditor = () => {
try {
const result = parseValue();
if (index < str.length) {
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
}
return result;
} catch (error) {
throw new Error(`Parse error at position ${index}: ${error.message}`);
@@ -563,7 +560,6 @@ const ObjectEditor = () => {
serialized
});
} catch (error) {
console.error('Error generating outputs:', error);
setOutputs({
jsonPretty: 'Error generating JSON',
jsonMinified: 'Error generating JSON',
@@ -610,8 +606,6 @@ const ObjectEditor = () => {
// Fetch data from URL with advanced content extraction
const handleFetchData = async (advancedOptions = null) => {
console.log('🚀 handleFetchData called with URL:', fetchUrl);
console.log('🔧 Advanced options:', advancedOptions);
const urlToFetch = advancedOptions?.url || fetchUrl.trim();
@@ -623,7 +617,6 @@ const ObjectEditor = () => {
setFetching(true);
setError('');
console.log('✅ Starting fetch process...');
try {
// Add protocol if missing
@@ -635,11 +628,10 @@ const ObjectEditor = () => {
// Determine if this is an advanced request (has custom options)
const isAdvancedRequest = advancedOptions && (
advancedOptions.method !== 'GET' ||
Object.keys(advancedOptions.headers || {}).length > 0 ||
Object.keys( advancedOptions.headers || {}).length > 0 ||
advancedOptions.body
);
console.log('🎯 Is advanced request:', isAdvancedRequest);
// Build fetch options for advanced mode
const fetchOptions = advancedOptions ? {
@@ -648,7 +640,6 @@ const ObjectEditor = () => {
body: advancedOptions.body || undefined
} : {};
console.log('📡 Fetch options:', fetchOptions);
// Try direct fetch first (for APIs)
try {
@@ -700,7 +691,6 @@ const ObjectEditor = () => {
throw e;
}
// CORS error or network error, will try with content extractor for simple GET
console.log('Direct fetch failed, trying content extractor:', e.message);
}
// Use advanced content extractor ONLY for simple GET requests to HTML pages
@@ -747,7 +737,6 @@ const ObjectEditor = () => {
});
} catch (err) {
console.error('Fetch error:', err);
setError(`Failed to fetch data: ${err.message}`);
setUrlDataSummary(null);
} finally {

View File

@@ -167,20 +167,19 @@ const PrivacyPolicy = () => {
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Globe className="h-5 w-5 text-teal-600" />
5. Future Advertising (Google AdSense)
5. Advertising (Adsterra)
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 mb-4">
<h3 className="font-semibold text-amber-800 dark:text-amber-200 mb-2">
🔮 Planned Implementation:
Advertising:
</h3>
<p className="text-amber-700 dark:text-amber-300 text-sm leading-relaxed mb-3">
To keep our tools free, we plan to display Google AdSense advertisements. When implemented:
To keep our tools free, we display Adsterra advertisements.
</p>
<ul className="list-disc list-inside text-amber-700 dark:text-amber-300 space-y-1 text-sm">
<li>Ads will be clearly marked and non-intrusive</li>
<li>No impact on tool functionality or performance</li>
<li>Google may use cookies for ad personalization</li>
<li>You can opt-out of personalized ads via Google settings</li>
<li>The ad network may use cookies for ad delivery</li>
<li><strong>We will NEVER share your tool usage data with advertisers</strong></li>
</ul>
</div>
@@ -218,9 +217,9 @@ const PrivacyPolicy = () => {
</p>
</div>
<div className="border-l-4 border-green-500 pl-4">
<h3 className="font-semibold text-slate-800 dark:text-white">Google AdSense (Future)</h3>
<h3 className="font-semibold text-slate-800 dark:text-white">Adsterra</h3>
<p className="text-slate-600 dark:text-slate-300 text-sm">
Privacy Policy: <a href="https://policies.google.com/privacy" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://policies.google.com/privacy</a>
Privacy Policy: <a href="https://adsterra.com/privacy-policy/" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://adsterra.com/privacy-policy/</a>
</p>
</div>
</div>

View File

@@ -126,7 +126,7 @@ const TermsOfService = () => {
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4">
<p className="text-amber-800 dark:text-amber-200 leading-relaxed">
<strong>Transparency Notice:</strong> We plan to implement Google AdSense advertisements in the future to support the free operation of this service. When implemented, ads will be clearly marked and will not interfere with tool functionality. Our privacy-first approach will remain unchanged - we will never sell or share your usage data with advertisers.
<strong>Transparency Notice:</strong> We display Adsterra advertisements to support the free operation of this service. Ads are clearly marked and do not interfere with tool functionality. Our privacy-first approach remains unchanged - we will never sell or share your usage data with advertisers.
</p>
</div>
</section>

View File

@@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
import { Copy } from 'lucide-react';
const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameRef, selectedElementInfo }) => {
console.log('🔍 ELEMENT EDITOR: Received props:', { selectedElementInfo, previewFrameRef: !!previewFrameRef });
const [edited, setEdited] = useState(null);
const textareaRefs = useRef({});
@@ -26,7 +25,6 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
});
setEdited(elementInfo);
console.log('🎯 ENHANCED EDITOR: Initialized with selected element:', elementInfo);
} else {
// Clear the editor when no element is selected
setEdited(null);
@@ -62,7 +60,6 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
// ENHANCED OPTION A: Field change handler using PreviewFrame API
const handleFieldChange = (field, value) => {
console.log(`⌨️ ENHANCED: Field '${field}' changed to '${value}'`);
setEdited(prev => ({ ...prev, [field]: value }));
// Use Enhanced Option A API for direct DOM manipulation
@@ -80,19 +77,13 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
success = previewFrameRef.current.updateElementStyle(edited.cascadeId, field, value);
}
if (success) {
console.log(`✅ ENHANCED UPDATE: ${field} updated in iframe DOM (scroll preserved)`);
} else {
console.error(`❌ ENHANCED UPDATE: Failed to update ${field}`);
if (!success) {
// Silently handle failure
}
} else {
console.warn('⚠️ ENHANCED UPDATE: PreviewFrame ref or cascadeId not available');
}
};
const handleSave = () => {
console.log('💾 ENHANCED OPTION A SAVE: Using PreviewFrame API to get iframe content');
try {
// Use Enhanced Option A API to get iframe content
if (previewFrameRef?.current?.getIframeContent) {
@@ -110,17 +101,11 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
// ENHANCED OPTION A: Update htmlInput with the cleaned iframe content
// This is the ONLY allowed setHtmlInput call during inspector operations (explicit save)
setHtmlInput(cleanedHtml);
console.log('✅ ENHANCED SAVE: Successfully extracted and cleaned iframe DOM via API');
} else {
console.warn('⚠️ Save: No content returned from PreviewFrame API');
}
} else {
console.error('❌ Save: PreviewFrame API not available');
}
} catch (error) {
console.error('❌ Enhanced Option A Save failed:', error);
// Silently handle error
}
// Trigger parent save callback
@@ -133,12 +118,9 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
// 5. ENHANCED OPTION A: Cancel reverts changes in iframe DOM directly
const handleCancel = () => {
console.log('🔄 ENHANCED CANCEL: Reverting changes in iframe DOM without triggering refresh');
// For Enhanced Option A, we don't need to revert HTML input
// The iframe DOM changes will be discarded when the inspector closes
// Just close the inspector without syncing changes
console.log('✅ ENHANCED CANCEL: Changes discarded, iframe DOM remains stable');
onClose();
};
@@ -158,9 +140,8 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
const elementString = `<${edited.tagName}${edited.id ? ` id="${edited.id}"` : ''}${edited.className ? ` class="${edited.className}"` : ''}${otherAttributes.map(attr => ` ${attr}="${edited[attr]}"`).join('')}>${edited.innerHTML || ''}</${edited.tagName}>`;
await navigator.clipboard.writeText(elementString);
console.log('✅ Element copied to clipboard');
} catch (err) {
console.error('❌ Failed to copy element:', err);
// Silently handle error
}
};

View File

@@ -1,674 +0,0 @@
import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
// Device Frame CSS - Converted from SCSS
const deviceFrameCSS = `
/* iPhone 14 Pro Device Frame */
.device-iphone-14-pro {
height: 780px;
width: 384px;
transform-origin: center;
position: relative;
margin: 0 auto;
}
.device-iphone-14-pro .device-frame {
background: #010101;
border: 1px solid #2a242f;
border-radius: 61px;
box-shadow: inset 0 0 4px 2px #a8a4b0, inset 0 0 0 5px #342C3F;
height: 780px;
padding: 17px;
width: 384px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-iphone-14-pro .device-screen {
border-radius: 56px;
height: 746px;
width: 350px;
overflow: hidden;
scale: 0.75;
min-width: 130%;
height: 130%;
}
.device-iphone-14-pro .device-screen iframe {
width: 130%; /* 100% / 0.75 = 133.33% to compensate for 0.75 scale */
height: 130%;
transform: scale(0.75);
transform-origin: top left;
}
/* Mobile scrollbar styling for iPhone */
.device-iphone-14-pro .device-screen::-webkit-scrollbar {
width: 2px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
}
.device-iphone-14-pro .device-stripe::after,
.device-iphone-14-pro .device-stripe::before {
border: solid rgba(1, 1, 1, 0.25);
border-width: 0 7px;
content: "";
height: 7px;
left: 0;
position: absolute;
width: 100%;
z-index: 9;
}
.device-iphone-14-pro .device-stripe::after {
top: 77px;
}
.device-iphone-14-pro .device-stripe::before {
bottom: 77px;
}
.device-iphone-14-pro .device-header {
background: #010101;
border-radius: 18px;
height: 31px;
left: 50%;
margin-left: -54px;
position: absolute;
top: 32px;
width: 108px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::after,
.device-iphone-14-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-iphone-14-pro .device-sensors::after {
background: #010101;
border-radius: 16px;
height: 30px;
left: 50%;
margin-left: -54px;
top: 33px;
width: 67px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 8px;
left: 50%;
margin-left: 24px;
top: 44px;
width: 8px;
z-index: 10;
}
.device-iphone-14-pro .device-btns {
background: #2a242f;
border-radius: 1px;
height: 24px;
left: -2px;
position: absolute;
top: 86px;
width: 2px;
}
.device-iphone-14-pro .device-btns::after,
.device-iphone-14-pro .device-btns::before {
background: #2a242f;
border-radius: 1px;
content: "";
height: 46px;
left: 0;
position: absolute;
width: 2px;
}
.device-iphone-14-pro .device-btns::after {
top: 45px;
}
.device-iphone-14-pro .device-btns::before {
top: 105px;
}
.device-iphone-14-pro .device-power {
background: #2a242f;
border-radius: 1px;
height: 75px;
right: -2px;
position: absolute;
top: 150px;
width: 2px;
}
/* iPad Pro Device Frame */
.device-ipad-pro {
height: 840px;
width: 600px;
transform-origin: center;
margin-top: 40px;
position: relative;
margin-left: auto;
margin-right: auto;
}
.device-ipad-pro .device-frame {
background: #0d0d0d;
border-radius: 32px;
box-shadow: inset 0 0 0 1px #c1c2c3, inset 0 0 1px 2px #e2e3e4;
height: 800px;
padding: 24px;
width: 576px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-ipad-pro .device-screen {
border: 2px solid #0f0f0f;
border-radius: 10px;
overflow: hidden;
min-width: 200%;
height: 200%;
scale: 0.5;
}
.device-ipad-pro .device-screen iframe {
/* Set the iframe to the actual device resolution and scale it down */
width: 834px; /* iPad Pro 11" logical width */
height: 1194px; /* iPad Pro 11" logical height */
transform: scale(0.6331); /* 528px (screen width) / 834px (logical width) */
transform-origin: top left;
background: #fff; /* Ensure bg color for content */
}
/* Mobile scrollbar styling for iPad */
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 3px;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.device-ipad-pro .device-power {
background: #2a242f;
border-radius: 2px;
height: 2px;
width: 38px;
right: 76px;
top: -2px;
position: absolute;
}
/* Reposition buttons specifically for iPad Pro */
.device-ipad-pro .device-btns {
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume up */
width: 2px;
right: 22px;
top: 90px;
position: absolute;
}
.device-ipad-pro .device-btns::after {
content: "";
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume down */
width: 2px;
left: 0;
top: 40px; /* Space between buttons */
position: absolute;
}
.device-ipad-pro .device-btns::before {
display: none; /* Hide the third button from iPhone */
}
.device-ipad-pro .device-sensors::after,
.device-ipad-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-ipad-pro .device-sensors::after {
background: #141414;
border-radius: 16px;
box-shadow: -18px 0 #141414, 64px 0 #141414;
height: 10px;
left: 50%;
margin-left: -28px;
top: 11px;
width: 10px;
}
.device-ipad-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 6px;
left: 50%;
margin-left: -3px;
top: 13px;
width: 5px;
}
/* Enable smooth scrolling on iOS */
.device-iphone-14-pro .device-screen,
.device-ipad-pro .device-screen {
-webkit-overflow-scrolling: touch; /* smooth momentum scroll on iOS */
overflow-y: auto;
}
/* Mobile custom scrollbar */
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track,
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb,
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Optional: Hide scrollbar on larger screens for desktop */
/* This media query hides the scrollbar on desktops where touch scrolling is not needed */
@media (pointer: fine) and (hover: hover) {
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
display: none;
}
}
`;
const PreviewFrame = ({
htmlInput,
cssInput,
jsInput,
selectedDevice,
inspectMode,
onElementClick,
isFullscreen
}) => {
const iframeRef = useRef(null);
// Handle iframe click for element selection - defined first to avoid initialization errors
const handleIframeClick = useCallback((e) => {
if (!inspectMode) return;
e.preventDefault();
e.stopPropagation();
const clickedElement = e.target;
const elementInfo = {
tagName: clickedElement.tagName.toLowerCase(),
innerText: clickedElement.innerText || clickedElement.textContent || '',
attributes: {}
};
Array.from(clickedElement.attributes).forEach(attr => {
elementInfo.attributes[attr.name] = attr.value;
});
onElementClick(elementInfo);
}, [inspectMode, onElementClick]);
// Function to setup inspect mode styles and event handlers with MutationObserver
const setupInspectModeStyles = useCallback((iframeDoc) => {
console.log('🎨 PreviewFrame: Setting up robust inspect mode with MutationObserver');
// Remove existing inspect styles and observers
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
if (existingStyle) existingStyle.remove();
// Clean up any existing observer
if (iframeDoc._inspectObserver) {
iframeDoc._inspectObserver.disconnect();
delete iframeDoc._inspectObserver;
}
// Add inspect mode styles with better hover highlights
const style = iframeDoc.createElement('style');
style.id = 'inspect-mode-styles';
style.textContent = `
/* High specificity selectors for inspect mode */
html * {
cursor: crosshair !important;
pointer-events: auto !important;
}
/* Hover highlights with maximum specificity */
html body *:hover {
outline: 2px solid #3b82f6 !important;
outline-offset: 1px !important;
background-color: rgba(59, 130, 246, 0.1) !important;
transition: all 0.1s ease !important;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3) !important;
}
/* Selected element styles */
html body [data-original="false"] {
outline: 2px solid #10b981 !important;
outline-offset: 1px !important;
background-color: rgba(16, 185, 129, 0.1) !important;
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3) !important;
}
/* Selected element hover */
html body [data-original="false"]:hover {
outline: 2px solid #059669 !important;
outline-offset: 1px !important;
background-color: rgba(5, 150, 105, 0.15) !important;
box-shadow: 0 0 0 1px rgba(5, 150, 105, 0.4) !important;
}
/* Prevent text selection during inspect */
html * {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
/* Override any existing hover styles */
html body *:hover {
border: none !important;
}
`;
iframeDoc.head.appendChild(style);
// Add event handlers
const preventInteraction = (e) => {
e.preventDefault();
e.stopPropagation();
return false;
};
// Add click handler for element selection
iframeDoc.addEventListener('click', handleIframeClick, true);
// Prevent other interactions during inspect mode
iframeDoc.addEventListener('mousedown', preventInteraction, true);
iframeDoc.addEventListener('mouseup', preventInteraction, true);
iframeDoc.addEventListener('contextmenu', preventInteraction, true);
iframeDoc.addEventListener('dragstart', preventInteraction, true);
iframeDoc.addEventListener('selectstart', preventInteraction, true);
// Setup MutationObserver to reapply styles when DOM changes
const observer = new MutationObserver((mutations) => {
let needsReapply = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
needsReapply = true;
}
});
if (needsReapply) {
console.log('🔄 PreviewFrame: DOM changed, reapplying inspect styles');
// Reapply styles after a short delay to ensure new elements are rendered
setTimeout(() => {
const currentStyle = iframeDoc.getElementById('inspect-mode-styles');
if (!currentStyle) {
// Style was removed, reapply it
const newStyle = iframeDoc.createElement('style');
newStyle.id = 'inspect-mode-styles';
newStyle.textContent = style.textContent;
iframeDoc.head.appendChild(newStyle);
}
}, 50);
}
});
// Start observing DOM changes
observer.observe(iframeDoc.body, {
childList: true,
subtree: true,
attributes: false
});
// Store observer for cleanup
iframeDoc._inspectObserver = observer;
console.log('✅ PreviewFrame: Robust inspect mode with MutationObserver applied');
}, [handleIframeClick]);
const generateHtmlContent = useCallback(() => {
const scrollbarCSS = `
@media (max-width: 960px) {
html, body {
overflow-y: overlay; /* scrollbar overlays content, no space reserved */
-webkit-overflow-scrolling: touch;
padding-right: 0; /* No padding, since scrollbar overlays */
box-sizing: border-box; /* Use border-box for correct sizing */
scrollbar-width: thin;
scrollbar-color: rgba(0,0,0,0.2) transparent;
}
body::-webkit-scrollbar {
width: 0;
background: transparent;
}
/* Custom scrollbar styles */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.1);
}
/* Firefox custom scrollbar */
html {
scrollbar-width: thin;
scrollbar-color: rgba(0,0,0,0.2) transparent;
}
}
`;
const finalCss = `${scrollbarCSS}\n${cssInput || ''}`;
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
if (isFullDocument) {
let content = htmlInput;
const headEndIndex = content.toLowerCase().indexOf('</head>');
if (headEndIndex !== -1) {
const styleTag = `\n<style>\n${finalCss}\n</style>\n`;
content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
}
if (jsInput && jsInput.trim()) {
const bodyEndIndex = content.toLowerCase().lastIndexOf('</body>');
if (bodyEndIndex !== -1) {
const scriptTag = `\n<script>\n(function() {\n${jsInput}\n})();\n</script>\n`;
content = content.slice(0, bodyEndIndex) + scriptTag + content.slice(bodyEndIndex);
}
}
return content;
} else {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preview</title>
<style>${finalCss}</style>
</head>
<body>
${htmlInput}
<script>
(function() {
${jsInput || ''}
})();
</script>
</body>
</html>
`;
}
}, [htmlInput, cssInput, jsInput]);
// Effect for loading content and managing inspect mode
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const content = generateHtmlContent();
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Write content
doc.open();
doc.write(content);
doc.close();
// Apply inspect mode immediately after writing content
if (inspectMode) {
console.log('🎨 Applying inspect mode styles.');
setupInspectModeStyles(doc);
}
}, [htmlInput, cssInput, jsInput, inspectMode, selectedDevice, isFullscreen, generateHtmlContent, setupInspectModeStyles]);
// Effect for injecting device frame CSS into the main document
useEffect(() => {
const styleId = 'device-frame-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = deviceFrameCSS;
document.head.appendChild(style);
}
return () => {
const style = document.getElementById(styleId);
if (style) {
style.remove();
}
};
}, []);
const getDeviceWrapper = () => {
console.log('🔧 Device Frame Debug:', { isFullscreen, selectedDevice });
// Non-fullscreen always uses iPhone frame (mobile view)
if (!isFullscreen) {
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
}
// Fullscreen desktop mode: no frame
if (selectedDevice === 'desktop') {
console.log('🖥️ Desktop fullscreen: No device frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
switch (selectedDevice) {
case 'tablet':
console.log('📟 Rendering iPad Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'ipad-pro'
};
case 'mobile':
console.log('📱 Rendering iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
default:
console.log('❓ Unknown device, no frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
};
const { wrapperClass, deviceFrame } = getDeviceWrapper();
if (deviceFrame) {
// Render with device frame (iPhone 14 Pro or iPad Pro)
console.log(`🎨 Rendering device frame: device-${deviceFrame}`);
return (
<div className={wrapperClass}>
<div className={`device device-${deviceFrame}`}>
<div className="device-frame">
<iframe
ref={iframeRef}
key={`device-${deviceFrame}-${selectedDevice}-${isFullscreen}`}
className="device-screen w-full h-full border-0"
title="HTML Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/>
</div>
<div className="device-stripe"></div>
<div className="device-header"></div>
<div className="device-sensors"></div>
<div className="device-btns"></div>
<div className="device-power"></div>
</div>
</div>
);
}
// Render without device frame (desktop or non-fullscreen)
return (
<div className={`${wrapperClass} bg-white rounded-lg shadow-lg`}>
<iframe
ref={iframeRef}
key={`no-device-${selectedDevice}-${isFullscreen}`}
className="w-full h-full border-0 overflow-hidden"
title="HTML Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
style={{ maxWidth: '100%', maxHeight: '100%' }}
/>
</div>
);
};
export default PreviewFrame;

View File

@@ -1,720 +0,0 @@
import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
// Device Frame CSS - Converted from SCSS
const deviceFrameCSS = `
/* iPhone 14 Pro Device Frame */
.device-iphone-14-pro {
height: 780px;
width: 384px;
transform-origin: center;
position: relative;
margin: 0 auto;
}
.device-iphone-14-pro .device-frame {
background: #010101;
border: 1px solid #2a242f;
border-radius: 61px;
box-shadow: inset 0 0 4px 2px #a8a4b0, inset 0 0 0 5px #342C3F;
height: 780px;
padding: 17px;
width: 384px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-iphone-14-pro .device-screen {
border-radius: 56px;
height: 746px;
width: 350px;
overflow: hidden;
scale: 0.75;
min-width: 130%;
height: 130%;
}
.device-iphone-14-pro .device-screen iframe {
width: 130%; /* 100% / 0.75 = 133.33% to compensate for 0.75 scale */
height: 130%;
transform: scale(0.75);
transform-origin: top left;
}
/* Mobile scrollbar styling for iPhone */
.device-iphone-14-pro .device-screen::-webkit-scrollbar {
width: 2px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
}
.device-iphone-14-pro .device-stripe::after,
.device-iphone-14-pro .device-stripe::before {
border: solid rgba(1, 1, 1, 0.25);
border-width: 0 7px;
content: "";
height: 7px;
left: 0;
position: absolute;
width: 100%;
z-index: 9;
}
.device-iphone-14-pro .device-stripe::after {
top: 77px;
}
.device-iphone-14-pro .device-stripe::before {
bottom: 77px;
}
.device-iphone-14-pro .device-header {
background: #010101;
border-radius: 18px;
height: 31px;
left: 50%;
margin-left: -54px;
position: absolute;
top: 32px;
width: 108px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::after,
.device-iphone-14-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-iphone-14-pro .device-sensors::after {
background: #010101;
border-radius: 16px;
height: 30px;
left: 50%;
margin-left: -54px;
top: 33px;
width: 67px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 8px;
left: 50%;
margin-left: 24px;
top: 44px;
width: 8px;
z-index: 10;
}
.device-iphone-14-pro .device-btns {
background: #2a242f;
border-radius: 1px;
height: 24px;
left: -2px;
position: absolute;
top: 86px;
width: 2px;
}
.device-iphone-14-pro .device-btns::after,
.device-iphone-14-pro .device-btns::before {
background: #2a242f;
border-radius: 1px;
content: "";
height: 46px;
left: 0;
position: absolute;
width: 2px;
}
.device-iphone-14-pro .device-btns::after {
top: 45px;
}
.device-iphone-14-pro .device-btns::before {
top: 105px;
}
.device-iphone-14-pro .device-power {
background: #2a242f;
border-radius: 1px;
height: 75px;
right: -2px;
position: absolute;
top: 150px;
width: 2px;
}
/* iPad Pro Device Frame */
.device-ipad-pro {
height: 840px;
width: 600px;
transform-origin: center;
margin-top: 40px;
position: relative;
margin-left: auto;
margin-right: auto;
}
.device-ipad-pro .device-frame {
background: #0d0d0d;
border-radius: 32px;
box-shadow: inset 0 0 0 1px #c1c2c3, inset 0 0 1px 2px #e2e3e4;
height: 800px;
padding: 24px;
width: 576px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-ipad-pro .device-screen {
border: 2px solid #0f0f0f;
border-radius: 10px;
overflow: hidden;
min-width: 200%;
height: 200%;
scale: 0.5;
}
.device-ipad-pro .device-screen iframe {
/* Set the iframe to the actual device resolution and scale it down */
width: 834px; /* iPad Pro 11" logical width */
height: 1194px; /* iPad Pro 11" logical height */
transform: scale(0.6331); /* 528px (screen width) / 834px (logical width) */
transform-origin: top left;
background: #fff; /* Ensure bg color for content */
}
/* Mobile scrollbar styling for iPad */
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 3px;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.device-ipad-pro .device-power {
background: #2a242f;
border-radius: 2px;
height: 2px;
width: 38px;
right: 76px;
top: -2px;
position: absolute;
}
/* Reposition buttons specifically for iPad Pro */
.device-ipad-pro .device-btns {
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume up */
width: 2px;
right: 22px;
top: 90px;
position: absolute;
}
.device-ipad-pro .device-btns::after {
content: "";
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume down */
width: 2px;
left: 0;
top: 40px; /* Space between buttons */
position: absolute;
}
.device-ipad-pro .device-btns::before {
display: none; /* Hide the third button from iPhone */
}
.device-ipad-pro .device-sensors::after,
.device-ipad-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-ipad-pro .device-sensors::after {
background: #141414;
border-radius: 16px;
box-shadow: -18px 0 #141414, 64px 0 #141414;
height: 10px;
left: 50%;
margin-left: -28px;
top: 11px;
width: 10px;
}
.device-ipad-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 6px;
left: 50%;
margin-left: -3px;
top: 13px;
width: 5px;
}
/* Enable smooth scrolling on iOS */
.device-iphone-14-pro .device-screen,
.device-ipad-pro .device-screen {
-webkit-overflow-scrolling: touch; /* smooth momentum scroll on iOS */
overflow-y: auto;
}
/* Mobile custom scrollbar */
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track,
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb,
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Optional: Hide scrollbar on larger screens for desktop */
/* This media query hides the scrollbar on desktops where touch scrolling is not needed */
@media (pointer: fine) and (hover: hover) {
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
display: none;
}
}
`;
const PreviewFrameExperimental = ({
htmlInput,
cssInput,
jsInput,
selectedDevice,
inspectMode,
onElementClick,
isFullscreen
}) => {
const iframeRef = useRef(null);
const [isInitialized, setIsInitialized] = useState(false);
const [lastJsInput, setLastJsInput] = useState('');
// Handle iframe click for element selection
const handleIframeClick = useCallback((e) => {
if (!inspectMode) return;
e.preventDefault();
e.stopPropagation();
const clickedElement = e.target;
const elementInfo = {
tagName: clickedElement.tagName.toLowerCase(),
innerText: clickedElement.innerText || clickedElement.textContent || '',
attributes: {}
};
Array.from(clickedElement.attributes).forEach(attr => {
elementInfo.attributes[attr.name] = attr.value;
});
onElementClick(elementInfo);
}, [inspectMode, onElementClick]);
// Function to setup inspect mode styles and event handlers
const setupInspectModeStyles = useCallback((iframeDoc) => {
console.log('🎨 PreviewFrame Experimental: Setting up inspect mode');
// Remove existing inspect styles and observers
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
if (existingStyle) existingStyle.remove();
// Clean up any existing observer
if (iframeDoc._inspectObserver) {
iframeDoc._inspectObserver.disconnect();
delete iframeDoc._inspectObserver;
}
// Add inspect mode styles with better hover highlights
const style = iframeDoc.createElement('style');
style.id = 'inspect-mode-styles';
style.textContent = `
/* High specificity selectors for inspect mode */
html * {
cursor: crosshair !important;
pointer-events: auto !important;
}
/* Hover highlights with maximum specificity */
html body *:hover {
outline: 2px solid #3b82f6 !important;
outline-offset: 1px !important;
background-color: rgba(59, 130, 246, 0.1) !important;
transition: all 0.1s ease !important;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3) !important;
}
/* Selected element styles */
html body [data-selected="true"] {
outline: 2px solid #10b981 !important;
outline-offset: 1px !important;
background-color: rgba(16, 185, 129, 0.1) !important;
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3) !important;
}
/* Selected element hover */
html body [data-selected="true"]:hover {
outline: 2px solid #059669 !important;
outline-offset: 1px !important;
background-color: rgba(5, 150, 105, 0.15) !important;
box-shadow: 0 0 0 1px rgba(5, 150, 105, 0.4) !important;
}
/* Prevent text selection during inspect */
html * {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
`;
iframeDoc.head.appendChild(style);
// Add event handlers
const preventInteraction = (e) => {
e.preventDefault();
e.stopPropagation();
return false;
};
// Enhanced click handler that marks selected elements
const enhancedClickHandler = (e) => {
if (!inspectMode) return;
e.preventDefault();
e.stopPropagation();
// Remove previous selection
const previousSelected = iframeDoc.querySelector('[data-selected="true"]');
if (previousSelected) {
previousSelected.removeAttribute('data-selected');
}
// Mark new selection
const clickedElement = e.target;
clickedElement.setAttribute('data-selected', 'true');
// Call the original handler
handleIframeClick(e);
};
// Add click handler for element selection
iframeDoc.addEventListener('click', enhancedClickHandler, true);
// Prevent other interactions during inspect mode
iframeDoc.addEventListener('mousedown', preventInteraction, true);
iframeDoc.addEventListener('mouseup', preventInteraction, true);
iframeDoc.addEventListener('contextmenu', preventInteraction, true);
iframeDoc.addEventListener('dragstart', preventInteraction, true);
iframeDoc.addEventListener('selectstart', preventInteraction, true);
console.log('✅ PreviewFrame Experimental: Inspect mode applied');
}, [handleIframeClick, inspectMode]);
// Generate base HTML structure (without user JS)
const generateBaseHtml = useCallback(() => {
const scrollbarCSS = `
/* Custom Scrollbar Styles */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #555; }
html { scrollbar-width: thin; scrollbar-color: #888 #f1f1f1; }
`;
const finalCss = `${scrollbarCSS}\n${cssInput || ''}`;
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
if (isFullDocument) {
let content = htmlInput;
const headEndIndex = content.toLowerCase().indexOf('</head>');
if (headEndIndex !== -1) {
const styleTag = `\n<style>\n${finalCss}\n</style>\n`;
content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
}
return content;
} else {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preview</title>
<style>${finalCss}</style>
</head>
<body>
${htmlInput}
</body>
</html>
`;
}
}, [htmlInput, cssInput]);
// Initialize iframe with base content (runs once or when device changes)
const initializeIframe = useCallback(() => {
const iframe = iframeRef.current;
if (!iframe) return;
console.log('🚀 PreviewFrame Experimental: Initializing iframe');
const content = generateBaseHtml();
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Write base content
doc.open();
doc.write(content);
doc.close();
setIsInitialized(true);
setLastJsInput(''); // Reset JS tracking
console.log('✅ PreviewFrame Experimental: Base iframe initialized');
}, [generateBaseHtml]);
// Update only JavaScript content (without full page reload)
const updateJavaScript = useCallback(() => {
if (!isInitialized || !jsInput || jsInput === lastJsInput) return;
const iframe = iframeRef.current;
if (!iframe) return;
console.log('📝 PreviewFrame Experimental: Updating JavaScript only');
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Remove previous script if exists
const existingScript = doc.getElementById('user-script');
if (existingScript) {
existingScript.remove();
}
// Add new script with IIFE wrapper
if (jsInput.trim()) {
const script = doc.createElement('script');
script.id = 'user-script';
script.textContent = `
(function() {
try {
${jsInput}
} catch(e) {
console.error('User script error:', e);
}
})();
`;
doc.body.appendChild(script);
}
setLastJsInput(jsInput);
console.log('✅ PreviewFrame Experimental: JavaScript updated');
}, [isInitialized, jsInput, lastJsInput]);
// Update HTML content (requires partial reload)
const updateHtmlContent = useCallback(() => {
if (!isInitialized) return;
const iframe = iframeRef.current;
if (!iframe) return;
console.log('🔄 PreviewFrame Experimental: Updating HTML content');
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Update body content only (preserve head and scripts)
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
if (!isFullDocument) {
doc.body.innerHTML = htmlInput;
// Re-add user script after HTML update
updateJavaScript();
// Re-apply inspect mode if enabled
if (inspectMode) {
setupInspectModeStyles(doc);
}
} else {
// For full documents, we need to reinitialize
initializeIframe();
}
console.log('✅ PreviewFrame Experimental: HTML content updated');
}, [isInitialized, htmlInput, inspectMode, updateJavaScript, setupInspectModeStyles, initializeIframe]);
// Effect for iframe initialization (runs when device changes)
useEffect(() => {
initializeIframe();
}, [selectedDevice, isFullscreen, initializeIframe]);
// Effect for HTML content updates
useEffect(() => {
if (isInitialized) {
updateHtmlContent();
}
}, [htmlInput, cssInput, updateHtmlContent, isInitialized]);
// Effect for JavaScript updates
useEffect(() => {
updateJavaScript();
}, [jsInput, updateJavaScript]);
// Effect for inspect mode
useEffect(() => {
if (!isInitialized) return;
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument || iframe.contentWindow.document;
if (inspectMode) {
console.log('🎨 PreviewFrame Experimental: Enabling inspect mode');
setupInspectModeStyles(doc);
} else {
console.log('🎨 PreviewFrame Experimental: Disabling inspect mode');
const existingStyle = doc.getElementById('inspect-mode-styles');
if (existingStyle) existingStyle.remove();
// Remove selected attributes
const selectedElements = doc.querySelectorAll('[data-selected="true"]');
selectedElements.forEach(el => el.removeAttribute('data-selected'));
}
}, [inspectMode, isInitialized, setupInspectModeStyles]);
// Effect for injecting device frame CSS into the main document
useEffect(() => {
const styleId = 'device-frame-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = deviceFrameCSS;
document.head.appendChild(style);
}
return () => {
const style = document.getElementById(styleId);
if (style) {
style.remove();
}
};
}, []);
const getDeviceWrapper = () => {
console.log('🔧 Device Frame Debug (Experimental):', { isFullscreen, selectedDevice });
// Non-fullscreen always uses iPhone frame (mobile view)
if (!isFullscreen) {
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
}
// Fullscreen desktop mode: no frame
if (selectedDevice === 'desktop') {
console.log('🖥️ Desktop fullscreen: No device frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
switch (selectedDevice) {
case 'tablet':
console.log('📟 Rendering iPad Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'ipad-pro'
};
case 'mobile':
console.log('📱 Rendering iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
default:
console.log('❓ Unknown device, no frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
};
const { wrapperClass, deviceFrame } = getDeviceWrapper();
if (deviceFrame) {
// Render with device frame (iPhone 14 Pro or iPad Pro)
console.log(`🎨 Rendering device frame (Experimental): device-${deviceFrame}`);
return (
<div className={wrapperClass}>
<div className={`device device-${deviceFrame}`}>
<div className="device-frame">
<iframe
ref={iframeRef}
key={`experimental-device-${deviceFrame}-${selectedDevice}-${isFullscreen}`}
className="device-screen w-full h-full border-0"
title="HTML Preview (Experimental)"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/>
</div>
<div className="device-stripe"></div>
<div className="device-header"></div>
<div className="device-sensors"></div>
<div className="device-btns"></div>
<div className="device-power"></div>
</div>
</div>
);
}
// Render without device frame (desktop or non-fullscreen)
return (
<div className={`${wrapperClass} bg-white rounded-lg shadow-lg`}>
<iframe
ref={iframeRef}
key={`experimental-no-device-${selectedDevice}-${isFullscreen}`}
className="w-full h-full border-0 overflow-hidden"
title="HTML Preview (Experimental)"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
style={{ maxWidth: '100%', maxHeight: '100%' }}
/>
</div>
);
};
export default PreviewFrameExperimental;

View File

@@ -345,13 +345,11 @@ const PreviewFrame = ({
// Mark new selection
const clickedElement = e.target;
console.log(`🎯 ELEMENT SELECT: Clicked on <${clickedElement.tagName.toLowerCase()}> element`);
clickedElement.setAttribute('data-selected', 'true');
// Assign unique cascade-id for inspector operations
const cascadeId = `cascade-${Date.now()}`;
clickedElement.setAttribute('data-cascade-id', cascadeId);
console.log(`🏷️ ELEMENT SELECT: Assigned cascade-id: ${cascadeId}`);
const elementInfo = {
tagName: clickedElement.tagName.toLowerCase(),
@@ -372,9 +370,8 @@ const PreviewFrame = ({
} else {
window.currentIframeDom = updatedHtml;
}
console.log('💾 ELEMENT SELECT: Stored current iframe DOM with cascade-id');
} catch (error) {
console.warn('⚠️ Could not store iframe DOM:', error);
// silently handle iframe DOM storage failures
}
onElementClick(elementInfo);
@@ -382,7 +379,6 @@ const PreviewFrame = ({
// Function to setup inspect mode styles and event handlers with MutationObserver
const setupInspectModeStyles = useCallback((iframeDoc) => {
console.log('🎨 PreviewFrame: Setting up robust inspect mode with MutationObserver');
// Remove existing inspect styles and observers
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
@@ -485,7 +481,6 @@ const PreviewFrame = ({
});
if (needsReapply) {
console.log('🔄 PreviewFrame: DOM changed, reapplying inspect styles');
// Reapply styles after a short delay to ensure new elements are rendered
setTimeout(() => {
const currentStyle = iframeDoc.getElementById('inspect-mode-styles');
@@ -509,8 +504,6 @@ const PreviewFrame = ({
// Store observer for cleanup
iframeDoc._inspectObserver = observer;
console.log('✅ PreviewFrame: Robust inspect mode with MutationObserver applied');
}, [handleIframeClick]);
const generateHtmlContent = useCallback(() => {
@@ -674,18 +667,14 @@ const PreviewFrame = ({
const justCommitted = window.justCommittedInspectorChanges || false;
if (isInspectorActive && !isGenuineCodeChange) {
console.log('🚫 SKIP REFRESH: Inspector is active, iframe DOM is source of truth');
return;
}
if (justCommitted) {
console.log('🚫 SKIP REFRESH: Just committed inspector changes, preventing immediate refresh');
window.justCommittedInspectorChanges = false;
return;
}
console.log('✅ IFRAME UPDATE: Proceeding with iframe update (genuine code change or inspector inactive)');
// Update clean content refs
lastCleanHtmlRef.current = cleanHtml;
lastCleanCssRef.current = cleanCss;
@@ -717,7 +706,6 @@ const PreviewFrame = ({
// Skip update only if normalized content hasn't changed AND no device change
if (normalizedNew === normalizedLast && lastContentRef.current !== '' && !isDeviceChange) {
console.log('📋 Content unchanged and no device change, skipping update');
return;
}
@@ -740,7 +728,6 @@ const PreviewFrame = ({
// Add safety check for document readiness
if (!doc || !doc.body) {
console.log('⚠️ Document not ready for content update, using fallback...');
// Fallback to document.write for safety
if (iframe.contentDocument) {
iframe.contentDocument.open();
@@ -773,13 +760,10 @@ const PreviewFrame = ({
try {
// SMART SCROLL RESTORATION: Only restore for genuine code changes, not inspector operations
if (skipScrollRestorationRef.current) {
console.log('🚫 SKIP: Scroll restoration skipped for inspector operation');
skipScrollRestorationRef.current = false;
return;
}
console.log('🔄 SMART RESTORE: Attempting scroll restoration for genuine code change');
const storedScroll = window.localStorage.getItem('htmlPreview_scrollPosition');
if (!storedScroll) {
@@ -842,7 +826,6 @@ const PreviewFrame = ({
if (hasInspectorEdits && !isDeviceChange) {
// INSPECTOR FIELD EDIT - Visual-only iframe update (no scroll disruption)
console.log('📝 INSPECTOR SAVE: Updating iframe content');
// Skip scroll restoration for inspector edits
skipScrollRestorationRef.current = true;
@@ -865,20 +848,16 @@ const PreviewFrame = ({
editedElement.setAttribute(attr.name, attr.value);
}
});
console.log('🚀 Visual-only update complete - no scroll disruption');
}
}
} else {
// CODE/DEVICE CHANGE - Full content reload with scroll preservation
console.log('🔄 Code or device change: Full content reload');
const currentBody = doc.body;
const tempDiv = doc.createElement('div');
tempDiv.innerHTML = newContent.match(/<body[^>]*>([\s\S]*)<\/body>/i)?.[1] || newContent;
if (isDeviceChange) {
console.log('📱 Device change detected - canceling inspector session and fresh reload');
// Cancel any active inspector session by generating clean content from code boxes
const cleanContent = generateHtmlContent();
@@ -892,15 +871,12 @@ const PreviewFrame = ({
const restoreScrollAfterDeviceChange = () => {
try {
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
console.log('📍 Device change: Attempting scroll restoration...', { storedScroll });
if (!storedScroll) {
console.log('⚠️ No stored scroll position found');
return;
}
if (!iframe.contentWindow) {
console.log('⚠️ iframe.contentWindow is null, retrying in 100ms...');
setTimeout(restoreScrollAfterDeviceChange, 100);
return;
}
@@ -909,7 +885,6 @@ const PreviewFrame = ({
if (parsed && typeof parsed.x === 'number' && typeof parsed.y === 'number') {
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
if (!doc || !doc.body || !iframe.contentWindow) {
console.log('⚠️ Device change: Document or contentWindow not ready, retrying...');
setTimeout(restoreScrollAfterDeviceChange, 100);
return;
}
@@ -920,21 +895,10 @@ const PreviewFrame = ({
const safeX = Math.min(parsed.x, maxScrollX);
const safeY = Math.min(parsed.y, maxScrollY);
console.log('📍 Device change scroll debug:', {
stored: { x: parsed.x, y: parsed.y },
safe: { x: safeX, y: safeY },
contentHeight: doc.body.scrollHeight,
viewportHeight: iframe.contentWindow.innerHeight,
maxScrollY
});
iframe.contentWindow.scrollTo(safeX, safeY);
console.log(`✅ Device change: Scroll restored to ${safeX}, ${safeY}`);
} else {
console.log('⚠️ Invalid scroll data:', parsed);
}
} catch (e) {
console.error('❌ Device change scroll restoration error:', e);
// Silently handle error
}
};
@@ -942,10 +906,7 @@ const PreviewFrame = ({
setTimeout(restoreScrollAfterDeviceChange, 100);
setTimeout(restoreScrollAfterDeviceChange, 300);
setTimeout(restoreScrollAfterDeviceChange, 500);
console.log('📱 Device change: Fresh reload with clean content completed');
} else {
console.log('📝 Code change detected - reload with scroll preservation');
currentBody.innerHTML = tempDiv.innerHTML;
@@ -953,15 +914,12 @@ const PreviewFrame = ({
const restoreScrollToElement = () => {
try {
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
console.log('📍 Code change: Attempting element-based scroll restoration...', { storedScroll });
if (!storedScroll) {
console.log('⚠️ Code change: No stored scroll data found');
return;
}
if (!iframe.contentWindow) {
console.log('⚠️ Code change: iframe.contentWindow not ready, retrying...');
setTimeout(restoreScrollToElement, 50);
return;
}
@@ -970,7 +928,6 @@ const PreviewFrame = ({
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
if (!doc || !doc.body || !iframe.contentWindow) {
console.log('⚠️ Code change: Document or contentWindow not ready, retrying...');
setTimeout(restoreScrollToElement, 50);
return;
}
@@ -980,10 +937,8 @@ const PreviewFrame = ({
const targetElement = doc.querySelector(parsed.elementSelector);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'instant', block: 'start' });
console.log(`✅ Code change: Scrolled to element ${parsed.elementSelector}`);
return;
} else {
console.log(`⚠️ Code change: Element ${parsed.elementSelector} not found, trying text match...`);
// Fallback: find element by text content
if (parsed.elementText) {
@@ -991,7 +946,6 @@ const PreviewFrame = ({
for (const el of allElements) {
if (el.textContent && el.textContent.includes(parsed.elementText.substring(0, 20))) {
el.scrollIntoView({ behavior: 'instant', block: 'start' });
console.log(`✅ Code change: Scrolled to element by text match`);
return;
}
}
@@ -1009,12 +963,11 @@ const PreviewFrame = ({
if (safeX > 0 || safeY > 0) {
iframe.contentWindow.scrollTo(safeX, safeY);
console.log(`✅ Code change: Fallback coordinate scroll to ${safeX}, ${safeY}`);
}
}
} catch (e) {
console.error('❌ Code change: Element-based scroll restoration error:', e);
// Silently handle error
}
};
@@ -1022,34 +975,31 @@ const PreviewFrame = ({
setTimeout(restoreScrollToElement, 50);
setTimeout(restoreScrollToElement, 150);
}
// Update head styles
const styleMatch = newContent.match(/<style[^>]*>([\s\S]*?)<\/style>/gi);
if (styleMatch) {
const oldStyles = doc.querySelectorAll('style:not(#inspect-mode-styles):not(#device-frame-styles)');
oldStyles.forEach(style => style.remove());
styleMatch.forEach(styleTag => {
const styleEl = doc.createElement('style');
styleEl.textContent = styleTag.replace(/<\/?style[^>]*>/gi, '');
doc.head.appendChild(styleEl);
});
// Update head styles
const styleMatch = newContent.match(/<style[^>]*>([\s\S]*?)<\/style>/gi);
if (styleMatch) {
const oldStyles = doc.querySelectorAll('style:not(#inspect-mode-styles):not(#device-frame-styles)');
oldStyles.forEach(style => style.remove());
styleMatch.forEach(styleTag => {
const styleEl = doc.createElement('style');
styleEl.textContent = styleTag.replace(/<\/?style[^>]*>/gi, '');
doc.head.appendChild(styleEl);
});
}
}
console.log('🚀 Full reload complete');
}
}
} catch (error) {
console.error('Error updating iframe content:', error);
// Fallback to document.write if selective update fails
try {
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
doc.open();
doc.write(newContent);
doc.close();
console.log('🔄 Fallback to full rewrite');
} catch (fallbackError) {
console.error('Fallback update also failed:', fallbackError);
// Fallback update failed
}
} finally {
isLoadingRef.current = false;
@@ -1074,10 +1024,8 @@ const PreviewFrame = ({
if (!doc || !doc.body) return;
if (inspectMode) {
console.log('🎨 Applying inspect mode styles.');
setupInspectModeStyles(doc);
} else {
console.log('🧹 Removing inspect mode styles.');
// Remove inspect mode styles when disabled
const existingStyle = doc.getElementById('inspect-mode-styles');
if (existingStyle) {
@@ -1104,7 +1052,6 @@ const PreviewFrame = ({
const currentX = win.pageXOffset || win.scrollX || 0;
const currentY = win.pageYOffset || win.scrollY || 0;
console.log(`📊 TRACKING SCROLL: Current position (${currentX}, ${currentY})`);
scrollPositionRef.current = { x: currentX, y: currentY };
// Store scroll position with element-based tracking
@@ -1113,7 +1060,6 @@ const PreviewFrame = ({
try {
// Store in PARENT window localStorage, not iframe localStorage
window.localStorage.setItem('htmlPreview_scrollPosition', JSON.stringify(scrollData));
console.log(`💾 STORED SCROLL: Saved position (${currentX}, ${currentY}) to localStorage`);
} catch (e) {
// Silently handle localStorage errors
}
@@ -1191,11 +1137,9 @@ const PreviewFrame = ({
}, []);
const getDeviceWrapper = () => {
console.log('🔧 Device Frame Debug:', { isFullscreen, selectedDevice });
// Non-fullscreen always uses iPhone frame (mobile view)
if (!isFullscreen) {
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
@@ -1204,7 +1148,6 @@ const PreviewFrame = ({
// Fullscreen desktop mode: no frame
if (selectedDevice === 'desktop') {
console.log('🖥️ Desktop fullscreen: No device frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
@@ -1213,19 +1156,16 @@ const PreviewFrame = ({
switch (selectedDevice) {
case 'tablet':
console.log('📟 Rendering iPad Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'ipad-pro'
};
case 'mobile':
console.log('📱 Rendering iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
default:
console.log('❓ Unknown device, no frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
@@ -1242,7 +1182,6 @@ const PreviewFrame = ({
if (deviceFrame) {
// Render with device frame (iPhone 14 Pro or iPad Pro)
console.log(`🎨 Rendering device frame: device-${deviceFrame}`);
return (
<div className={wrapperClass}>
<div className={`device device-${deviceFrame}`}>

View File

@@ -28,7 +28,6 @@ const Toolbar = ({
cleanupInspectorState();
} else {
// Activate inspect mode (no need to cleanup when activating)
console.log('🎯 TOOLBAR: Activating inspect mode');
setInspectMode(true);
}
};

View File

@@ -1,8 +1,10 @@
// Google Analytics utility for React SPA
// Implements best practices for Single Page Applications
import { initConsentMode, applyStoredConsent } from './consentManager';
// Google Analytics configuration
const GA_MEASUREMENT_ID = 'G-S3K5P2PWV6';
const GA_MEASUREMENT_ID = process.env.REACT_APP_GA_ID || 'G-S3K5P2PWV6';
// Initialize Google Analytics with Consent Mode v2
export const initGA = () => {
@@ -22,7 +24,6 @@ export const initGA = () => {
window.gtag = gtag;
// Initialize Consent Mode v2 BEFORE loading GA script
const { initConsentMode, applyStoredConsent } = require('./consentManager');
initConsentMode();
// Create script elements

View File

@@ -122,18 +122,14 @@ export const initBrowserCompat = () => {
// Add specific fixes for Telegram browser
if (browserInfo.isTelegram) {
console.log('Telegram browser detected - applying compatibility fixes');
// Add Telegram-specific error handling
window.addEventListener('error', (event) => {
console.log('Global error caught in Telegram browser:', event.error);
// Prevent the error from bubbling up and showing the error overlay
event.preventDefault();
return true;
});
window.addEventListener('unhandledrejection', (event) => {
console.log('Unhandled promise rejection in Telegram browser:', event.reason);
// Prevent the error from bubbling up
event.preventDefault();
return true;

View File

@@ -185,7 +185,7 @@ export const getConsentBannerData = () => {
{
id: CONSENT_CATEGORIES.ADVERTISING,
name: 'Advertising',
description: 'Future ad personalization (not yet implemented)',
description: 'Ad personalization and targeting (Adsterra)',
required: false
}
],

View File

@@ -79,7 +79,7 @@ export const fetchUrlContent = async (url) => {
}
}
} catch (directError) {
console.log('Direct fetch failed, trying CORS proxy:', directError.message);
// Direct fetch failed, trying CORS proxy
}
// Try CORS proxies
@@ -124,7 +124,6 @@ export const fetchUrlContent = async (url) => {
}
} catch (proxyError) {
lastError = proxyError;
console.log(`Proxy ${proxy} failed:`, proxyError.message);
continue;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -44,7 +44,6 @@ export const fetchGitHubReleases = async (owner, repo, token = null) => {
url: commit.html_url
}));
} catch (error) {
console.error('Failed to fetch GitHub releases:', error);
return [];
}
};
@@ -79,7 +78,6 @@ export const fetchGiteaReleases = async (owner, repo, token, baseUrl) => {
url: `${baseUrl}/${owner}/${repo}/commit/${commit.sha}`
}));
} catch (error) {
console.error('Failed to fetch Gitea releases:', error);
return [];
}
};
@@ -95,7 +93,6 @@ export const fetchCustomReleases = async (apiEndpoint) => {
return await response.json();
} catch (error) {
console.error('Failed to fetch custom releases:', error);
return [];
}
};
@@ -111,7 +108,6 @@ export const fetchStaticReleases = async () => {
return await response.json();
} catch (error) {
console.error('Failed to fetch static releases:', error);
return [];
}
};

View File

@@ -248,7 +248,7 @@ export const getCoreWebVitalsOptimizations = () => {
// Cumulative Layout Shift (CLS)
cls: {
setImageDimensions: true,
reserveSpaceForAds: true, // Important for future AdSense
reserveSpaceForAds: true,
avoidDynamicContent: true,
useTransforms: true
}