diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a9ca6e4 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Образец .env. Сам .env лежит вне git с правами 600, у dev и prod свои значения. +DATABASE_URL=postgresql://vx_dev@/veilix_dev?host=/var/run/postgresql +# случайная строка: node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))" +PAYLOAD_SECRET= + +# хост админки: только на нём работают /admin и REST Payload +ADMIN_HOST=dev.plc.veilix.cc + +# адрес приложения изнутри сервера и токен для служебных запросов /svc/internal/* +INTERNAL_URL=http://127.0.0.1:3100 +INTERNAL_TOKEN= + +# только для dev: хосты, с которых браузер обращается к dev-серверу Next +DEV_ALLOWED_ORIGINS=dev.veilix.app,dev.my.veilix.app,dev.tg.veilix.app,dev.veilixvpn.com,dev.my.veilixvpn.com,dev.tg.veilixvpn.com,dev.plc.veilix.cc + +# не отправлять анонимную телеметрию Next.js +NEXT_TELEMETRY_DISABLED=1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..473d525 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# зависимости +/node_modules + +# next.js +/.next/ +/out/ +next-env.d.ts +*.tsbuildinfo + +# секреты и локальные настройки +.env +.env*.local + +# загруженные файлы +/media + +# тесты +/coverage + +# прочее +.DS_Store +*.pem +npm-debug.log* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9ed67ea --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +.next/ +design/ +docs/ +media/ +CLAUDE.md +README.md +pnpm-lock.yaml +src/payload-types.ts +src/migrations/ +src/app/(payload)/admin/importMap.js diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..81cfbfd --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "semi": false, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./src/styles/globals.css" +} diff --git a/CLAUDE.md b/CLAUDE.md index a778c28..e7ae74d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,4 +35,21 @@ ## 5. Команды -Заполняется на этапе 1a. +Из `/home/vx_dev/veilix_dev` под vx_dev. Dev-сервер работает под systemd, вручную `pnpm dev` не запускаем: порт 3100 занят. + +| Команда | Что делает | +|---|---| +| `systemctl --user restart veilix-dev` | перезапустить dev-сервер (`pnpm dev`, 127.0.0.1:3100); юнит — `ops/systemd/veilix-dev.service` | +| `journalctl --user -u veilix-dev -f` | логи dev-сервера | +| `pnpm lint` | ESLint | +| `pnpm typecheck` | типы маршрутов Next и `tsc --noEmit` | +| `pnpm test` | модульные тесты (Vitest, файлы `*.test.ts`) | +| `pnpm build` | production-сборка; dev-серверу не мешает, он пишет в `.next/dev` | +| `pnpm format` | Prettier (`pnpm format:check` — только проверка) | +| `pnpm seed` | начальные данные dev; повторный запуск обновляет записи и возвращает значения seed. `SEED_ENV=prod` — данные prod | +| `pnpm generate:types` | типы Payload в `src/payload-types.ts` после изменения коллекций | +| `pnpm generate:importmap` | карта импортов админки после добавления своих компонентов | +| `pnpm db:reset` | очистить `veilix_dev` (только её), если push схемы задаёт вопросы (переименование, удаление): без терминала их некому подтвердить. Затем перезапуск dev-сервера и `pnpm seed` | +| `pnpm payload migrate:create <имя>` | миграция для prod; до первой выкладки (1h) миграции не создаём | + +Проверка без nginx: `curl -A 'Mozilla/5.0' -H 'Host: dev.veilix.app' http://127.0.0.1:3100/…`. Без браузерного User-Agent curl считается ботом и не перенаправляется; заголовок `X-Geo-Country` так можно подставить, через nginx — нет. diff --git a/docs/00-index.md b/docs/00-index.md index a654847..14cef25 100644 --- a/docs/00-index.md +++ b/docs/00-index.md @@ -24,7 +24,7 @@ | Этап | Содержание | Статус | |---|---|---| | [0](stages/stage-0-docs.md) | Документация | готово | -| [1a](stages/stage-1a-foundation.md) | Фундамент: Next + Payload, сайты, домены, языки, валюты, темы | не начат | +| [1a](stages/stage-1a-foundation.md) | Фундамент: Next + Payload, сайты, домены, языки, валюты, темы | готово | | [1b](stages/stage-1b-marketing.md) | Маркетинговые страницы обоих брендов, контент, формы с ALTCHA, SEO, юр. тексты | не начат | | [1c](stages/stage-1c-auth.md) | Вход по коду, почта, сессии | не начат | | [1d](stages/stage-1d-billing.md) | Биллинг без эквайринга, оформление заказа, продления | не начат | @@ -64,7 +64,7 @@ | № | Вопрос | Предложение | Нужно к этапу | |---|---|---|---| -| Q-02 | Цены mass в USD, суммы пополнения и реферального бонуса в USD для обоих брендов | — нужны цифры | 1b | +| Q-02 | Цены mass в USD, суммы пополнения и реферального бонуса в USD для обоих брендов | Нужны цифры. Пока в seed временные $6.99 и $69.90 (около $1 за 100 ₽, как у dedicated в макете) | 1b | | Q-08 | Email поддержки брендов | `help@veilix.app` и `help@veilixvpn.com` (в макете были домены .cc) | 1b | | Q-09 | Часовой пояс часов поддержки (10–20 в рабочие дни) | МСК | 1b | | Q-10 | Имена Telegram-ботов: 2 основных, 2 поддержки, тестовые | — | 3 | diff --git a/docs/02-architecture.md b/docs/02-architecture.md index d34db06..d94c159 100644 --- a/docs/02-architecture.md +++ b/docs/02-architecture.md @@ -11,19 +11,22 @@ | Payload CMS | 3.91.x | Payload 4 в бете; переход после стабильного релиза ([D-002](decisions/D-002-payload-version.md)) | | PostgreSQL | 18 (на хосте) | адаптер `@payloadcms/db-postgres`, `idType: 'uuid'` | | Tailwind CSS | 4.x | токены из макета как CSS-переменные | +| TypeScript | 6.0 | не 7.0: typescript-eslint из eslint-config-next поддерживает только TS < 6.1 | +| ESLint | 9.39 | не 10: eslint-plugin-react, import и jsx-a11y не заявляют поддержку ESLint 10 | +| Vitest | 5.x | модульные тесты | | next-intl | 4.x | переводы интерфейса, префиксы языков в URL | | Nodemailer | через `@payloadcms/email-nodemailer` | транспорты SMTP, sendmail, «только журнал» | | ALTCHA | 3.x | капча без третьих сторон | | grammY | 1.x | Telegram-боты, этап 3 | | Stripe SDK | актуальный | этап 4 | -Точные версии фиксируются в `package.json` на этапе 1a. Если к старту этапа 1a выйдет стабильный Payload 4, начинаем сразу на нём. +Точные версии закреплены в `package.json`: на этапе 1a — Next 16.4.2, Payload 3.91.0, React 19.3.1, next-intl 4.14.9, Tailwind 4.3.3, pnpm 12.14. На старте 1a Payload 4 был в бете (4.0.0-beta.3), поэтому начали на 3.91 ([D-002](decisions/D-002-payload-version.md)). Телеметрия Next.js и Payload выключена. ## 2. Одно приложение на все домены 1. Одно Next.js-приложение, Payload установлен внутрь как библиотека: админка, API и фронтенды — один процесс ([D-001](decisions/D-001-single-app.md)). -2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит запись в коллекции «Домены» и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список доменов кэшируется в памяти и сбрасывается при изменении записи. -3. Proxy переписывает запрос во внутренний маршрут вида `/{тип}/{сайт}/{язык}/…`, для маркетинговых страниц ещё и `/{валюта}/`. Снаружи эти пути недоступны: прямой запрос к ним даёт 404. +2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит домен и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список активных доменов proxy берёт у `/svc/internal/domains` (по токену из `.env`) и держит в памяти 15 секунд: правка домена в админке применяется в пределах 15 секунд ([D-022](decisions/D-022-proxy-routing.md)). +3. Proxy переписывает запрос во внутренний маршрут по хосту: `/m/{хост}/{язык}/{валюта}/…` — сайт, `/c/{хост}/{язык}/…` — кабинет, `/t/{хост}/…` — MiniApp. Снаружи эти пути недоступны: прямой запрос к ним получает ещё один префикс и попадает в 404. Решения proxy — чистая функция `src/core/routing/decide.ts` с модульными тестами. 4. Хост админки (`plc.veilix.cc`, `dev.plc.veilix.cc`) задаётся в `.env`. Только на нём работают `/admin` и REST API Payload. На доменах брендов эти пути закрыты. 5. Сайтам, кабинетам и MiniApp хост админки не нужен: a. данные Payload они читают в том же процессе через Local API — это вызов функции, а не HTTP-запрос; @@ -32,29 +35,38 @@ d. файлы медиатеки отдаются через домены сайтов. 6. Ядро общее (биллинг, ключи, вход, рефералы, почта), UI-кит общий, а страницы у каждого бренда свои: данные в дизайнах представлены по-разному. -## 3. Структура кода (план) +## 3. Структура кода ``` src/ - proxy.ts — домен → сайт, тип фронтенда, язык, валюта + proxy.ts — хост → домен, язык, валюта, внутренний маршрут payload.config.ts + payload-types.ts — типы Payload (pnpm generate:types) app/ (payload)/ — админка и REST API Payload (стандартные файлы) - (front)/m/[site]/[locale]/[currency]/… — маркетинговые сайты - (front)/c/[site]/[locale]/… — кабинеты и оформление заказа - (front)/t/[site]/… — MiniApp (этап 3) - svc/… — свои эндпоинты: ALTCHA, вебхуки, health + (front)/m/[host]/[locale]/[currency]/(site)/… — маркетинговые сайты + (front)/c/[host]/[locale]/(shell)/… — кабинет с навигацией + (front)/c/[host]/[locale]/(plain)/… — вход и оформление заказа + (front)/t/[host]/… — MiniApp (этап 3) + (front)/context.ts — домен, язык и валюта страницы, данные оболочек + svc/… — свои эндпоинты: health, internal/domains; дальше ALTCHA, вебхуки payload/ - collections/ globals/ hooks/ access/ fields/ + collections/ access/ fields/ hooks/ components/ core/ — бизнес-логика без привязки к UI - billing/ keys/ auth/ referral/ email/ payments/ currency/ geo/ + sites.ts domains/ routing/ currencies/ payments/ — этап 1a + billing/ keys/ auth/ referral/ email/ geo/ — следующие этапы brands/ - dedicated/ mass/ — страницы и компоненты каждого бренда - ui/ — общие компоненты: кнопки, поля, шиты, переключатели, флаги, карта - i18n/ — конфиг next-intl и messages/{en,ru,de,fr,es}.json - styles/ — Tailwind, токены брендов, темы + dedicated/ mass/ — различия брендов; страницы и компоненты — с этапа 1b + ui/ — общие компоненты: оболочки сайта и кабинета, переключатели, тема, логотип, иконки + i18n/ — языки, конфиг next-intl и messages/{en,ru,de,fr,es}.json + styles/ — Tailwind, токены брендов + seed/ — начальные данные (pnpm seed) +ops/systemd/ — юниты systemd +scripts/ — служебные скрипты (очистка dev-базы) ``` +Корневые layout сайта и кабинета дают только документ: `` с `lang`, `data-site` и скриптом темы. Шапка, подвал и навигация — во вложенных группах `(site)`, `(shell)` и `(plain)`. + Бизнес-логика живёт в `core/` и вызывается одинаково из хуков Payload, server actions, вебхуков, фоновых задач и (на этапе 3) бота. ## 4. Окружения @@ -66,7 +78,7 @@ src/ | Запуск | `pnpm dev` (горячая перезагрузка) | `pnpm start` из `current` после `pnpm build` | | Порт | 3100 | 3000 | | База | `veilix_dev` | `veilix_prod` | -| Схема базы | синхронизируется автоматически (push) | миграции Payload, применяются при выкладке | +| Схема базы | синхронизируется автоматически (push); если push задаёт вопросы — `pnpm db:reset` и `pnpm seed` | миграции Payload, применяются при выкладке | | Домены | `dev.*`, `dev.plc.veilix.cc` | основные, `plc.veilix.cc` | | Секреты | `/home/vx_dev/veilix_dev/.env` | `/home/vx_prod/veilix/shared/.env` | | Файлы (media) | `/home/vx_dev/veilix_dev/media` | `/home/vx_prod/veilix/shared/media` | @@ -77,7 +89,7 @@ src/ ## 5. Выкладка в prod -1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза. +1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза. До первой выкладки (этап 1h) миграции не накапливаем: перед ней создаём одну начальную по итоговой схеме. Механизм проверен на этапе 1a: миграция, применённая к пустой схеме, создаёт те же таблицы, что push в dev. 2. Выкладка — команда `deploy <тег>` пользователя vx_prod, появится на этапе 1h. Запускает её пользователь; по желанию — sudo-правило, разрешающее vx_dev запускать только эту команду (Q-18). Шаги: a. взять тег из Gitea (у vx_prod ключ только на чтение) в новую папку `releases/<дата-тег>`; b. `pnpm install --frozen-lockfile` и `pnpm build`; ссылки на `shared/.env` и `shared/media`; @@ -89,7 +101,7 @@ src/ ## 6. Рендеринг и кэширование -1. Маркетинговые страницы собираются статически во всех вариантах «язык × валюта» и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты. +1. Маркетинговые страницы собираются статически в вариантах «домен × язык × валюта» при первом запросе (ISR) и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты. 2. Изменение в админке (цена, статус локации, FAQ) вызывает хук Payload, и Next.js пересобирает затронутые страницы за секунды (on-demand revalidation по тегам). Запасной вариант — плановое обновление раз в 5 минут. 3. При оформлении заказа наличие и цена проверяются заново на сервере. Устаревшая страница не может продать то, чего нет. 4. Кабинет рендерится на сервере (SSR) для каждого запроса, без кэша. diff --git a/docs/03-data-model.md b/docs/03-data-model.md index f7558a0..468c870 100644 --- a/docs/03-data-model.md +++ b/docs/03-data-model.md @@ -8,6 +8,8 @@ 4. Деньги — целое число в минимальных единицах (`amount`: 249900 = 2 499,00 ₽) плюс `currency` — код ISO 4217 (`RUB`, `USD`). Код проверяется по справочнику «Валюты». 5. Даты — `timestamptz`, в UTC. 6. Секреты в полях (конфиги WG, ссылки подписок, SMTP-пароль, id сохранённых карт) шифруются приложением (AES-256-GCM, ключ в `.env`). +7. Валюта везде хранится кодом ISO 4217 — и в деньгах, и в настройках (валюты домена и платёжной системы, строки цен). Код проверяется по справочнику, в админке это выпадающий список из «Валют». +8. Переводы полей Payload хранятся в таблицах `<коллекция>_l10n` (настройка адаптера `localesSuffix`). Имя по умолчанию `<коллекция>_locales` совпало бы с таблицей поля `locales` у сайтов и доменов. ## 2. Схема связей @@ -48,6 +50,7 @@ erDiagram | code | select `dedicated` \| `mass`, уникальное | бренд: по нему выбираются тема и страницы | | name | text (L) | «Вейликс» / «Veilix» | | orderPrefix | text | префикс номеров: `VX`, `VV` | +| locales | multi-select из языков конфига | языки контента сайта; en обязателен; домены показывают часть этих языков ([D-023](decisions/D-023-site-locales.md)) | | supportEmail | email | почта поддержки (Q-08) | | emailFrom | group: name, address | отправитель писем, напр. «Вейликс» | | supportBot, mainBot | text | имена ботов (этап 3) | @@ -60,22 +63,26 @@ erDiagram | expiringBadgeDays | number, по умолчанию 7 | когда показывать «Истекает» | | seo | group: titleTemplate (L), description (L), ogImage | значения по умолчанию | +На этапе 1a сделаны `code` (после создания не меняется), `name`, `orderPrefix` и `locales`. Остальные поля добавляют этапы, которые ими пользуются: SEO и контакты — 1b, отправитель писем — 1c, сроки и пресеты — 1d и 1f, рефералы — 1g, боты — 3. + ### 3.2 `domains` — Домены (сайт) | Поле | Тип | Описание | |---|---|---| | hostname | text, уникальное | `veilix.app`, `my.veilix.app`, … | | app | select `marketing` \| `cabinet` \| `miniapp` | тип фронтенда | -| locales | multi-select из языков конфига | включённые языки | -| defaultLocale | select | обычно `en` | +| locales | multi-select из языков конфига | включённые языки — часть языков сайта | +| defaultLocale | select | обычно `en`; открывается без префикса в адресе | | currencies | multi-select валют | включённые валюты | | defaultCurrency | select | валюта по умолчанию | | localeCurrency | array: locale, currency | валюта по умолчанию для языка: ru → RUB | -| isPrimary | checkbox | основной домен этого типа; используется в ссылках и sitemap | +| isPrimary | checkbox | основной домен этого типа (у сайта один на каждый тип); используется в ссылках и sitemap | | indexable | checkbox | разрешена ли индексация (для кабинета и MiniApp — нет) | | umamiWebsiteId | text | этап 1h | | active | checkbox | | +Язык и валюта по умолчанию должны входить в списки домена, язык в правилах «язык → валюта» не повторяется — это проверяется при сохранении. + ### 3.3 `currencies` — Валюты | Поле | Тип | Описание | @@ -99,10 +106,10 @@ erDiagram | icon | upload | | | currencies | multi-select валют | в каких валютах доступна | | purposes | multi-select `order` \| `topup` | оплата заказа, пополнение | -| visibleToCustomers | checkbox | у `manual` выключено: ей пользуются только администраторы | +| visibleToCustomers | checkbox | у `manual` всегда выключено (проверяется при сохранении): ей пользуются только администраторы | | mode | select `test` \| `live` | | | limits | array: currency, min, max | | -| capabilities | только чтение, из адаптера | `recurring`, `refunds`, `saveMethod` | +| capabilities | только чтение, из адаптера | `recurring`, `refunds`, `saveMethod`; объявлены в `src/core/payments/adapters.ts`, в базе не хранятся | | active, sort | | | Ключи доступа к провайдерам — только в `.env` (`PAYMENT_<КОД>_…`), не в базе. @@ -123,7 +130,7 @@ erDiagram | country | relationship → countries | уникальна в пределах сайта | | city | text (L) | | | lat, lng | number | точка города на карте | -| status | select `available` \| `on_demand` \| `soon` | dedicated: «Серверы в наличии» / «Установим за 24 часа»; mass: «Включено» / «Скоро» | +| status | select `available` \| `on_demand` \| `soon` | dedicated: «Серверы в наличии» / «Установим за 24 часа»; mass: «Включено» / «Скоро». Чужой для бренда статус не сохранится | | freeServers | number, необязательное | счётчик (решение о показе — этап 1b) | | visible, sort | | | @@ -153,13 +160,13 @@ erDiagram ### 4.1 `admins` — Администраторы -Стандартная авторизация Payload (email и пароль), `maxLoginAttempts: 5`, `lockTime: 15 мин`. +Стандартная авторизация Payload (email и пароль), `maxLoginAttempts: 5`, `lockTime: 15 мин`. Первый администратор окружения (экран первого входа) становится owner. Последнего owner нельзя лишить роли и удалить. Роли и сайты меняет только owner, остальные правят только своё имя и пароль. | Поле | Тип | Описание | |---|---|---| | name | text | | | roles | multi-select `owner` \| `admin` \| `support` \| `content` | см. раздел 8 | -| sites | доступ к тенантам | owner и admin видят все сайты | +| sites | доступ к тенантам | owner и admin видят все сайты, поэтому поле показывается только ролям support и content; на экране первого входа скрыто | ### 4.2 `customers` — Пользователи (сайт) @@ -362,7 +369,9 @@ QR-коды строятся на лету из `payload`, не хранятся | Коллекция | owner, admin | support | content | пользователь (через приложение) | |---|---|---|---|---| -| Настройки и справочники | всё | чтение | чтение | — | +| Настройки и справочники | всё (удаление сайта — только owner) | чтение | чтение | — | +| admins | owner — всё, admin — только себя | только себя | только себя | — | +| media | всё | чтение | всё | файлы открыты всем: их показывают сайты | | pages, faq, email-templates, локализация | всё | чтение | всё | — | | customers | всё | чтение, правка заметок, блокировка | — | только свой профиль | | keys, connections | всё | всё | — | только свои, чтение | diff --git a/docs/05-auth.md b/docs/05-auth.md index 89e1350..af86540 100644 --- a/docs/05-auth.md +++ b/docs/05-auth.md @@ -87,7 +87,7 @@ 1. Стандартная авторизация Payload: email и пароль, cookie только на хосте админки. 2. Админка открывается только на `plc.veilix.cc` и `dev.plc.veilix.cc`; nginx пускает туда только с разрешённых IP. 3. 5 неудачных попыток — блокировка на 15 минут. -4. Первый администратор каждого окружения создаётся на экране первого входа Payload. +4. Первый администратор каждого окружения создаётся на экране первого входа Payload и автоматически получает роль owner. ## 8. Cookies diff --git a/docs/06-i18n.md b/docs/06-i18n.md index 3ff61c8..864795c 100644 --- a/docs/06-i18n.md +++ b/docs/06-i18n.md @@ -20,7 +20,7 @@ ## 3. URL -1. Английский без префикса, остальные — с префиксом: `veilix.app/pricing`, `veilix.app/ru/pricing`, `veilix.app/de/pricing`. +1. Английский без префикса, остальные — с префиксом: `veilix.app/pricing`, `veilix.app/ru/pricing`, `veilix.app/de/pricing`. Точнее, без префикса открывается язык домена по умолчанию (`domains.defaultLocale`, сейчас везде en), а адрес с его префиксом (`/en/pricing`) перенаправляется на адрес без префикса. 2. Так же в кабинете: `my.veilix.app/keys`, `my.veilix.app/ru/keys`. 3. MiniApp: адрес в BotFather один, поэтому язык берётся из профиля пользователя или из Telegram; префикс в URL необязателен. 4. Пути одинаковые для всех языков (английские): `/pricing`, `/locations`, `/support`, `/legal/terms`, `/keys`, `/billing`, `/referral`, `/order`, `/login`. @@ -29,9 +29,9 @@ ## 4. Определение языка 1. Порядок: язык из URL → выбранный ранее (cookie `NEXT_LOCALE`, у вошедшего — язык профиля) → язык браузера (`Accept-Language`) среди включённых → страна из заголовка nginx `X-Geo-Country` (MaxMind) → `en`. -2. Перенаправление на версию с префиксом — только при первом заходе на адрес без префикса, без cookie, и только если выбран не английский. Поисковых ботов не перенаправляем никогда: так рекомендует Google. +2. Перенаправление на версию с префиксом — с адреса без префикса и только если выбран не язык домена по умолчанию ([D-024](decisions/D-024-locale-redirect.md)). Есть cookie `NEXT_LOCALE` — ведём на язык из cookie; cookie нет — определяем по языку браузера и стране. Перенаправляются только GET и HEAD. Поисковых ботов не перенаправляем никогда: так рекомендует Google. 3. Переключатель языка ставит cookie `NEXT_LOCALE`. У вошедшего пользователя язык сохраняется ещё и в профиле: на нём уходят письма и сообщения бота. -4. Соответствие стран языкам по умолчанию: RU, BY, KZ, KG → ru; DE, AT → de; FR → fr; ES и страны Латинской Америки → es; остальные → en. +4. Соответствие стран языкам по умолчанию: RU, BY, KZ, KG → ru; DE, AT → de; FR → fr; ES и испаноязычные страны Латинской Америки → es (Бразилия — en, пока нет pt-br); остальные → en. Таблица — `src/core/routing/locale.ts`. ## 5. SEO diff --git a/docs/07-frontend.md b/docs/07-frontend.md index 61221ec..6e77b1e 100644 --- a/docs/07-frontend.md +++ b/docs/07-frontend.md @@ -32,6 +32,7 @@ | `/order` | оформление заказа: шаги «Почта» и «Оплата», сводка | | `/billing` | баланс, пополнение, карта, ближайшие списания, история | | `/referral` | реферальная программа | +| `/more` | вкладка «Ещё» на узком экране: счёт, рефералы, поддержка, тема, язык | | `/r/[код]` | реферальная ссылка: ставит cookie и ведёт на сайт бренда | Навигация по макету: на широком экране — боковая панель, на узком — плавающая панель вкладок «Главная, Ключи, Купить, Ещё». @@ -39,7 +40,7 @@ ## 4. Темы и токены 1. Tailwind v4: токены макета — CSS-переменные, подключённые в `@theme`. Бренд задаёт атрибут `data-site="dedicated|mass"` на ``, тему — `data-theme="light|dark"`. Режим «Как в системе» следует `prefers-color-scheme`. -2. Переключатель темы — 3 режима, как в макете. Выбор хранится в localStorage, тема применяется скриптом в `` до отрисовки. +2. Переключатель темы — 3 режима, как в макете. Выбор хранится в localStorage (ключ `vx_theme`), тема применяется скриптом в `` до отрисовки. Новый посетитель видит «Как в системе». localStorage у каждого хоста свой: тема, выбранная на сайте, не переходит в кабинет. 3. Токены в OKLCH, как в макете. Ниже — для ориентира; точные значения — в `