/* global React */ const { Calculator, Card, Badge, Button, Input, Textarea, Tabs, Select, NumberStepper } = window.DoslovnoByDesignSystem_88f6e6; const Icon = window.DBIcon; const MAXW = "var(--container-max)"; function scrollToId(id) { const el = document.getElementById(id); if (!el) return; const y = el.getBoundingClientRect().top + window.scrollY - 70; window.scrollTo({ top: y, behavior: "smooth" }); } /* ----------------------------------------------------------- layout helpers */ function Section({ children, bg = "var(--surface-page)", id, style = {}, pad = "112px 24px" }) { return (
{children}
); } function Eyebrow({ children, onDark = false }) { return (
{children}
); } function H2({ children, onDark = false, style = {} }) { return (

{children}

); } /* ----------------------------------------------------- language switcher */ const HEADER_LANGS = [["RU", "Русский"], ["BY", "Беларуская"], ["EN", "English"], ["PL", "Polski"]]; function LangSwitcher() { const [cur, setCur] = React.useState("RU"); return (
); } /* --------------------------------------------------------------- lead sink */ // Единая точка приёма заявок. Сейчас: сохраняем в localStorage (черновая «база» // до интеграции) и, если задан LEAD_ENDPOINT, отправляем POST. Позже это тело // заменяем на реальный бэкенд / CRM / сервис статистики лидов — UI не трогаем. const LEAD_ENDPOINT = ""; // напр. "/api/lead" или вебхук Telegram-бота. Пусто — пока только локально. async function submitLead(payload) { const lead = { id: Date.now().toString(36) + Math.random().toString(36).slice(2, 7), ts: new Date().toISOString(), page: typeof location !== "undefined" ? location.pathname : "", ...payload, }; try { const k = "doslovno_leads"; const all = JSON.parse(localStorage.getItem(k) || "[]"); all.push(lead); localStorage.setItem(k, JSON.stringify(all)); } catch (e) { /* приватный режим — пропускаем локальное сохранение */ } if (LEAD_ENDPOINT) { await fetch(LEAD_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(lead), }); } console.log("[lead]", lead); return lead; } // --- режим взаимодействия: скролл к секции или модалка поверх экрана --- // Техфлаг: "scroll" — скроллим к калькулятору/форме; "modal" — открываем поверх. const INTERACTION_MODE = "modal"; // Открыть калькулятор. opts.mode: "quick" | "send". function openCalc(opts) { opts = opts || {}; if (INTERACTION_MODE === "modal") { window.dispatchEvent(new CustomEvent("doslovno:open-modal", { detail: { kind: "calc", mode: opts.mode || "quick" } })); } else { if (opts.mode) window.dispatchEvent(new CustomEvent("doslovno:set-calc-mode", { detail: { mode: opts.mode } })); scrollToId("hero"); } } // Открыть форму заявки. opts.message — предзаполнить текст. // В модалке текст передаём через detail (форма ещё не смонтирована — событие // prefill-lead она бы не поймала), на встроенную форму внизу — событием. function openLead(opts) { opts = opts || {}; if (INTERACTION_MODE === "modal") { window.dispatchEvent(new CustomEvent("doslovno:open-modal", { detail: { kind: "lead", message: opts.message || "" } })); } else { if (opts.message) window.dispatchEvent(new CustomEvent("doslovno:prefill-lead", { detail: { message: opts.message } })); scrollToId("lead"); } } function closeModal() { window.dispatchEvent(new CustomEvent("doslovno:close-modal")); } // Совместимость: старое имя. function prefillLead(message) { openLead({ message: message }); } // Грубая валидация номера: после префикса +375 должно остаться 9 цифр. function isValidPhone(raw) { return (raw || "").replace(/\D/g, "").length >= 9; } /* --------------------------------------------------------- form helpers */ // Звёздочка обязательного поля в label. function reqLabel(text) { return {text} *; } // Чекбокс согласия на обработку персональных данных. function Consent(props) { return ( ); } /* ------------------------------------------------------------- calculator */ const CALC_DOCS = ["Свидетельство о рождении", "Диплом / аттестат", "Справка", "Договор", "Паспорт", "Иное"]; const CALC_LANGS = ["Русский", "Белорусский", "Английский", "Польский", "Немецкий", "Литовский", "Чешский", "Украинский", "Итальянский", "Испанский", "Французский", "Китайский", "Арабский", "Персидский", "Туркменский"]; const LANG_RATE = { "Английский": 25, "Немецкий": 25, "Польский": 35, "Литовский": 35, "Чешский": 35, "Итальянский": 35, "Испанский": 30, "Французский": 30, "Украинский": 20, "Русский": 20, "Белорусский": 20, "Китайский": 58, "Арабский": 58, "Персидский": 55, "Туркменский": 45 }; // Заверение зависит от направления: на русский/белорусский — 75, на иностранный — 150 (из них 90 — гос.тариф нотариуса, п.36). const NOTARY_FEE = (to) => (to === "Русский" || to === "Белорусский") ? 75 : 150; // Дефолтное состояние калькулятора. Если пользователь ничего не менял — // в заявку расчёт не подставляем (примечание остаётся пустым). const CALC_DEFAULTS = { doc: CALC_DOCS[0], from: "Русский", to: "Английский", notary: false, pages: 1 }; function CustomCalc(props) { const uid = React.useId(); const [mode, setMode] = React.useState((props && props.initialMode) || "quick"); const [doc, setDoc] = React.useState(CALC_DEFAULTS.doc); const [from, setFrom] = React.useState(CALC_DEFAULTS.from); const [to, setTo] = React.useState(CALC_DEFAULTS.to); const [notary, setNotary] = React.useState(CALC_DEFAULTS.notary); // по умолчанию без заверения — цена не пугает const [pages, setPages] = React.useState(CALC_DEFAULTS.pages); // среднее по документам, клиент подправит // режим «Отправить на оценку» const [sendPhone, setSendPhone] = React.useState(""); const [sendFileName, setSendFileName] = React.useState(""); const [sendConsent, setSendConsent] = React.useState(false); const [sendStatus, setSendStatus] = React.useState("idle"); // idle | sending | sent | error const [sendTouched, setSendTouched] = React.useState(false); // переключение вкладки извне (скролл-режим) React.useEffect(() => { const h = (e) => { if (e.detail && e.detail.mode) setMode(e.detail.mode); }; window.addEventListener("doslovno:set-calc-mode", h); return () => window.removeEventListener("doslovno:set-calc-mode", h); }, []); const sendPhoneOk = isValidPhone(sendPhone); const submitEstimate = async () => { setSendTouched(true); if (!sendPhoneOk || !sendConsent) return; setSendStatus("sending"); try { await submitLead({ kind: "calc_estimate", phone: "+375" + sendPhone.replace(/\D/g, "").slice(-9), file: sendFileName || null }); setSendStatus("sent"); } catch (e) { setSendStatus("error"); } }; const transFee = (LANG_RATE[to] || 25) * pages; const notaryFee = notary ? NOTARY_FEE(to) : 0; const est = transFee + notaryFee; // тронут ли калькулятор — хоть одно поле отличается от дефолта const calcTouched = doc !== CALC_DEFAULTS.doc || from !== CALC_DEFAULTS.from || to !== CALC_DEFAULTS.to || notary !== CALC_DEFAULTS.notary || pages !== CALC_DEFAULTS.pages; const goLeadFromCalc = () => calcTouched ? prefillLead(`${doc}, ${from} → ${to}${notary ? ", с заверением" : ""}, ${pages} стр. Расчёт ≈ ${est} BYN`) : openLead(); const swap = () => { setFrom(to); setTo(from); }; const Pill = ({ active, children, onClick }) => ( ); const tabBtn = (id, label) => { const on = mode === id; return ( ); }; return (
{tabBtn("quick", "Быстрый расчёт")} {tabBtn("send", "Отправить на оценку")}
{mode === "quick" ? (
setFrom(e.target.value)} options={CALC_LANGS} /> setSendFileName(e.target.files && e.target.files[0] ? e.target.files[0].name : "")} /> setSendPhone(e.target.value)} error={sendTouched && !sendPhoneOk ? "Укажите номер телефона" : undefined} /> setSendConsent(e.target.checked)} error={sendTouched && !sendConsent} /> {sendStatus === "error" ?
Не удалось отправить. Позвоните нам или попробуйте ещё раз.
: null}
Посчитаем точную стоимость сами и свяжемся в течение 30 минут
)}
); } /* ----------------------------------------------------------------- header */ function Header() { const [scrolled, setScrolled] = React.useState(false); React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 12); window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); }, []); const links = [["Услуги", "services"], ["Доставка", "delivery"], ["Цены", "prices"], ["Вики", "wiki"], ["Контакты", "lead"]]; const [hover, setHover] = React.useState(null); return (
+375 29 623-41-76
); } /* ------------------------------------------------------------------- hero */ function Hero() { const trust = [ ["clock-3", "Готовность от 4 часов"], ["shield-check", "Принимают в любых инстанциях"], ["badge-check", "Заверение у нотариуса"], ]; return (
 Рядом с визовым центром  Заберём и доставим по Минску

Нотариальный перевод.
Ценим ваше время.

Приходите с документом — уходите с готовым переводом, заверенным у нотариуса. Берём на себя нервы, очереди и бюрократию.

{trust.map(([ic, tx]) => (
{tx}
))}
); } /* --------------------------------------------------------- how we work */ function HowWeWork() { const steps = [ ["file-text", "Приносите или присылаете документ", "Загляните к нам у визового центра или отправьте скан / фото в мессенджер."], ["calculator", "Считаем и согласовываем", "Сразу называем точную цену и срок. Никаких скрытых доплат за заверение."], ["badge-check", "Переводим и заверяем у нотариуса", "Делает дипломированный переводчик, заверяет нотариус — документ примут везде."], ["truck", "Забираете или привозим", "Готовый перевод отдадим в офисе или бесплатно доставим курьером по Минску."], ]; const flowWords = ["передаём", "считаем", "переводим", "доставляем"]; const reduce = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const [active, setActive] = React.useState(0); React.useEffect(() => { if (reduce) return; const id = setInterval(() => setActive((a) => (a + 1) % 4), 1900); return () => clearInterval(id); }, [reduce]); return (
Как мы работаем

Четыре шага — и документ готов

Вам не нужно разбираться в требованиях инстанций и стоять в очередях. Это наша работа.

{/* document gliding from step to step */}
{[0, 1, 2, 3].map((i) => (
))}
{steps.map(([ic, t, d], i) => { const on = i === active && !reduce; return (
{i + 1}

{t}

{d}

); })}
); } /* --------------------------------------------------------- services / tabs */ const SERVICE_DATA = { emb: { note: "Документы для подачи в посольства и консульства — переводим под требования конкретной страны.", items: [ ["file-check-2", "Справка о несудимости", "Перевод + заверение, частый документ для виз и ВНЖ."], ["file-text", "Выписка из банка / о доходах", "Финансовые документы для визовых анкет."], ["badge-check", "Согласие на выезд ребёнка", "Нотариальный перевод согласия от родителей."], ["file-text", "Паспорт и внутренние документы", "Перевод страниц паспорта, ID-карты, прописки."], ], }, abroad: { note: "Переезд, ПМЖ и легализация документов за границей — полный пакет под ключ.", items: [ ["file-check-2", "Свидетельства ЗАГС", "О рождении, браке, разводе — с заверением."], ["badge-check", "Документы для ПМЖ", "Полный комплект под требования страны."], ["file-text", "Водительское удостоверение", "Перевод для обмена прав за рубежом."], ["file-check-2", "Апостиль и легализация", "Подскажем, что заверять нотариально, а что апостилем."], ], }, edu: { note: "Поступление и учёба за рубежом — переводим документы об образовании.", items: [ ["file-check-2", "Диплом с приложением", "Перевод диплома и вкладыша с оценками."], ["file-text", "Аттестат об образовании", "Школьный аттестат для поступления."], ["badge-check", "Академические справки", "Справки о периоде обучения, транскрипты."], ["file-text", "Мотивационные письма", "Перевод сопроводительных документов."], ], }, med: { note: "Лечение и медицинские визы — переводим документы для клиник за рубежом.", items: [ ["file-check-2", "Медицинские выписки", "История болезни, эпикризы, заключения."], ["file-text", "Результаты обследований", "Анализы, снимки, протоколы исследований."], ["badge-check", "Справки и направления", "Для записи в зарубежные клиники."], ["file-text", "Рецепты и назначения", "Перевод назначений лечащего врача."], ], }, }; function ServiceCard({ ic, t, d }) { const [h, setH] = React.useState(false); return ( setH(true)} onMouseLeave={() => setH(false)} onClick={() => openLead()} style={{ display: "flex", flexDirection: "column", gap: 14, height: "100%", cursor: "pointer", borderColor: h ? "var(--color-primary)" : "var(--color-border)", boxShadow: h ? "0 14px 30px -20px rgba(20,20,58,0.45)" : "none", transform: h ? "translateY(-3px)" : "none", transition: "transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease" }}>

{t}

{d}

); } function Services() { const [tab, setTab] = React.useState("emb"); const data = SERVICE_DATA[tab]; return (
Услуги и направления

Что мы переводим

Мы делаем одно дело и делаем его хорошо: нотариальные переводы документов. Выберите свою ситуацию.

{data.note}

{data.items.map(([ic, t, d]) => ( ))}
Не нашли свой документ? — переведём почти всё.
); } /* --------------------------------------------------------------- delivery */ function SchematicMap() { return (
{/* faint street grid */}
{/* pin */}
doslovno.by
Рядом с визовым центром
); } function Delivery() { return (
Забор и доставка

Заберём и привезём документ по Минску

Не нужно подстраивать день под визит. Курьер заберёт оригинал у вас и вернёт готовый перевод — туда, куда удобно.

{[["truck", "Бесплатный курьер по городу"], ["clock-3", "Забор в день обращения"], ["map-pin", "Или приходите сами — мы у визового центра"]].map(([ic, tx]) => (
{tx}
))}
); } /* ----------------------------------------------------------------- prices */ // Комплект «под ключ» на иностранный (эмиграция): перевод 25 + заверение 150. Многостраничные — заверение разовое. const PRICES = [ ["Свидетельство о рождении / браке", "≈ 175 BYN"], ["Паспорт", "≈ 175 BYN"], ["Справка о несудимости", "≈ 175 BYN"], ["Согласие на выезд ребёнка", "≈ 175 BYN"], ["Диплом с приложением", "≈ 200 BYN"], ["Договор / нотариальный документ", "от ≈ 90 BYN / стр."], ]; function Prices() { return (
Цены примером

Понятные цены без сюрпризов

{PRICES.map(([doc, price], i) => (
{doc} {price}
))}
Полный прайс по всем услугам и языкам
 Цена за минуту

Точную цену подтвердим после просмотра документа

Примеры под ключ для выезда — перевод на иностранный язык с заверением у нотариуса, всё включено. Перевод на русский (для документов в Беларуси) — дешевле, от ≈ 100 BYN. Без скрытых доплат.

); } /* ------------------------------------------------------------------- wiki */ function WikiTeaser() { const countries = ["Польша", "Германия", "Литва", "Чехия", "США", "Канада", "Италия"]; return (
Полезная вики

Какие документы нужны — по странам и целям

Бесплатная база знаний: разбираем, какой пакет документов в какое консульство, что заверять у нотариуса, а что — апостилем. Обновляем по мере изменения требований.

{countries.map((c) => ( {c} ))} и ещё…
Например, «диплом для Польши»
{[["Польша: документы для визы D (учёба)", "Польша · Учёба"], ["Воссоединение семьи в Германии", "Германия · Семья"], ["Справка о несудимости: перевод и апостиль", "Общее"]].map(([t, m]) => (
{t}
{m}
))}
); } /* --------------------------------------------------------------- lead form */ function LeadForm(props) { const embedded = props && props.embedded; const uid = React.useId(); const [name, setName] = React.useState(""); const [phone, setPhone] = React.useState(""); const [message, setMessage] = React.useState((props && props.initialMessage) || ""); const [consent, setConsent] = React.useState(false); const [status, setStatus] = React.useState("idle"); // idle | sending | sent | error const [touched, setTouched] = React.useState(false); const sent = status === "sent"; React.useEffect(() => { const h = (e) => { if (e.detail && e.detail.message) setMessage(e.detail.message); if (status === "sent") setStatus("idle"); }; window.addEventListener("doslovno:prefill-lead", h); return () => window.removeEventListener("doslovno:prefill-lead", h); }, [status]); const nameOk = name.trim().length > 0; const phoneOk = isValidPhone(phone); const handleSubmit = async () => { setTouched(true); if (!nameOk || !phoneOk || !consent) return; setStatus("sending"); try { await submitLead({ kind: "lead_form", name: name.trim(), phone: "+375" + phone.replace(/\D/g, "").slice(-9), message: message.trim() }); setStatus("sent"); } catch (e) { setStatus("error"); } }; const card = ( {sent ? (

Заявка принята

Перезвоним в течение 30 минут.

) : (
{embedded ?

Оставьте заявку

: null} setName(e.target.value)} error={touched && !nameOk ? "Укажите имя" : undefined} /> setPhone(e.target.value)} error={touched && !phoneOk ? "Укажите номер телефона" : undefined} />