initial site
This commit is contained in:
commit
da5b259d06
167 files changed
+34157
No files matched your search
@@ -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}`,
|
||||
);
|
||||
Reference in new issue
Block a user