generated from wiz4apps/templates.OnlineShop
feat: implement La Rose storefront design (home, catalog, PDP)
Port the ready OpenPencil design (designs/design.fig) into the runnable storefront (public/): - design tokens + components (buttons, inputs, badges, chips, cards, skeletons, stepper, variants, pagination, states) from the canvas; Playfair Display + Inter, mobile-first at 390/1440. - shell: topbar, header (nav, search, favourites, cart badge), mobile header, bottom nav, footer with real contacts and WhatsApp/Telegram. - screens: Home (hero, trust, 11 categories, hits, delivery banner, info cards), Catalog (breadcrumbs, facets: price/categories/badges, sort, 6-per-page pagination, empty state), PDP (gallery + lightbox, variants 15/19/25, stepper, sticky buy, delivery block, tabs, specs, related). - cart + single-page checkout + success built from the same tokens (not drawn on the canvas yet, flagged as follow-up). - extend /api/catalog with category counts, variants, occasion, care, price range, badge counts and delivery constants; add /api/pages for info/FAQ content; export CONTENT_PAGES from the seed. Money stays integer BYN minor units, formatted via Intl (en-US).
This commit is contained in:
parent
32bb265252
commit
93b927dfdc
5
.codegraph/.gitignore
vendored
Normal file
5
.codegraph/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
BIN
designs/design.fig
Normal file
BIN
designs/design.fig
Normal file
Binary file not shown.
12
index.html
Normal file
12
index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>semianiakavy-la-rose - Dev server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
allowBuilds:
|
||||
esbuild: true
|
||||
1910
public/app.js
1910
public/app.js
File diff suppressed because it is too large
Load diff
|
|
@ -3,31 +3,232 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="La Rose — доставка цветов и подарков по Минску." />
|
||||
<title>La Rose — цветы и подарки</title>
|
||||
<meta name="theme-color" content="#D6336C" />
|
||||
<meta
|
||||
name="description"
|
||||
content="La Rose — доставка свежих букетов и подарков по Минску за 2 часа. 93 букета, 281 реальное фото, бесплатная доставка от 100 BYN."
|
||||
/>
|
||||
<title>La Rose — цветы и подарки с доставкой по Минску</title>
|
||||
|
||||
<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&family=Playfair+Display:wght@600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#app">Перейти к содержимому</a>
|
||||
|
||||
<header class="site-header">
|
||||
<a class="brand" href="/" data-link>
|
||||
<span class="brand-name">La Rose</span>
|
||||
<span class="brand-sub">цветы и подарки · Минск</span>
|
||||
</a>
|
||||
<nav class="site-nav">
|
||||
<a href="/" data-link>Каталог</a>
|
||||
<span class="nav-note">Доставка по Минску 2–4 часа</span>
|
||||
</nav>
|
||||
<div class="topbar">
|
||||
<div class="container topbar__inner">
|
||||
<p class="topbar__note">Минск · доставка цветов и подарков до 2 часов · онлайн-заказы 24/7</p>
|
||||
<div class="topbar__right">
|
||||
<span class="topbar__pickup">Самовывоз: Притыцкого, 10 · 09:00–21:00</span>
|
||||
<a
|
||||
class="pill pill--wa"
|
||||
href="https://wa.me/375296573000"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span class="pill__ico" aria-hidden="true">✆</span> WhatsApp
|
||||
</a>
|
||||
<a class="pill pill--tg" href="https://t.me/bot_a_nik_vsop" target="_blank" rel="noopener">
|
||||
<span class="pill__ico" aria-hidden="true">➤</span> Telegram
|
||||
</a>
|
||||
<a class="topbar__phone" href="tel:+375296573000">+375 (29) 657-30-00</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<div class="container header__inner">
|
||||
<a class="brand" href="/" data-link aria-label="La Rose — на главную">
|
||||
<span class="brand__name">La Rose</span>
|
||||
<span class="brand__sub">ЦВЕТЫ · ПОДАРКИ · МИНСК</span>
|
||||
</a>
|
||||
|
||||
<nav class="mainnav" aria-label="Каталог">
|
||||
<ul id="nav-links" class="mainnav__list"></ul>
|
||||
<div class="mainnav__more-wrap">
|
||||
<button type="button" class="mainnav__more" id="more-btn" aria-expanded="false" aria-controls="menu-panel">
|
||||
Ещё <span aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div id="menu-panel" class="menu-panel" hidden></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="header__actions">
|
||||
<form class="search" id="search-form" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="search-input">Поиск по каталогу</label>
|
||||
<svg class="search__ico" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M16.5 16.5 21 21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
<input id="search-input" name="query" type="search" placeholder="Найти букет…" autocomplete="off" />
|
||||
</form>
|
||||
|
||||
<a class="iconbtn iconbtn--fav" id="fav-link" href="/favorites" data-link aria-label="Избранное">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 20s-7-4.4-7-9.4A3.9 3.9 0 0 1 12 7.6 3.9 3.9 0 0 1 19 10.6c0 5-7 9.4-7 9.4Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="count" id="fav-count" hidden>0</span>
|
||||
</a>
|
||||
|
||||
<a class="cartbtn" href="/cart" data-link aria-label="Корзина">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M6 8h12l-1 11H7L6 8Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M9.5 8a2.5 2.5 0 0 1 5 0" fill="none" stroke="currentColor" stroke-width="1.8" />
|
||||
</svg>
|
||||
<span class="count" id="cart-count" hidden>0</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mobile-actions">
|
||||
<button type="button" class="iconbtn iconbtn--round" id="m-search" aria-label="Поиск">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M16.5 16.5 21 21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<a class="iconbtn iconbtn--round" id="m-fav" href="/favorites" data-link aria-label="Избранное">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 20s-7-4.4-7-9.4A3.9 3.9 0 0 1 12 7.6 3.9 3.9 0 0 1 19 10.6c0 5-7 9.4-7 9.4Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
/>
|
||||
</svg>
|
||||
<span class="count count--m" id="fav-count-m" hidden>0</span>
|
||||
</a>
|
||||
<a class="cartbtn cartbtn--m" href="/cart" data-link aria-label="Корзина">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M6 8h12l-1 11H7L6 8Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
|
||||
<path d="M9.5 8a2.5 2.5 0 0 1 5 0" fill="none" stroke="currentColor" stroke-width="1.8" />
|
||||
</svg>
|
||||
<span class="count count--m" id="cart-count-m" hidden>0</span>
|
||||
</a>
|
||||
<button type="button" class="iconbtn iconbtn--round" id="burger" aria-label="Меню" aria-expanded="false">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 7h16M4 12h16M4 17h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-search" id="mobile-search" hidden>
|
||||
<form class="container mobile-search__inner" id="mobile-search-form" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="mobile-search-input">Поиск по каталогу</label>
|
||||
<input id="mobile-search-input" name="query" type="search" placeholder="Найти букет…" autocomplete="off" />
|
||||
<button type="submit" class="btn btn--primary btn--sm">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mobile-menu" id="mobile-menu" hidden></div>
|
||||
</header>
|
||||
|
||||
<main id="app" class="app" aria-live="polite">
|
||||
<p class="state">Загрузка каталога…</p>
|
||||
<div class="container">
|
||||
<div class="skeleton skeleton--hero"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<span>La Rose — базовый фронтенд (этап каталога).</span>
|
||||
<span>Данные: сид каталога · 93 товара</span>
|
||||
<div class="container footer__grid">
|
||||
<div class="footer__brand">
|
||||
<span class="footer__logo">La Rose</span>
|
||||
<p class="footer__about">
|
||||
Доставка свежих букетов и подарков по Минску с 2018 года. Свои флористы, свой курьер,
|
||||
фото — это реальные букеты.
|
||||
</p>
|
||||
<div class="footer__pills">
|
||||
<a class="pill pill--wa" href="https://wa.me/375296573000" target="_blank" rel="noopener">WhatsApp</a>
|
||||
<a class="pill pill--tg" href="https://t.me/bot_a_nik_vsop" target="_blank" rel="noopener">Telegram</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="footer__col" aria-label="Каталог">
|
||||
<h2 class="footer__title">Каталог</h2>
|
||||
<ul class="footer__list" id="footer-catalog"></ul>
|
||||
</nav>
|
||||
|
||||
<nav class="footer__col" aria-label="Покупателям">
|
||||
<h2 class="footer__title">Покупателям</h2>
|
||||
<ul class="footer__list">
|
||||
<li><a href="/pages/dostavka" data-link>Доставка и оплата</a></li>
|
||||
<li><a href="/pages/kak-zakazat" data-link>Как заказать</a></li>
|
||||
<li><a href="/pages/faq" data-link>Вопросы и ответы</a></li>
|
||||
<li><a href="/pages/vozvrat" data-link>Возврат</a></li>
|
||||
<li><a href="/pages/b2b" data-link>Корпоративным клиентам</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="footer__col">
|
||||
<h2 class="footer__title">Контакты и часы работы</h2>
|
||||
<ul class="footer__list footer__list--contacts">
|
||||
<li><a href="tel:+375296573000">+375 (29) 657-30-00</a></li>
|
||||
<li><a href="tel:+375336573000">+375 (33) 657-30-00</a></li>
|
||||
<li><a href="mailto:info@la-rose.by">info@la-rose.by</a></li>
|
||||
<li>ул. Притыцкого, 10 · м. Пушкинская</li>
|
||||
<li>Ежедневно 09:00–21:00</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer__bottom">
|
||||
<div class="container footer__bottom-inner">
|
||||
<span>© 2018–2026 La Rose · Минск. Все цены в BYN.</span>
|
||||
<span>ООО «Диапазон Плюс» · УНП 190438401 · демо-витрина, платёжный провайдер — mock</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<nav class="bottomnav" aria-label="Мобильная навигация">
|
||||
<a class="bottomnav__item" href="/" data-link data-nav="home">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 11 12 4l8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1v-8Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
|
||||
<span>Главная</span>
|
||||
</a>
|
||||
<a class="bottomnav__item" href="/catalog" data-link data-nav="catalog">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h7v7H4V5Zm9 0h7v7h-7V5ZM4 14h7v5H4v-5Zm9 0h7v5h-7v-5Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
|
||||
<span>Каталог</span>
|
||||
</a>
|
||||
<a class="bottomnav__item" href="https://wa.me/375296573000" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 0 0-7.7 13.7L3 21l4.4-1.2A9 9 0 1 0 12 3Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
|
||||
<span>Чаты</span>
|
||||
</a>
|
||||
<a class="bottomnav__item" href="/cart" data-link data-nav="cart">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 8h12l-1 11H7L6 8Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M9.5 8a2.5 2.5 0 0 1 5 0" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>
|
||||
<span>Корзина</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="lightbox" id="lightbox" hidden>
|
||||
<button type="button" class="lightbox__close" id="lb-close" aria-label="Закрыть">✕</button>
|
||||
<button type="button" class="lightbox__nav lightbox__nav--prev" id="lb-prev" aria-label="Предыдущее фото">‹</button>
|
||||
<figure class="lightbox__figure">
|
||||
<img id="lb-img" src="" alt="" />
|
||||
<figcaption id="lb-cap" class="lightbox__cap"></figcaption>
|
||||
</figure>
|
||||
<button type="button" class="lightbox__nav lightbox__nav--next" id="lb-next" aria-label="Следующее фото">›</button>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
|
||||
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
2555
public/styles.css
2555
public/styles.css
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,8 @@ import { extname, join, normalize } from 'node:path';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { CATEGORIES, readCatalog } from '../src/db/catalog';
|
||||
import { imageUrl, readImages } from '../src/db/images';
|
||||
import { STORE_CURRENCY } from '../src/store/constants';
|
||||
import { CONTENT_PAGES } from '../src/db/seed';
|
||||
import { DELIVERY, STORE, STORE_CURRENCY } from '../src/store/constants';
|
||||
|
||||
/**
|
||||
* Minimal storefront server for the basic frontend stage.
|
||||
|
|
@ -15,9 +16,10 @@ import { STORE_CURRENCY } from '../src/store/constants';
|
|||
* the same seed sources as the database (`db/seed/catalog.tsv` + `db/seed/images.tsv`).
|
||||
*
|
||||
* Routes:
|
||||
* GET /api/catalog → { currency, count, categories, products[] }
|
||||
* GET /api/catalog → { currency, count, priceRange, badges, delivery, categories[], products[] }
|
||||
* GET /api/pages → { pages[] } (info/FAQ content from the seed)
|
||||
* GET /images/... → product gallery files from public/
|
||||
* GET /product/<slug> → index.html (client-side route, SPA fallback)
|
||||
* GET /<route without ext> → index.html (client-side route, SPA fallback)
|
||||
* GET /<file with ext> → public/<file> or 404
|
||||
*/
|
||||
|
||||
|
|
@ -58,16 +60,28 @@ interface ApiImage {
|
|||
position: number;
|
||||
}
|
||||
|
||||
interface ApiVariant {
|
||||
label: string;
|
||||
priceMinor: number;
|
||||
stock: number;
|
||||
}
|
||||
|
||||
interface ApiCategory {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
/** R13: hidden source categories stay reachable by direct URL but never in the menu. */
|
||||
visible: boolean;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ApiProduct {
|
||||
n: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
composition: string;
|
||||
care: string;
|
||||
priceMinor: number;
|
||||
oldPriceMinor: number | null;
|
||||
currency: string;
|
||||
|
|
@ -76,17 +90,57 @@ interface ApiProduct {
|
|||
stems: number | null;
|
||||
heightCm: number;
|
||||
deliveryTime: string;
|
||||
occasion: string;
|
||||
categories: string[];
|
||||
images: ApiImage[];
|
||||
variants: ApiVariant[];
|
||||
}
|
||||
|
||||
interface ApiCatalog {
|
||||
currency: string;
|
||||
count: number;
|
||||
priceRange: { minMinor: number; maxMinor: number };
|
||||
badges: { hit: number; new: number; sale: number };
|
||||
delivery: {
|
||||
freeOverMinor: number;
|
||||
courierFeeMinor: number;
|
||||
leadTimeText: string;
|
||||
outsideMkadText: string;
|
||||
pickupAddress: string;
|
||||
workingHours: string;
|
||||
phone1: string;
|
||||
phone2: string;
|
||||
email: string;
|
||||
whatsapp: string;
|
||||
telegram: string;
|
||||
};
|
||||
categories: ApiCategory[];
|
||||
products: ApiProduct[];
|
||||
}
|
||||
|
||||
/** Variants seeded in src/db/seed.ts §5 (source PDP «15/19/25 роз»). */
|
||||
const PRODUCT_VARIANTS: Record<string, ApiVariant[]> = {
|
||||
'buket-iz-krasnykh-roz-s-evkaliptom': [
|
||||
{ label: '15 роз', priceMinor: 198_00, stock: 10 },
|
||||
{ label: '19 роз', priceMinor: 240_00, stock: 6 },
|
||||
{ label: '25 роз', priceMinor: 325_00, stock: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
/** Occasion facet, same mapping the seed writes to product_feature (§5). */
|
||||
function occasionFor(categories: string[]): string {
|
||||
if (categories.includes('svadebnye-bukety-nevesty')) return 'Невесте';
|
||||
if (categories.includes('monobuket')) return 'Любимой';
|
||||
if (
|
||||
categories.includes('kompozitsii') ||
|
||||
categories.includes('tsvety-v-korzine') ||
|
||||
categories.includes('tsvety-v-yashchike')
|
||||
) {
|
||||
return 'Коллеге, Партнёру';
|
||||
}
|
||||
return 'Любимой, Маме, Другу';
|
||||
}
|
||||
|
||||
/** Build the storefront payload from the same seed files the database is seeded from. */
|
||||
function buildCatalog(): ApiCatalog {
|
||||
const imagesBySlug = new Map<string, ApiImage[]>();
|
||||
|
|
@ -97,10 +151,12 @@ function buildCatalog(): ApiCatalog {
|
|||
}
|
||||
|
||||
const products: ApiProduct[] = readCatalog().map((product) => ({
|
||||
n: product.n,
|
||||
slug: product.slug,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
composition: product.composition,
|
||||
care: product.care,
|
||||
priceMinor: product.priceMinor,
|
||||
oldPriceMinor: product.oldPriceMinor,
|
||||
currency: product.currency,
|
||||
|
|
@ -109,19 +165,59 @@ function buildCatalog(): ApiCatalog {
|
|||
stems: product.stems,
|
||||
heightCm: product.heightCm,
|
||||
deliveryTime: product.deliveryTime,
|
||||
occasion: occasionFor(product.categories),
|
||||
categories: product.categories,
|
||||
images: (imagesBySlug.get(product.slug) ?? []).sort((a, b) => a.position - b.position),
|
||||
variants: PRODUCT_VARIANTS[product.slug] ?? [],
|
||||
}));
|
||||
|
||||
const countByCategory = new Map<string, number>();
|
||||
for (const product of products) {
|
||||
for (const slug of new Set(product.categories)) {
|
||||
countByCategory.set(slug, (countByCategory.get(slug) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const prices = products.map((product) => product.priceMinor);
|
||||
|
||||
return {
|
||||
currency: STORE_CURRENCY,
|
||||
count: products.length,
|
||||
categories: CATEGORIES.filter((category) => category.visible).map(({ slug, name }) => ({ slug, name })),
|
||||
priceRange: {
|
||||
minMinor: prices.length ? Math.min(...prices) : 0,
|
||||
maxMinor: prices.length ? Math.max(...prices) : 0,
|
||||
},
|
||||
badges: {
|
||||
hit: products.filter((product) => product.badge === 'hit').length,
|
||||
new: products.filter((product) => product.badge === 'new').length,
|
||||
sale: products.filter((product) => product.badge === 'sale').length,
|
||||
},
|
||||
delivery: {
|
||||
freeOverMinor: DELIVERY.freeOverMinor,
|
||||
courierFeeMinor: DELIVERY.courierFeeMinor,
|
||||
leadTimeText: DELIVERY.leadTimeText,
|
||||
outsideMkadText: DELIVERY.outsideMkadText,
|
||||
pickupAddress: STORE.pickupAddress,
|
||||
workingHours: STORE.workingHours,
|
||||
phone1: STORE.phone1,
|
||||
phone2: STORE.phone2,
|
||||
email: STORE.email,
|
||||
whatsapp: STORE.whatsapp,
|
||||
telegram: STORE.telegram,
|
||||
},
|
||||
categories: CATEGORIES.map((category) => ({
|
||||
slug: category.slug,
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
visible: category.visible,
|
||||
count: countByCategory.get(category.slug) ?? 0,
|
||||
})),
|
||||
products,
|
||||
};
|
||||
}
|
||||
|
||||
const catalog = buildCatalog();
|
||||
const pages = CONTENT_PAGES.map(({ slug, title, body }) => ({ slug, title, body }));
|
||||
|
||||
function sendJson(res: import('node:http').ServerResponse, status: number, body: unknown): void {
|
||||
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
||||
|
|
@ -141,6 +237,11 @@ const server = createServer(async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (requestPath === '/api/pages' || requestPath === '/api/pages/') {
|
||||
sendJson(res, 200, { pages });
|
||||
return;
|
||||
}
|
||||
|
||||
const relative = normalize(requestPath === '/' ? '/index.html' : requestPath).replace(/^(\.\.[/\\])+/, '');
|
||||
const filePath = join(PUBLIC_DIR, relative);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const PLACEHOLDER_CATEGORY_SLUGS = [
|
|||
'hidden-3',
|
||||
];
|
||||
|
||||
const CONTENT_PAGES = [
|
||||
export const CONTENT_PAGES = [
|
||||
{ slug: 'o-kompanii', title: 'О компании', body: 'La Rose — минская доставка цветов и подарков. Работаем ежедневно, собираем букеты вручную.' },
|
||||
{ slug: 'dostavka', title: 'Доставка', body: 'Курьер по Минску и самовывоз из салона. Бесплатно при заказе от 100 BYN, иначе 15 BYN. За МКАД — 15 BYN + 0.50 BYN/км.' },
|
||||
{ slug: 'oplata', title: 'Оплата', body: 'Оплата картой онлайн (Visa, Mastercard, МИР, Белкарт, ЕРИП) или наличными курьеру при получении.' },
|
||||
|
|
|
|||
24
src/main.js
Normal file
24
src/main.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import './style.css'
|
||||
|
||||
const projectName = "semianiakavy-la-rose"
|
||||
|
||||
document.querySelector('#app').innerHTML = `
|
||||
<main class="banner">
|
||||
<span class="pill">Dev server</span>
|
||||
<h1 id="project-name"></h1>
|
||||
<p class="lede">
|
||||
This repository had no application files yet, so this auto-generated starter
|
||||
page was created for it.
|
||||
</p>
|
||||
<ul class="facts">
|
||||
<li>Edit <code>src/main.js</code> or <code>src/style.css</code> - Vite hot-reloads the page instantly.</li>
|
||||
<li>Dependencies are installed inside the container on every start (<code>pnpm install</code>).</li>
|
||||
<li>Live URL: <code id="live-url"></code></li>
|
||||
<li>Commit and push these files to keep them in the repository.</li>
|
||||
</ul>
|
||||
<p class="hint">Starter files: <code>index.html</code>, <code>src/main.js</code>, <code>src/style.css</code></p>
|
||||
</main>
|
||||
`
|
||||
|
||||
document.getElementById('project-name').textContent = projectName
|
||||
document.getElementById('live-url').textContent = window.location.origin
|
||||
81
src/style.css
Normal file
81
src/style.css
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #f5f6f8;
|
||||
--fg: #1b1f24;
|
||||
--muted: #5b6472;
|
||||
--card: #ffffff;
|
||||
--border: #e3e6eb;
|
||||
--pill-bg: #e8eefc;
|
||||
--pill-fg: #1f4fd8;
|
||||
--code-bg: #f0f2f5;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0f1216;
|
||||
--fg: #e7eaee;
|
||||
--muted: #9aa4b2;
|
||||
--card: #171b21;
|
||||
--border: #262c35;
|
||||
--pill-bg: #1b2740;
|
||||
--pill-fg: #9db8ff;
|
||||
--code-bg: #10141a;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: min(560px, 100%);
|
||||
padding: 32px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
background: var(--card);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--pill-bg);
|
||||
color: var(--pill-fg);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.lede { margin: 0 0 20px; color: var(--muted); }
|
||||
|
||||
.facts { margin: 0; padding-left: 20px; }
|
||||
.facts li { margin-bottom: 8px; }
|
||||
|
||||
code {
|
||||
padding: 1px 5px;
|
||||
border-radius: 5px;
|
||||
background: var(--code-bg);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.hint { margin: 20px 0 0; color: var(--muted); font-size: 13px; }
|
||||
Loading…
Reference in a new issue