/* ============================================================
resume_doc.jsx — сам документ резюме.
Лист всегда белый и на светлом фоне: так он выглядит в PDF,
который человек отправит работодателю, независимо от темы
приложения. Никаких таблиц, рамок и колонок — их не читает ATS.
============================================================ */
const PAPER_W = 300; // ширина «листа» в макете, пропорция ≈ A4
function CVSection({ title, children, tight }) {
return (
);
}
/* Фото: настоящее мок-фото — так документ выглядит как готовый продукт. */
function CVPhoto({ on, src }) {
if (!on) return null;
return (
);
}
/*
CVDocument — один лист.
cv — результат buildCV(), docLang — язык документа ('en' | 'ru' | 'uz'),
t — строки интерфейса (для русских подписей разделов).
*/
function CVDocument({ cv, docLang, t }) {
const S = docLang === 'en'
? CV_EN
: { summary: t.cv_summary, exp: t.cv_exp, skills: t.cv_skills, certs: t.cv_certs, langs: t.cv_langs, pers: t.cv_pers, ready: t.cv_ready, present: t.cv_present };
const period = docLang === 'en'
? `${2026 - (cv.years || 1)} – present`
: `${2026 - (cv.years || 1)} – ${S.present}`;
return (
{/* шапка */}
{cv.name}
{cv.title}
{[cv.city, cv.age ? (docLang === 'en' ? `${cv.age} years old` : `${cv.age}`) : null, cv.phone]
.filter(Boolean).join(' · ')}
{cv.summary}
{cv.title}
{period}
{cv.place}
{cv.bullets.map((b, i) => (
- {b}
))}
{cv.skills.join(' · ')}
{cv.certs.length > 0 && (
{cv.certs.map((c, i) => — {c}
)}
)}
{cv.langs.map((l, i) => (
{l.name}{l.level}
))}
);
}
/* Лист в масштабе: высоту меряем по факту, чтобы длинный документ
не вылезал за пределы карточки. */
function CVPaper({ cv, docLang, t, scale = 1, blurFrom = null }) {
const inner = useRef(null);
const [h, setH] = useState(PAPER_W * 1.414);
useLayoutEffect(() => {
if (inner.current) setH(inner.current.getBoundingClientRect().height / scale);
}, [cv, docLang, scale]);
return (
{blurFrom != null && (
)}
);
}
Object.assign(window, { CVDocument, CVPaper, PAPER_W });