87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
||
import { useParams } from "react-router-dom";
|
||
|
||
import { api } from "../api";
|
||
|
||
const A4_WIDTH_PX = (210 / 25.4) * 96;
|
||
const A4_HEIGHT_PX = (297 / 25.4) * 96;
|
||
|
||
// Anonymous read-only public CV at /cv/:slug. Renders the server-produced HTML in a sandboxed
|
||
// iframe. noindex is enforced server-side (X-Robots-Tag) and reinforced with a meta tag here.
|
||
export default function PublicCvPage() {
|
||
const { slug } = useParams();
|
||
const pdfUrl = `${api.defaults.baseURL}/public-cv/${slug}/pdf`;
|
||
const [html, setHtml] = useState<string | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [frameScale, setFrameScale] = useState(1);
|
||
const [frameHeight, setFrameHeight] = useState(A4_HEIGHT_PX);
|
||
|
||
useEffect(() => {
|
||
const meta = document.createElement("meta");
|
||
meta.name = "robots";
|
||
meta.content = "noindex, nofollow";
|
||
document.head.appendChild(meta);
|
||
|
||
let alive = true;
|
||
api.get<{ html: string; name: string }>(`/public-cv/${slug}`)
|
||
.then((r) => { if (alive) { setHtml(r.data.html); if (r.data.name) document.title = `${r.data.name} — CV`; } })
|
||
.catch((err) => {
|
||
if (!alive) return;
|
||
// 404 = unknown slug or the owner made it private again. Anything else is a transient error.
|
||
setError(err?.response?.status === 404
|
||
? "This CV isn’t available. The link may be wrong, or the owner has made it private."
|
||
: "This CV can’t be loaded right now. Please try again in a moment.");
|
||
});
|
||
|
||
return () => {
|
||
alive = false;
|
||
document.head.removeChild(meta);
|
||
};
|
||
}, [slug]);
|
||
|
||
useEffect(() => {
|
||
const updateScale = () => setFrameScale(Math.min(1, Math.max(0.1, (window.innerWidth - 32) / A4_WIDTH_PX)));
|
||
updateScale();
|
||
window.addEventListener("resize", updateScale);
|
||
return () => window.removeEventListener("resize", updateScale);
|
||
}, []);
|
||
|
||
if (error) {
|
||
return (
|
||
<div style={{ display: "flex", height: "100vh", alignItems: "center", justifyContent: "center", fontFamily: "system-ui", color: "#374151" }}>
|
||
{error}
|
||
</div>
|
||
);
|
||
}
|
||
if (html === null) {
|
||
return (
|
||
<div role="status" style={{ display: "flex", height: "100vh", alignItems: "center", justifyContent: "center", fontFamily: "system-ui", color: "#4b5563" }}>
|
||
Loading…
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{ minHeight: "100vh", background: "#e9edf2", padding: "24px 16px", display: "flex", flexDirection: "column", alignItems: "center", gap: 16, overflowX: "hidden" }}>
|
||
<a href={pdfUrl} download style={{ padding: "10px 16px", borderRadius: 8, background: "#1d4ed8", color: "#fff", font: "600 14px system-ui", textDecoration: "none" }}>
|
||
Download PDF
|
||
</a>
|
||
<div
|
||
data-testid="public-cv-frame-container"
|
||
style={{ width: A4_WIDTH_PX * frameScale, height: frameHeight * frameScale, maxWidth: "100%", overflow: "hidden", boxShadow: "0 8px 30px rgba(0,0,0,0.18)" }}
|
||
>
|
||
<iframe
|
||
title="Public CV"
|
||
srcDoc={html}
|
||
sandbox="allow-same-origin"
|
||
onLoad={(event) => {
|
||
const documentHeight = event.currentTarget.contentDocument?.documentElement.scrollHeight ?? A4_HEIGHT_PX;
|
||
setFrameHeight(Math.max(A4_HEIGHT_PX, documentHeight));
|
||
}}
|
||
style={{ width: A4_WIDTH_PX, height: frameHeight, transform: `scale(${frameScale})`, transformOrigin: "top left", border: "none", background: "#fff", display: "block" }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|