144 lines
5.1 KiB
JavaScript
144 lines
5.1 KiB
JavaScript
// Патч: под словесным знаком «НЕОН» появляется подпись «(ex Shelby)» —
|
|
// прежнее название бара. Подпись живёт до конца 2026 года: с 1 января 2027
|
|
// клиент прячет её сам (useEffect), пересборка не требуется.
|
|
// Скрипт не идемпотентен: повторный запуск откажется работать.
|
|
import { readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
|
|
const HEADER = "src/components/Header.tsx";
|
|
const FOOTER = "src/components/Footer.tsx";
|
|
|
|
const die = (msg) => {
|
|
console.error("PATCH_FAIL: " + msg);
|
|
process.exit(1);
|
|
};
|
|
|
|
// ---------- Header.tsx ----------
|
|
let src = readFileSync(HEADER, "utf8");
|
|
copyFileSync(HEADER, HEADER + ".bak");
|
|
|
|
const MARK = "export function Logo(";
|
|
const first = src.indexOf(MARK);
|
|
if (first === -1) die("Logo declaration not found in Header.tsx");
|
|
if (src.indexOf(MARK, first + 1) !== -1) die("Logo declared more than once");
|
|
|
|
// конец списка параметров: скобочный баланс от первой ( после MARK
|
|
let i = src.indexOf("(", first);
|
|
let parenDepth = 0;
|
|
for (; i < src.length; i++) {
|
|
if (src[i] === "(") parenDepth++;
|
|
else if (src[i] === ")") {
|
|
parenDepth--;
|
|
if (parenDepth === 0) {
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// пропускаем пробелы до тела функции
|
|
while (i < src.length && /\s/.test(src[i])) i++;
|
|
if (src[i] !== "{") die("Logo body brace not found");
|
|
|
|
// тело функции: баланс фигурных скобок
|
|
let depth = 0;
|
|
let end = -1;
|
|
for (; i < src.length; i++) {
|
|
if (src[i] === "{") depth++;
|
|
else if (src[i] === "}") {
|
|
depth--;
|
|
if (depth === 0) {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (end === -1) die("Logo body end not found");
|
|
const original = src.slice(first, end + 1);
|
|
if (!original.includes("</a>") || !original.includes("ОН")) {
|
|
die("Logo body does not look like the wordmark: " + JSON.stringify(original.slice(0, 240)));
|
|
}
|
|
console.log("LOGO_FOUND: " + original.length + " chars, href=" + (original.includes('#home') ? "#home" : "?"));
|
|
|
|
const replacement = `const FORMER_NAME_DEADLINE = new Date("2027-01-01T00:00:00").getTime();
|
|
|
|
export function Logo({ className }: { className?: string }) {
|
|
// «(ex Shelby)» — прежнее название бара; подпись видна до конца 2026 года,
|
|
// после новогодней ночи клиент прячет её сам, пересборка не требуется.
|
|
// Изначально true, чтобы пререндеренный HTML и первая клиентская отрисовка
|
|
// совпадали (без hydration-мисматча); эффект убирает надпись после дедлайна.
|
|
const [showFormerName, setShowFormerName] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (Date.now() >= FORMER_NAME_DEADLINE) setShowFormerName(false);
|
|
}, []);
|
|
|
|
return (
|
|
<a
|
|
href="#home"
|
|
className={cn(
|
|
"group inline-flex flex-col font-display text-xl font-extrabold tracking-[0.18em] sm:text-2xl",
|
|
className,
|
|
)}
|
|
>
|
|
<span className="flex">
|
|
<span className="text-glow-violet text-white transition-all duration-300 group-hover:text-fuchsia-200">
|
|
НЕ
|
|
</span>
|
|
<span className="text-glow-cyan text-white transition-all duration-300 group-hover:text-cyan-200">
|
|
ОН
|
|
</span>
|
|
</span>
|
|
{showFormerName && (
|
|
<span className="mt-1 text-[9px] font-semibold tracking-[0.32em] text-mist/45 transition-colors duration-300 group-hover:text-mist/75">
|
|
(ex Shelby)
|
|
</span>
|
|
)}
|
|
</a>
|
|
);
|
|
}`;
|
|
|
|
src = src.slice(0, first) + replacement + src.slice(end + 1);
|
|
|
|
// импорты useState/useEffect, если их ещё нет до объявления Logo
|
|
const beforeLogo = src.slice(0, src.indexOf("export function Logo"));
|
|
if (!/\buseEffect\b/.test(beforeLogo)) {
|
|
if (/import \{[^}]*\} from "react";/.test(src)) {
|
|
src = src.replace(/import \{([^}]*)\} from "react";/, (m, names) => {
|
|
const names2 = names
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
for (const n of ["useState", "useEffect"]) {
|
|
if (!names2.includes(n)) names2.push(n);
|
|
}
|
|
return `import { ${names2.join(", ")} } from "react";`;
|
|
});
|
|
} else {
|
|
src = `import { useEffect, useState } from "react";\n` + src;
|
|
}
|
|
console.log("REACT_IMPORT_PATCHED");
|
|
} else {
|
|
console.log("REACT_IMPORT_ALREADY_THERE");
|
|
}
|
|
|
|
writeFileSync(HEADER, src);
|
|
console.log("HEADER_PATCHED");
|
|
|
|
// ---------- Footer.tsx ----------
|
|
const footer = readFileSync(FOOTER, "utf8");
|
|
if (/<Logo\b/.test(footer)) {
|
|
console.log("FOOTER_USES_SHARED_LOGO");
|
|
} else {
|
|
console.log("FOOTER_NEEDS_MANUAL: no <Logo usage; wordmark occurrences:");
|
|
let idx = footer.indexOf("ОН");
|
|
let n = 0;
|
|
while (idx !== -1 && n < 5) {
|
|
console.log(
|
|
"--- @" + idx + ": " + JSON.stringify(footer.slice(Math.max(0, idx - 140), idx + 180)),
|
|
);
|
|
idx = footer.indexOf("ОН", idx + 1);
|
|
n++;
|
|
}
|
|
}
|
|
|
|
console.log("PATCH_OK");
|