feat(public): Phase 6 — публичный маркетинговый сайт food-market.public на Astro

Новый пакет src/food-market.public/ — отдельный фронт для маркетинга и
самостоятельной регистрации магазинов-клиентов в SaaS Food Market.
Существующая админка food-market.web НЕ затронута.

Стек: Astro 4 + React 19 islands + Tailwind v3 (палитра идентична
food-market.web — единый бренд), TypeScript 6, content collections для
юр.документов. Static-сайт через nginx, gzip + immutable cache на assets.

Карта страниц (23):
- /                            — главная (Hero + 3 выгоды + скриншот +
                                   6 вертикалей + 6 модулей + Касса +
                                   Интеграции + 3 тарифа + соцпруф +
                                   FAQ + финальный CTA)
- /features                    — модули по сценариям
- /pricing                     — тарифы + интерактивный конструктор
                                   «Бизнес» (per-unit: 10000 база +
                                   2000/магазин + 500/касса + 500/склад,
                                   слайдеры, передача params в /signup)
- /pos                         — УТП лендинг кассы для Windows + весов
- /migration-from-other-system     — УТП лендинг миграции с сторонняя система
                                   (сравнительная таблица + 3 шага)
- /integrations                — список интеграций
- /for-grocery|pharmacy|cafe|alcohol|clothing|household — 6 вертикалей
                                   с уникальными фишками (весовой,
                                   серии/сроки, модификаторы и комбо,
                                   акцизные марки, размерные сетки,
                                   гарантийные сроки)
- /signup                      — регистрация (React-island форма)
- /about /contacts /kb /blog /status /changelog — компания + ресурсы
- /legal/{offer,privacy,consent,requisites} — реальные юр.документы
                                   из /tmp/legal/ как Astro content
                                   collection (markdown с frontmatter,
                                   динамический [slug].astro template,
                                   720px max-width, line-height 1.7,
                                   prose-legal стили)
- /sitemap.xml                 — ручной генератор (sitemap-плагин
                                   конфликтует с Astro 4.16, заменён
                                   на простой APIRoute)

React-острова (3):
- BusinessTariffBuilder — слайдеры + расчёт total + ссылка на signup
- SignupForm — email/password/orgName/phone/plan + валидация + agree
- FAQ — accordion 7 вопросов

API: новый POST /api/auth/signup создаёт Organization + AppUser
(Identity Admin role) + Owner Employee + полный bootstrap через
DevDataSeeder.SeedTenantReferencesAsync (units, price-types, store,
cassa, 6 ролей). Токены НЕ выпускает — фронт сразу делает обычный
/connect/token (password grant) и получает access/refresh без
дублирования OpenIddict-логики. На signup-форме — auth-bridge:
токены передаются через URL fragment в админку
APP_URL/auth-bridge#access=...&refresh=...&welcome=1, AuthBridgePage
кладёт в localStorage и редиректит на /?welcome=signup.

URL-домены через env-переменные (юзер ещё выбирает финальный):
- PUBLIC_SITE_URL — canonical/OG/sitemap (default https://food-market.kz)
- PUBLIC_APP_URL — admin/API endpoint (default https://food-market.zat.kz)
Nginx-конфиг для деплоя сайта — заготовка-template в
deploy/nginx/food-market-public.conf.template, не применён —
ждёт решения по домену.

Dockerfile multi-stage (node:20-alpine build → nginx:1.27 runtime),
build-args PUBLIC_SITE_URL/PUBLIC_APP_URL, deploy/nginx.conf gzip +
immutable cache + try_files для pretty URLs.

SEO: OG-теги, twitter-card, canonical, JSON-LD SoftwareApplication
схема, robots.txt → sitemap-index, lang=ru-KZ.

Admin-side: /auth-bridge route в food-market.web — принимает токены
из URL fragment, кладёт в localStorage (fm.access_token / fm.refresh_token),
редиректит на /. Fragment чтобы access_token не попадал в Referer.

23 страницы билдятся без ошибок. Контейнер собирается. Деплой на
конкретный домен — отдельным шагом после решения юзера.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-26 19:11:27 +05:00
parent fc3f63c49a
commit ad09d48a89
52 changed files with 6455 additions and 0 deletions

3
.gitignore vendored
View file

@ -90,3 +90,6 @@ postgres-data/
## Claude Code personal settings
.claude/settings.local.json
src/food-market.public/.astro/
src/food-market.public/dist/
src/food-market.public/node_modules/

View file

@ -0,0 +1,32 @@
# Шаблон nginx-конфига для публичного сайта food-market.public.
# НЕ ПРИМЕНЯТЬ ПОКА ЮЗЕР НЕ ВЫБЕРЕТ ДОМЕН.
#
# Сборка контейнера: docker compose --build food-market-public (см.
# deploy/docker-compose.yml; контейнер слушает на 127.0.0.1:8082).
#
# Использование (когда домен решится):
# 1. Заменить SERVER_NAME ниже на финальный домен.
# 2. Скопировать в /etc/nginx/conf.d/food-market-public.conf.
# 3. sudo certbot --nginx -d <SERVER_NAME>.
# 4. sudo nginx -t && sudo systemctl reload nginx.
#
# Архитектура после переезда (план):
# <PUBLIC_DOMAIN> → этот блок (публичный Astro)
# app.<PUBLIC_DOMAIN> → существующий блок food-market-stage.conf (админка)
# API остаётся на app.* под /api/*.
server {
server_name SERVER_NAME;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / {
proxy_pass http://127.0.0.1:8082;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 80;
}

View file

@ -0,0 +1,96 @@
using foodmarket.Api.Seed;
using foodmarket.Domain.Organizations;
using foodmarket.Infrastructure.Identity;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace foodmarket.Api.Controllers;
/// <summary>Самообслуживание: регистрация новой организации с публичного
/// маркетингового сайта. Создаёт Organization + bootstrap (Stores, Roles,
/// Units, PriceTypes, Cassa) + первого Owner-Employee-AppUser-Admin.
///
/// Токены НЕ выпускаются здесь — фронт получает их обычным запросом
/// /connect/token (password grant) сразу после успешного signup. Это
/// убирает дублирование с OpenIddict и упрощает контракт. Phase 6: без
/// email-верификации (переедет в Phase 7).</summary>
[ApiController]
[Route("api/auth")]
public class AuthSignupController : ControllerBase
{
private readonly AppDbContext _db;
private readonly UserManager<User> _userMgr;
public AuthSignupController(AppDbContext db, UserManager<User> userMgr)
{
_db = db; _userMgr = userMgr;
}
public record SignupInput(string Email, string Password, string OrganizationName, string? Phone, string? Plan);
public record SignupResult(Guid OrganizationId, string Email);
[HttpPost("signup")]
public async Task<ActionResult<SignupResult>> Signup([FromBody] SignupInput input, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(input.Email) || string.IsNullOrWhiteSpace(input.Password)
|| string.IsNullOrWhiteSpace(input.OrganizationName))
return BadRequest(new { error = "Email, пароль и название обязательны." });
if (input.Password.Length < 8)
return BadRequest(new { error = "Пароль минимум 8 символов." });
var existing = await _userMgr.FindByEmailAsync(input.Email);
if (existing is not null)
return BadRequest(new { error = "Пользователь с таким email уже зарегистрирован." });
// 1. Organization + полный bootstrap tenant-сущностей.
var kzt = await _db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct);
var org = new Organization
{
Name = input.OrganizationName.Trim(),
CountryCode = "KZ",
DefaultCurrencyId = kzt?.Id,
Phone = string.IsNullOrWhiteSpace(input.Phone) ? null : input.Phone.Trim(),
Email = input.Email.Trim(),
};
_db.Organizations.Add(org);
await _db.SaveChangesAsync(ct);
await DevDataSeeder.SeedTenantReferencesAsync(_db, org.Id, ct);
// 2. AppUser в роли Identity Admin, привязан к этой организации.
var user = new User
{
UserName = input.Email.Trim(),
Email = input.Email.Trim(),
EmailConfirmed = true,
FullName = input.OrganizationName.Trim(),
OrganizationId = org.Id,
IsActive = true,
};
var ur = await _userMgr.CreateAsync(user, input.Password);
if (!ur.Succeeded)
{
// Откат: убираем органзацию чтобы не оставить orphan.
_db.Organizations.Remove(org);
await _db.SaveChangesAsync(ct);
return BadRequest(new { error = string.Join("; ", ur.Errors.Select(e => e.Description)) });
}
await _userMgr.AddToRoleAsync(user, "Admin");
// 3. Owner Employee с системной ролью «Администратор».
var adminRole = await _db.EmployeeRoles.IgnoreQueryFilters()
.FirstAsync(r => r.OrganizationId == org.Id && r.IsSystem && r.Name == "Администратор", ct);
_db.Employees.Add(new Employee
{
OrganizationId = org.Id, UserId = user.Id,
LastName = input.OrganizationName.Trim(), FirstName = "Owner",
Position = "Владелец", Email = input.Email.Trim(),
RoleId = adminRole.Id, IsActive = true,
});
org.AccountOwnerUserId = user.Id;
await _db.SaveChangesAsync(ct);
return new SignupResult(org.Id, user.Email);
}
}

View file

@ -0,0 +1,183 @@
declare module 'astro:content' {
interface RenderResult {
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}
interface Render {
'.md': Promise<RenderResult>;
}
export interface RenderedContent {
html: string;
metadata?: {
imagePaths: Array<string>;
[key: string]: unknown;
};
}
}
declare module 'astro:content' {
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
/** @deprecated Use `getEntry` instead. */
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
/** @deprecated Use `getEntry` instead. */
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E,
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(entry: {
collection: C;
slug: E;
}): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(entry: {
collection: C;
id: E;
}): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E,
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: {
collection: C;
slug: ValidContentEntrySlug<C>;
}[],
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: {
collection: C;
id: keyof DataEntryMap[C];
}[],
): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof AnyEntryMap>(
entry: AnyEntryMap[C][string],
): Promise<RenderResult>;
export function reference<C extends keyof AnyEntryMap>(
collection: C,
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? {
collection: C;
slug: ValidContentEntrySlug<C>;
}
: {
collection: C;
id: keyof DataEntryMap[C];
}
>;
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
export function reference<C extends string>(
collection: C,
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
"legal": {
"consent.md": {
id: "consent.md";
slug: "consent";
body: string;
collection: "legal";
data: InferEntrySchema<"legal">
} & { render(): Render[".md"] };
"offer.md": {
id: "offer.md";
slug: "offer";
body: string;
collection: "legal";
data: InferEntrySchema<"legal">
} & { render(): Render[".md"] };
"privacy.md": {
id: "privacy.md";
slug: "privacy";
body: string;
collection: "legal";
data: InferEntrySchema<"legal">
} & { render(): Render[".md"] };
"requisites.md": {
id: "requisites.md";
slug: "requisites";
body: string;
collection: "legal";
data: InferEntrySchema<"legal">
} & { render(): Render[".md"] };
};
};
type DataEntryMap = {
};
type AnyEntryMap = ContentEntryMap & DataEntryMap;
export type ContentConfig = typeof import("../../src/content/config.js");
}

View file

@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="astro/content.d.ts" />

View file

@ -0,0 +1,5 @@
# Публичный URL самого сайта (canonical, OG, sitemap)
PUBLIC_SITE_URL=https://food-market.kz
# Админка / API — куда уходит юзер после signup, и куда шлются POST /api/auth/signup
PUBLIC_APP_URL=https://food-market.zat.kz

View file

@ -0,0 +1,22 @@
# Multi-stage build для Astro static site.
# Build envs:
# PUBLIC_SITE_URL — публичный URL сайта (canonical/OG/sitemap), default https://food-market.kz
# PUBLIC_APP_URL — admin/API endpoint (для Header «Войти» и signup-формы)
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json ./
# pnpm 10 — как в основной web-build'е
RUN corepack enable && corepack prepare pnpm@10 --activate
COPY pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile || pnpm install
COPY . .
ARG PUBLIC_SITE_URL=https://food-market.kz
ARG PUBLIC_APP_URL=https://food-market.zat.kz
ENV PUBLIC_SITE_URL=$PUBLIC_SITE_URL
ENV PUBLIC_APP_URL=$PUBLIC_APP_URL
RUN pnpm build
FROM nginx:1.27-alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

View file

@ -0,0 +1,14 @@
import { defineConfig } from 'astro/config'
import react from '@astrojs/react'
import tailwind from '@astrojs/tailwind'
// sitemap-плагин временно отключён — конфликтует с Astro 4.16 (bug в
// astro:build:done hook'е). Добавим обратно после фикса плагина или
// заменим на ручную генерацию sitemap.xml.
export default defineConfig({
site: process.env.PUBLIC_SITE_URL || 'https://food-market.kz',
integrations: [react(), tailwind({ applyBaseStyles: false })],
output: 'static',
build: { inlineStylesheets: 'auto' },
prefetch: { prefetchAll: false, defaultStrategy: 'hover' },
})

View file

@ -0,0 +1,22 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Astro static gzip + длинный кэш для assets с хешами в имени.
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml text/plain;
gzip_min_length 1000;
location ~* ^/assets/.*\.(js|css|woff2?|svg|png|jpg|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Pretty URLs: /pricing /pricing/index.html или /pricing.html
location / {
try_files $uri $uri/ $uri.html /index.html;
}
}

View file

@ -0,0 +1,23 @@
{
"name": "food-market.public",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^4.16.18",
"@astrojs/react": "^3.6.3",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^5.1.4",
"tailwindcss": "^3.4.17",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"lucide-react": "^1.8.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#00B207"/><text x="32" y="44" font-family="Inter,system-ui,sans-serif" font-weight="900" font-size="28" fill="#fff" text-anchor="middle">FM</text></svg>

After

Width:  |  Height:  |  Size: 256 B

View file

@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://food-market.zat.kz/sitemap-index.xml

View file

@ -0,0 +1,72 @@
import { useMemo, useState } from 'react'
const BASE = 10000
const PER_SHOP = 2000
const PER_CASH = 500
const PER_WAREHOUSE = 500
interface SliderProps {
label: string; value: number; min: number; max: number; onChange: (v: number) => void; suffix?: string
}
function Slider({ label, value, min, max, onChange, suffix }: SliderProps) {
return (
<div>
<div className="flex items-baseline justify-between">
<label className="text-sm font-medium">{label}</label>
<span className="text-2xl font-bold tabular-nums">{value}{suffix && <span className="text-sm text-slate-500">{suffix}</span>}</span>
</div>
<input
type="range" min={min} max={max} value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="w-full mt-2 accent-[#00B207]"
/>
<div className="flex justify-between text-xs text-slate-400 mt-1"><span>{min}</span><span>{max}</span></div>
</div>
)
}
export default function BusinessTariffBuilder() {
const [shops, setShops] = useState(1)
const [cashes, setCashes] = useState(1)
const [warehouses, setWarehouses] = useState(1)
const [employees, setEmployees] = useState(5)
const total = useMemo(() => {
return BASE
+ Math.max(0, shops - 1) * PER_SHOP
+ Math.max(0, cashes - 1) * PER_CASH
+ Math.max(0, warehouses - 1) * PER_WAREHOUSE
}, [shops, cashes, warehouses])
const fmt = new Intl.NumberFormat('ru')
const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}`
return (
<div className="rounded-xl border-2 border-brand bg-white p-6">
<div className="flex items-baseline justify-between mb-6">
<div>
<h3 class="text-lg font-bold">Конструктор «Бизнес»</h3>
<p className="text-xs text-slate-500 mt-0.5">База 10 000 + ваши параметры</p>
</div>
<div className="text-right">
<div className="text-3xl font-extrabold">{fmt.format(total)} </div>
<div className="text-xs text-slate-500">в месяц</div>
</div>
</div>
<div className="space-y-5">
<Slider label="Магазинов (точек продаж)" value={shops} min={1} max={3} onChange={setShops} />
<Slider label="Касс" value={cashes} min={1} max={5} onChange={setCashes} />
<Slider label="Складов" value={warehouses} min={1} max={5} onChange={setWarehouses} />
<Slider label="Сотрудников" value={employees} min={5} max={10} onChange={setEmployees} />
</div>
<div className="mt-6 text-xs text-slate-500 grid grid-cols-3 gap-2">
<div>+{fmt.format(PER_SHOP)} / магазин</div>
<div>+{fmt.format(PER_CASH)} / касса</div>
<div>+{fmt.format(PER_WAREHOUSE)} / склад</div>
</div>
<a href={signupHref} className="block text-center mt-6 px-4 py-3 bg-brand text-white rounded-md font-semibold hover:bg-[#009305]">
Выбрать конфигурацию · {fmt.format(total)} /мес
</a>
</div>
)
}

View file

@ -0,0 +1,33 @@
import { useState } from 'react'
const ITEMS = [
{ q: 'Что такое Food Market?', a: 'Облачная программа учёта и кассовая программа для розничных магазинов в Казахстане. Включает товарный учёт, склад, кассу с поддержкой весов, импорт из МойСклада и интеграции с банками и ОФД РК.' },
{ q: 'Чем отличаетесь от МойСклад / UMag?', a: 'Касса с поддержкой весов Масса-К из коробки, импорт из МойСклада за 1 клик, единый тариф без скрытых доплат за CRM/финансы/лояльность, цены в тенге, локальная поддержка KZ.' },
{ q: 'Нужно ли покупать железо?', a: 'Касса работает на любом Windows-компьютере. Весы Масса-К подключаются по USB или COM-порту. Сканер штрихкодов и чековый принтер можно купить за 30-50 тыс. тг отдельно.' },
{ q: 'Как импортировать товары из МойСклад?', a: 'Подключаем ваш токен МойСклад API в настройках, нажимаете «Импортировать» — товары, группы, штрихкоды и остатки переносятся автоматически за 510 минут.' },
{ q: 'Можно ли отменить подписку?', a: 'Да, в любой момент. Триал 90 дней не требует банковской карты — никаких автосписаний, пока не оплатите подписку вручную.' },
{ q: 'Есть ли мобильное приложение?', a: 'Веб-админка адаптирована под мобильные браузеры. Нативное приложение для iOS/Android — в дорожной карте на 2026.' },
{ q: 'Где вы хранитe данные?', a: 'Данные хранятся в дата-центре в Казахстане. Резервные копии шифруются и хранятся отдельно. Соответствие ЗРК «О персональных данных».' },
]
export default function FAQ() {
const [open, setOpen] = useState<number | null>(0)
return (
<ul className="divide-y divide-slate-200 border border-slate-200 rounded-xl bg-white">
{ITEMS.map((it, i) => (
<li key={i}>
<button
onClick={() => setOpen(open === i ? null : i)}
className="w-full flex items-center justify-between gap-3 px-5 py-4 text-left hover:bg-slate-50"
>
<span className="font-medium">{it.q}</span>
<span className={`text-slate-400 transition ${open === i ? 'rotate-45' : ''}`}>+</span>
</button>
{open === i && (
<div className="px-5 pb-4 text-sm text-slate-600">{it.a}</div>
)}
</li>
))}
</ul>
)
}

View file

@ -0,0 +1,58 @@
---
import Logo from './Logo.astro'
---
<footer class="bg-slate-50 border-t border-slate-200 mt-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-10 grid grid-cols-2 md:grid-cols-5 gap-6 text-sm">
<div class="col-span-2 md:col-span-1">
<Logo />
<p class="text-slate-500 mt-3 text-xs">
Программа учёта и касса для розничных магазинов в Казахстане.
</p>
</div>
<div>
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Продукт</h3>
<ul class="space-y-1.5">
<li><a href="/features" class="text-slate-700 hover:text-brand">Возможности</a></li>
<li><a href="/pos" class="text-slate-700 hover:text-brand">Касса для Windows</a></li>
<li><a href="/migration-from-moysklad" class="text-slate-700 hover:text-brand">Импорт из МойСклад</a></li>
<li><a href="/integrations" class="text-slate-700 hover:text-brand">Интеграции</a></li>
<li><a href="/pricing" class="text-slate-700 hover:text-brand">Тарифы</a></li>
<li><a href="/changelog" class="text-slate-700 hover:text-brand">Дорожная карта</a></li>
</ul>
</div>
<div>
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Для кого</h3>
<ul class="space-y-1.5">
<li><a href="/for-grocery" class="text-slate-700 hover:text-brand">Продуктовый магазин</a></li>
<li><a href="/for-pharmacy" class="text-slate-700 hover:text-brand">Аптека</a></li>
<li><a href="/for-cafe" class="text-slate-700 hover:text-brand">Кафе и общепит</a></li>
<li><a href="/for-alcohol" class="text-slate-700 hover:text-brand">Алкоголь</a></li>
<li><a href="/for-clothing" class="text-slate-700 hover:text-brand">Одежда</a></li>
<li><a href="/for-household" class="text-slate-700 hover:text-brand">Дом и быт</a></li>
</ul>
</div>
<div>
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Ресурсы</h3>
<ul class="space-y-1.5">
<li><a href="/kb" class="text-slate-700 hover:text-brand">База знаний</a></li>
<li><a href="/blog" class="text-slate-700 hover:text-brand">Блог</a></li>
<li><a href="/status" class="text-slate-700 hover:text-brand">Status</a></li>
<li><a href="/changelog" class="text-slate-700 hover:text-brand">Changelog</a></li>
</ul>
</div>
<div>
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Компания</h3>
<ul class="space-y-1.5">
<li><a href="/about" class="text-slate-700 hover:text-brand">О нас</a></li>
<li><a href="/contacts" class="text-slate-700 hover:text-brand">Контакты</a></li>
<li><a href="/legal/offer" class="text-slate-700 hover:text-brand">Оферта</a></li>
<li><a href="/legal/privacy" class="text-slate-700 hover:text-brand">Политика ПДн</a></li>
<li><a href="/legal/consent" class="text-slate-700 hover:text-brand">Согласие ПДн</a></li>
<li><a href="/legal/requisites" class="text-slate-700 hover:text-brand">Реквизиты</a></li>
</ul>
</div>
</div>
<div class="border-t border-slate-200 py-4 text-center text-xs text-slate-400">
© {new Date().getFullYear()} Food Market. Все права защищены.
</div>
</footer>

View file

@ -0,0 +1,34 @@
---
import Logo from './Logo.astro'
const APP_URL = import.meta.env.PUBLIC_APP_URL || 'https://food-market.zat.kz'
---
<header class="sticky top-0 z-40 bg-white/95 backdrop-blur border-b border-slate-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
<a href="/" class="flex items-center gap-2"><Logo /></a>
<nav class="hidden lg:flex items-center gap-1 text-sm">
<a href="/features" class="px-3 py-2 hover:text-brand transition">Возможности</a>
<a href="/pos" class="px-3 py-2 hover:text-brand transition">Касса</a>
<div class="relative group">
<button class="px-3 py-2 hover:text-brand transition">Для кого ▾</button>
<div class="absolute hidden group-hover:block top-full left-0 w-56 bg-white border border-slate-200 rounded-lg shadow-lg p-1.5">
<a href="/for-grocery" class="block px-3 py-1.5 rounded hover:bg-slate-50 text-sm">Продуктовый магазин</a>
<a href="/for-pharmacy" class="block px-3 py-1.5 rounded hover:bg-slate-50 text-sm">Аптека</a>
<a href="/for-cafe" class="block px-3 py-1.5 rounded hover:bg-slate-50 text-sm">Кафе и общепит</a>
<a href="/for-alcohol" class="block px-3 py-1.5 rounded hover:bg-slate-50 text-sm">Алкоголь</a>
<a href="/for-clothing" class="block px-3 py-1.5 rounded hover:bg-slate-50 text-sm">Одежда</a>
<a href="/for-household" class="block px-3 py-1.5 rounded hover:bg-slate-50 text-sm">Дом и быт</a>
</div>
</div>
<a href="/integrations" class="px-3 py-2 hover:text-brand transition">Интеграции</a>
<a href="/pricing" class="px-3 py-2 hover:text-brand transition">Тарифы</a>
<a href="/kb" class="px-3 py-2 hover:text-brand transition">База знаний</a>
<a href="/blog" class="px-3 py-2 hover:text-brand transition">Блог</a>
</nav>
<div class="flex items-center gap-2">
<a href={`${APP_URL}/login`} class="px-3 py-1.5 text-sm text-slate-700 hover:text-brand">Войти</a>
<a href="/signup" class="px-3 py-1.5 text-sm font-medium bg-brand text-white rounded-md hover:bg-brand-hover">Начать бесплатно</a>
</div>
</div>
</header>

View file

@ -0,0 +1,9 @@
---
interface Props { variant?: 'light' | 'dark' }
const { variant = 'light' } = Astro.props
const isDark = variant === 'dark'
---
<div class="inline-flex flex-col leading-none select-none">
<span class={`font-black tracking-[0.08em] text-base ${isDark ? 'text-slate-50' : 'text-slate-900'}`}>FOOD</span>
<span class="font-black text-[11px] tracking-[0.24em] mt-0.5" style={`color: ${isDark ? '#34D399' : '#00B207'}`}>MARKET</span>
</div>

View file

@ -0,0 +1,119 @@
import { useState } from 'react'
// Админский / API endpoint — переключается через PUBLIC_APP_URL на этапе
// билда. Дефолт пока — текущий рабочий food-market.zat.kz.
const APP_URL = (import.meta.env.PUBLIC_APP_URL as string | undefined) ?? 'https://food-market.zat.kz'
const API_URL = APP_URL
interface Props {
defaultPlan?: string
defaultShops?: number
defaultCashes?: number
defaultWarehouses?: number
}
export default function SignupForm({ defaultPlan = 'start' }: Props) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [orgName, setOrgName] = useState('')
const [phone, setPhone] = useState('')
const [plan, setPlan] = useState(defaultPlan)
const [agree, setAgree] = useState(false)
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
const submit = async (e: React.FormEvent) => {
e.preventDefault()
if (!agree) { setError('Подтвердите согласие с офертой и политикой ПДн.'); return }
if (password.length < 8) { setError('Пароль минимум 8 символов.'); return }
setError(null); setBusy(true)
try {
// 1. Создаём организацию + Owner-Employee на /api/auth/signup.
const res = await fetch(`${API_URL}/api/auth/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, organizationName: orgName, phone: phone || null, plan }),
})
if (!res.ok) {
const j = await res.json().catch(() => ({}))
throw new Error(j.error ?? `HTTP ${res.status}`)
}
// 2. Получаем JWT через стандартный password-grant — тот же путь, что
// делает админка при обычном логине. Никакого дублирования OpenIddict.
const tokRes = await fetch(`${API_URL}/connect/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'password', username: email, password,
client_id: 'food-market-web',
scope: 'openid profile email roles api offline_access',
}),
})
if (!tokRes.ok) throw new Error('Регистрация прошла, но токен не выпущен. Войдите вручную.')
const tok = await tokRes.json() as { access_token: string; refresh_token: string }
// 3. Auth-bridge: передаём токены через URL fragment в админку.
const url = `${APP_URL}/auth-bridge#access=${encodeURIComponent(tok.access_token)}&refresh=${encodeURIComponent(tok.refresh_token)}&welcome=1`
window.location.assign(url)
} catch (e) {
setError(e instanceof Error ? e.message : 'Ошибка регистрации')
} finally { setBusy(false) }
}
return (
<form onSubmit={submit} className="space-y-4">
{error && (
<div className="p-3 rounded-md bg-red-50 text-red-700 text-sm border border-red-200">{error}</div>
)}
<Field label="Email">
<input type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full h-10 rounded-md border border-slate-300 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand" autoComplete="email" />
</Field>
<Field label="Пароль" hint="Минимум 8 символов, цифра и заглавная.">
<input type="password" required minLength={8} value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full h-10 rounded-md border border-slate-300 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand" autoComplete="new-password" />
</Field>
<Field label="Название магазина">
<input type="text" required value={orgName} onChange={(e) => setOrgName(e.target.value)} placeholder="ИП Иванов / Магазин у дома"
className="w-full h-10 rounded-md border border-slate-300 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand" />
</Field>
<Field label="Телефон (необязательно)">
<input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="+7 ..."
className="w-full h-10 rounded-md border border-slate-300 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand" />
</Field>
<div>
<div className="text-sm font-medium mb-2">Тариф</div>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'start', name: 'Старт', price: '5 000 ₸' },
{ id: 'business', name: 'Бизнес', price: 'от 10 000 ₸' },
{ id: 'network', name: 'Сеть', price: 'по запросу' },
].map((p) => (
<label key={p.id} className={`p-2.5 rounded-md border cursor-pointer text-center text-sm ${plan === p.id ? 'border-brand bg-brand/5' : 'border-slate-200'}`}>
<input type="radio" name="plan" className="hidden" checked={plan === p.id} onChange={() => setPlan(p.id)} />
<div className="font-semibold">{p.name}</div>
<div className="text-xs text-slate-500">{p.price}</div>
</label>
))}
</div>
</div>
<label className="flex items-start gap-2 text-sm">
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} className="mt-1" />
<span>Я согласен с <a href="/legal/offer" className="text-brand underline">офертой</a> и <a href="/legal/privacy" className="text-brand underline">политикой обработки ПДн</a>.</span>
</label>
<button type="submit" disabled={busy} className="w-full px-4 py-3 bg-brand text-white font-semibold rounded-md hover:bg-[#009305] disabled:opacity-60">
{busy ? 'Регистрация…' : 'Начать бесплатно (90 дней)'}
</button>
<p className="text-xs text-center text-slate-500">Уже есть аккаунт? <a href={`${APP_URL}/login`} className="text-brand underline">Войти</a></p>
</form>
)
}
function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
return (
<label className="block">
<span className="text-sm font-medium block mb-1.5">{label}</span>
{children}
{hint && <span className="text-xs text-slate-500 block mt-1">{hint}</span>}
</label>
)
}

View file

@ -0,0 +1,13 @@
import { defineCollection, z } from 'astro:content'
const legal = defineCollection({
type: 'content',
// Astro резервирует поле `slug` — если оно есть в frontmatter, оно
// используется как slug страницы, но в схеме его описывать нельзя.
schema: z.object({
title: z.string(),
last_updated: z.coerce.date(),
}),
})
export const collections = { legal }

View file

@ -0,0 +1,113 @@
---
title: Согласие на сбор и обработку персональных данных
slug: consent
last_updated: 2026-04-26
---
# Согласие на сбор и обработку персональных данных
> **Внимание:** Настоящий документ является типовой формой согласия и подлежит правовой экспертизе перед публичным использованием.
**Дата публикации:** 26.04.2026
---
Настоящим я, как субъект персональных данных, регистрируясь в сервисе **Food Market** (далее — «**Сервис**»), размещённом на сайте https://food-market.kz, и проставляя отметку в чекбоксе «Согласен с офертой и политикой обработки персональных данных» при создании учётной записи, в соответствии со статьёй 8 Закона Республики Казахстан от 21 мая 2013 года № 94-V «О персональных данных и их защите», свободно, своей волей и в своём интересе даю своё согласие **ТОО «[НАЗВАНИЕ ТОО]»**, БИН [БИН], юридический адрес: [Юр.адрес] (далее — «**Оператор**»), на сбор, обработку, хранение, использование, обезличивание и уничтожение моих персональных данных в нижеуказанном объёме и для нижеуказанных целей.
## 1. Перечень персональных данных, на обработку которых даётся согласие
- адрес электронной почты;
- фамилия, имя, отчество (при указании);
- контактный телефон (при указании);
- должность (при указании);
- наименование организации (юридического лица или индивидуального предпринимателя);
- идентификатор учётной записи в Сервисе;
- пароль (хранится в виде криптографического хеша);
- IP-адрес устройства, с которого осуществляется доступ к Сервису;
- технические данные устройства и браузера: тип браузера, версия, операционная система, разрешение экрана;
- данные о действиях в Сервисе (журналы операций) — для целей обеспечения безопасности и аудита;
- иные данные, предоставленные мной добровольно при использовании Сервиса.
## 2. Цели обработки персональных данных
Согласие даётся на обработку персональных данных в следующих целях:
2.1. Регистрация и идентификация в Сервисе.
2.2. Предоставление функциональных возможностей Сервиса в соответствии с выбранным тарифом.
2.3. Исполнение обязательств Оператора по Публичной оферте на использование Сервиса.
2.4. Обеспечение технической поддержки и связи с пользователем.
2.5. Информирование о новых функциях, обновлениях и условиях использования Сервиса (сервисные уведомления).
2.6. Обеспечение безопасности учётной записи и защита от несанкционированного доступа, мошенничества, злоупотреблений.
2.7. Расчёт стоимости услуг, выставление счетов, проведение платежей.
2.8. Подготовка финансовой и налоговой отчётности в соответствии с законодательством Республики Казахстан.
2.9. Анализ качества работы Сервиса, улучшение пользовательского опыта (на основе обезличенных данных).
2.10. Соблюдение требований законодательства Республики Казахстан.
## 3. Действия с персональными данными
Согласие даётся на совершение следующих действий с персональными данными:
- сбор;
- запись;
- систематизация;
- накопление;
- хранение;
- уточнение (обновление, изменение);
- извлечение;
- использование;
- передача (предоставление, доступ) — лицам, указанным в Политике обработки персональных данных Оператора;
- обезличивание;
- блокирование;
- удаление;
- уничтожение.
Обработка персональных данных осуществляется как с использованием средств автоматизации, так и без них.
## 4. Передача персональных данных третьим лицам
Я согласен с тем, что для целей предоставления Сервиса Оператор вправе передавать мои персональные данные третьим лицам — поставщикам услуг, обеспечивающим функционирование Сервиса (хостинг, платёжные системы, провайдеры email/SMS-уведомлений, провайдеры аналитики), при условии обеспечения адекватного уровня защиты данных.
Передача персональных данных в иностранные государства осуществляется только при условии обеспечения адекватного уровня защиты прав субъектов персональных данных в соответствии со статьёй 16 Закона о ПДн.
## 5. Срок действия согласия
5.1. Настоящее согласие действует с момента его предоставления и до момента отзыва субъектом персональных данных или до момента прекращения деятельности Оператора.
5.2. Я уведомлён, что вправе отозвать согласие на обработку персональных данных в любой момент путём направления соответствующего письменного уведомления Оператору по адресу электронной почты [privacy@food-market.kz] или удаления учётной записи в Сервисе.
5.3. Я уведомлён, что отзыв согласия влечёт за собой прекращение возможности использования Сервиса, поскольку обработка персональных данных является необходимым условием его предоставления.
5.4. Я уведомлён, что Оператор вправе продолжить обработку персональных данных без согласия субъекта в случаях, прямо предусмотренных Законом о ПДн (статья 9), в том числе для исполнения обязательств, предусмотренных законодательством Республики Казахстан, защиты прав и законных интересов Оператора.
## 6. Ознакомление с правами
Я подтверждаю, что ознакомлен с:
6.1. Политикой обработки персональных данных Оператора, размещённой на сайте https://food-market.kz/legal/privacy.
6.2. Своими правами, предусмотренными Законом о ПДн, в том числе правом:
- получать информацию об обработке моих персональных данных;
- требовать уточнения, блокирования или уничтожения моих персональных данных;
- отзывать настоящее согласие;
- обжаловать действия Оператора в уполномоченный государственный орган или в суд.
## 7. Подтверждение согласия
Подтверждение настоящего согласия осуществляется в электронной форме путём проставления отметки в чекбоксе «Согласен с офертой и политикой обработки персональных данных» при регистрации в Сервисе.
В соответствии с пунктом 4 статьи 7 Закона Республики Казахстан «Об электронном документе и электронной цифровой подписи» подтверждение согласия в электронной форме признаётся юридически равнозначным подтверждению на бумажном носителе.
Дата и время предоставления согласия фиксируются Оператором автоматически в момент регистрации с указанием IP-адреса субъекта.
---
*Настоящая форма согласия составлена в соответствии с законодательством Республики Казахстан и применяется с даты, указанной в шапке документа.*

View file

@ -0,0 +1,194 @@
---
title: Публичная оферта на использование сервиса Food Market
slug: offer
last_updated: 2026-04-26
---
# Публичная оферта на использование сервиса Food Market
> **Внимание:** Настоящий документ является типовой публичной офертой и подлежит правовой экспертизе перед публичным использованием. Финальная редакция утверждается уполномоченным лицом Лицензиара.
**Дата публикации:** 26.04.2026
**Действует с:** 26.04.2026
**Юрисдикция:** Республика Казахстан
## 1. Общие положения
1.1. Настоящий документ является публичной офертой (далее — «**Оферта**») в соответствии со статьями 395, 396 Гражданского кодекса Республики Казахстан (далее — «**ГК РК**»). Оферта адресована любому физическому или юридическому лицу (далее — «**Пользователь**»), желающему получить доступ к программно-аппаратному комплексу «Food Market» (далее — «**Сервис**»).
1.2. Лицензиаром Сервиса выступает **ТОО «[НАЗВАНИЕ ТОО]»**, БИН [БИН], зарегистрированное в соответствии с законодательством Республики Казахстан (далее — «**Лицензиар**»). Полные реквизиты Лицензиара указаны в разделе «Реквизиты» сайта.
1.3. Безусловным акцептом настоящей Оферты в соответствии с пунктом 3 статьи 396 ГК РК являются следующие действия Пользователя, совершённые в совокупности:
- регистрация учётной записи на сайте https://food-market.kz (или ином доменном имени, используемом Лицензиаром);
- проставление отметки в чекбоксе «Согласен с офертой и политикой обработки персональных данных»;
- начало использования Сервиса.
1.4. С момента акцепта между Лицензиаром и Пользователем заключается лицензионный договор присоединения на условиях, изложенных в настоящей Оферте (далее — «**Договор**»).
1.5. Лицензиар вправе в одностороннем порядке вносить изменения в Оферту с публикацией новой редакции на сайте не менее чем за 14 (четырнадцать) календарных дней до вступления изменений в силу. Продолжение использования Сервиса после вступления изменений в силу означает их акцепт Пользователем.
## 2. Термины и определения
**Сервис** — программный продукт «Food Market», представляющий собой облачную систему учёта розничной торговли с функциями управления товарами, складом, продажами, кассовыми операциями, аналитикой и сопутствующими сервисами, размещённый по адресу https://food-market.kz и связанных с ним поддоменах.
**Учётная запись** — совокупность данных Пользователя, обеспечивающих его идентификацию и авторизацию в Сервисе.
**Организация** — обособленное рабочее пространство Пользователя в Сервисе, в рамках которого ведётся учёт деятельности одного или нескольких розничных магазинов.
**Тариф** — план обслуживания, определяющий объём предоставляемых функций, лимиты использования и стоимость.
**Подписка** — оплаченный период использования Сервиса в рамках выбранного Тарифа.
**Триал** — пробный бесплатный период использования Сервиса продолжительностью 90 (девяносто) календарных дней, предоставляемый Пользователю при первичной регистрации без обязательства оплаты.
**Контент Пользователя** — любые данные, загруженные Пользователем в Сервис: справочники товаров, контрагентов, операции, документы и иная информация.
## 3. Предмет договора
3.1. Лицензиар предоставляет Пользователю на условиях простой (неисключительной) лицензии право использования Сервиса в объёме функциональности, предусмотренной выбранным Тарифом, в течение оплаченного срока Подписки или периода Триала.
3.2. Сервис предоставляется по модели SaaS (Software as a Service): Пользователь получает доступ к Сервису посредством сети Интернет; экземпляр программного обеспечения не передаётся Пользователю.
3.3. Право использования Сервиса предоставляется на территории Республики Казахстан и иных государств, в которых Сервис технически доступен и не запрещён к использованию законодательством соответствующего государства.
## 4. Регистрация и учётная запись
4.1. Для использования Сервиса Пользователь обязан создать Учётную запись, предоставив достоверные данные: адрес электронной почты, наименование Организации, контактный телефон (при необходимости), а также установить пароль.
4.2. Пользователь обязуется:
- использовать только достоверные данные при регистрации;
- не передавать данные Учётной записи третьим лицам;
- незамедлительно уведомлять Лицензиара о любом несанкционированном доступе к Учётной записи;
- актуализировать данные Учётной записи при их изменении.
4.3. Лицензиар не несёт ответственности за действия третьих лиц, получивших доступ к Учётной записи Пользователя по причине нарушения Пользователем обязанностей, установленных пунктом 4.2 настоящей Оферты.
## 5. Триал
5.1. При первичной регистрации Учётной записи Пользователю автоматически предоставляется бесплатный пробный период использования Сервиса продолжительностью 90 (девяносто) календарных дней.
5.2. В течение Триала Пользователь имеет доступ ко всем функциям выбранного Тарифа без ограничений по объёму, за исключением функций, явно отмеченных как недоступные в Триале.
5.3. По истечении Триала доступ к функциям Сервиса ограничивается до момента активации платной Подписки. Контент Пользователя сохраняется в течение 30 (тридцати) календарных дней после окончания Триала; по истечении указанного срока Лицензиар вправе удалить Контент Пользователя без дополнительного уведомления.
5.4. Триал предоставляется без обязательства Пользователя по оплате; реквизиты платёжной карты при активации Триала не требуются.
## 6. Тарифы и порядок оплаты
6.1. Стоимость использования Сервиса определяется выбранным Пользователем Тарифом. Актуальная сетка Тарифов и их состав публикуются на сайте Лицензиара по адресу https://food-market.kz/pricing.
6.2. Оплата Подписки производится авансом за выбранный период (1, 3, 6 или 12 месяцев) посредством платёжных систем, указанных на сайте Лицензиара.
6.3. Все платежи осуществляются в национальной валюте Республики Казахстан — тенге (KZT). Стоимость указана с учётом всех применимых налогов в соответствии с законодательством Республики Казахстан.
6.4. Лицензиар вправе изменять Тарифы с уведомлением Пользователя не менее чем за 30 (тридцать) календарных дней до вступления изменений в силу. Изменения не распространяются на ранее оплаченные Подписки до окончания их срока действия.
6.5. Возврат денежных средств за неиспользованный период Подписки осуществляется в случаях, предусмотренных законодательством Республики Казахстан, по письменному заявлению Пользователя на основании пункта 1 статьи 401 ГК РК.
6.6. В случае непоступления оплаты в течение 7 (семи) календарных дней с даты окончания текущего оплаченного периода Подписки доступ к Сервису ограничивается. Контент Пользователя сохраняется в течение срока, установленного системными настройками Лицензиара (но не менее 30 календарных дней), после чего может быть удалён без дополнительного уведомления.
## 7. Права и обязанности сторон
### 7.1. Лицензиар обязуется:
7.1.1. Обеспечивать круглосуточный доступ Пользователя к Сервису, за исключением периодов проведения регламентных и аварийных работ.
7.1.2. Предпринимать разумные технические и организационные меры для обеспечения сохранности Контента Пользователя и защиты от несанкционированного доступа.
7.1.3. Оказывать техническую поддержку Пользователя по каналам, указанным на сайте Лицензиара, в режиме и в сроки, соответствующие выбранному Тарифу.
7.1.4. Обрабатывать персональные данные Пользователя в соответствии с Политикой обработки персональных данных, опубликованной на сайте Лицензиара.
### 7.2. Лицензиар вправе:
7.2.1. Проводить регламентные технические работы с предварительным уведомлением Пользователя не менее чем за 24 часа.
7.2.2. Ограничивать или приостанавливать доступ Пользователя к Сервису в случае нарушения Пользователем условий настоящей Оферты, а также при наличии обоснованных подозрений в нарушении.
7.2.3. Привлекать третьих лиц для исполнения отдельных обязательств по настоящей Оферте, неся ответственность за их действия как за свои собственные.
### 7.3. Пользователь обязуется:
7.3.1. Использовать Сервис в соответствии с условиями настоящей Оферты и законодательством Республики Казахстан.
7.3.2. Своевременно оплачивать выбранный Тариф.
7.3.3. Не использовать Сервис для:
- осуществления противоправной деятельности;
- хранения или распространения информации, нарушающей права третьих лиц или законодательство;
- попыток несанкционированного доступа к Сервису или его компонентам;
- автоматизированного сбора данных, нагружающего инфраструктуру Лицензиара;
- модификации, декомпиляции, обратной инженерии Сервиса.
7.3.4. Самостоятельно нести ответственность за достоверность, законность и точность Контента Пользователя.
### 7.4. Пользователь вправе:
7.4.1. В любой момент прекратить использование Сервиса с предварительной выгрузкой собственного Контента средствами, предоставляемыми Сервисом.
7.4.2. Запрашивать у Лицензиара информацию о хранимых персональных данных, требовать их исправления или удаления в соответствии с действующим законодательством Республики Казахстан.
## 8. Интеллектуальная собственность
8.1. Все исключительные права на Сервис, его компоненты, дизайн, программный код, документацию, торговые знаки, используемые в Сервисе, принадлежат Лицензиару или его правообладателям-партнёрам.
8.2. Пользователь не приобретает каких-либо прав на Сервис, кроме права использования в объёме, предусмотренном настоящей Офертой.
8.3. Контент Пользователя является собственностью Пользователя. Лицензиар обрабатывает Контент Пользователя исключительно для целей предоставления Сервиса и не претендует на права интеллектуальной собственности на Контент Пользователя.
## 9. Ответственность сторон
9.1. Лицензиар не гарантирует абсолютной бесперебойной работы Сервиса. Допустимый уровень доступности Сервиса соответствует выбранному Тарифу и публикуется на сайте Лицензиара.
9.2. Лицензиар не несёт ответственности за:
- убытки Пользователя, возникшие вследствие неправомерных действий третьих лиц, получивших доступ к Учётной записи Пользователя;
- упущенную выгоду Пользователя;
- содержание и достоверность Контента Пользователя;
- утрату данных, не подкреплённых регулярным резервным копированием со стороны Пользователя (при наличии такой возможности в рамках Тарифа).
9.3. Совокупная ответственность Лицензиара перед Пользователем по настоящему Договору ограничена суммой, фактически уплаченной Пользователем за последний календарный месяц использования Сервиса.
9.4. Стороны освобождаются от ответственности за неисполнение обязательств вследствие обстоятельств непреодолимой силы (форс-мажор) в соответствии со статьёй 359 ГК РК.
## 10. Конфиденциальность и защита данных
10.1. Лицензиар обязуется обеспечивать конфиденциальность Контента Пользователя и не раскрывать его третьим лицам, кроме случаев, предусмотренных законодательством Республики Казахстан.
10.2. Обработка персональных данных Пользователя осуществляется в соответствии с Законом Республики Казахстан от 21 мая 2013 года № 94-V «О персональных данных и их защите» и Политикой обработки персональных данных Лицензиара.
10.3. Лицензиар применяет шифрование данных при передаче (TLS), резервное копирование, контроль доступа и иные меры защиты, соответствующие отраслевым стандартам.
## 11. Срок действия и прекращение договора
11.1. Договор вступает в силу с момента акцепта Оферты Пользователем и действует в течение всего периода использования Сервиса.
11.2. Договор может быть прекращён:
- по инициативе Пользователя — путём удаления Учётной записи в Сервисе или направления соответствующего уведомления Лицензиару;
- по инициативе Лицензиара — в случае нарушения Пользователем условий Оферты с уведомлением не менее чем за 7 календарных дней;
- по соглашению Сторон;
- в иных случаях, предусмотренных законодательством Республики Казахстан.
11.3. После прекращения Договора Контент Пользователя сохраняется в течение 30 календарных дней для возможной выгрузки, после чего удаляется без возможности восстановления.
## 12. Применимое право и порядок разрешения споров
12.1. К отношениям сторон по настоящему Договору применяется материальное право Республики Казахстан.
12.2. Все споры и разногласия, возникающие из настоящего Договора или в связи с ним, подлежат разрешению путём переговоров.
12.3. В случае невозможности разрешения спора путём переговоров в течение 30 (тридцати) календарных дней с момента предъявления претензии спор подлежит передаче на рассмотрение в специализированный межрайонный экономический суд по месту нахождения Лицензиара.
## 13. Заключительные положения
13.1. Если какое-либо положение настоящей Оферты будет признано недействительным судом, это не повлияет на действительность остальных положений.
13.2. Уведомления, направляемые Пользователю в рамках исполнения Договора, считаются доставленными:
- при отправке на адрес электронной почты, указанный в Учётной записи, — с момента отправки;
- при публикации в личном кабинете Пользователя — с момента публикации;
- при опубликовании на сайте Лицензиара — с момента опубликования.
13.3. Реквизиты Лицензиара публикуются на сайте Лицензиара в разделе «Реквизиты» и являются неотъемлемой частью настоящей Оферты.
---
*Настоящая Оферта составлена в соответствии с законодательством Республики Казахстан и применяется с даты, указанной в шапке документа.*

View file

@ -0,0 +1,230 @@
---
title: Политика обработки персональных данных
slug: privacy
last_updated: 2026-04-26
---
# Политика обработки персональных данных
> **Внимание:** Настоящий документ является типовой политикой и подлежит правовой экспертизе перед публичным использованием. Финальная редакция утверждается уполномоченным лицом Лицензиара.
**Дата публикации:** 26.04.2026
**Действует с:** 26.04.2026
## 1. Общие положения
1.1. Настоящая Политика обработки персональных данных (далее — «**Политика**») определяет порядок обработки и защиты персональных данных, осуществляемой **ТОО «[НАЗВАНИЕ ТОО]»**, БИН [БИН] (далее — «**Оператор**»), при предоставлении доступа к программно-аппаратному комплексу «Food Market» (далее — «**Сервис**»).
1.2. Политика разработана в соответствии с:
- Конституцией Республики Казахстан;
- Законом Республики Казахстан от 21 мая 2013 года № 94-V «О персональных данных и их защите» (далее — «**Закон о ПДн**»);
- Гражданским кодексом Республики Казахстан;
- Законом Республики Казахстан «Об электронном документе и электронной цифровой подписи»;
- иными нормативными правовыми актами Республики Казахстан.
1.3. Настоящая Политика применяется ко всем персональным данным, обрабатываемым Оператором с использованием средств автоматизации и без таковых.
1.4. Действующая редакция Политики постоянно доступна на сайте Оператора по адресу https://food-market.kz/legal/privacy.
## 2. Термины и определения
**Персональные данные (ПДн)** — сведения, относящиеся к определённому или определяемому на их основании субъекту персональных данных, зафиксированные на электронном, бумажном и (или) ином материальном носителе.
**Субъект персональных данных** — физическое лицо, к которому относятся персональные данные.
**Оператор** — ТОО «[НАЗВАНИЕ ТОО]», осуществляющее сбор, обработку и защиту персональных данных.
**Обработка персональных данных** — действия, направленные на сбор, накопление, хранение, изменение, использование, распространение, обезличивание, блокирование и уничтожение персональных данных.
**Согласие субъекта** — свободное и осознанное волеизъявление субъекта персональных данных на обработку его персональных данных.
## 3. Принципы обработки персональных данных
Оператор осуществляет обработку персональных данных на основании следующих принципов:
3.1. Соблюдение конституционных прав и свобод человека и гражданина при обработке его ПДн.
3.2. Законность, целесообразность, открытость и прозрачность процесса обработки ПДн.
3.3. Обработка ПДн только для конкретных, заранее определённых и законных целей.
3.4. Соответствие содержания и объёма обрабатываемых ПДн заявленным целям обработки.
3.5. Обеспечение точности, достаточности, актуальности и достоверности ПДн.
3.6. Хранение ПДн не дольше, чем этого требуют цели обработки.
3.7. Обеспечение конфиденциальности и защиты ПДн.
## 4. Категории субъектов и состав обрабатываемых данных
### 4.1. Пользователи Сервиса
**Цель обработки:** регистрация и идентификация в Сервисе, предоставление функций Сервиса, выполнение договорных обязательств, оказание технической поддержки.
**Состав данных:**
- адрес электронной почты;
- имя, фамилия, отчество (при указании);
- контактный телефон (при указании);
- должность в организации (при указании);
- наименование организации (юридического лица или индивидуального предпринимателя);
- идентификатор учётной записи (ID), пароль (в зашифрованном виде);
- IP-адреса, данные о браузере, операционной системе, устройстве, дата и время использования Сервиса;
- информация об активности в Сервисе (логи действий) — для целей безопасности и аудита.
**Правовое основание:** согласие субъекта (статья 8 Закона о ПДн), исполнение договора, стороной которого является субъект (статья 9 Закона о ПДн).
### 4.2. Сотрудники Пользователей Сервиса
**Цель обработки:** обеспечение функционирования учётных записей сотрудников в рамках Организации Пользователя.
**Состав данных:**
- адрес электронной почты;
- имя, фамилия, отчество;
- должность;
- роль в Организации;
- идентификатор учётной записи, пароль (в зашифрованном виде);
- логи действий в Сервисе — для целей аудита и безопасности.
**Правовое основание:** согласие субъекта (через согласие, оформляемое работодателем — Пользователем Сервиса, выступающим Оператором по отношению к собственным сотрудникам).
### 4.3. Контрагенты, клиенты Пользователей Сервиса
При использовании функционала ведения базы клиентов и контрагентов Пользователь самостоятельно загружает в Сервис персональные данные третьих лиц.
В этом случае Пользователь Сервиса является Оператором персональных данных в отношении таких лиц и несёт самостоятельную ответственность за получение их согласия и соблюдение Закона о ПДн. Оператор Сервиса (ТОО) выступает в роли Оператора Обработчика, действующего по поручению Пользователя.
### 4.4. Лица, обращающиеся к Оператору через формы обратной связи
**Цель обработки:** ответ на обращение, обработка заявок на консультацию, маркетинговые рассылки (при наличии отдельного согласия).
**Состав данных:**
- имя;
- адрес электронной почты;
- телефон;
- содержание сообщения.
**Правовое основание:** согласие субъекта.
## 5. Источники персональных данных
5.1. Персональные данные предоставляются субъектами ПДн непосредственно Оператору при:
- регистрации в Сервисе;
- использовании функционала Сервиса;
- заполнении форм обратной связи на сайте;
- направлении обращений по электронной почте, телефону, в мессенджерах.
5.2. Оператор не получает персональные данные субъектов из иных источников, кроме случаев, прямо предусмотренных законодательством Республики Казахстан.
## 6. Сроки хранения персональных данных
6.1. Персональные данные обрабатываются Оператором в течение срока, необходимого для достижения целей обработки, а также в течение сроков, установленных законодательством Республики Казахстан.
6.2. Персональные данные Пользователей хранятся в течение всего срока действия учётной записи, а после прекращения договора (отказа от использования Сервиса) — в течение 30 (тридцати) календарных дней для возможной выгрузки и восстановления, после чего уничтожаются.
6.3. Логи действий и иные служебные данные, необходимые для обеспечения безопасности, хранятся в течение 1 (одного) года.
6.4. Финансовая и бухгалтерская документация, содержащая ПДн, хранится в течение сроков, установленных Налоговым кодексом Республики Казахстан и законодательством о бухгалтерском учёте.
## 7. Передача персональных данных третьим лицам
7.1. Оператор не продаёт, не сдаёт в аренду и иным образом не передаёт персональные данные третьим лицам в коммерческих целях.
7.2. Оператор передаёт персональные данные третьим лицам только в следующих случаях:
**7.2.1. Поставщикам услуг,** обеспечивающим функционирование Сервиса:
- хостинг-провайдеры (с заключением соглашения о защите данных);
- провайдеры платёжных систем (для обработки платежей);
- провайдеры электронной почты и SMS-уведомлений;
- провайдеры аналитики и мониторинга качества Сервиса.
При передаче данных подрядчикам Оператор требует от них соблюдения уровня защиты, не ниже установленного законодательством Республики Казахстан.
**7.2.2. По требованию государственных органов** Республики Казахстан в случаях и в порядке, предусмотренных законом.
**7.2.3. С согласия субъекта персональных данных.**
7.3. Трансграничная передача персональных данных в иностранные государства осуществляется при условии обеспечения адекватного уровня защиты прав субъектов ПДн в соответствии со статьёй 16 Закона о ПДн.
## 8. Меры по защите персональных данных
Оператор реализует следующие технические и организационные меры защиты ПДн:
### 8.1. Технические меры
- шифрование передаваемых данных по протоколу TLS 1.2 и выше;
- хеширование паролей с использованием стойких криптографических алгоритмов;
- разграничение доступа к информационным системам;
- регулярное резервное копирование данных;
- мониторинг и аудит действий пользователей;
- защита от несанкционированного доступа, вредоносного программного обеспечения, DDoS-атак;
- использование межсетевых экранов и систем обнаружения вторжений;
- регулярное обновление программного обеспечения и устранение уязвимостей.
### 8.2. Организационные меры
- назначение лица, ответственного за организацию обработки персональных данных;
- утверждение внутренних документов, регулирующих процессы обработки и защиты ПДн;
- обучение сотрудников Оператора правилам работы с ПДн;
- ограничение круга лиц, имеющих доступ к ПДн, минимально необходимым;
- заключение соглашений о неразглашении с сотрудниками и подрядчиками;
- регулярные аудиты состояния защиты ПДн.
## 9. Права субъекта персональных данных
В соответствии с Законом о ПДн субъект персональных данных имеет право:
9.1. Получать информацию, касающуюся обработки его ПДн (статья 24 Закона о ПДн), включая:
- цели и сроки обработки;
- источник получения данных;
- перечень обрабатываемых данных;
- сведения о третьих лицах, которым передаются данные.
9.2. Требовать от Оператора уточнения, блокирования или уничтожения ПДн, если они являются неполными, устаревшими, недостоверными, незаконно полученными или не являются необходимыми для заявленных целей обработки (статья 25 Закона о ПДн).
9.3. Отзывать согласие на обработку ПДн (статья 26 Закона о ПДн). Отзыв согласия влечёт прекращение использования Сервиса в отношении субъекта.
9.4. Обжаловать действия (бездействие) Оператора в уполномоченный орган по защите ПДн или в суд.
9.5. Требовать защиты своих прав и законных интересов, в том числе возмещения убытков и (или) компенсации морального вреда в судебном порядке.
## 10. Порядок реализации прав субъекта
10.1. Для реализации прав, предусмотренных разделом 9 настоящей Политики, субъект ПДн направляет Оператору письменное обращение по адресу электронной почты **[email-адрес для запросов по ПДн]** или почтовому адресу, указанному в реквизитах Оператора.
10.2. Обращение должно содержать:
- фамилию, имя, отчество субъекта;
- сведения, позволяющие идентифицировать субъекта в системе Оператора (адрес электронной почты, ID учётной записи);
- суть требования;
- подпись субъекта (для письменного обращения).
10.3. Оператор рассматривает обращение и направляет ответ в срок не позднее 15 (пятнадцати) календарных дней с даты поступления обращения.
10.4. В случае отказа в удовлетворении обращения Оператор направляет субъекту мотивированный ответ с указанием причин отказа.
## 11. Cookies и аналогичные технологии
11.1. Сайт и Сервис Оператора используют файлы cookies и аналогичные технологии для:
- обеспечения функционирования Сервиса (необходимые cookies — авторизация, сохранение пользовательских настроек);
- анализа использования Сервиса (аналитические cookies);
- персонализации контента и маркетинга (с согласия субъекта).
11.2. Пользователь может управлять настройками cookies через интерфейс своего браузера. Отключение необходимых cookies может привести к неполной функциональности Сервиса.
## 12. Изменения в Политике
12.1. Оператор вправе вносить изменения в настоящую Политику. Актуальная версия размещается на сайте Оператора по адресу https://food-market.kz/legal/privacy с указанием даты последнего обновления.
12.2. Существенные изменения, затрагивающие права субъектов ПДн, вступают в силу не ранее чем через 14 (четырнадцать) календарных дней после публикации новой редакции, с уведомлением Пользователей по адресу электронной почты, указанному в учётной записи.
## 13. Контактная информация
**Оператор:** ТОО «[НАЗВАНИЕ ТОО
**БИН:** [БИН]
**Юридический адрес:** [Юр.адрес]
**Адрес электронной почты для вопросов по ПДн:** [privacy@food-market.kz]
**Лицо, ответственное за организацию обработки ПДн:** [ФИО, должность]
---
*Настоящая Политика составлена в соответствии с законодательством Республики Казахстан и применяется с даты, указанной в шапке документа.*

View file

@ -0,0 +1,60 @@
---
title: Реквизиты компании
slug: requisites
last_updated: 2026-04-26
---
# Реквизиты компании
> **Внимание:** Настоящий раздел содержит placeholder-значения. Перед публикацией сайта в продакшене значения должны быть заполнены реальными реквизитами ТОО.
## Юридическое лицо
**Полное наименование:** Товарищество с ограниченной ответственностью «[НАЗВАНИЕ ТОО
**Сокращённое наименование:** ТОО «[НАЗВАНИЕ ТОО
## Регистрационные данные
**БИН:** [12-значный БИН]
**Дата государственной регистрации:** [ДД.ММ.ГГГГ]
**Орган регистрации:** [Министерство юстиции РК / иное]
## Адрес
**Юридический адрес:** [Индекс], Республика Казахстан, [Город], [улица, дом, офис]
**Фактический адрес:** [то же или иной]
**Почтовый адрес:** [то же или иной]
## Банковские реквизиты
**Банк-получатель:** [Название банка]
**БИК:** [BIK]
**ИИК (IBAN):** [KZxx xxxx xxxx xxxx xxxx]
**Кбе:** [Код бенефициара, обычно 17 для ТОО]
**Валюта счёта:** KZT (тенге)
## Налоговая информация
**Режим налогообложения:** [Общеустановленный / Упрощённый / иной]
**Плательщик НДС:** [Да / Нет]
**Свидетельство о постановке на учёт по НДС:** [серия, номер, дата] *(если применимо)*
## Руководство
**Директор:** [ФИО]
**Действует на основании:** Устава
## Контактная информация
**Email общий:** [info@food-market.kz]
**Email по вопросам персональных данных:** [privacy@food-market.kz]
**Email технической поддержки:** [support@food-market.kz]
**Телефон:** [+7 (___) ___-__-__]
## Часы работы поддержки
**Поддержка пользователей:** ежедневно с [09:00] до [18:00] по времени Астаны (UTC+5)
---
*Реквизиты могут уточняться. Актуальная редакция всегда размещена по адресу https://food-market.kz/legal/requisites.*

1
src/food-market.public/src/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

View file

@ -0,0 +1,53 @@
---
import '@/styles/global.css'
import Header from '@/components/Header.astro'
import Footer from '@/components/Footer.astro'
interface Props {
title: string
description?: string
ogImage?: string
}
const { title, description = 'Программа учёта и касса для розничных магазинов в Казахстане. Бесплатно 90 дней.', ogImage = '/og.png' } = Astro.props
const canonical = new URL(Astro.url.pathname, Astro.site).toString()
---
<!doctype html>
<html lang="ru-KZ">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title} · Food Market</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:type" content="website" />
<meta property="og:title" content={`${title} · Food Market`} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={new URL(ogImage, Astro.site).toString()} />
<meta property="og:locale" content="ru_KZ" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={`${title} · Food Market`} />
<meta name="twitter:description" content={description} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" />
<script type="application/ld+json" set:html={JSON.stringify({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Food Market',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web, Windows',
offers: { '@type': 'Offer', price: '5000', priceCurrency: 'KZT' },
})} />
</head>
<body class="min-h-screen flex flex-col">
<Header />
<main class="flex-1"><slot /></main>
<Footer />
</body>
</html>

View file

@ -0,0 +1,26 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
---
<BaseLayout title="О нас" description="Food Market — программа учёта и касса для розничных магазинов Казахстана.">
<section class="max-w-3xl mx-auto px-4 sm:px-6 py-14">
<h1 class="text-4xl font-extrabold">О Food Market</h1>
<p class="mt-6 text-lg text-slate-700">
Food Market — облачная программа учёта и кассовая программа для розничных магазинов Казахстана.
</p>
<p class="mt-4 text-slate-600">
Мы делаем продукт для тех, кому надоели тяжёлые российские системы с доплатами за каждую функцию.
У нас один тариф — все возможности. Касса с весами из коробки, импорт из МойСклад одной кнопкой,
KZ-локализация на всех уровнях (тенге, ОКЕИ, ОФД РК).
</p>
<h2 class="text-2xl font-bold mt-12 mb-4">Чем мы отличаемся</h2>
<ul class="space-y-3 text-slate-700">
<li>✓ <strong>Сделано в Казахстане</strong> — для казахстанских магазинов, с учётом местных требований и интеграций.</li>
<li>✓ <strong>Без скрытых доплат</strong> — CRM, лояльность, финансы, поддержка включены во все тарифы.</li>
<li>✓ <strong>Касса с весами Масса-К</strong> из коробки — не отдельный модуль и не сторонняя интеграция.</li>
<li>✓ <strong>Импорт за 1 клик</strong> — переход с МойСклада за 510 минут.</li>
<li>✓ <strong>90 дней триал без карты</strong> — без автосписаний и сюрпризов.</li>
</ul>
<h2 class="text-2xl font-bold mt-12 mb-4">Дорожная карта</h2>
<p class="text-slate-600">Что готово сейчас и что мы планируем — в разделе <a href="/changelog" class="text-brand underline">Changelog</a>.</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,12 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
---
<BaseLayout title="Блог" description="Блог Food Market: новости, релизы, лучшие практики автоматизации розницы.">
<section class="max-w-3xl mx-auto px-4 sm:px-6 py-14 text-center">
<h1 class="text-4xl font-extrabold">Блог</h1>
<p class="text-slate-500 mt-3">Новости, релизы, кейсы и практические статьи.</p>
<div class="mt-12 rounded-xl border border-dashed border-slate-300 bg-slate-50 p-10 text-slate-500">
✍️ Раздел в разработке. Скоро появятся первые посты.
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,27 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const releases = [
{ date: '2026-04', title: 'Phase 6 — Публичный сайт', items: ['Маркетинговый сайт food-market.zat.kz','Регистрация без админа платформы','Тарифы Старт / Бизнес-конструктор / Сеть'] },
{ date: '2026-04', title: 'Phase 4 — SuperAdmin консоль', items: ['Управление организациями','Журнал действий','Read-only «открыть как…»','Edit-mode с reason + audit-trail','Настраиваемый retention period'] },
{ date: '2026-04', title: 'Phase 3 — Цены и роли', items: ['Расширенная модель цен','Сотрудники и роли','Системные роли (Администратор, Кассир)'] },
{ date: '2026-04', title: 'Phase 2 — Закупки и приёмки', items: ['Документ приёмки со сканером','Skользящее среднее себестоимости','Inline quick-add позиций'] },
{ date: '2026-03', title: 'Phase 1 — Каталог и tenant-изоляция', items: ['Multi-tenant архитектура','Товары, группы, штрихкоды, остатки','Импорт из МойСклад'] },
]
---
<BaseLayout title="Changelog" description="История релизов Food Market: что нового в продукте.">
<section class="max-w-3xl mx-auto px-4 sm:px-6 py-14">
<h1 class="text-4xl font-extrabold">Changelog</h1>
<p class="text-slate-500 mt-2">История релизов Food Market.</p>
<div class="mt-10 space-y-8">
{releases.map((r) => (
<article class="rounded-xl border border-slate-200 bg-white p-5">
<div class="flex items-baseline justify-between mb-3">
<h2 class="font-bold text-lg">{r.title}</h2>
<span class="text-xs text-slate-500 font-mono">{r.date}</span>
</div>
<ul class="space-y-1.5 text-sm">{r.items.map((it) => <li>• {it}</li>)}</ul>
</article>
))}
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,23 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
---
<BaseLayout title="Контакты" description="Контакты Food Market: техподдержка, отдел продаж, юридический адрес.">
<section class="max-w-2xl mx-auto px-4 sm:px-6 py-14">
<h1 class="text-4xl font-extrabold">Контакты</h1>
<div class="mt-8 space-y-6">
<div class="rounded-xl border border-slate-200 bg-white p-5">
<h2 class="font-semibold">Техническая поддержка</h2>
<p class="text-sm text-slate-500 mt-1">Для клиентов с активным аккаунтом — через чат в админке. Здесь будет email и телефон позже.</p>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-5">
<h2 class="font-semibold">Отдел продаж и партнёрство</h2>
<p class="text-sm text-slate-500 mt-1">Тариф «Сеть», корпоративные внедрения, white-label партнёрство — пишите по контактам ниже.</p>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-5">
<h2 class="font-semibold">Юридический адрес и реквизиты</h2>
<p class="text-sm text-slate-500 mt-1"><a href="/legal/requisites" class="text-brand underline">Реквизиты компании</a> — отдельная страница.</p>
</div>
</div>
<p class="text-xs text-slate-400 mt-10 italic">Раздел в разработке. Финальные контакты появятся после регистрации юр.лица и запуска поддержки.</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,30 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const sections = [
{ id: 'catalog', icon: '📦', title: 'Товары и каталог', items: ['Иерархические группы и подгруппы','Штрихкоды EAN-13/EAN-8/Code128','Цены: розничная, оптовая, эталонная','Автогенерация артикула и штрихкода','Импорт из МойСклад','Картинки товаров','Атрибуты и характеристики'] },
{ id: 'sales', icon: '💳', title: 'Продажи и касса', items: ['Касса для Windows','Поддержка весов Масса-К','Сканер штрихкодов USB','Чековые принтеры ESC/POS','Kaspi Pay интеграция','Скидки и акции','Возвраты на кассе'] },
{ id: 'stock', icon: '🏬', title: 'Склад и остатки', items: ['Несколько складов','Приёмка со сканером','Автоматический расчёт себестоимости','Инвентаризация','Списание (брак/просрочка)','Оприходование','Перемещения между складами','История движений'] },
{ id: 'purchase', icon: '🚚', title: 'Закупки и поставщики', items: ['База контрагентов','Заказы поставщикам','Документы приёмки','Скользящее среднее себестоимости','Эталонная цена'] },
{ id: 'crm', icon: '👥', title: 'Клиенты и лояльность', items: ['База клиентов','Скидочные карты (скоро)','Программы лояльности (скоро)','Сегментация (скоро)','SMS-рассылки (скоро)'] },
{ id: 'finance', icon: '📊', title: 'Финансы и аналитика', items: ['Дашборд продаж','Отчёт по марже','Отчёт по остаткам','Аналитика клиентов (скоро)','API для BI-инструментов'] },
]
---
<BaseLayout title="Возможности" description="Полный список возможностей Food Market: каталог, касса, склад, закупки, CRM, аналитика.">
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-12">
<h1 class="text-4xl font-extrabold text-center">Возможности</h1>
<p class="text-center text-slate-500 mt-3">6 модулей вместо 40 функций. Группируем по сценариям.</p>
<div class="space-y-8 mt-12">
{sections.map((s) => (
<section id={s.id} class="rounded-xl border border-slate-200 bg-white p-6">
<div class="flex items-center gap-3 mb-4">
<span class="text-3xl">{s.icon}</span>
<h2 class="text-2xl font-bold">{s.title}</h2>
</div>
<ul class="grid sm:grid-cols-2 gap-1.5 text-sm">
{s.items.map((it) => <li>✓ {it}</li>)}
</ul>
</section>
))}
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,34 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const features = [
{ icon: '🏷️', title: 'Акцизные марки', text: 'Сканирование марок при приёмке, привязка к товарным позициям, контроль продаж.' },
{ icon: '📜', title: 'ЕГАИС-ready', text: 'Архитектура совместима с ЕГАИС-РК (когда систему развернут полностью). Подготовлены отчёты.' },
{ icon: '🕐', title: 'Контроль времени продаж', text: 'Запрет продаж в часы после 23:00 / до 9:00 (по требованию законодательства).' },
{ icon: '🆔', title: 'Проверка возраста', text: 'Запрос подтверждения возраста на кассе перед продажей алкогольных позиций.' },
]
---
<BaseLayout title="Для алкогольных магазинов" description="Food Market для алкоголя: акцизные марки, ЕГАИС-ready, контроль времени продаж, проверка возраста.">
<section class="bg-gradient-to-br from-brand-light/30 via-white to-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 py-14">
<span class="text-3xl">🍷</span>
<h1 class="text-4xl font-extrabold mt-2">Программа учёта для алкоголя</h1>
<p class="mt-4 text-lg text-slate-600">Акцизные марки, ЕГАИС-ready, контроль времени продаж и возраста.</p>
<a href="/signup?plan=start" class="inline-block mt-7 px-5 py-3 bg-brand text-white font-semibold rounded-md">Начать бесплатно</a>
</div>
</section>
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-10">Что важно для алкомаркета</h2>
<div class="grid sm:grid-cols-2 gap-5">
{features.map((f) => (
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">{f.icon}</div>
<h3 class="font-semibold mt-2">{f.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{f.text}</p>
</div>
))}
</div>
</section>
<section class="bg-slate-50 py-14 text-center">
<p class="italic text-slate-700 max-w-2xl mx-auto px-4">«Раздел кейсов в разработке.»</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,34 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const features = [
{ icon: '🍔', title: 'Модификаторы и комбо', text: 'Опции к блюдам (без лука, добавить сыр), комбо-меню с автоматической ценой.' },
{ icon: '👨‍🍳', title: 'Технологические карты', text: 'Состав блюд, нормы списания ингредиентов, автоматический пересчёт остатков продуктов.' },
{ icon: '🧾', title: 'Заказы по столикам', text: 'Открытие чека на столик, перемещение между столами, разделение чека.' },
{ icon: '📈', title: 'Анализ блюд', text: 'Какие блюда продаются, какие нет. Маржинальность по позиции.' },
]
---
<BaseLayout title="Для кафе и общепита" description="Food Market для кафе и общепита: модификаторы, комбо, тех.карты, заказы по столикам.">
<section class="bg-gradient-to-br from-brand-light/30 via-white to-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 py-14">
<span class="text-3xl">☕</span>
<h1 class="text-4xl font-extrabold mt-2">Программа учёта для кафе и общепита</h1>
<p class="mt-4 text-lg text-slate-600">Модификаторы, комбо, технологические карты, заказы по столикам. Списание ингредиентов автоматом.</p>
<a href="/signup?plan=start" class="inline-block mt-7 px-5 py-3 bg-brand text-white font-semibold rounded-md">Начать бесплатно</a>
</div>
</section>
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-10">Что важно для кафе</h2>
<div class="grid sm:grid-cols-2 gap-5">
{features.map((f) => (
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">{f.icon}</div>
<h3 class="font-semibold mt-2">{f.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{f.text}</p>
</div>
))}
</div>
</section>
<section class="bg-slate-50 py-14 text-center">
<p class="italic text-slate-700 max-w-2xl mx-auto px-4">«Раздел кейсов в разработке.»</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,34 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const features = [
{ icon: '📏', title: 'Размерные сетки', text: 'Один товар — много размеров и цветов. Учёт остатков по каждой комбинации.' },
{ icon: '🎨', title: 'Цвета и принты', text: 'Атрибуты товара: цвет, материал, сезон. Поиск и фильтры по атрибутам.' },
{ icon: '🛍️', title: 'Маркетплейсы', text: 'Выгрузка на Kaspi Magazin, Ozon, Wildberries (скоро) — синхронизация остатков.' },
{ icon: '🔄', title: 'Возвраты', text: 'Удобный возврат на кассе с возвратом денег или обменом на другой размер.' },
]
---
<BaseLayout title="Для магазина одежды" description="Food Market для одежды: размерные сетки, цвета, маркетплейсы, удобные возвраты.">
<section class="bg-gradient-to-br from-brand-light/30 via-white to-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 py-14">
<span class="text-3xl">👔</span>
<h1 class="text-4xl font-extrabold mt-2">Программа учёта для магазина одежды</h1>
<p class="mt-4 text-lg text-slate-600">Размерные сетки, цвета, выгрузка на маркетплейсы, удобные возвраты.</p>
<a href="/signup?plan=start" class="inline-block mt-7 px-5 py-3 bg-brand text-white font-semibold rounded-md">Начать бесплатно</a>
</div>
</section>
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-10">Что важно для одежды</h2>
<div class="grid sm:grid-cols-2 gap-5">
{features.map((f) => (
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">{f.icon}</div>
<h3 class="font-semibold mt-2">{f.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{f.text}</p>
</div>
))}
</div>
</section>
<section class="bg-slate-50 py-14 text-center">
<p class="italic text-slate-700 max-w-2xl mx-auto px-4">«Раздел кейсов в разработке.»</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,34 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const features = [
{ icon: '⚖️', title: 'Весовой товар', text: 'Касса принимает вес с весов Масса-К напрямую — стоимость пересчитывается сразу.' },
{ icon: '📅', title: 'Скоропорт и сроки', text: 'Контроль сроков годности, уведомление за N дней, списание просрочки одним кликом.' },
{ icon: '🏷️', title: 'Печать ценников и штрихкодов', text: 'Внутренняя нумерация EAN-13 для весовых товаров, автогенерация при заведении.' },
{ icon: '📊', title: 'ABC-анализ', text: 'Ходовые и неходовые позиции по сумме / марже / штукам. Решения по матрице за минуту.' },
]
---
<BaseLayout title="Для продуктового магазина" description="Food Market для продуктового магазина: весовой товар, штрихкоды, скоропорт, ABC-анализ.">
<section class="bg-gradient-to-br from-brand-light/30 via-white to-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 py-14">
<span class="text-3xl">🛒</span>
<h1 class="text-4xl font-extrabold mt-2">Программа учёта для продуктового магазина</h1>
<p class="mt-4 text-lg text-slate-600">Весовой товар, скоропорт, штрихкоды, касса с весами Масса-К из коробки. Импорт из МойСклада.</p>
<a href="/signup?plan=start" class="inline-block mt-7 px-5 py-3 bg-brand text-white font-semibold rounded-md">Начать бесплатно</a>
</div>
</section>
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-10">Что важно для продуктового</h2>
<div class="grid sm:grid-cols-2 gap-5">
{features.map((f) => (
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">{f.icon}</div>
<h3 class="font-semibold mt-2">{f.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{f.text}</p>
</div>
))}
</div>
</section>
<section class="bg-slate-50 py-14 text-center">
<p class="italic text-slate-700 max-w-2xl mx-auto px-4">«Раздел кейсов в разработке. Здесь появятся истории магазинов «у дома» и минимаркетов.»</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,34 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const features = [
{ icon: '🛡️', title: 'Гарантийные сроки', text: 'Учёт сроков гарантии на товар, печать гарантийного талона при продаже.' },
{ icon: '🔧', title: 'Серийные номера', text: 'Привязка серийного номера к продаже для возможности возврата по гарантии.' },
{ icon: '🎁', title: 'Комплекты товаров', text: 'Продажа набора (например посуда) одной позицией с автоматическим списанием компонентов.' },
{ icon: '📤', title: 'Маркетплейсы', text: 'Выгрузка на Kaspi Magazin, синхронизация цен и остатков (скоро).' },
]
---
<BaseLayout title="Для магазина дом и быт" description="Food Market для магазина товаров для дома: гарантийные сроки, серийные номера, комплекты, маркетплейсы.">
<section class="bg-gradient-to-br from-brand-light/30 via-white to-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 py-14">
<span class="text-3xl">🏠</span>
<h1 class="text-4xl font-extrabold mt-2">Программа учёта для дома и быта</h1>
<p class="mt-4 text-lg text-slate-600">Гарантийные сроки, серийные номера, комплекты, выгрузка на маркетплейсы.</p>
<a href="/signup?plan=start" class="inline-block mt-7 px-5 py-3 bg-brand text-white font-semibold rounded-md">Начать бесплатно</a>
</div>
</section>
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-10">Что важно для дом/быт</h2>
<div class="grid sm:grid-cols-2 gap-5">
{features.map((f) => (
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">{f.icon}</div>
<h3 class="font-semibold mt-2">{f.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{f.text}</p>
</div>
))}
</div>
</section>
<section class="bg-slate-50 py-14 text-center">
<p class="italic text-slate-700 max-w-2xl mx-auto px-4">«Раздел кейсов в разработке.»</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,34 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const features = [
{ icon: '🧪', title: 'Серии и сроки годности', text: 'Партионный учёт с серийными номерами и контролем сроков. Просрочка не пробивается на кассе.' },
{ icon: '💊', title: 'Рецептурный учёт', text: 'Отметка рецептурных препаратов, требование подтверждения отпуска фармацевтом.' },
{ icon: '📋', title: 'Группы товаров по МНН', text: 'Международные непатентованные наименования, поиск аналогов, замена при отсутствии.' },
{ icon: '🔒', title: 'Контроль доступа', text: 'Роли «Фармацевт» и «Заведующий» с разными правами. Логирование всех операций.' },
]
---
<BaseLayout title="Для аптеки" description="Food Market для аптеки: партионный учёт, серии и сроки годности, рецептурные препараты, МНН-группы.">
<section class="bg-gradient-to-br from-brand-light/30 via-white to-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 py-14">
<span class="text-3xl">💊</span>
<h1 class="text-4xl font-extrabold mt-2">Программа учёта для аптеки</h1>
<p class="mt-4 text-lg text-slate-600">Серии и сроки годности, рецептурный отпуск, группировка по МНН. Импорт из МойСклада.</p>
<a href="/signup?plan=start" class="inline-block mt-7 px-5 py-3 bg-brand text-white font-semibold rounded-md">Начать бесплатно</a>
</div>
</section>
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-10">Что важно для аптеки</h2>
<div class="grid sm:grid-cols-2 gap-5">
{features.map((f) => (
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">{f.icon}</div>
<h3 class="font-semibold mt-2">{f.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{f.text}</p>
</div>
))}
</div>
</section>
<section class="bg-slate-50 py-14 text-center">
<p class="italic text-slate-700 max-w-2xl mx-auto px-4">«Раздел кейсов в разработке.»</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,226 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
import FAQ from '@/components/FAQ.tsx'
const verticals = [
{ href: '/for-grocery', title: 'Продуктовый магазин', icon: '🛒', desc: 'Весовой товар, штрихкоды, скоропорт' },
{ href: '/for-pharmacy', title: 'Аптека', icon: '💊', desc: 'Серии и сроки годности' },
{ href: '/for-cafe', title: 'Кафе и общепит', icon: '☕', desc: 'Модификаторы и комбо' },
{ href: '/for-alcohol', title: 'Алкоголь', icon: '🍷', desc: 'Акцизные марки, ЕГАИС-ready' },
{ href: '/for-clothing', title: 'Одежда', icon: '👔', desc: 'Размерные сетки и цвета' },
{ href: '/for-household', title: 'Дом и быт', icon: '🏠', desc: 'Гарантийные сроки' },
]
const modules = [
{ icon: '📦', title: 'Товары и каталог', text: 'Группы, штрихкоды, цены, остатки, импорт из МойСклад одной кнопкой.' },
{ icon: '💳', title: 'Продажи и касса', text: 'Касса для Windows с поддержкой весов Масса-К, штрихкодов, чековых принтеров.' },
{ icon: '🏬', title: 'Склад и приёмки', text: 'Несколько складов, приёмка со сканером, инвентаризация, списание, оприходование.' },
{ icon: '🚚', title: 'Закупки и поставщики', text: 'Документы приёмки с автоматическим расчётом себестоимости (скользящее среднее).' },
{ icon: '👥', title: 'Клиенты и лояльность', text: 'База контрагентов, скидки, акции, программы лояльности (скоро).' },
{ icon: '📊', title: 'Финансы и аналитика', text: 'Отчёты по продажам, остаткам, прибыли. Дашборд кассира и владельца.' },
]
const integrations = [
'Kaspi Pay', 'Halyk Bank', 'Jusan Bank', 'Forte Bank', 'ОФД РК', 'Kaspi Magazin', 'Ozon', 'Wildberries',
]
---
<BaseLayout
title="Программа учёта для розничных магазинов"
description="Программа учёта и касса для розничных магазинов в Казахстане. Касса с весами, импорт из МойСклада, интеграция с Kaspi и ОФД РК. Бесплатно 90 дней без карты."
>
{/* 1. Hero */}
<section class="bg-gradient-to-br from-brand-light/40 via-white to-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-12 sm:py-20 grid lg:grid-cols-2 gap-10 items-center">
<div>
<h1 class="text-4xl sm:text-5xl font-extrabold leading-tight">
Программа учёта и&nbsp;касса для розничных магазинов в&nbsp;Казахстане
</h1>
<p class="mt-5 text-lg text-slate-600">
Касса с весами Масса-К, импорт из МойСклада, интеграция с Kaspi и ОФД РК.
<strong>Бесплатно 90 дней без карты.</strong>
</p>
<div class="mt-7 flex flex-wrap gap-3">
<a href="/signup" class="px-5 py-3 bg-brand text-white text-sm font-semibold rounded-md hover:bg-brand-hover">Начать бесплатно</a>
<a href="#screenshot" class="px-5 py-3 border border-slate-300 text-sm font-semibold rounded-md hover:bg-slate-50">Посмотреть демо</a>
</div>
<p class="mt-3 text-xs text-slate-500">Без банковской карты · 90 дней триал · Подписка от 5 000 ₸/мес</p>
</div>
<div class="rounded-xl border border-slate-200 bg-white shadow-xl overflow-hidden">
{/* Реальный скриншот админки. Пока placeholder с типографическим mock'ом дашборда. */}
<div class="aspect-[16/10] bg-gradient-to-br from-slate-50 to-slate-100 p-4 grid grid-cols-2 gap-3">
<div class="bg-white rounded-lg border border-slate-200 p-3"><div class="text-[10px] uppercase text-slate-400">Организаций</div><div class="text-2xl font-bold mt-1">2</div></div>
<div class="bg-white rounded-lg border border-slate-200 p-3"><div class="text-[10px] uppercase text-slate-400">Товаров</div><div class="text-2xl font-bold mt-1">29 540</div></div>
<div class="bg-white rounded-lg border border-slate-200 p-3"><div class="text-[10px] uppercase text-slate-400">Приёмок / мес</div><div class="text-2xl font-bold mt-1">187</div></div>
<div class="bg-white rounded-lg border border-slate-200 p-3"><div class="text-[10px] uppercase text-slate-400">Чеков сегодня</div><div class="text-2xl font-bold mt-1 text-brand">432</div></div>
</div>
</div>
</div>
</section>
{/* 2. 3 ключевых выгоды */}
<section class="max-w-7xl mx-auto px-4 sm:px-6 py-12 grid sm:grid-cols-3 gap-4">
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">🛒</div>
<h3 class="font-semibold mt-2">Касса с весами</h3>
<p class="text-sm text-slate-600 mt-1">Поддержка весов Масса-К из коробки. Чёткие чеки в формате ОФД РК.</p>
</div>
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">⬇️</div>
<h3 class="font-semibold mt-2">Импорт из МойСклада</h3>
<p class="text-sm text-slate-600 mt-1">Перенос товаров, остатков, контрагентов одной кнопкой за 10 минут.</p>
</div>
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">🎁</div>
<h3 class="font-semibold mt-2">90 дней бесплатно</h3>
<p class="text-sm text-slate-600 mt-1">Триал без банковской карты. Тариф «Старт» от 5 000 ₸/мес.</p>
</div>
</section>
{/* 3. Скриншот продукта */}
<section id="screenshot" class="max-w-7xl mx-auto px-4 sm:px-6 py-12">
<div class="rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-6">
<div class="aspect-[16/9] bg-white rounded-lg border border-slate-200 flex items-center justify-center text-slate-400 text-sm">
Скриншот админки · реальный экран /dashboard
</div>
</div>
</section>
{/* 4. Для кого */}
<section class="max-w-7xl mx-auto px-4 sm:px-6 py-12">
<h2 class="text-3xl font-bold text-center mb-2">Для кого Food Market</h2>
<p class="text-center text-slate-500 mb-8">Шесть вертикалей. Свои фишки в каждой.</p>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{verticals.map((v) => (
<a href={v.href} class="rounded-xl border border-slate-200 p-5 hover:shadow-md hover:border-brand transition group">
<div class="text-3xl">{v.icon}</div>
<h3 class="font-semibold mt-2 group-hover:text-brand">{v.title}</h3>
<p class="text-sm text-slate-600 mt-1">{v.desc}</p>
<span class="text-xs text-brand mt-3 inline-block">Подробнее →</span>
</a>
))}
</div>
</section>
{/* 5. Возможности */}
<section class="bg-slate-50 py-14">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<h2 class="text-3xl font-bold text-center mb-2">6 модулей вместо 40 функций</h2>
<p class="text-center text-slate-500 mb-10">Группируем по сценариям работы, а не списком из 40 пунктов.</p>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{modules.map((m) => (
<div class="bg-white rounded-xl border border-slate-200 p-5">
<div class="text-2xl">{m.icon}</div>
<h3 class="font-semibold mt-2">{m.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{m.text}</p>
<a href="/features" class="text-xs text-brand mt-3 inline-block">Подробнее →</a>
</div>
))}
</div>
</div>
</section>
{/* 6. Касса */}
<section class="max-w-7xl mx-auto px-4 sm:px-6 py-14 grid lg:grid-cols-2 gap-10 items-center">
<div>
<span class="inline-block text-xs uppercase tracking-wider text-brand font-semibold">Касса для Windows</span>
<h2 class="text-3xl font-bold mt-2">Кассовая программа с поддержкой весов Масса-К</h2>
<p class="mt-4 text-slate-600">
Установщик для Windows 10/11. Работает офлайн, синхронизируется с админкой,
принимает Kaspi Pay, печатает фискальные чеки в ОФД РК.
</p>
<div class="mt-6 flex gap-3">
<a href="/pos" class="px-5 py-3 bg-brand text-white text-sm font-semibold rounded-md hover:bg-brand-hover">Подробнее о кассе</a>
<span class="px-5 py-3 border border-slate-300 text-sm font-semibold rounded-md text-slate-400 cursor-not-allowed" title="В разработке">Скачать установщик</span>
</div>
</div>
<div class="aspect-video rounded-xl border border-slate-200 bg-slate-900 flex items-center justify-center text-slate-500 text-sm">
Видео работы кассы · скоро
</div>
</section>
{/* 7. Интеграции */}
<section class="bg-slate-50 py-14">
<div class="max-w-7xl mx-auto px-4 sm:px-6">
<h2 class="text-3xl font-bold text-center mb-2">Интеграции</h2>
<p class="text-center text-slate-500 mb-8">Кассы, банки, ОФД, маркетплейсы. Подключаем по запросу.</p>
<div class="flex flex-wrap justify-center gap-3">
{integrations.map((label) => (
<span class="px-4 py-2 bg-white border border-slate-200 rounded-md text-sm font-medium">{label}</span>
))}
</div>
</div>
</section>
{/* 8. Тарифы */}
<section class="max-w-7xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-2">Простые тарифы. Никаких доплат.</h2>
<p class="text-center text-slate-500 mb-10">CRM / финансы / лояльность включены во все тарифы.</p>
<div class="grid lg:grid-cols-3 gap-4">
<div class="rounded-xl border border-slate-200 p-6 bg-white">
<h3 class="font-bold text-lg">Старт</h3>
<p class="text-3xl font-bold mt-2">5 000 ₸<span class="text-sm font-normal text-slate-500">/мес</span></p>
<p class="text-xs text-slate-500 mt-1">90 дней триал</p>
<ul class="text-sm space-y-1.5 mt-5">
<li>1 магазин · 1 касса · 1 склад</li>
<li>2 сотрудника</li>
<li>Без лимита товаров</li>
<li>Касса с весами</li>
<li>Импорт из МойСклад</li>
</ul>
<a href="/signup?plan=start" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Начать</a>
</div>
<div class="rounded-xl border-2 border-brand p-6 bg-white relative">
<span class="absolute -top-3 left-1/2 -translate-x-1/2 bg-brand text-white text-xs px-2 py-0.5 rounded-full">Популярный</span>
<h3 class="font-bold text-lg">Бизнес</h3>
<p class="text-3xl font-bold mt-2">от 10 000 ₸<span class="text-sm font-normal text-slate-500">/мес</span></p>
<p class="text-xs text-slate-500 mt-1">Конструктор по объёму бизнеса</p>
<ul class="text-sm space-y-1.5 mt-5">
<li>До 3 магазинов</li>
<li>До 5 касс и складов</li>
<li>До 10 сотрудников</li>
<li>API + все интеграции</li>
</ul>
<a href="/pricing" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Настроить тариф</a>
</div>
<div class="rounded-xl border border-slate-200 p-6 bg-white">
<h3 class="font-bold text-lg">Сеть</h3>
<p class="text-3xl font-bold mt-2">По запросу</p>
<p class="text-xs text-slate-500 mt-1">Демо + индивидуальный SLA</p>
<ul class="text-sm space-y-1.5 mt-5">
<li>Unlimited магазины / кассы</li>
<li>Выделенный менеджер</li>
<li>Расширенный API</li>
<li>Гарантии доступности</li>
</ul>
<a href="/contacts" class="block text-center mt-6 px-4 py-2.5 border border-slate-300 rounded-md font-semibold">Связаться</a>
</div>
</div>
<p class="text-center text-sm text-slate-500 mt-8">
Никаких доплат за CRM, финансы, лояльность, поддержку. Всё включено в каждый тариф.
</p>
</section>
{/* 9. Соцпруф */}
<section class="bg-slate-50 py-14">
<div class="max-w-3xl mx-auto px-4 sm:px-6 text-center">
<p class="text-xs uppercase tracking-wider text-slate-500 mb-4">Кейсы</p>
<p class="text-xl text-slate-700 italic">«Раздел в разработке. Здесь появятся истории магазинов, которые перешли на Food Market.»</p>
</div>
</section>
{/* 10. FAQ */}
<section class="max-w-3xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-8">Частые вопросы</h2>
<FAQ client:visible />
</section>
{/* 11. Финальный CTA */}
<section class="bg-brand-light/30 py-16">
<div class="max-w-3xl mx-auto px-4 sm:px-6 text-center">
<h2 class="text-3xl sm:text-4xl font-extrabold">Запустите магазин за 15 минут</h2>
<p class="mt-3 text-slate-600">Регистрация — минута. Импорт товаров — десять минут. Касса работает на следующий день.</p>
<a href="/signup" class="inline-block mt-7 px-7 py-3 bg-brand text-white font-semibold rounded-md hover:bg-brand-hover">Начать бесплатно — 90 дней</a>
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,27 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const groups = [
{ title: 'Платёжные системы', items: ['Kaspi Pay', 'Halyk Bank', 'Jusan Bank', 'Forte Bank'] },
{ title: 'Фискализация', items: ['ОФД РК (все операторы)', 'Чековые принтеры ESC/POS'] },
{ title: 'Маркетплейсы (скоро)', items: ['Kaspi Magazin', 'Ozon', 'Wildberries'] },
{ title: 'Учётные системы', items: ['МойСклад (импорт каталога)', 'Excel CSV-импорт'] },
{ title: 'Оборудование', items: ['Весы Масса-К (USB/COM)', 'Сканеры штрихкодов USB', 'Денежные ящики', 'Дисплей покупателя'] },
]
---
<BaseLayout title="Интеграции" description="Кассы, банки, ОФД, маркетплейсы, учётные системы — все интеграции Food Market.">
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-12">
<h1 class="text-4xl font-extrabold text-center">Интеграции</h1>
<p class="text-center text-slate-500 mt-3">Стандартные подключения для розницы Казахстана.</p>
<div class="grid sm:grid-cols-2 gap-5 mt-10">
{groups.map((g) => (
<div class="rounded-xl border border-slate-200 bg-white p-5">
<h2 class="font-semibold mb-3">{g.title}</h2>
<ul class="space-y-1 text-sm text-slate-700">
{g.items.map((it) => <li>• {it}</li>)}
</ul>
</div>
))}
</div>
<p class="text-center text-sm text-slate-500 mt-10">Нужна интеграция, которой нет в списке? <a href="/contacts" class="text-brand underline">Напишите нам</a> — добавим в дорожную карту.</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,12 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
---
<BaseLayout title="База знаний" description="База знаний Food Market: статьи и инструкции для пользователей.">
<section class="max-w-3xl mx-auto px-4 sm:px-6 py-14 text-center">
<h1 class="text-4xl font-extrabold">База знаний</h1>
<p class="text-slate-500 mt-3">Статьи и инструкции по работе с Food Market.</p>
<div class="mt-12 rounded-xl border border-dashed border-slate-300 bg-slate-50 p-10 text-slate-500">
📚 Раздел в разработке. Здесь появятся пошаговые инструкции по приёмке, инвентаризации, кассе и интеграциям.
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,53 @@
---
import { getCollection } from 'astro:content'
import BaseLayout from '@/layouts/BaseLayout.astro'
export async function getStaticPaths() {
const docs = await getCollection('legal')
return docs.map((d) => ({ params: { slug: d.slug }, props: { doc: d } }))
}
const { doc } = Astro.props
const { Content } = await doc.render()
const updated = doc.data.last_updated.toLocaleDateString('ru-KZ', { day: '2-digit', month: 'long', year: 'numeric' })
---
<BaseLayout title={doc.data.title} description={`Юридический документ Food Market: ${doc.data.title}.`}>
<article class="max-w-[720px] mx-auto px-4 sm:px-6 py-12">
<p class="text-xs text-slate-500 mb-2">Обновлено: {updated}</p>
<div class="prose-legal">
<Content />
</div>
<p class="mt-12 pt-6 border-t border-slate-200 text-xs text-slate-500">
По вопросам — <a href="/contacts" class="text-brand underline">контакты</a>.
</p>
</article>
</BaseLayout>
<style is:global>
.prose-legal {
font-size: 15px;
line-height: 1.7;
color: #1e293b;
}
.prose-legal h1 { font-size: 1.875rem; font-weight: 800; line-height: 1.2; margin-top: 1.5rem; margin-bottom: 1rem; }
.prose-legal h2 { font-size: 1.375rem; font-weight: 700; line-height: 1.3; margin-top: 2rem; margin-bottom: 0.75rem; }
.prose-legal h3 { font-size: 1.125rem; font-weight: 600; line-height: 1.4; margin-top: 1.5rem; margin-bottom: 0.5rem; }
.prose-legal p { margin: 0.75rem 0; }
.prose-legal ul, .prose-legal ol { margin: 0.75rem 0 0.75rem 1.5rem; }
.prose-legal li { margin: 0.25rem 0; }
.prose-legal blockquote {
border-left: 3px solid #fbbf24;
background: #fffbeb;
padding: 0.75rem 1rem;
margin: 1.25rem 0;
color: #92400e;
font-size: 0.9rem;
}
.prose-legal a { color: var(--color-brand, #00B207); text-decoration: underline; }
.prose-legal hr { margin: 2rem 0; border: 0; border-top: 1px solid #e2e8f0; }
.prose-legal code { background: #f1f5f9; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; }
.prose-legal table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.9rem; }
.prose-legal th, .prose-legal td { border: 1px solid #e2e8f0; padding: 0.5rem 0.75rem; text-align: left; }
.prose-legal th { background: #f8fafc; font-weight: 600; }
.prose-legal strong { font-weight: 600; }
</style>

View file

@ -0,0 +1,72 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const compare = [
{ row: 'Цена за базовый тариф', moy: 'от 4 900 ₽ ≈ 30 000 ₸', us: '5 000 ₸' },
{ row: 'Касса с весами Масса-К', moy: '— (отдельный модуль)', us: '✓ из коробки' },
{ row: 'CRM и лояльность', moy: '+ доплата', us: '✓ включено' },
{ row: 'Финансовый учёт', moy: '+ доплата', us: '✓ включено' },
{ row: 'Скорость интерфейса', moy: '~3 сек загрузка', us: '<1 сек' },
{ row: 'Поддержка KZ-локализации', moy: 'базовая', us: 'полная (тенге, ОКЕИ, ОФД РК)' },
{ row: 'Триал', moy: '14 дней', us: '90 дней без карты' },
]
---
<BaseLayout title="Переход с МойСклада" description="Перенос товаров, штрихкодов, контрагентов и остатков из МойСклада в Food Market за один клик.">
<section class="bg-gradient-to-br from-brand-light/40 via-white to-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 py-14">
<h1 class="text-4xl font-extrabold">Переходите с МойСклада<br/>за один клик</h1>
<p class="mt-4 text-lg text-slate-600">Импорт товаров, групп, контрагентов, остатков и штрихкодов через ваш существующий API-токен МойСклада. 510 минут — и магазин уже работает в Food Market.</p>
<a href="/signup?plan=start" class="inline-block mt-7 px-5 py-3 bg-brand text-white font-semibold rounded-md hover:bg-brand-hover">Начать миграцию</a>
</div>
</section>
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-8">МойСклад vs Food Market</h2>
<div class="overflow-x-auto rounded-xl border border-slate-200 bg-white">
<table class="w-full text-sm">
<thead class="bg-slate-50">
<tr>
<th class="text-left px-4 py-3 font-semibold">Параметр</th>
<th class="text-left px-4 py-3 font-semibold">МойСклад</th>
<th class="text-left px-4 py-3 font-semibold text-brand">Food Market</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{compare.map((c) => (
<tr>
<td class="px-4 py-3 font-medium">{c.row}</td>
<td class="px-4 py-3 text-slate-600">{c.moy}</td>
<td class="px-4 py-3 text-slate-900 font-medium">{c.us}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section class="bg-slate-50 py-14">
<div class="max-w-4xl mx-auto px-4 sm:px-6">
<h2 class="text-3xl font-bold text-center mb-2">Что переносится</h2>
<p class="text-center text-slate-500 mb-8">Один токен — все данные.</p>
<div class="grid sm:grid-cols-2 gap-3 text-sm">
{['Товары и услуги','Группы и категории','Штрихкоды и артикулы','Цены (розничные + оптовые)','Контрагенты (поставщики и клиенты)','Остатки по складам','Единицы измерения','Атрибуты товаров'].map((x) => (
<div class="bg-white rounded-md border border-slate-200 px-4 py-3">✓ {x}</div>
))}
</div>
</div>
</section>
<section class="max-w-3xl mx-auto px-4 sm:px-6 py-14">
<h2 class="text-3xl font-bold text-center mb-10">3 шага до запуска</h2>
<ol class="space-y-6">
<li class="flex gap-4"><div class="flex-shrink-0 w-10 h-10 rounded-full bg-brand text-white font-bold flex items-center justify-center">1</div>
<div><h3 class="font-semibold">Зарегистрируйтесь в Food Market</h3><p class="text-sm text-slate-600 mt-1">Создайте аккаунт за минуту. 90 дней триал без банковской карты.</p></div></li>
<li class="flex gap-4"><div class="flex-shrink-0 w-10 h-10 rounded-full bg-brand text-white font-bold flex items-center justify-center">2</div>
<div><h3 class="font-semibold">Подключите токен МойСклада</h3><p class="text-sm text-slate-600 mt-1">Настройки → Интеграции → МойСклад → ваш API-токен. Кнопка «Импортировать».</p></div></li>
<li class="flex gap-4"><div class="flex-shrink-0 w-10 h-10 rounded-full bg-brand text-white font-bold flex items-center justify-center">3</div>
<div><h3 class="font-semibold">Откройте кассу и работайте</h3><p class="text-sm text-slate-600 mt-1">Скачайте установщик кассы для Windows, подключите весы и сканер. Магазин запущен.</p></div></li>
</ol>
<div class="text-center mt-10">
<a href="/signup?plan=start" class="px-5 py-3 bg-brand text-white font-semibold rounded-md hover:bg-brand-hover">Начать миграцию</a>
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,60 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
const features = [
{ icon: '⚖️', title: 'Весы Масса-К', text: 'Подключение по USB/COM. Вес автоматически попадает в чек, цена пересчитывается на лету.' },
{ icon: '📡', title: 'Офлайн-режим', text: 'Касса работает при отсутствии интернета. Чеки синхронизируются после восстановления связи.' },
{ icon: '🧾', title: 'Фискальные чеки ОФД РК', text: 'Чеки в формате ОФД Казахстана с автоматической отправкой в налоговую.' },
{ icon: '💳', title: 'Kaspi Pay и наличные', text: 'Оплата картой через Kaspi POS, наличными, смешанная оплата в одном чеке.' },
{ icon: '🎫', title: 'Скидки и акции', text: 'Скидка % или фиксированная сумма, скан карт лояльности, накопительные программы.' },
{ icon: '📦', title: 'Сканер штрихкодов', text: 'Любой USB-сканер, EAN-13/EAN-8/Code128/Datamatrix. Поиск товара за миллисекунды.' },
]
const hardware = [
'Весы Масса-К серий MK / 4D / Tiger',
'Сканеры штрихкодов USB (любые HID)',
'Чековые принтеры ESC/POS (Xprinter, Sam4s)',
'Денежный ящик RJ-11/RJ-12',
'Дисплей покупателя VFD',
]
---
<BaseLayout title="Касса для Windows" description="Кассовая программа Food Market для Windows: поддержка весов Масса-К, офлайн-режим, фискальные чеки ОФД РК, Kaspi Pay.">
<section class="bg-gradient-to-br from-brand-light/40 via-white to-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-14">
<span class="inline-block text-xs uppercase tracking-wider text-brand font-semibold">Касса</span>
<h1 class="text-4xl font-extrabold mt-2">Касса для Windows<br/>с поддержкой весов Масса-К</h1>
<p class="mt-4 max-w-xl text-slate-600">
Установщик для Windows 10/11. Работает офлайн. Печатает фискальные чеки в ОФД РК.
Принимает Kaspi Pay, наличные и смешанные платежи.
</p>
<div class="mt-7 flex gap-3">
<span class="px-5 py-3 bg-slate-200 text-slate-500 rounded-md font-semibold cursor-not-allowed" title="В разработке">Скачать установщик</span>
<a href="/pricing" class="px-5 py-3 border border-slate-300 rounded-md font-semibold hover:bg-slate-50">Тарифы</a>
</div>
</div>
</section>
<section class="max-w-7xl mx-auto px-4 sm:px-6 py-14">
<div class="aspect-video rounded-xl bg-slate-900 flex items-center justify-center text-slate-500">Видео работы кассы · скоро</div>
</section>
<section class="max-w-7xl mx-auto px-4 sm:px-6 py-12">
<h2 class="text-3xl font-bold text-center mb-10">Что внутри</h2>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
{features.map((f) => (
<div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">{f.icon}</div>
<h3 class="font-semibold mt-2">{f.title}</h3>
<p class="text-sm text-slate-600 mt-1.5">{f.text}</p>
</div>
))}
</div>
</section>
<section class="bg-slate-50 py-14">
<div class="max-w-3xl mx-auto px-4 sm:px-6">
<h2 class="text-3xl font-bold text-center mb-6">Поддерживаемое железо</h2>
<ul class="bg-white rounded-xl border border-slate-200 divide-y divide-slate-100">
{hardware.map((h) => <li class="px-5 py-3.5 text-sm">✓ {h}</li>)}
</ul>
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,53 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
import BusinessTariffBuilder from '@/components/BusinessTariffBuilder.tsx'
---
<BaseLayout title="Тарифы" description="Простые тарифы Food Market: Старт от 5 000 ₸/мес, Бизнес-конструктор, Сеть по запросу. 90 дней бесплатно без банковской карты.">
<section class="max-w-6xl mx-auto px-4 sm:px-6 py-12">
<h1 class="text-4xl font-extrabold text-center">Тарифы Food Market</h1>
<p class="text-center text-slate-500 mt-3">Никаких скрытых доплат. CRM, финансы, лояльность включены во все тарифы.</p>
<div class="grid lg:grid-cols-3 gap-5 mt-10">
<div class="rounded-xl border border-slate-200 p-6 bg-white">
<h2 class="font-bold text-lg">Старт</h2>
<p class="text-3xl font-bold mt-2">5 000 ₸<span class="text-sm font-normal text-slate-500">/мес</span></p>
<p class="text-xs text-slate-500 mt-1">90 дней бесплатно, без карты</p>
<ul class="text-sm space-y-1.5 mt-5 text-slate-700">
<li>✓ 1 магазин · 1 касса · 1 склад</li>
<li>✓ 2 сотрудника</li>
<li>✓ Без лимита товаров</li>
<li>✓ Касса с весами Масса-К</li>
<li>✓ Импорт из МойСклад</li>
<li>✓ Интеграции Kaspi / ОФД</li>
<li class="text-slate-400">— API</li>
<li class="text-slate-400">— SLA</li>
</ul>
<a href="/signup?plan=start" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Начать</a>
</div>
<div class="lg:row-span-2">
<BusinessTariffBuilder client:load />
</div>
<div class="rounded-xl border border-slate-200 p-6 bg-white">
<h2 class="font-bold text-lg">Сеть</h2>
<p class="text-3xl font-bold mt-2">По запросу</p>
<p class="text-xs text-slate-500 mt-1">Демо + индивидуальный SLA</p>
<ul class="text-sm space-y-1.5 mt-5 text-slate-700">
<li>✓ Unlimited магазины / кассы / склады</li>
<li>✓ Unlimited сотрудники</li>
<li>✓ Расширенный API</li>
<li>✓ Выделенный менеджер</li>
<li>✓ Гарантии доступности</li>
<li>✓ Приоритетная поддержка</li>
</ul>
<a href="/contacts" class="block text-center mt-6 px-4 py-2.5 border border-slate-300 rounded-md font-semibold">Связаться</a>
</div>
</div>
<div class="mt-12 rounded-xl bg-brand-light/30 p-6 text-center">
<h3 class="font-bold">Никаких доплат за модули</h3>
<p class="text-sm text-slate-600 mt-2">CRM, финансы, лояльность, импорт, поддержка — всё включено в каждый тариф. Без сюрпризов в счёте.</p>
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,16 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
import SignupForm from '@/components/SignupForm.tsx'
const url = Astro.url
const plan = url.searchParams.get('plan') ?? 'start'
---
<BaseLayout title="Регистрация" description="Регистрация в Food Market — 90 дней бесплатно, без банковской карты.">
<section class="max-w-md mx-auto px-4 sm:px-6 py-12">
<h1 class="text-3xl font-extrabold text-center">Создать аккаунт</h1>
<p class="text-center text-slate-500 mt-2">90 дней бесплатно. Без банковской карты.</p>
<div class="mt-8 rounded-xl border border-slate-200 p-6 bg-white">
<SignupForm client:load defaultPlan={plan} />
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,18 @@
import type { APIRoute } from 'astro'
const PAGES = [
'', 'features', 'pricing', 'pos', 'migration-from-moysklad', 'integrations',
'for-grocery', 'for-pharmacy', 'for-cafe', 'for-alcohol', 'for-clothing', 'for-household',
'signup', 'about', 'contacts', 'kb', 'blog', 'status', 'changelog',
'legal/offer', 'legal/privacy', 'legal/consent', 'legal/requisites',
]
export const GET: APIRoute = ({ site }) => {
const base = (site?.toString() ?? 'https://food-market.kz').replace(/\/$/, '')
const urls = PAGES.map((p) => `<url><loc>${base}/${p}</loc></url>`).join('\n ')
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`
return new Response(xml, { headers: { 'Content-Type': 'application/xml' } })
}

View file

@ -0,0 +1,15 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
---
<BaseLayout title="Status" description="Статус систем Food Market: API, админка, касса.">
<section class="max-w-2xl mx-auto px-4 sm:px-6 py-14">
<h1 class="text-4xl font-extrabold text-center">Статус систем</h1>
<ul class="mt-10 divide-y divide-slate-200 border border-slate-200 rounded-xl bg-white">
<li class="flex items-center justify-between px-5 py-4"><span>API</span><span class="text-emerald-600 text-sm">● Работает</span></li>
<li class="flex items-center justify-between px-5 py-4"><span>Админка</span><span class="text-emerald-600 text-sm">● Работает</span></li>
<li class="flex items-center justify-between px-5 py-4"><span>База данных</span><span class="text-emerald-600 text-sm">● Работает</span></li>
<li class="flex items-center justify-between px-5 py-4"><span>Касса (Windows)</span><span class="text-slate-400 text-sm">○ В разработке</span></li>
</ul>
<p class="text-xs text-slate-400 mt-6 text-center">Раздел в разработке. Скоро добавим автоматический мониторинг и историю инцидентов.</p>
</section>
</BaseLayout>

View file

@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html { scroll-behavior: smooth; }
body { background: #fff; color: #0f172a; }

View file

@ -0,0 +1,23 @@
import type { Config } from 'tailwindcss'
// Палитра совпадает с food-market.web — тот же бренд (зелёный 00B207).
export default {
content: ['./src/**/*.{astro,tsx,ts,jsx,js,md,mdx,html}'],
theme: {
extend: {
colors: {
brand: {
DEFAULT: '#00B207',
hover: '#009305',
dark: '#007605',
light: '#E8F5E9',
tint: '#D7F2D9',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif'],
},
},
},
plugins: [],
} satisfies Config

View file

@ -0,0 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
}
}

View file

@ -1,6 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { LoginPage } from '@/pages/LoginPage'
import { AuthBridgePage } from '@/pages/AuthBridgePage'
import { DashboardPage } from '@/pages/DashboardPage'
import { OnboardingPage } from '@/pages/OnboardingPage'
import { SuperAdminDashboardPage } from '@/pages/SuperAdminDashboardPage'
@ -48,6 +49,7 @@ export default function App() {
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/auth-bridge" element={<AuthBridgePage />} />
<Route element={<ProtectedRoute />}>
{/* SuperAdmin консоль отдельный layout c индиго-сайдбаром,
* системными разделами и быстрым «Открыть организацию» в topbar.

View file

@ -0,0 +1,47 @@
import { useEffect, useState } from 'react'
/** Принимает токены из URL fragment (#access=...&refresh=...&welcome=1)
* приходит сюда после успешного signup на публичном сайте кладёт
* в localStorage в формате который ожидает auth.ts, и редиректит в /.
*
* Через fragment чтобы access_token не попадал в server-логи / Referer
* заголовки (URL fragment не отправляется на бэк). */
export function AuthBridgePage() {
const [error, setError] = useState<string | null>(null)
useEffect(() => {
try {
const hash = (window.location.hash || '').replace(/^#/, '')
const params = new URLSearchParams(hash)
const access = params.get('access')
const refresh = params.get('refresh')
const welcome = params.get('welcome') === '1'
if (!access) { setError('Токен не получен. Войдите вручную.'); return }
localStorage.setItem('fm.access_token', access)
if (refresh) localStorage.setItem('fm.refresh_token', refresh)
// Чистим fragment чтобы при F5 страница не пыталась повторно «принять»
// токены из URL.
const target = welcome ? '/?welcome=signup' : '/'
window.location.replace(target)
} catch (e) {
setError((e as Error).message)
}
}, [])
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50">
<div className="rounded-xl border border-slate-200 bg-white p-8 max-w-md text-center">
{error ? (
<>
<p className="text-red-600 font-semibold">{error}</p>
<a href="/login" className="mt-4 inline-block px-4 py-2 bg-[var(--color-brand)] text-white rounded-md">К странице входа</a>
</>
) : (
<>
<div className="w-12 h-12 mx-auto rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center text-xl"></div>
<p className="mt-3 font-semibold">Регистрация прошла</p>
<p className="text-sm text-slate-500 mt-1">Перенаправляем в админку</p>
</>
)}
</div>
</div>
)
}