Merge pull request 'feat(ui): public offer page at /offer/ with a landing footer link' (#222) from feature/public-offer-page into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 11s
CI / integration (push) Successful in 18s
CI / ui (push) Successful in 1m9s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m38s
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 11s
CI / integration (push) Successful in 18s
CI / ui (push) Successful in 1m9s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m38s
This commit was merged in pull request #222.
This commit is contained in:
@@ -497,6 +497,22 @@ jobs:
|
||||
docker logs --tail 50 scrabble-backend || true
|
||||
exit 1
|
||||
|
||||
- name: Probe the /offer/ public offer page is served
|
||||
run: |
|
||||
set -u
|
||||
# /offer/ is a static page baked into the landing image (rendered from
|
||||
# ui/legal/offer_ru.md). If the landing Caddyfile stops routing it, the request
|
||||
# silently falls through to the landing shell (also 200) — so assert offer-specific
|
||||
# content, never just the status.
|
||||
out="$(docker run --rm --network edge alpine:3.20 wget -q -O - http://scrabble/offer/ 2>&1 || true)"
|
||||
if echo "$out" | grep -q "290210610742"; then
|
||||
echo "ok: /offer/ serves the public offer page"
|
||||
else
|
||||
echo "FAIL: /offer/ did not serve the offer page (fell through to the landing shell?)"
|
||||
docker logs --tail 50 scrabble-landing || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Probe the /dict edge route reaches the gateway
|
||||
run: |
|
||||
set -u
|
||||
|
||||
@@ -18,10 +18,25 @@
|
||||
@shell not path /assets/*
|
||||
header @shell Cache-Control "no-cache"
|
||||
|
||||
# The static public offer page, rendered from ui/legal/offer_ru.md at build
|
||||
# time into dist/offer/index.html (vite emit-offer plugin). Served with its own
|
||||
# index so /offer/ resolves to /srv/offer/index.html rather than falling to the
|
||||
# landing shell below (whose index is landing.html). A bare /offer redirects in.
|
||||
handle /offer {
|
||||
redir * /offer/ permanent
|
||||
}
|
||||
handle /offer/* {
|
||||
file_server {
|
||||
index index.html
|
||||
}
|
||||
}
|
||||
|
||||
# An unknown path falls back to the landing shell (the gateway's old "/"
|
||||
# behaviour); "/" itself resolves through the index below.
|
||||
handle {
|
||||
try_files {path} /landing.html
|
||||
file_server {
|
||||
index landing.html
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1328,7 +1328,8 @@ in-compose **caddy** is the contour's edge: it owns a single `/_gm` Basic-Auth a
|
||||
routes `/_gm/grafana/*` to **Grafana** (anonymous-admin, so the one shared login gates
|
||||
it with no per-user Grafana accounts) and the rest of `/_gm/*` to the backend-rendered
|
||||
**admin console**; `/app/`, `/telegram/`, `/vk/` and the Connect path go to the gateway; the
|
||||
catch-all — notably the landing at `/` — goes to the landing container. The
|
||||
catch-all — notably the landing at `/`, plus the static public offer at `/offer/`
|
||||
(rendered from `ui/legal/offer_ru.md` at build time) — goes to the landing container. The
|
||||
**Telegram validator** runs as a separate container with **no public ingress**,
|
||||
answering only internal gRPC (HMAC, no Telegram egress). The **Telegram bot** holds
|
||||
no inbound port either: it dials the gateway's **bot-link** (mTLS) and egresses to
|
||||
|
||||
@@ -58,3 +58,14 @@ test('the landing shows a web-version entry linking /app/, with a caption', asyn
|
||||
expect(await web.getAttribute('href')).toContain('/app/');
|
||||
await expect(page.getByText('Веб-версия')).toBeVisible();
|
||||
});
|
||||
|
||||
// The footer carries a public-offer link to the static /offer/ page (rendered from
|
||||
// ui/legal/offer_ru.md at build; the legal document a purchase accepts).
|
||||
test('the landing footer links to the public offer at /offer/', async ({ page }) => {
|
||||
await page.goto('/landing.html');
|
||||
await expect(page.getByText(/Играй в «Эрудита»/)).toBeVisible(); // Russian by default
|
||||
|
||||
const offer = page.getByRole('link', { name: 'Публичная оферта' });
|
||||
await expect(offer).toBeVisible();
|
||||
expect(await offer.getAttribute('href')).toBe('/offer/');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# Публичная оферта
|
||||
|
||||
Публичная оферта о заключении договора купли-продажи.
|
||||
|
||||
## 1. Общие положения
|
||||
|
||||
В настоящей Публичной оферте содержатся условия заключения Договора купли-продажи (далее по тексту — «Договор купли-продажи» и/или «Договор»). Настоящей офертой признается предложение, адресованное одному или нескольким конкретным лицам, которое достаточно определенно и выражает намерение лица, сделавшего предложение, считать себя заключившим Договор с адресатом, которым будет принято предложение.
|
||||
|
||||
Совершение указанных в настоящей Оферте действий является подтверждением согласия обеих Сторон заключить Договор купли-продажи на условиях, в порядке и объеме, изложенных в настоящей Оферте.
|
||||
|
||||
Нижеизложенный текст Публичной оферты является официальным публичным предложением Продавца, адресованный заинтересованному кругу лиц заключить Договор купли-продажи в соответствии с положениями пункта 2 статьи 437 Гражданского кодекса РФ.
|
||||
|
||||
Договор купли-продажи считается заключенным и приобретает силу с момента совершения Сторонами действий, предусмотренных в настоящей Оферте, и, означающих безоговорочное, а также полное принятие всех условий настоящей Оферты без каких-либо изъятий или ограничений на условиях присоединения.
|
||||
|
||||
### Термины и определения
|
||||
|
||||
**Договор** — текст настоящей Оферты с Приложениями, являющимися неотъемлемой частью настоящей Оферты, акцептованный Покупателем путем совершения конклюдентных действий, предусмотренных настоящей Офертой.
|
||||
|
||||
**Конклюдентные действия** — это поведение, которое выражает согласие с предложением контрагента заключить, изменить или расторгнуть договор. Действия состоят в полном или частичном выполнении условий, которые предложил контрагент.
|
||||
|
||||
**Сайт Продавца в сети «Интернет»** — совокупность программ для электронных вычислительных машин и иной информации, содержащейся в информационной системе, доступ к которой обеспечивается посредством сети «Интернет» по доменному имени и сетевому адресу: `erudit-game.ru`.
|
||||
|
||||
**Стороны Договора (Стороны)** — Продавец и Покупатель.
|
||||
|
||||
**Товар** — товаром по договору купли-продажи могут быть любые вещи с соблюдением правил, предусмотренных статьей 129 Гражданского кодекса РФ.
|
||||
|
||||
## 2. Предмет Договора
|
||||
|
||||
**2.1.** По настоящему Договору Продавец обязуется передать вещь (Товар) в собственность Покупателя, а Покупатель обязуется принять Товар и уплатить за него определенную денежную сумму.
|
||||
|
||||
**2.2.** Наименование, количество, а также ассортимент Товара, его стоимость, порядок доставки и иные условия определяются на основании сведений Продавца при оформлении заявки Покупателем, либо устанавливаются на сайте Продавца в сети «Интернет» `erudit-game.ru`.
|
||||
|
||||
**2.3.** Акцепт настоящей Оферты выражается в совершении конклюдентных действий, в частности:
|
||||
|
||||
- действиях, связанных с регистрацией учетной записи на Сайте Продавца в сети «Интернет» при наличии необходимости регистрации учетной записи;
|
||||
- путем составления и заполнения заявки на оформление заказа Товара;
|
||||
- путем сообщения требуемых для заключения Договора сведений по телефону, электронной почте, указанными на сайте Продавца в сети «Интернет», в том числе, при обратном звонке Продавца по заявке Покупателя;
|
||||
- оплаты Товара Покупателем.
|
||||
|
||||
Данный перечень не является исчерпывающим, могут быть и другие действия, которые ясно выражают намерение лица принять предложение контрагента.
|
||||
|
||||
## 3. Права и обязанности Сторон
|
||||
|
||||
### 3.1. Права и обязанности Продавца
|
||||
|
||||
**3.1.1.** Продавец вправе требовать оплаты Товаров и их доставки в порядке и на условиях, предусмотренных Договором;
|
||||
|
||||
**3.1.2.** Отказать в заключении Договора на основании настоящей Оферты Покупателю в случае его недобросовестного поведения, в частности, в случае:
|
||||
|
||||
- более 2 (Двух) отказов от Товаров надлежащего качества в течение года;
|
||||
- предоставления заведомо недостоверной персональной информации;
|
||||
- возврата испорченного Покупателем Товара или Товара, бывшего в употреблении;
|
||||
- иных случаях недобросовестного поведения, свидетельствующих о заключении Покупателем Договора с целью злоупотребления правами, и отсутствия обычной экономической цели Договора — приобретения Товара.
|
||||
|
||||
**3.1.3.** Продавец обязуется передать Покупателю Товар надлежащего качества и в надлежащей упаковке;
|
||||
|
||||
**3.1.4.** Передать Товар свободным от прав третьих лиц;
|
||||
|
||||
**3.1.5.** Организовать доставку Товаров Покупателю;
|
||||
|
||||
**3.1.6.** Предоставить Покупателю всю необходимую информацию в соответствии с требованиями действующего законодательства РФ и настоящей Оферты;
|
||||
|
||||
### 3.2. Права и обязанности Покупателя
|
||||
|
||||
**3.2.1.** Покупатель вправе требовать передачи Товара в порядке и на условиях, предусмотренных Договором.
|
||||
|
||||
**3.2.2.** Требовать предоставления всей необходимой информации в соответствии с требованиями действующего законодательства РФ и настоящей Оферты;
|
||||
|
||||
**3.2.3.** Отказаться от Товара по основаниям, предусмотренным Договором и действующим законодательством Российской Федерации.
|
||||
|
||||
**3.2.4.** Покупатель обязуется предоставить Продавцу достоверную информацию, необходимую для надлежащего исполнения Договора;
|
||||
|
||||
**3.2.5.** Принять и оплатить Товар в соответствии с условиями Договора;
|
||||
|
||||
**3.2.6.** Покупатель гарантирует, что все условия Договора ему понятны; Покупатель принимает условия без оговорок, а также в полном объеме.
|
||||
|
||||
## 4. Цена и порядок расчетов
|
||||
|
||||
**4.1.** Стоимость, а также порядок оплаты Товара определяется на основании сведений Продавца при оформлении заявки Покупателем, либо устанавливаются на сайте Продавца в сети «Интернет»: `erudit-game.ru`.
|
||||
|
||||
**4.2.** Все расчеты по Договору производятся в безналичном порядке.
|
||||
|
||||
## 5. Обмен и возврат Товара
|
||||
|
||||
**5.1.** Покупатель вправе осуществить возврат (обмен) Продавцу Товара, приобретенный дистанционным способом, за исключением перечня товаров, не подлежащих обмену и возврату согласно действующему законодательству Российской Федерации. Условия, сроки и порядок возврата Товара надлежащего и ненадлежащего качества установлены в соответствии с Гражданским кодексом РФ, Закона РФ от 07.02.1992 N 2300-1 «О защите прав потребителей», Правил, утвержденных Постановлением Правительства РФ от 31.12.2020 N 2463.
|
||||
|
||||
**5.2.** Требование Покупателя об обмене либо о возврате Товара рассматривается индивидуально при условии неиспользования приобретенного товара и наличии уважительных причин (технический сбой, ошибка в описании товара).
|
||||
|
||||
## 6. Конфиденциальность и безопасность
|
||||
|
||||
**6.1.** При реализации настоящего Договора Стороны обеспечивают конфиденциальность и безопасность персональных данных в соответствии с актуальной редакцией ФЗ от 27.07.2006 г. № 152-ФЗ «О персональных данных» и ФЗ от 27.07.2006 г. № 149-ФЗ «Об информации, информационных технологиях и о защите информации».
|
||||
|
||||
**6.2.** Стороны обязуются сохранять конфиденциальность информации, полученной в ходе исполнения настоящего Договора, и принять все возможные меры, чтобы предохранить полученную информацию от разглашения.
|
||||
|
||||
**6.3.** Под конфиденциальной информацией понимается любая информация, передаваемая Продавцом и Покупателем в процессе реализации Договора и подлежащая защите, исключения указаны ниже.
|
||||
|
||||
**6.4.** Такая информация может содержаться в предоставляемых Продавцом локальных нормативных актах, договорах, письмах, отчетах, аналитических материалах, результатах исследований, схемах, графиках, спецификациях и других документах, оформленных как на бумажных, так и на электронных носителях.
|
||||
|
||||
## 7. Форс-мажор
|
||||
|
||||
**7.1.** Стороны освобождаются от ответственности за неисполнение или ненадлежащее исполнение обязательств по Договору, если надлежащее исполнение оказалось невозможным вследствие непреодолимой силы, то есть чрезвычайных и непредотвратимых при данных условиях обстоятельств, под которыми понимаются: запретные действия властей, эпидемии, блокада, эмбарго, землетрясения, наводнения, пожары или другие стихийные бедствия.
|
||||
|
||||
**7.2.** В случае наступления этих обстоятельств Сторона обязана в течение 30 (Тридцати) рабочих дней уведомить об этом другую Сторону.
|
||||
|
||||
**7.3.** Документ, выданный уполномоченным государственным органом, является достаточным подтверждением наличия и продолжительности действия непреодолимой силы.
|
||||
|
||||
**7.4.** Если обстоятельства непреодолимой силы продолжают действовать более 60 (Шестидесяти) рабочих дней, то каждая Сторона вправе отказаться от настоящего Договора в одностороннем порядке.
|
||||
|
||||
## 8. Ответственность Сторон
|
||||
|
||||
**8.1.** В случае неисполнения и/или ненадлежащего исполнения своих обязательств по Договору, Стороны несут ответственность в соответствии с условиями настоящей Оферты.
|
||||
|
||||
**8.2.** Сторона, не исполнившая или ненадлежащим образом исполнившая обязательства по Договору, обязана возместить другой Стороне причиненные такими нарушениями убытки.
|
||||
|
||||
## 9. Срок действия настоящей Оферты
|
||||
|
||||
**9.1.** Оферта вступает в силу с момента размещения на Сайте Продавца и действует до момента её отзыва Продавцом.
|
||||
|
||||
**9.2.** Продавец оставляет за собой право внести изменения в условия Оферты и/или отозвать Оферту в любой момент по своему усмотрению. Сведения об изменении или отзыве Оферты доводятся до Покупателя по выбору Продавца посредством размещения на сайте Продавца в сети «Интернет», в Личном кабинете Покупателя, либо путем направления соответствующего уведомления на электронный или почтовый адрес, указанный Покупателем при заключении Договора или в ходе его исполнения.
|
||||
|
||||
**9.3.** Договор вступает в силу с момента Акцепта условий настоящей Оферты Покупателем и действует до полного исполнения Сторонами обязательств по Договору.
|
||||
|
||||
**9.4.** Изменения, внесенные Продавцом в Договор и опубликованные на сайте в форме актуализированной Оферты, считаются принятыми Покупателем в полном объеме.
|
||||
|
||||
## 10. Дополнительные условия
|
||||
|
||||
**10.1.** Договор, его заключение и исполнение регулируется действующим законодательством Российской Федерации. Все вопросы, не урегулированные настоящей Офертой или урегулированные не полностью, регулируются в соответствии с материальным правом Российской Федерации.
|
||||
|
||||
**10.2.** В случае возникновения спора, который может возникнуть между Сторонами в ходе исполнения ими своих обязательств по Договору, заключенному на условиях настоящей Оферты, Стороны обязаны урегулировать спор мирным путем до начала судебного разбирательства.
|
||||
|
||||
Судебное разбирательство осуществляется в соответствии с законодательством Российской Федерации.
|
||||
|
||||
Споры или разногласия, по которым Стороны не достигли договоренности, подлежат разрешению в соответствии с законодательством РФ. Досудебный порядок урегулирования спора является обязательным.
|
||||
|
||||
**10.3.** В качестве языка Договора, заключаемого на условиях настоящей Оферты, а также языка, используемого при любом взаимодействии Сторон (включая ведение переписки, предоставление требований / уведомлений / разъяснений, предоставление документов и т. д.), Стороны определили русский язык.
|
||||
|
||||
**10.4.** Все документы, подлежащие предоставлению в соответствии с условиями настоящей Оферты, должны быть составлены на русском языке либо иметь перевод на русский язык, удостоверенный в установленном порядке.
|
||||
|
||||
**10.5.** Бездействие одной из Сторон в случае нарушения условий настоящей Оферты не лишает права заинтересованной Стороны осуществлять защиту своих интересов позднее, а также не означает отказа от своих прав в случае совершения одной из Сторон подобных либо сходных нарушений в будущем.
|
||||
|
||||
**10.6.** Если на Сайте Продавца в сети «Интернет» есть ссылки на другие веб-сайты и материалы третьих лиц, такие ссылки размещены исключительно в целях информирования, и Продавец не имеет контроля в отношении содержания таких сайтов или материалов. Продавец не несет ответственность за любые убытки или ущерб, которые могут возникнуть в результате использования таких ссылок.
|
||||
|
||||
## 11. Реквизиты Продавца
|
||||
|
||||
Денисов Илья Аркадьевич, ИНН 290210610742.
|
||||
@@ -28,6 +28,7 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@types/node": "^22.10.0",
|
||||
"core-js-bundle": "^3.49.0",
|
||||
"marked": "^18.0.5",
|
||||
"svelte": "^5.15.0",
|
||||
"svelte-check": "^4.1.0",
|
||||
"typescript": "^5.7.0",
|
||||
|
||||
Generated
+10
@@ -39,6 +39,9 @@ importers:
|
||||
core-js-bundle:
|
||||
specifier: ^3.49.0
|
||||
version: 3.49.0
|
||||
marked:
|
||||
specifier: ^18.0.5
|
||||
version: 18.0.5
|
||||
svelte:
|
||||
specifier: ^5.15.0
|
||||
version: 5.56.0
|
||||
@@ -1628,6 +1631,11 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
marked@18.0.5:
|
||||
resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3877,6 +3885,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
marked@18.0.5: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
minimatch@10.2.5:
|
||||
|
||||
+14
-1
@@ -123,7 +123,10 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="ft">{t('about.version', { v: __APP_VERSION__ })}</footer>
|
||||
<footer class="ft">
|
||||
<a class="offer" href="/offer/">{t('landing.offer')}</a>
|
||||
<span>{t('about.version', { v: __APP_VERSION__ })}</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@@ -275,8 +278,18 @@
|
||||
}
|
||||
.ft {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.ft .offer {
|
||||
color: inherit;
|
||||
}
|
||||
.ft .offer:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -275,6 +275,7 @@ export const en = {
|
||||
'landing.captionTelegram': 'Telegram',
|
||||
'landing.captionVK': 'VK',
|
||||
'landing.captionWeb': 'Web',
|
||||
'landing.offer': 'Public offer',
|
||||
|
||||
'install.title': 'Install the app',
|
||||
'install.subtitle': 'Put the app icon on your desktop (home screen) to open the game in one tap.',
|
||||
|
||||
@@ -275,6 +275,7 @@ export const ru: Record<MessageKey, string> = {
|
||||
'landing.captionTelegram': 'Telegram',
|
||||
'landing.captionVK': 'VK',
|
||||
'landing.captionWeb': 'Веб-версия',
|
||||
'landing.offer': 'Публичная оферта',
|
||||
|
||||
'install.title': 'Установить приложение',
|
||||
'install.subtitle': 'Поместите иконку приложения на рабочий стол (домашний экран), чтобы открывать игру одним нажатием.',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderOfferHtml } from './offer';
|
||||
|
||||
describe('renderOfferHtml', () => {
|
||||
it('wraps rendered markdown in a standalone Russian HTML document', () => {
|
||||
const html = renderOfferHtml('# Публичная оферта\n\nо заключении договора купли-продажи');
|
||||
expect(html).toContain('<!doctype html>');
|
||||
expect(html).toContain('lang="ru"');
|
||||
expect(html).toContain('<title>Публичная оферта — Эрудит</title>');
|
||||
// The markdown heading is rendered, not left as literal source.
|
||||
expect(html).toContain('<h1>Публичная оферта</h1>');
|
||||
expect(html).not.toContain('# Публичная оферта');
|
||||
// A back link to the landing root is always present.
|
||||
expect(html).toContain('href="/"');
|
||||
});
|
||||
|
||||
it('renders headings, bold clause numbers and lists', () => {
|
||||
const html = renderOfferHtml('## 2. Предмет\n\n**2.1.** Текст пункта.\n\n- первый\n- второй');
|
||||
expect(html).toContain('<h2>2. Предмет</h2>');
|
||||
expect(html).toContain('<strong>2.1.</strong>');
|
||||
expect(html).toContain('<li>первый</li>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { marked } from 'marked';
|
||||
|
||||
/**
|
||||
* renderOfferHtml renders the public-offer markdown source into a standalone,
|
||||
* self-contained HTML document served statically at `/offer/`. The build emits
|
||||
* the result as `dist/offer/index.html` (see the `emit-offer` plugin in
|
||||
* `vite.config.ts`), which the landing container serves.
|
||||
*
|
||||
* The input `markdown` is trusted repository content (the owner-edited
|
||||
* `ui/legal/offer_ru.md`), not user input, so the rendered HTML is deliberately
|
||||
* not sanitised. The page carries its own minimal light/dark styling so it needs
|
||||
* neither the app bundle nor `app.css`.
|
||||
*/
|
||||
export function renderOfferHtml(markdown: string): string {
|
||||
const body = marked.parse(markdown, { async: false }) as string;
|
||||
return `<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Публичная оферта — Эрудит</title>
|
||||
<link rel="canonical" href="https://erudit-game.ru/offer/" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f3f4f6" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f1115" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #ffffff;
|
||||
--text: #1a1c20;
|
||||
--accent: #2563eb;
|
||||
--rule: #e5e7eb;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0f1115;
|
||||
--text: #e7e9ee;
|
||||
--accent: #6ea8fe;
|
||||
--rule: #242832;
|
||||
}
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
main {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 20px 64px;
|
||||
}
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
.back {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.6rem;
|
||||
text-align: center;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 2em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.05rem;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
ul {
|
||||
padding-left: 1.25em;
|
||||
}
|
||||
li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<a class="back" href="/">← На главную</a>
|
||||
${body}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"include": ["vite.config.ts", "src/lib/offer.ts"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
|
||||
import { defineConfig, type Plugin } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import { renderOfferHtml } from './src/lib/offer';
|
||||
|
||||
/**
|
||||
* injectBootVersion stamps the app version into index.html's boot-capability guard, replacing its
|
||||
@@ -38,6 +39,22 @@ function emitPolyfills(): Plugin {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* emitOffer renders the public offer markdown (legal/offer_ru.md) to a standalone
|
||||
* static page and emits it as dist/offer/index.html, which the landing container
|
||||
* serves at /offer/ (deploy/landing/Caddyfile). The markdown is the owner-editable
|
||||
* source of truth, so the page is regenerated from it on every build.
|
||||
*/
|
||||
function emitOffer(): Plugin {
|
||||
return {
|
||||
name: 'emit-offer',
|
||||
generateBundle() {
|
||||
const md = readFileSync(resolve(import.meta.dirname, 'legal/offer_ru.md'), 'utf8');
|
||||
this.emitFile({ type: 'asset', fileName: 'offer/index.html', source: renderOfferHtml(md) });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// The edge Connect service is scrabble.edge.v1.Gateway; the gateway serves it over
|
||||
// h2c on :8081 by default. In dev we proxy the RPC path so the browser (which can
|
||||
// not speak h2c directly) talks to the dev server on the same origin. In `mock`
|
||||
@@ -60,6 +77,7 @@ export default defineConfig(({ mode }) => ({
|
||||
plugins: [
|
||||
svelte(),
|
||||
emitPolyfills(),
|
||||
emitOffer(),
|
||||
injectBootVersion(),
|
||||
// App-shell precache for the offline mode: a custom (injectManifest) service worker precaches
|
||||
// index.html + the hashed assets so the installed web PWA cold-launches with no network. It
|
||||
|
||||
Reference in New Issue
Block a user