path correction
This commit is contained in:
+45
-15
@@ -73,9 +73,12 @@ function Toast({ message, type }) {
|
||||
|
||||
function LoginPreview() {
|
||||
const viewportRef = useRef(null);
|
||||
const iframeRef = useRef(null);
|
||||
const [scale, setScale] = useState(0.5);
|
||||
const [frameBlocked, setFrameBlocked] = useState(false);
|
||||
const previewWidth = 1280;
|
||||
const previewHeight = 800;
|
||||
const previewSrc = `${BASE}preview.html`;
|
||||
|
||||
useEffect(() => {
|
||||
const el = viewportRef.current;
|
||||
@@ -91,6 +94,22 @@ function LoginPreview() {
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Host CSP (frame-ancestors 'none' / XFO DENY) can block the embed — show a fallback.
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const doc = iframeRef.current?.contentDocument;
|
||||
if (!doc?.body || doc.body.childElementCount === 0) {
|
||||
setFrameBlocked(true);
|
||||
}
|
||||
} catch {
|
||||
setFrameBlocked(true);
|
||||
}
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="login-preview">
|
||||
<div className="login-preview-intro">
|
||||
@@ -116,21 +135,32 @@ function LoginPreview() {
|
||||
ref={viewportRef}
|
||||
style={{ height: previewHeight * scale }}
|
||||
>
|
||||
<div
|
||||
className="login-preview-stage"
|
||||
style={{ width: previewWidth * scale, height: previewHeight * scale }}
|
||||
>
|
||||
<iframe
|
||||
src={`${BASE}preview.html`}
|
||||
title="Website preview"
|
||||
className="login-preview-iframe"
|
||||
style={{
|
||||
width: previewWidth,
|
||||
height: previewHeight,
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{frameBlocked ? (
|
||||
<div className="login-preview-fallback">
|
||||
<p>Inline preview is blocked by the server's frame policy.</p>
|
||||
<a href={previewSrc} target="_blank" rel="noopener noreferrer" className="login-preview-link">
|
||||
<ExternalLink size={14} />
|
||||
Open preview
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="login-preview-stage"
|
||||
style={{ width: previewWidth * scale, height: previewHeight * scale }}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={previewSrc}
|
||||
title="Website preview"
|
||||
className="login-preview-iframe"
|
||||
style={{
|
||||
width: previewWidth,
|
||||
height: previewHeight,
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -893,6 +893,28 @@ body {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.login-preview-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
background: linear-gradient(160deg, #0f172a 0%, #1e293b 55%, #0f172a 100%);
|
||||
}
|
||||
|
||||
.login-preview-fallback p {
|
||||
margin: 0;
|
||||
max-width: 280px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.login-preview-iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
+9
-1
@@ -22,7 +22,15 @@ async function apiFetch(path, options = {}) {
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || 'Request failed');
|
||||
if (!res.ok) {
|
||||
const fallback =
|
||||
res.status === 404
|
||||
? 'API not found. Deploy server/ and run: npm run server (port 3001)'
|
||||
: res.status === 502
|
||||
? 'API server unreachable. Start Node on the host (port 3001).'
|
||||
: 'Request failed';
|
||||
throw new Error(data.error || fallback);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user