/* 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 (
);
}
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 (
setCur(e.target.value)}
style={{ appearance: "none", WebkitAppearance: "none", MozAppearance: "none", border: "none", outline: "none", background: "transparent", font: "inherit", fontFamily: "var(--font-body)", fontSize: 14, fontWeight: 700, color: "var(--color-navy-deep)", cursor: "pointer", padding: 0 }}>
{HEADER_LANGS.map(([code, name]) => ({name} ))}
▾
);
}
/* --------------------------------------------------------------- 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 }) => (
{children}
);
const tabBtn = (id, label) => {
const on = mode === id;
return (
setMode(id)} style={{ flex: 1, padding: "10px 12px", border: "none", borderRadius: "var(--radius-pill)", cursor: "pointer", fontFamily: "var(--font-body)", fontSize: 14, fontWeight: 600, color: on ? "var(--color-navy-deep)" : "var(--color-body)", background: on ? "var(--color-canvas)" : "transparent", boxShadow: on ? "0 1px 2px rgba(20,20,58,0.08)" : "none" }}>{label}
);
};
return (
{tabBtn("quick", "Быстрый расчёт")}
{tabBtn("send", "Отправить на оценку")}
{mode === "quick" ? (
setDoc(e.target.value)} options={CALC_DOCS} />
Направление перевода
Поменять
setFrom(e.target.value)} options={CALC_LANGS} />
setTo(e.target.value)} options={CALC_LANGS} />
Нотариальное заверение
setNotary(true)}>Да
setNotary(false)}>Нет
Примерная стоимость
≈ {est} BYN
Оставить заявку
Перевод {LANG_RATE[to] || 25}{pages > 1 ? `×${pages}` : ""} = {transFee} BYN{notary ? ` + заверение ${notaryFee} BYN` : ""}
{notary && notaryFee === 150 ? из них 90 BYN — гос.тариф нотариуса, не наша наценка : null}
Точную стоимость подтвердим после просмотра документа
) : sendStatus === "sent" ? (
Документ получен
Посчитаем точную стоимость и свяжемся в течение 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 (
window.scrollTo({ top: 0, behavior: "smooth" })}
style={{ border: "none", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", gap: 10, padding: 0 }}>
doslovno.by
{links.map(([label, id]) => (
(id === "wiki" ? (window.location.href = "wiki.html") : scrollToId(id))}
onMouseEnter={() => setHover(id)} onMouseLeave={() => setHover(null)}
style={{
border: "none", background: "transparent", font: "inherit", fontSize: 15, fontWeight: 600,
cursor: "pointer", padding: "8px 12px", borderRadius: "var(--radius-pill)",
color: hover === id ? "var(--color-navy-deep)" : "var(--color-body)",
transition: "color 120ms ease",
}}>{label}
))}
);
}
/* ------------------------------------------------------------------- hero */
function Hero() {
const trust = [
["clock-3", "Готовность от 4 часов"],
["shield-check", "Принимают в любых инстанциях"],
["badge-check", "Заверение у нотариуса"],
];
return (
Рядом с визовым центром
Заберём и доставим по Минску
Нотариальный перевод. Ценим ваше время .
Приходите с документом — уходите с готовым переводом, заверенным у нотариуса. Берём на себя нервы, очереди и бюрократию.
openCalc()}>Рассчитать стоимость
openLead()}>
Перезвоните мне
{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 (
{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]) => (
))}
Не нашли свой документ? openLead()} style={{ border: "none", background: "transparent", padding: 0, cursor: "pointer", color: "#b06f12", fontWeight: 700, fontFamily: "inherit", fontSize: 15 }}>Напишите нам — переведём почти всё.
);
}
/* --------------------------------------------------------------- delivery */
function SchematicMap() {
return (
{/* faint street grid */}
{/* pin */}
Рядом с визовым центром
);
}
function Delivery() {
return (
Забор и доставка
Заберём и привезём документ по Минску
Не нужно подстраивать день под визит. Курьер заберёт оригинал у вас и вернёт готовый перевод — туда, куда удобно.
{[["truck", "Бесплатный курьер по городу"], ["clock-3", "Забор в день обращения"], ["map-pin", "Или приходите сами — мы у визового центра"]].map(([ic, tx]) => (
{tx}
))}
openLead()}>
Вызвать курьера
);
}
/* ----------------------------------------------------------------- prices */
// Комплект «под ключ» на иностранный (эмиграция): перевод 25 + заверение 150. Многостраничные — заверение разовое.
const PRICES = [
["Свидетельство о рождении / браке", "≈ 175 BYN"],
["Паспорт", "≈ 175 BYN"],
["Справка о несудимости", "≈ 175 BYN"],
["Согласие на выезд ребёнка", "≈ 175 BYN"],
["Диплом с приложением", "≈ 200 BYN"],
["Договор / нотариальный документ", "от ≈ 90 BYN / стр."],
];
function Prices() {
return (
Цена за минуту
Точную цену подтвердим после просмотра документа
Примеры под ключ для выезда — перевод на иностранный язык с заверением у нотариуса, всё включено. Перевод на русский (для документов в Беларуси) — дешевле, от ≈ 100 BYN. Без скрытых доплат.
openCalc({ mode: "send" })}>
Рассчитать мой документ
);
}
/* ------------------------------------------------------------------- wiki */
function WikiTeaser() {
const countries = ["Польша", "Германия", "Литва", "Чехия", "США", "Канада", "Италия"];
return (
Полезная вики
Какие документы нужны — по странам и целям
Бесплатная база знаний: разбираем, какой пакет документов в какое консульство, что заверять у нотариуса, а что — апостилем. Обновляем по мере изменения требований.
{countries.map((c) => (
{c}
))}
и ещё…
{ window.location.href = "wiki.html"; }} iconRight={ }>
Открыть раздел «Вики»
Например, «диплом для Польши»
{[["Польша: документы для визы D (учёба)", "Польша · Учёба"], ["Воссоединение семьи в Германии", "Германия · Семья"], ["Справка о несудимости: перевод и апостиль", "Общее"]].map(([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 минут.
) : (
)}
);
if (embedded) return card;
return (
Ответим в течение 30 минут
Оставьте заявку
Напишите, что нужно перевести. Мы посчитаем точную стоимость и перезвоним — или сразу заберём документ по Минску.
{[["truck", "Бесплатно заберём и доставим по городу"], ["phone-call", "+375 29 623-41-76 · Telegram · Viber"], ["map-pin", "Минск, рядом с визовым центром"]].map(([ic, tx]) => (
{tx}
))}
{card}
);
}
/* --------------------------------------------------------------- modal host */
function ModalHost() {
const [modal, setModal] = React.useState(null); // null | { kind: "calc"|"lead", mode }
React.useEffect(() => {
const open = (e) => setModal(e.detail || {});
const close = () => setModal(null);
window.addEventListener("doslovno:open-modal", open);
window.addEventListener("doslovno:close-modal", close);
return () => { window.removeEventListener("doslovno:open-modal", open); window.removeEventListener("doslovno:close-modal", close); };
}, []);
// обработка переходов с других страниц: index.html#do-calc-send и т.п.
React.useEffect(() => {
const h = window.location.hash;
let act = null;
if (h === "#do-calc-send") act = () => openCalc({ mode: "send" });
else if (h === "#do-calc") act = () => openCalc({ mode: "quick" });
else if (h === "#do-lead") act = () => openLead({});
if (act) { history.replaceState(null, "", window.location.pathname); act(); }
}, []);
React.useEffect(() => {
document.body.style.overflow = modal ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [modal]);
React.useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") setModal(null); };
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
if (!modal) return null;
return (
setModal(null)} style={{ position: "fixed", inset: 0, zIndex: 100, background: "rgba(20,20,58,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "flex-start", justifyContent: "center", padding: "6vh 16px 16px", overflowY: "auto" }}>
e.stopPropagation()} style={{ width: "100%", maxWidth: modal.kind === "calc" ? 460 : 460, position: "relative" }}>
setModal(null)} aria-label="Закрыть" style={{ position: "absolute", top: -42, right: 0, background: "transparent", border: "none", cursor: "pointer", color: "#fff", display: "inline-flex", alignItems: "center", gap: 6, fontFamily: "var(--font-body)", fontSize: 15, fontWeight: 600 }}>
Закрыть
{modal.kind === "calc" ? : }
);
}
/* ----------------------------------------------------------------- footer */
// Независимый счётчик главной: считает только первый заход браузера.
// Обновление страницы повторно не увеличивает счётчик (свои ключи, не связан с ширмой).
function useHomeVisits() {
const [n, setN] = React.useState(null);
React.useEffect(() => {
const KEY = "dby_home_views";
const SEEN = "dby_home_seen";
let v = parseInt(localStorage.getItem(KEY) || "41", 10);
if (isNaN(v)) v = 41;
if (!localStorage.getItem(SEEN)) {
v = v + 1;
localStorage.setItem(KEY, String(v));
localStorage.setItem(SEEN, "1");
}
setN(v);
}, []);
return n;
}
function Footer() {
const visits = useHomeVisits();
const col = (title, links) => (
{title}
{links.map(([label, id]) => (
(id ? scrollToId(id) : null)} style={{ border: "none", background: "transparent", textAlign: "left", padding: 0, cursor: "pointer", color: "var(--color-on-dark-mute)", fontSize: 14, fontFamily: "var(--font-body)" }}>{label}
))}
);
return (
doslovno.by
Бюро нотариальных переводов в Минске. Рядом с визовым центром. Заберём и доставим документ по городу.
{col("Услуги", [["Посольства", "services"], ["Выезд за границу", "services"], ["Учёба", "services"], ["Медицина", "services"]])}
{col("Полезное", [["Вики: по странам", "wiki"], ["Цены", "prices"], ["Доставка по Минску", "delivery"]])}
{col("Контакты", [["+375 29 623-41-76", "lead"], ["Оставить заявку", "lead"], ["Как нас найти", "delivery"]])}
© 2026 doslovno.by · Минск
{visits != null ? visits : "—"}
);
}
/* ------------------------------------------------------------------- page */
function Home() {
return (
);
}
window.DBHome = Home;