Files
jobtrackingapp/job-tracker-ui/src/views/PublicCvPage.tsx
T
cesnimda a7c25499bb
CI and Deploy / test (pull_request) Successful in 4m54s
CI and Deploy / deploy (pull_request) Has been skipped
fix(a11y): close cross-app interaction gaps
2026-08-15 17:49:42 +02:00

87 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}