initial site
This commit is contained in:
commit
da5b259d06
167 files changed
+34157
No files matched your search
@@ -0,0 +1,575 @@
|
||||
// Патч «больше миксов из табаков сайта + стоп-лист миксов».
|
||||
// 1) mixEngine: generateShowcaseMixes принимает исключённые вкусы и берёт
|
||||
// до 2 миксов на комбинацию «профиль × крепость × холодок»;
|
||||
// + withStopWarnings/STOP_LIST_WARNING (помечает миксы со стоп-табаком).
|
||||
// 2) catalog: блок витрины переносится ПОСЛЕ tobaccoFlavors, витрина
|
||||
// собирается только из табаков, реально доступных на сайте.
|
||||
// 3) MixesPage: стоп-лист CRM — микс уходит в стоп-лист (бейдж, гашение,
|
||||
// красная подсветка стоп-вкусов, предупреждение на карточке).
|
||||
// 4) AiMixPage: выдача и микс мастера перепроверяются на стоп-табак
|
||||
// (share-восстановление и мастер-подбор не применяли стоп-лист).
|
||||
// 5) HeroActions: русское множественное для счётчика миксов.
|
||||
// Каждый якорь проверяется на точное число вхождений; при расхождении —
|
||||
// die() без записи файла. Повторный запуск: пропускает уже пропатченное.
|
||||
import { readFileSync, writeFileSync, copyFileSync, rmSync, existsSync } from "node:fs";
|
||||
|
||||
const die = (msg) => {
|
||||
console.error("PATCH_FAIL: " + msg);
|
||||
process.exit(1);
|
||||
};
|
||||
const rep = (src, from, to, label) => {
|
||||
const parts = src.split(from);
|
||||
if (parts.length - 1 !== 1) die(`${label}: found ${parts.length - 1}, expected 1`);
|
||||
return parts.join(to);
|
||||
};
|
||||
|
||||
/* ------------------------- 1. mixEngine.ts ------------------------- */
|
||||
{
|
||||
const FILE = "src/lib/mixEngine.ts";
|
||||
let src = readFileSync(FILE, "utf8");
|
||||
if (src.includes("withStopWarnings")) {
|
||||
console.log("1 mixEngine: ALREADY_PATCHED");
|
||||
} else {
|
||||
copyFileSync(FILE, FILE + ".bak");
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
"export interface ShowcaseMix {",
|
||||
[
|
||||
"/* ---------------- CRM stop-list: warnings in results ------------------- */",
|
||||
"",
|
||||
"/** Warning on a mix card whose tobacco has gone into the CRM stop-list. */",
|
||||
'export const STOP_LIST_WARNING =',
|
||||
' "Some tobaccos in the composition are in the stop-list — the hookah lounge attendant will pick a replacement.";',
|
||||
"",
|
||||
"/**",
|
||||
" * Marks mixes whose composition contains a stopped tobacco (matching by",
|
||||
" * the engine flavor's catalogId). If there is nothing to change, returns",
|
||||
" * the same references — safe to call on every stop-list poll.",
|
||||
" */",
|
||||
"export function withStopWarnings<T extends { items: { flavorId: string }[]; warning?: string }>(",
|
||||
" mixes: T[],",
|
||||
" stoppedCatalogIds: ReadonlySet<string>,",
|
||||
"): T[] {",
|
||||
" if (stoppedCatalogIds.size === 0) return mixes;",
|
||||
" let changed = false;",
|
||||
" const next = mixes.map((mix) => {",
|
||||
" const hit = mix.items.some((item) => {",
|
||||
" const flavor = byId.get(item.flavorId);",
|
||||
" return !!flavor && stoppedCatalogIds.has(flavor.catalogId ?? flavor.id);",
|
||||
" });",
|
||||
" if (!hit || mix.warning === STOP_LIST_WARNING) return mix;",
|
||||
" changed = true;",
|
||||
" return { ...mix, warning: STOP_LIST_WARNING };",
|
||||
" });",
|
||||
" return changed ? next : mixes;",
|
||||
"}",
|
||||
"",
|
||||
"export interface ShowcaseMix {",
|
||||
].join("\n"),
|
||||
"mixEngine: helpers before ShowcaseMix",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
"export function generateShowcaseMixes(): ShowcaseMix[] {",
|
||||
"export function generateShowcaseMixes(excludedFlavorIds: string[] = []): ShowcaseMix[] {",
|
||||
"mixEngine: showcase signature",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
" excludedCategories: [],",
|
||||
" excludedFlavorIds: [],",
|
||||
" }, seed++);",
|
||||
" const pick = mixes.find((m) => !m.unverified && !seen.has(m.signature));",
|
||||
" if (!pick) continue;",
|
||||
" seen.add(pick.signature);",
|
||||
" let name = pick.name;",
|
||||
" if (seenNames.has(name)) {",
|
||||
" name = v.cooling ? `${name} · с холодком` : `${name} · без холодка`;",
|
||||
" let n = 2;",
|
||||
" while (seenNames.has(name)) {",
|
||||
" name = `${pick.name} · вариант ${n}`;",
|
||||
" n += 1;",
|
||||
" }",
|
||||
" }",
|
||||
" seenNames.add(name);",
|
||||
" out.push({ mix: { ...pick, name }, strength: v.level, cooling: v.cold });",
|
||||
].join("\n"),
|
||||
[
|
||||
" excludedCategories: [],",
|
||||
" excludedFlavorIds,",
|
||||
" }, seed++);",
|
||||
" // до двух миксов на комбинацию «профиль × крепость × холодок»:",
|
||||
" // витрина заметно больше, составы по-прежнему уникальны и без гипотез",
|
||||
" const picks = mixes",
|
||||
" .filter((m) => !m.unverified && !seen.has(m.signature))",
|
||||
" .slice(0, 2);",
|
||||
" for (const pick of picks) {",
|
||||
" seen.add(pick.signature);",
|
||||
" let name = pick.name;",
|
||||
" if (seenNames.has(name)) {",
|
||||
" name = v.cooling ? `${name} · с холодком` : `${name} · без холодка`;",
|
||||
" let n = 2;",
|
||||
" while (seenNames.has(name)) {",
|
||||
" name = `${pick.name} · вариант ${n}`;",
|
||||
" n += 1;",
|
||||
" }",
|
||||
" }",
|
||||
" seenNames.add(name);",
|
||||
" out.push({ mix: { ...pick, name }, strength: v.level, cooling: v.cold });",
|
||||
" }",
|
||||
].join("\n"),
|
||||
"mixEngine: showcase picks",
|
||||
);
|
||||
|
||||
writeFileSync(FILE, src);
|
||||
console.log("1 mixEngine: PATCHED");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------- 2. catalog.ts -------------------------- */
|
||||
{
|
||||
const FILE = "src/data/catalog.ts";
|
||||
let src = readFileSync(FILE, "utf8");
|
||||
if (src.includes("siteUnavailableFlavorIds")) {
|
||||
console.log("2 catalog: ALREADY_PATCHED");
|
||||
} else {
|
||||
copyFileSync(FILE, FILE + ".bak");
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
'import { CATEGORY_LABELS } from "./flavors";',
|
||||
'import { CATEGORY_LABELS, FLAVORS } from "./flavors";',
|
||||
"catalog: FLAVORS import",
|
||||
);
|
||||
|
||||
// вырезаем блок витрины (до tobaccoFlavors ещё не инициализирован)
|
||||
const BLOCK_START = "/** Витрина: генерации движка по всем профилям/крепостям/холодку. */";
|
||||
const BLOCK_END =
|
||||
' "Цены указаны за один кальян. Крепкие миксы — 1 800 ₽, остальные — 1 400 ₽.";';
|
||||
const startIdx = src.indexOf(BLOCK_START);
|
||||
if (startIdx === -1) die("catalog: showcase block start not found");
|
||||
if (src.indexOf(BLOCK_START, startIdx + 1) !== -1) die("catalog: block start not unique");
|
||||
const endIdx = src.indexOf(BLOCK_END, startIdx);
|
||||
if (endIdx === -1) die("catalog: showcase block end not found");
|
||||
const blockEnd = endIdx + BLOCK_END.length;
|
||||
let block = src.slice(startIdx, blockEnd);
|
||||
|
||||
block = rep(
|
||||
block,
|
||||
"const showcase = generateShowcaseMixes();",
|
||||
[
|
||||
"/** Табаки, которых нет на сайте (или они помечены недоступными), в витрину",
|
||||
" * не берём: каждый микс собран из реального ассортимента «Вкусов». */",
|
||||
"const siteUnavailableFlavorIds: string[] = FLAVORS.filter((f) => {",
|
||||
" const t = tobaccoFlavors.find((x) => x.id === (f.catalogId ?? f.id));",
|
||||
" return !!t && !t.available;",
|
||||
"}).map((f) => f.id);",
|
||||
"",
|
||||
"/** Витрина: генерации движка по всем профилям/крепостям/холодку. */",
|
||||
"const showcase = generateShowcaseMixes(siteUnavailableFlavorIds);",
|
||||
].join("\n"),
|
||||
"catalog: showcase call",
|
||||
);
|
||||
|
||||
// удаляем блок из старого места
|
||||
src = src.slice(0, startIdx) + src.slice(blockEnd);
|
||||
|
||||
// вставляем перед секцией PlayStation 5
|
||||
const psIdx = src.indexOf("/* PlayStation 5");
|
||||
if (psIdx === -1) die("catalog: PS5 section not found");
|
||||
const boxStart = src.lastIndexOf("/*", psIdx - 1);
|
||||
if (boxStart === -1) die("catalog: PS5 comment box not found");
|
||||
src = src.slice(0, boxStart) + block + "\n" + src.slice(boxStart);
|
||||
|
||||
writeFileSync(FILE, src);
|
||||
console.log("2 catalog: PATCHED (block moved, " + block.length + " chars)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------ 3. MixesPage.tsx ------------------------- */
|
||||
{
|
||||
const FILE = "src/pages/MixesPage.tsx";
|
||||
let src = readFileSync(FILE, "utf8");
|
||||
if (src.includes("markedCards")) {
|
||||
console.log("3 MixesPage: ALREADY_PATCHED");
|
||||
} else {
|
||||
copyFileSync(FILE, FILE + ".bak");
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
'import { useCustomCatalog, type CustomMix } from "../lib/crm";',
|
||||
'import { useCustomCatalog, useStoplist, type CustomMix } from "../lib/crm";',
|
||||
"MixesPage: crm import",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
" /** вкус-холодок (синяя точка и роль «Холодок») */",
|
||||
" cold: boolean;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
[
|
||||
" /** вкус-холодок (синяя точка и роль «Холодок») */",
|
||||
" cold: boolean;",
|
||||
" /** табак в каталоге сайта — по нему сверяется стоп-лист CRM */",
|
||||
" catalogId: string | null;",
|
||||
" /** этот вкус ушёл в стоп-лист */",
|
||||
" stopped: boolean;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
"MixesPage: UnifiedItem",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
" items: UnifiedItem[];",
|
||||
"}",
|
||||
].join("\n"),
|
||||
[
|
||||
" items: UnifiedItem[];",
|
||||
" /** хотя бы один вкус состава в стоп-листе CRM — карточка гасится */",
|
||||
" stopped: boolean;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
"MixesPage: UnifiedMix",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
" return {",
|
||||
" title: flavor?.name ?? item.flavorId,",
|
||||
" sub: flavor && flavor.name !== translation ? translation : null,",
|
||||
" cold: !!flavor && isColdFlavor(flavor),",
|
||||
" };",
|
||||
].join("\n"),
|
||||
[
|
||||
" return {",
|
||||
" title: flavor?.name ?? item.flavorId,",
|
||||
" sub: flavor && flavor.name !== translation ? translation : null,",
|
||||
" cold: !!flavor && isColdFlavor(flavor),",
|
||||
" catalogId: flavor ? flavor.catalogId ?? flavor.id : null,",
|
||||
" stopped: false,",
|
||||
" };",
|
||||
].join("\n"),
|
||||
"MixesPage: itemsFromAi",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
" return {",
|
||||
" title: f.flavor,",
|
||||
" sub: f.brand,",
|
||||
" cold: !!siteFlavor && isColdFlavor(siteFlavor),",
|
||||
" };",
|
||||
].join("\n"),
|
||||
[
|
||||
" return {",
|
||||
" title: f.flavor,",
|
||||
" sub: f.brand,",
|
||||
" cold: !!siteFlavor && isColdFlavor(siteFlavor),",
|
||||
" catalogId: f.id,",
|
||||
" stopped: false,",
|
||||
" };",
|
||||
].join("\n"),
|
||||
"MixesPage: itemsFromMaster",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" items: itemsFromAi(mix),\n}));",
|
||||
" items: itemsFromAi(mix),\n stopped: false,\n}));",
|
||||
"MixesPage: aiCards stopped",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" items: itemsFromMaster(mix),\n })),",
|
||||
" items: itemsFromMaster(mix),\n stopped: false,\n })),",
|
||||
"MixesPage: masterCards stopped",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" const catalog = useCustomCatalog();",
|
||||
" const catalog = useCustomCatalog();\n const stoplist = useStoplist();",
|
||||
"MixesPage: useStoplist hook",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" const allCards = useMemo(() => [...masterCards, ...aiCards], [masterCards]);",
|
||||
[
|
||||
" const allCards = useMemo(() => [...masterCards, ...aiCards], [masterCards]);",
|
||||
"",
|
||||
" // стоп-лист CRM: микс с остановленным табаком уходит в стоп-лист —",
|
||||
" // карточка гасится, стоп-вкусы в составе подсвечиваются красным",
|
||||
" const markedCards = useMemo(",
|
||||
" () =>",
|
||||
" allCards.map((card) => {",
|
||||
" const items = card.items.map((item) => ({",
|
||||
" ...item,",
|
||||
" stopped: item.catalogId != null && stoplist.tobacco.has(item.catalogId),",
|
||||
" }));",
|
||||
" return { ...card, items, stopped: items.some((item) => item.stopped) };",
|
||||
" }),",
|
||||
" [allCards, stoplist.tobacco],",
|
||||
" );",
|
||||
].join("\n"),
|
||||
"MixesPage: markedCards",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" return allCards.filter((m) => {",
|
||||
" return markedCards.filter((m) => {",
|
||||
"MixesPage: filtered source",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" }, [allCards, query, strength, cooling, flavor]);",
|
||||
" }, [markedCards, query, strength, cooling, flavor]);",
|
||||
"MixesPage: filtered deps",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" Показано {filtered.length} из {allCards.length}",
|
||||
" Показано {filtered.length} из {markedCards.length}",
|
||||
"MixesPage: counter line",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
' : "hover:border-neon-violet/40 hover:shadow-[0_18px_50px_-24px_rgba(176,38,255,0.5)]",',
|
||||
" )}",
|
||||
].join("\n"),
|
||||
[
|
||||
' : "hover:border-neon-violet/40 hover:shadow-[0_18px_50px_-24px_rgba(176,38,255,0.5)]",',
|
||||
' card.stopped && "opacity-70 saturate-[0.65]",',
|
||||
" )}",
|
||||
].join("\n"),
|
||||
"MixesPage: card dim",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
' {isMaster ? "Микс от мастера" : "Микс от ИИ"}',
|
||||
" </span>",
|
||||
" {card.tags.map((tag) => (",
|
||||
].join("\n"),
|
||||
[
|
||||
' {isMaster ? "Микс от мастера" : "Микс от ИИ"}',
|
||||
" </span>",
|
||||
" {card.stopped && (",
|
||||
' <span className="inline-flex items-center gap-1 rounded-full border border-red-400/50 bg-red-400/12 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-red-200">',
|
||||
" В стоп-листе",
|
||||
" </span>",
|
||||
" )}",
|
||||
" {card.tags.map((tag) => (",
|
||||
].join("\n"),
|
||||
"MixesPage: stop badge",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" style={{ background: segmentGradient(i, item.cold) }}",
|
||||
[
|
||||
" style={{",
|
||||
" background: item.stopped",
|
||||
' ? "linear-gradient(90deg,#f87171,#ef4444)"',
|
||||
" : segmentGradient(i, item.cold),",
|
||||
" }}",
|
||||
].join("\n"),
|
||||
"MixesPage: red dot",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
'<span className="block truncate font-semibold text-white/90">{item.title}</span>',
|
||||
'<span className={cn("block truncate font-semibold text-white/90", item.stopped && "text-red-300")}>{item.title}</span>',
|
||||
"MixesPage: red title",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" {/* крепость · холодок · цена */}",
|
||||
[
|
||||
" {card.stopped && (",
|
||||
' <p className="mt-3 rounded-xl border border-red-400/30 bg-red-400/10 p-3 text-[11px] leading-relaxed text-red-100">',
|
||||
" Часть табаков из состава сейчас в стоп-листе — кальянщик подберёт замену.",
|
||||
" </p>",
|
||||
" )}",
|
||||
"",
|
||||
" {/* крепость · холодок · цена */}",
|
||||
].join("\n"),
|
||||
"MixesPage: stop warning line",
|
||||
);
|
||||
|
||||
writeFileSync(FILE, src);
|
||||
console.log("3 MixesPage: PATCHED");
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------ 4. AiMixPage.tsx ------------------------- */
|
||||
{
|
||||
const FILE = "src/pages/AiMixPage.tsx";
|
||||
let src = readFileSync(FILE, "utf8");
|
||||
if (src.includes("withMasterStopNote")) {
|
||||
console.log("4 AiMixPage: ALREADY_PATCHED");
|
||||
} else {
|
||||
copyFileSync(FILE, FILE + ".bak");
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
'} from "../lib/mixEngine";',
|
||||
" withStopWarnings,\n} from \"../lib/mixEngine\";",
|
||||
"AiMixPage: engine import",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
"function pickMasterMix(",
|
||||
[
|
||||
"/** Предупреждение о стоп-табаке в миксе мастера: дописывается в note его",
|
||||
" * карточки (note рендерится на карточке «Микс от мастера» в выдаче).",
|
||||
" * Идемпотентно и обратимо: табак вернулся из стопа — note откатывается. */",
|
||||
'const MASTER_STOP_NOTE =',
|
||||
' "⚠ В составе есть табак из стоп-листа — мастер подберёт замену.";',
|
||||
"",
|
||||
"function withMasterStopNote(mix: CustomMix, stopped: ReadonlySet<string>): CustomMix {",
|
||||
" const hit = mix.flavors.some((f) => stopped.has(f.id));",
|
||||
' const base = (mix.note ?? "").split(MASTER_STOP_NOTE)[0].trim();',
|
||||
" if (!hit) {",
|
||||
' return base === (mix.note ?? "").trim() ? mix : { ...mix, note: base || null };',
|
||||
" }",
|
||||
' const next = [base, MASTER_STOP_NOTE].filter(Boolean).join(" ");',
|
||||
' return next === (mix.note ?? "").trim() ? mix : { ...mix, note: next };',
|
||||
"}",
|
||||
"",
|
||||
"function pickMasterMix(",
|
||||
].join("\n"),
|
||||
"AiMixPage: withMasterStopNote",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
[
|
||||
" [currentOptions, stopExcludedIds],",
|
||||
" );",
|
||||
].join("\n"),
|
||||
[
|
||||
" [currentOptions, stopExcludedIds],",
|
||||
" );",
|
||||
"",
|
||||
" /* Выдача и микс мастера перепроверяются на стоп-табак при каждом обновлении",
|
||||
" стоп-листа: восстановление из share-ссылки собирается без стоп-исключений",
|
||||
" (у каждого получателя свой стоп-лист), поэтому состав может содержать",
|
||||
" остановленный табак — тогда на карточке появляется предупреждение. */",
|
||||
' const stopKey = useMemo(() => [...stoplist.tobacco].sort().join("|"), [stoplist]);',
|
||||
" useEffect(() => {",
|
||||
' const stopped = new Set(stopKey ? stopKey.split("|") : []);',
|
||||
" setResults((prev) => (prev ? withStopWarnings(prev, stopped) : prev));",
|
||||
" setMasterPick((prev) => (prev ? withMasterStopNote(prev, stopped) : prev));",
|
||||
" }, [stopKey]);",
|
||||
].join("\n"),
|
||||
"AiMixPage: stopKey effect",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" setResults(mixes);",
|
||||
" setResults(withStopWarnings(mixes, stoplist.tobacco));",
|
||||
"AiMixPage: finish results",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" setResults(generateMixSet(shared.profile, options, shared.seed).mixes);",
|
||||
" setResults(",
|
||||
" withStopWarnings(generateMixSet(shared.profile, options, shared.seed).mixes, stoplist.tobacco),",
|
||||
" );",
|
||||
"AiMixPage: share restore",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" setMasterPick(pickMasterMix(catalog.mixes, currentOptions, runProfile, seed));",
|
||||
" setMasterPick(",
|
||||
" withMasterStopNote(pickMasterMix(catalog.mixes, currentOptions, runProfile, seed), stoplist.tobacco),",
|
||||
" );",
|
||||
"AiMixPage: finish master pick",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" setMasterPick(pickMasterMix(catalog.mixes, lastRun.options, lastRun.profile, lastRun.seed));",
|
||||
" setMasterPick(",
|
||||
" withMasterStopNote(pickMasterMix(catalog.mixes, lastRun.options, lastRun.profile, lastRun.seed), stoplist.tobacco),",
|
||||
" );",
|
||||
"AiMixPage: catalog-load master pick",
|
||||
);
|
||||
|
||||
writeFileSync(FILE, src);
|
||||
console.log("4 AiMixPage: PATCHED");
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------- 5. HeroActions.tsx ------------------------ */
|
||||
{
|
||||
const FILE = "src/components/HeroActions.tsx";
|
||||
let src = readFileSync(FILE, "utf8");
|
||||
if (src.includes("mixPlural")) {
|
||||
console.log("5 HeroActions: ALREADY_PATCHED");
|
||||
} else {
|
||||
copyFileSync(FILE, FILE + ".bak");
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
"const chips: Chip[] = [",
|
||||
[
|
||||
"/* 51 микс / 52 микса / 55 миксов — русское множественное для счётчика */",
|
||||
"const mixPlural = (n: number): string => {",
|
||||
" const mod10 = n % 10;",
|
||||
" const mod100 = n % 100;",
|
||||
' if (mod10 === 1 && mod100 !== 11) return `${n} микс`;',
|
||||
' if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return `${n} микса`;',
|
||||
" return `${n} миксов`;",
|
||||
"};",
|
||||
"",
|
||||
"const chips: Chip[] = [",
|
||||
].join("\n"),
|
||||
"HeroActions: mixPlural helper",
|
||||
);
|
||||
|
||||
src = rep(
|
||||
src,
|
||||
" { href: \"#/mixes\", icon: Sparkles, label: `${hookahMixes.length} миксов`, tone: \"violet\" },",
|
||||
' { href: "#/mixes", icon: Sparkles, label: mixPlural(hookahMixes.length), tone: "violet" },',
|
||||
"HeroActions: chip label",
|
||||
);
|
||||
|
||||
writeFileSync(FILE, src);
|
||||
console.log("5 HeroActions: PATCHED");
|
||||
}
|
||||
}
|
||||
|
||||
/* зачистка временных диагностических скриптов */
|
||||
for (const junk of ["scripts/diag-flavor-map.ts", "scripts/inspect-aimix.mjs"]) {
|
||||
if (existsSync(junk)) {
|
||||
rmSync(junk);
|
||||
console.log("removed: " + junk);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("PATCH_OK");
|
||||
Reference in new issue
Block a user