initial site

This commit is contained in:
merrow committed 2026-10-11 22:24:54 +05:00
commit da5b259d06
167 files changed
+34157

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
/**
* Экспорт меню и табаков сайта в seed-JSON для CRM (crm/seed/*.json).
*
* Запуск из папки site: npm run export:crm
* CRM при старте сеет этими данными таблицы menu_items / tobacco_flavors,
* библиотеку игр (games.json, обложки -> seed/covers/) и отдаёт
* авторские миксы сайта во вкладку «Миксы» (mixes.json).
* После изменения меню на сайте — перегенерируй файлы и пересобери CRM.
*/
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { barMenuItems } from "../src/data/barMenu";
import { beerList } from "../src/data/beerMenu";
import { authorTeas } from "../src/data/teaMenu";
import {
boardGameDifficultyLabels,
boardGames,
hookahMixes,
ps5Games,
tobaccoFlavors,
} from "../src/data/catalog";
// Атомарная запись (F-6): обрыв экспорта не оставит битый seed — рестарт CRM
// с повреждённым файлом роняется fail-closed'ом и не вычистит каталог.
const outDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../crm/seed");
const writeSeedJson = (name: string, data: unknown) => {
const target = resolve(outDir, name);
const tmp = target + ".tmp";
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
renameSync(tmp, target);
};
const siteRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const gamesImagesDir = resolve(siteRoot, "public/images/games");
const menu = [
...authorTeas.map((tea) => ({
id: tea.id,
name: tea.name,
category: tea.category,
price: tea.price,
portion: tea.volume,
})),
...beerList.map((beer) => ({
id: beer.id,
name: beer.name,
category: beer.category,
price: beer.price,
portion: beer.volume,
})),
...barMenuItems.map((item) => ({
id: item.id,
name: item.name,
category: item.category,
price: item.price,
portion: item.portion ?? null,
})),
];
// Стоп-лист в CRM живёт на уровне отдельных вкусов, а не брендов:
// экспортируем все вкусы каталога (включая «Добавку» — её вкусы тоже
// участвуют в конструкторе миксов).
const tobacco = tobaccoFlavors
.map((f) => ({
id: f.id,
brand: f.brand,
flavor: f.name,
note: f.note ?? null,
strength: f.strength,
cooling: f.cooling,
}))
.sort(
(a, b) =>
a.brand.localeCompare(b.brand, "ru") || a.flavor.localeCompare(b.flavor, "ru"),
);
mkdirSync(outDir, { recursive: true });
writeSeedJson("menu.json", menu);
writeSeedJson("tobacco.json", tobacco);
// ------------------------------------------------------------------ игры
// PS5: note = жанр и игроки; обложка копируется в crm/seed/covers/
// (.jfif переименовывается в .jpg — у CRM mimetype для .jfif не настроен).
// Настолки: note = категория, игроки, время, сложность.
const coversDir = resolve(outDir, "covers");
mkdirSync(coversDir, { recursive: true });
const coverFile = (image: string): string | null => {
if (!image) return null;
const srcName = image.split("/").pop() ?? "";
const src = resolve(gamesImagesDir, srcName);
if (!existsSync(src)) return null;
const dstName = srcName.toLowerCase().endsWith(".jfif")
? srcName.slice(0, -5) + ".jpg"
: srcName;
cpSync(src, resolve(coversDir, dstName));
return dstName;
};
const games = [
...ps5Games.map((g) => ({
seed_id: g.id,
kind: "ps5",
title: g.name,
note: `${g.genre} · до ${g.maxPlayers} игрок${g.maxPlayers === 1 ? "а" : "ов"}`,
cover: coverFile(g.image),
})),
...boardGames.map((g) => ({
seed_id: `board-${g.id}`,
kind: "board",
title: g.name,
note: `${g.category} · ${g.minPlayers}–${g.maxPlayers} игроков · ${g.duration} · `
+ `${boardGameDifficultyLabels[g.difficulty].toLowerCase()}${g.hit ? " · хит" : ""}`,
cover: null,
difficulty: g.difficulty,
icon: g.icon,
accent: g.accent,
})),
];
// ------------------------------------------------------------------ миксы сайта
// Авторские миксы из каталога сайта: CRM показывает их во вкладке «Миксы»
// только на чтение — на сайте они рендерятся сами, дубли в mix_recipes не нужны.
const siteMixes = hookahMixes.map((m) => ({
id: m.id,
name: m.name,
note: m.desc,
strength: m.strength,
cooling: m.cooling,
flavor_tags: m.flavorTags,
price: m.price,
featured: m.featured ?? false,
}));
writeSeedJson("games.json", games);
writeSeedJson("mixes.json", siteMixes);
const coversCopied = readdirSync(coversDir).length;
console.log(
`menu.json: ${menu.length} позиций, tobacco.json: ${tobacco.length} вкусов,`
+ ` games.json: ${games.length} игр (обложек скопировано: ${coversCopied}),`
+ ` mixes.json: ${siteMixes.length} миксов -> ${outDir}`,
);