7 Commits

Author SHA1 Message Date
admin acf929ed8c fix(landing): в английской версии подвала Russia и подписи INN/KPP/OGRN
Подвал выводил регион и подписи реквизитов только по-русски, хотя
остальная страница переключается на английский.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvcrP3ruGY4v6RDvSjf7xQ
2026-10-09 10:02:08 +03:00
admin 8ebc42dd09 fix(landing): счётчик продуктов подменяет только числовую плашку, а не «AI — в каждом продукте» 2026-09-25 19:18:34 +03:00
Claude c4e5a532a1 fix(docker): собирать bcrypt из исходников
Готовый бинарник с github внутри сборки не скачивается — запрос к
release-assets обрывается, и node-pre-gyp уходит в компиляцию, а в
node:20-alpine нет python3/make/g++. Ставим их временно в обеих
стадиях и сносим вместе с кэшем node-gyp, образ остаётся ~260 МБ.
2026-08-30 11:46:09 +03:00
Claude 913b66a9cb feat(fonts): локальные шрифты вместо fonts.googleapis.com
Inter (лендинг), Unbounded и Onest (страница визитки) — подмножества
cyrillic+latin. Под ограничениями доступа внешние шрифты не приезжали:
у визиток, выданных клиентам, съезжала вёрстка.
2026-08-30 11:34:13 +03:00
zeroday 4d01a65859 docs: реестр операторов и privacy@ в уведомлении о куки
Приведение всех четырёх сайтов ООО к единому виду. Полноценная политика здесь
по-прежнему не нужна — форм и сбора данных на сайте нет, — но реквизиты
дополнены записью в реестре операторов (рег. номер 35-26-021324, приказ № 127
от 07.08.2026) и единым адресом privacy@umbyte.ru для вопросов по обработке.
2026-08-26 11:00:13 +03:00
zeroday 0dbbc7487e feat(landing): уведомление о куки и баннер
Баннер сделан по образцу АгроТО, но БЕЗ выбора категорий. На umbyte.ru нет
ни аналитики, ни рекламных пикселей, ни форм — только техническая сессия
админки. Окно настроек с переключателями, которые ничего не переключают,
показывало бы пользователю выбор, которого нет. Две кнопки: «Хорошо» и
«Отклонить», выбор помнится 180 дней.

Рядом /cookies.html — короткое уведомление: что за куки, откуда, на какой
срок, как отказаться, и прямо сказано, что персональные данные посетителей
сайт не собирает (кнопка связи открывает почтовый клиент, форм нет). Поэтому
полноценная политика обработки ПДн здесь не нужна — в отношении посетителей
компания не выступает оператором.

Если появится Метрика или форма обратной связи — берём полный вариант с
категориями из АгроТО, механика хранения совпадает.
2026-08-25 22:40:34 +03:00
zeroday f8730d9d09 fix: тип запроса секций и догоняющая миграция английских колонок
Оба дефекта вскрылись сборкой образа после слияния веток.

1. public.ts: SQL выбирает content_json_en, а описание типа запроса пришло из
   ветки визиток и этой колонки не знало — tsc отказывался собирать. След
   слияния: запрос из одной ветки, тип из другой.

2. Английские колонки (sections.content_json_en, products.title_en/subtitle_en/
   description_en) существуют на боевой базе, но их не создаёт ни одна
   миграция — когда-то добавили руками. На бою работало, на любой ЧИСТОЙ базе
   приложение упало бы на первом запросе /api/content. Заведена 005 с
   IF NOT EXISTS: на бою пусто, на чистой базе создаст.

Проверено на живом сайте: образ собран, контейнер healthy, миграции 004 и 005
применились сами, /api/content отдаёт 4 раздела на двух языках, 2 продукта,
полные реквизиты; страница визитки отвечает 200.
2026-08-25 22:35:20 +03:00
31 changed files with 432 additions and 11 deletions
+11 -1
View File
@@ -15,6 +15,11 @@ RUN npm run build
# =====================================================
FROM node:20-alpine AS backend-build
# bcrypt — нативный модуль. Готовый бинарник с github внутри сборки не
# скачивается (запрос к release-assets обрывается), поэтому собираем из
# исходников: для этого нужны python3/make/g++.
RUN apk add --no-cache python3 make g++
WORKDIR /app/backend
COPY backend/package.json backend/package-lock.json* ./
RUN npm install
@@ -33,7 +38,12 @@ WORKDIR /app
# Backend production deps
COPY backend/package.json backend/package-lock.json* ./backend/
RUN cd backend && npm install --omit=dev
# То же самое для боевых зависимостей: компилятор ставим временно и сносим
# сразу после установки, чтобы не тащить его в итоговый образ.
RUN apk add --no-cache --virtual .build-deps python3 make g++ \
&& cd backend && npm install --omit=dev \
&& apk del .build-deps \
&& rm -rf /root/.cache
# Built backend
COPY --from=backend-build /app/backend/dist ./backend/dist
+1 -1
View File
@@ -6,7 +6,7 @@ export const publicRouter = Router();
publicRouter.get('/content', async (_req, res) => {
try {
const [sections, products, approach, settings] = await Promise.all([
query<{ key: string; title: string; content_json: unknown; sort_order: number }>(
query<{ key: string; title: string; content_json: unknown; content_json_en: unknown; sort_order: number }>(
`SELECT key, title, content_json, content_json_en, sort_order
FROM sections
WHERE is_active = true
+16
View File
@@ -0,0 +1,16 @@
-- Английские колонки для переключателя языка.
--
-- Колонки уже существуют на боевой базе — их когда-то добавили вручную, минуя
-- миграции. Найдено 25.08.2026 при слиянии веток: код их читает, а ни один
-- файл в db/ не создаёт, то есть на любой ЧИСТОЙ базе (новый стенд, локальная
-- разработка, восстановление из пустого) приложение падало бы на первом же
-- запросе /api/content.
--
-- IF NOT EXISTS делает миграцию безопасной для боевой базы: там она ничего не
-- изменит, только зафиксирует факт в _migrations.
ALTER TABLE sections ADD COLUMN IF NOT EXISTS content_json_en JSONB;
ALTER TABLE products ADD COLUMN IF NOT EXISTS title_en VARCHAR(256);
ALTER TABLE products ADD COLUMN IF NOT EXISTS subtitle_en VARCHAR(512);
ALTER TABLE products ADD COLUMN IF NOT EXISTS description_en TEXT;
+4 -3
View File
@@ -10,12 +10,13 @@
<meta property="og:description" content="Технологии, которые решают реальные задачи" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://umbyte.ru" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<!-- ШРИФТЫ ЛОКАЛЬНЫЕ: раньше тянулись с fonts.googleapis.com и при
ограничениях доступа не приезжали. Подмножества cyrillic+latin. -->
<link href="/fonts/fonts.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="/cookies.js" defer></script>
</body>
</html>
+82
View File
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Уведомление о файлах cookie — Умный Байт</title>
<meta name="robots" content="index, follow">
<style>
:root { --ink:#0f172a; --muted:#475569; --line:#e2e8f0; }
*{box-sizing:border-box}
body{margin:0;background:#f8fafc;color:var(--ink);
font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
line-height:1.6;padding:32px 20px}
.wrap{max-width:720px;margin:0 auto;background:#fff;border:1px solid var(--line);
border-radius:16px;padding:36px}
h1{font-size:1.6rem;margin:0 0 6px;letter-spacing:-.02em}
.sub{color:var(--muted);font-size:14px;margin:0 0 28px}
h2{font-size:1.05rem;margin:28px 0 8px}
p,li{color:var(--muted);font-size:15px}
table{width:100%;border-collapse:collapse;margin-top:10px;font-size:14px}
th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
th{color:var(--ink);font-weight:600}
.back{display:inline-block;margin-top:28px;font-size:14px;color:var(--ink)}
.req{margin-top:28px;padding-top:20px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}
</style>
</head>
<body>
<div class="wrap">
<h1>Уведомление о файлах cookie</h1>
<p class="sub">Сайт umbyte.ru. Обновлено 25 августа 2026 года.</p>
<p>Куки — небольшие файлы, которые сайт сохраняет в браузере. Ниже честно
описано, какие из них использует этот сайт и зачем.</p>
<h2>Что мы используем</h2>
<p>Только технические куки. На сайте <strong>нет систем веб-аналитики</strong>
(ни Яндекс Метрики, ни Google Analytics), <strong>нет рекламных пикселей</strong>
и виджетов сторонних сервисов, которые следили бы за посетителями.</p>
<table>
<tr><th>Назначение</th><th>Когда появляется</th><th>Срок</th></tr>
<tr>
<td>Сессия входа в панель управления сайтом</td>
<td>Только у сотрудников после входа в закрытый раздел</td>
<td>До выхода или окончания срока сессии</td>
</tr>
<tr>
<td>Запоминание выбора в уведомлении о куки</td>
<td>После нажатия кнопки в баннере</td>
<td>180 дней</td>
</tr>
</table>
<h2>Персональные данные</h2>
<p>Через сайт мы не собираем персональные данные посетителей: форм обратной
связи и подписки здесь нет. Кнопка связи открывает ваш почтовый клиент —
письмо уходит напрямую, минуя наш сервер. Поэтому в отношении посетителей
сайта мы не выступаем оператором персональных данных.</p>
<h2>Как отказаться</h2>
<p>Нажмите «Отклонить» в баннере. Кроме того, куки можно удалить или запретить
в настройках браузера — в разделе конфиденциальности. Учтите, что при полном
запрете технических куки вход в закрытый раздел работать не будет.</p>
<h2>Изменения</h2>
<p>Если на сайте появятся аналитика или формы сбора данных, это уведомление
будет дополнено, а в баннере появится выбор категорий куки.</p>
<div class="req">
ООО «Умный Байт», ИНН 3528347856, КПП 352801001, ОГРН 1263500003667.<br>
162620, Вологодская область, город Череповец, улица Центральная, дом 12.<br>
Регистрационный номер в реестре операторов персональных данных: 35-26-021324
(приказ Роскомнадзора № 127 от 07.08.2026).<br>
Вопросы по обработке данных: <a href="mailto:privacy@umbyte.ru">privacy@umbyte.ru</a>
</div>
<a class="back" href="/">← На главную</a>
</div>
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
/* Куки-баннер сайта «Умный Байт».
*
* Сделан по образцу баннера АгроТО (там он живёт в public/landing/cookies.js),
* но без выбора категорий: на umbyte.ru нет ни аналитики, ни рекламных
* пикселей, ни форм — только техническая сессия админки. Городить окно
* настроек с переключателями, которые ничего не переключают, честнее не
* делать: пользователю показывают выбор, которого нет.
*
* Если на сайте появится Метрика или форма обратной связи — берём полный
* вариант с категориями из АгроТО, механика хранения совпадает.
*
* Хранение: localStorage["umbyte_cookie_consent_v1"] = {accepted, ts}
* Программно: window.umbyteCookies.show() / .get() / .reset()
*/
(function () {
'use strict';
var STORAGE_KEY = 'umbyte_cookie_consent_v1';
var TTL_DAYS = 180;
function read() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
var p = JSON.parse(raw);
if (!p || !p.ts) return null;
if ((Date.now() - new Date(p.ts).getTime()) / 86400000 > TTL_DAYS) return null;
return p;
} catch (e) { return null; }
}
function write(accepted) {
var data = { accepted: !!accepted, ts: new Date().toISOString() };
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch (e) {}
return data;
}
var css = [
'.umb-cc{position:fixed;inset:auto 0 0 0;z-index:9999;display:flex;justify-content:center;padding:16px;pointer-events:none;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}',
'.umb-cc-bar{pointer-events:auto;background:#fff;border:1px solid #e2e8f0;border-radius:14px;box-shadow:0 12px 40px rgba(15,23,42,.12);padding:16px 20px;max-width:860px;width:100%;display:flex;gap:16px;align-items:center;animation:umbUp .25s ease-out}',
'@keyframes umbUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}',
'.umb-cc-text{flex:1;margin:0;font-size:13.5px;line-height:1.5;color:#475569}',
'.umb-cc-text a{color:#0f172a;text-decoration:underline;text-underline-offset:2px}',
'.umb-cc-actions{display:flex;gap:8px;flex-shrink:0}',
'.umb-cc-btn{font-family:inherit;font-size:13px;font-weight:500;padding:9px 18px;border-radius:9px;border:1.5px solid #e2e8f0;background:#fff;color:#475569;cursor:pointer;transition:all .15s;white-space:nowrap}',
'.umb-cc-btn:hover{border-color:#94a3b8;color:#0f172a}',
'.umb-cc-btn-primary{background:#0f172a;color:#fff;border-color:#0f172a}',
'.umb-cc-btn-primary:hover{background:#1e293b;border-color:#1e293b;color:#fff}',
/* На телефоне: узкие отступы, мелкий шрифт, кнопки в один ряд с переносом
текста внутри — иначе длинная подпись обрезается многоточием. */
'@media(max-width:640px){',
'.umb-cc{padding:8px}',
'.umb-cc-bar{flex-direction:column;align-items:stretch;gap:10px;padding:12px 14px;border-radius:12px}',
'.umb-cc-text{font-size:12.5px;line-height:1.4}',
'.umb-cc-actions{width:100%;gap:6px}',
'.umb-cc-btn{flex:1;padding:8px 6px;font-size:11.5px;white-space:normal;line-height:1.2;display:flex;align-items:center;justify-content:center}',
'}'
].join('');
function injectCss() {
if (document.getElementById('umb-cc-style')) return;
var s = document.createElement('style');
s.id = 'umb-cc-style';
s.textContent = css;
document.head.appendChild(s);
}
function show() {
if (document.getElementById('umb-cc-bar')) return;
injectCss();
var el = document.createElement('div');
el.id = 'umb-cc-bar';
el.className = 'umb-cc';
el.innerHTML = ''
+ '<div class="umb-cc-bar" role="dialog" aria-label="Файлы cookie">'
+ '<p class="umb-cc-text">Мы используем только технические куки — они нужны, чтобы сайт работал. '
+ 'Аналитики и рекламных сервисов на сайте нет. Подробнее в <a href="/cookies.html">уведомлении о куки</a>.</p>'
+ '<div class="umb-cc-actions">'
+ '<button type="button" class="umb-cc-btn" data-act="no">Отклонить</button>'
+ '<button type="button" class="umb-cc-btn umb-cc-btn-primary" data-act="yes">Хорошо</button>'
+ '</div>'
+ '</div>';
el.addEventListener('click', function (e) {
var act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
if (!act) return;
write(act === 'yes');
var b = document.getElementById('umb-cc-bar');
if (b) b.remove();
});
document.body.appendChild(el);
}
window.umbyteCookies = {
show: show,
get: read,
reset: function () {
try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
var b = document.getElementById('umb-cc-bar');
if (b) b.remove();
show();
}
};
function init() { if (!read()) show(); }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+198
View File
@@ -0,0 +1,198 @@
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Inter-400-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Inter-400-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/fonts/Inter-500-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/fonts/Inter-500-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Inter-600-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Inter-600-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/fonts/Inter-700-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/fonts/Inter-700-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Onest';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Onest-400-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Onest';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Onest-400-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Onest';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/fonts/Onest-500-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Onest';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/fonts/Onest-500-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Onest';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Onest-600-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Onest';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Onest-600-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Unbounded-400-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Unbounded-400-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/fonts/Unbounded-500-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/fonts/Unbounded-500-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Unbounded-600-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Unbounded-600-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/fonts/Unbounded-700-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin */
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/fonts/Unbounded-700-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+1 -1
View File
@@ -176,7 +176,7 @@ export function CardPage() {
}
const cardCss = `
@import url('https://fonts.googleapis.com/css2?family=Unbounded:wght@400;500;600;700&family=Onest:wght@400;500;600&display=swap');
@import url('/fonts/fonts.css');
.cardpage{--bg:#E9EDF7;--bg2:#DDE3F2;--card:#FFFFFF;--card2:#FBFCFF;--ink:#15132E;--ink2:#5C5984;--line:#E7E9F4;--indigo:#312E81;--indigo2:#4F4BC4;--cyan:#06B6D4;--cyan-glow:#22D3EE;--violet:#7C5CFF;--shadow:0 30px 70px -24px rgba(49,46,129,.32),0 10px 30px -18px rgba(6,182,212,.28);
font-family:'Onest',sans-serif;color:var(--ink);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:30px 18px;position:relative;overflow:hidden;perspective:1200px;background:linear-gradient(165deg,var(--bg),var(--bg2))}
@media (prefers-color-scheme:dark){.cardpage{--bg:#0C0B22;--bg2:#141233;--card:#17163a;--card2:#1c1a44;--ink:#F4F4FF;--ink2:#A6A3D9;--line:rgba(255,255,255,.12);--indigo:#4F4BC4;--indigo2:#6A66E0;--cyan:#22D3EE;--cyan-glow:#67E8F9;--shadow:0 30px 80px -24px rgba(0,0,0,.7),0 0 40px -10px rgba(34,211,238,.25)}}
+8 -5
View File
@@ -18,10 +18,12 @@ const UI: Record<Lang, Record<string, string>> = {
ru: {
products: 'Продукты', approach: 'Подход', contacts: 'Контакты',
discuss: 'Обсудить проект', director: 'Генеральный директор',
inn: 'ИНН', kpp: 'КПП', ogrn: 'ОГРН', region: 'Россия',
},
en: {
products: 'Products', approach: 'Approach', contacts: 'Contacts',
discuss: 'Discuss a project', director: 'CEO',
inn: 'INN', kpp: 'KPP', ogrn: 'OGRN', region: 'Russia',
},
};
@@ -182,7 +184,8 @@ export function Landing() {
{Array.isArray(hero?.stats) && hero.stats.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-14 pt-7 border-t border-slate-200">
{hero.stats.map((stat: any, idx: number) => {
const isProd = typeof stat.label === 'string' && stat.label.toUpperCase().includes('ПРОДУКТ');
// Подменяем только счётчик продуктов (число), а не любую подпись со словом «продукт»
const isProd = typeof stat.label === 'string' && /^\d+$/.test(String(stat.value).trim()) && stat.label.toUpperCase().includes('ПРОДУКТ');
const value = isProd ? String(productCount) : stat.value;
const label = isProd ? pluralProducts(productCount) : stat.label;
return (
@@ -344,9 +347,9 @@ export function Landing() {
<div className="text-xs text-slate-500 leading-relaxed mb-5 pb-5 border-b border-slate-200">
<div className="text-slate-600 font-medium mb-1">{(lang === 'en' && settings.company_name_en) || settings.company_name_full || 'ООО «Умный Байт»'}</div>
<div className="flex flex-wrap gap-x-4 gap-y-1">
{settings.company_inn && <span>ИНН {settings.company_inn}</span>}
{settings.company_kpp && <span>КПП {settings.company_kpp}</span>}
{settings.company_ogrn && <span>ОГРН {settings.company_ogrn}</span>}
{settings.company_inn && <span>{t('inn')} {settings.company_inn}</span>}
{settings.company_kpp && <span>{t('kpp')} {settings.company_kpp}</span>}
{settings.company_ogrn && <span>{t('ogrn')} {settings.company_ogrn}</span>}
</div>
{(settings.company_address || settings.company_address_en) && (
<div className="mt-1">{(lang === 'en' && settings.company_address_en) || settings.company_address}</div>
@@ -372,7 +375,7 @@ export function Landing() {
<Logo size={20} />
<span className="text-xs text-slate-600">© {new Date().getFullYear()} {(lang === 'en' && settings.company_name_en) || settings.company_name_full || 'ООО «Умный Байт»'}</span>
</div>
<div className="text-xs text-slate-500">{settings.domain || 'umbyte.ru'} · {settings.region || 'Россия'}</div>
<div className="text-xs text-slate-500">{settings.domain || 'umbyte.ru'} · {(lang === 'en' ? settings.region_en : settings.region) || t('region')}</div>
</div>
</div>
</footer>