chore: align ecosystem to Next.js 16.1.6 and v1.6.0, migrate to ESLint 9 Flat Config
Some checks failed
CI - Lint, Typecheck & Test / quality-assurance (push) Failing after 30s

This commit is contained in:
2026-02-09 23:23:31 +01:00
parent 6451a9e28e
commit eb388610de
85 changed files with 14182 additions and 30922 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,192 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import type { ProductData } from './types';
import { normalizeValue } from './utils';
type ExcelRow = Record<string, unknown>;
export type ExcelMatch = { rows: ExcelRow[]; units: Record<string, string> };
export type MediumVoltageCrossSectionExcelMatch = {
headerRow: ExcelRow;
rows: ExcelRow[];
units: Record<string, string>;
partNumberKey: string;
crossSectionKey: string;
ratedVoltageKey: string | null;
};
const EXCEL_SOURCE_FILES = [
path.join(process.cwd(), 'data/excel/high-voltage.xlsx'),
path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/excel/solar-cables.xlsx'),
];
// Medium-voltage cross-section table (new format with multi-row header).
// IMPORTANT: this must NOT be used for the technical data table.
const MV_CROSS_SECTION_FILE = path.join(process.cwd(), 'data/excel/medium-voltage-KM 170126.xlsx');
type MediumVoltageCrossSectionIndex = {
headerRow: ExcelRow;
units: Record<string, string>;
partNumberKey: string;
crossSectionKey: string;
ratedVoltageKey: string | null;
rowsByDesignation: Map<string, ExcelRow[]>;
};
let EXCEL_INDEX: Map<string, ExcelMatch> | null = null;
let MV_CROSS_SECTION_INDEX: MediumVoltageCrossSectionIndex | null = null;
export function normalizeExcelKey(value: string): string {
return String(value || '')
.toUpperCase()
.replace(/-\d+$/g, '')
.replace(/[^A-Z0-9]+/g, '');
}
function loadExcelRows(filePath: string): ExcelRow[] {
const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
const trimmed = out.trim();
const jsonStart = trimmed.indexOf('[');
if (jsonStart < 0) return [];
const jsonText = trimmed.slice(jsonStart);
try {
return JSON.parse(jsonText) as ExcelRow[];
} catch {
return [];
}
}
function findKeyByHeaderValue(headerRow: ExcelRow, pattern: RegExp): string | null {
for (const [k, v] of Object.entries(headerRow || {})) {
const text = normalizeValue(String(v ?? ''));
if (!text) continue;
if (pattern.test(text)) return k;
}
return null;
}
function getMediumVoltageCrossSectionIndex(): MediumVoltageCrossSectionIndex {
if (MV_CROSS_SECTION_INDEX) return MV_CROSS_SECTION_INDEX;
const rows = fs.existsSync(MV_CROSS_SECTION_FILE) ? loadExcelRows(MV_CROSS_SECTION_FILE) : [];
const headerRow = (rows[0] || {}) as ExcelRow;
const partNumberKey = findKeyByHeaderValue(headerRow, /^part\s*number$/i) || '__EMPTY';
const crossSectionKey = findKeyByHeaderValue(headerRow, /querschnitt|cross.?section/i) || '';
const ratedVoltageKey = findKeyByHeaderValue(headerRow, /rated voltage|voltage rating|nennspannung/i) || null;
const unitsRow = rows.find(r => normalizeValue(String((r as ExcelRow)?.[partNumberKey] ?? '')) === 'Units') || null;
const units: Record<string, string> = {};
if (unitsRow) {
for (const [k, v] of Object.entries(unitsRow)) {
if (k === partNumberKey) continue;
const unit = normalizeValue(String(v ?? ''));
if (unit) units[k] = unit;
}
}
const rowsByDesignation = new Map<string, ExcelRow[]>();
for (const r of rows) {
if (r === headerRow) continue;
const pn = normalizeValue(String((r as ExcelRow)?.[partNumberKey] ?? ''));
if (!pn || pn === 'Units' || pn === 'Part Number') continue;
const key = normalizeExcelKey(pn);
if (!key) continue;
const cur = rowsByDesignation.get(key) || [];
cur.push(r);
rowsByDesignation.set(key, cur);
}
MV_CROSS_SECTION_INDEX = { headerRow, units, partNumberKey, crossSectionKey, ratedVoltageKey, rowsByDesignation };
return MV_CROSS_SECTION_INDEX;
}
export function getExcelIndex(): Map<string, ExcelMatch> {
if (EXCEL_INDEX) return EXCEL_INDEX;
const idx = new Map<string, ExcelMatch>();
for (const file of EXCEL_SOURCE_FILES) {
if (!fs.existsSync(file)) continue;
const rows = loadExcelRows(file);
const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
const units: Record<string, string> = {};
if (unitsRow) {
for (const [k, v] of Object.entries(unitsRow)) {
if (k === 'Part Number') continue;
const unit = normalizeValue(String(v ?? ''));
if (unit) units[k] = unit;
}
}
for (const r of rows) {
const pn = r?.['Part Number'];
if (!pn || pn === 'Units') continue;
const key = normalizeExcelKey(String(pn));
if (!key) continue;
const cur = idx.get(key);
if (!cur) {
idx.set(key, { rows: [r], units });
} else {
cur.rows.push(r);
if (Object.keys(cur.units).length < Object.keys(units).length) cur.units = units;
}
}
}
EXCEL_INDEX = idx;
return idx;
}
export function findExcelForProduct(product: ProductData): ExcelMatch | null {
const idx = getExcelIndex();
const candidates = [
product.name,
product.slug ? product.slug.replace(/-\d+$/g, '') : '',
product.sku,
product.translationKey,
].filter(Boolean) as string[];
for (const c of candidates) {
const key = normalizeExcelKey(c);
const match = idx.get(key);
if (match && match.rows.length) return match;
}
return null;
}
export function findMediumVoltageCrossSectionExcelForProduct(product: ProductData): MediumVoltageCrossSectionExcelMatch | null {
const idx = getMediumVoltageCrossSectionIndex();
const candidates = [
product.name,
product.slug ? product.slug.replace(/-\d+$/g, '') : '',
product.sku,
product.translationKey,
].filter(Boolean) as string[];
for (const c of candidates) {
const key = normalizeExcelKey(c);
const rows = idx.rowsByDesignation.get(key) || [];
if (rows.length) {
return {
headerRow: idx.headerRow,
rows,
units: idx.units,
partNumberKey: idx.partNumberKey,
crossSectionKey: idx.crossSectionKey,
ratedVoltageKey: idx.ratedVoltageKey,
};
}
}
return null;
}

View File

@@ -1,54 +0,0 @@
export interface ProductData {
id: number;
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
applicationHtml: string;
images: string[];
featuredImage: string | null;
sku: string;
slug?: string;
path?: string;
translationKey?: string;
locale?: 'en' | 'de';
categories: Array<{ name: string }>;
attributes: Array<{
name: string;
options: string[];
}>;
voltageType?: string;
}
export type KeyValueItem = { label: string; value: string; unit?: string };
export type DatasheetVoltageTable = {
voltageLabel: string;
metaItems: KeyValueItem[];
columns: Array<{ key: string; label: string }>;
rows: Array<{ configuration: string; cells: string[] }>;
};
export type DatasheetModel = {
locale: 'en' | 'de';
product: {
id: number;
name: string;
sku: string;
categoriesLine: string;
descriptionText: string;
heroSrc: string | null;
productUrl: string;
};
labels: {
datasheet: string;
description: string;
technicalData: string;
crossSection: string;
sku: string;
noImage: string;
};
technicalItems: KeyValueItem[];
voltageTables: DatasheetVoltageTable[];
legendItems: KeyValueItem[];
};

View File

@@ -1,72 +0,0 @@
import * as path from 'path';
import type { ProductData } from './types';
export const CONFIG = {
siteUrl: 'https://klz-cables.com',
publicDir: path.join(process.cwd(), 'public'),
assetMapFile: path.join(process.cwd(), 'data/processed/asset-map.json'),
} as const;
export function stripHtml(html: string): string {
if (!html) return '';
let text = String(html).replace(/<[^>]*>/g, '').normalize('NFC');
text = text
.replace(/[\u00A0\u202F]/g, ' ')
.replace(/[\u2013\u2014]/g, '-')
.replace(/[\u2018\u2019]/g, "'")
.replace(/[\u201C\u201D]/g, '"')
.replace(/\u2026/g, '...')
.replace(/[\u2022]/g, '·')
.replace(/[\u2264]/g, '<=')
.replace(/[\u2265]/g, '>=')
.replace(/[\u2248]/g, '~')
.replace(/[\u03A9\u2126]/g, 'Ohm')
.replace(/[\u00B5\u03BC]/g, 'u')
.replace(/[\u2193]/g, 'v')
.replace(/[\u2191]/g, '^')
.replace(/[\u00B0]/g, '°');
// eslint-disable-next-line no-control-regex
text = text.replace(/[\u0000-\u001F\u007F]/g, '');
return text.replace(/\s+/g, ' ').trim();
}
export function normalizeValue(value: string): string {
return stripHtml(value).replace(/\s+/g, ' ').trim();
}
export function getProductUrl(product: ProductData): string {
if (product.path) return `${CONFIG.siteUrl}${product.path}`;
return CONFIG.siteUrl;
}
export function generateFileName(product: ProductData, locale: 'en' | 'de'): string {
const baseName = product.slug || product.translationKey || `product-${product.id}`;
const cleanSlug = baseName
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
return `${cleanSlug}-${locale}.pdf`;
}
export function getLabels(locale: 'en' | 'de') {
return {
en: {
datasheet: 'Technical Datasheet',
description: 'APPLICATION',
technicalData: 'TECHNICAL DATA',
crossSection: 'Cross-sections/Voltage',
sku: 'SKU',
noImage: 'No image available',
},
de: {
datasheet: 'Technisches Datenblatt',
description: 'ANWENDUNG',
technicalData: 'TECHNISCHE DATEN',
crossSection: 'Querschnitte/Spannung',
sku: 'ARTIKELNUMMER',
noImage: 'Kein Bild verfügbar',
},
}[locale];
}