init
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
const state = { rows: [], mode: "machine", suggestionIndex: -1 };
|
||||
const labels = {
|
||||
oil_fuel_filters: "Olej / paliwo",
|
||||
air_cooling_filters: "Powietrze / chłodzenie",
|
||||
hydraulic_other_filters: "Hydrauliczny / inne"
|
||||
};
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
|
||||
function parseCsv(text) {
|
||||
const rows = [], record = [];
|
||||
let field = "", quoted = false;
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
const char = text[i], next = text[i + 1];
|
||||
if (char === '"' && quoted && next === '"') { field += '"'; i += 1; }
|
||||
else if (char === '"') quoted = !quoted;
|
||||
else if (char === "," && !quoted) { record.push(field); field = ""; }
|
||||
else if ((char === "\n" || char === "\r") && !quoted) {
|
||||
if (char === "\r" && next === "\n") i += 1;
|
||||
record.push(field); field = "";
|
||||
if (record.some((value) => value.trim())) rows.push(record.splice(0));
|
||||
} else field += char;
|
||||
}
|
||||
record.push(field);
|
||||
if (record.some((value) => value.trim())) rows.push(record);
|
||||
const headers = rows.shift().map((header) => header.replace(/^\uFEFF/, "").trim());
|
||||
return rows.map((values) => Object.fromEntries(headers.map((header, index) => [header, (values[index] || "").trim()])));
|
||||
}
|
||||
|
||||
function normalized(value) {
|
||||
return (value || "").toLocaleLowerCase("pl-PL").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||
}
|
||||
function escapeHtml(value) {
|
||||
return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
||||
}
|
||||
function highlighted(value, query) {
|
||||
const safe = escapeHtml(value);
|
||||
if (!query) return safe;
|
||||
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return safe.replace(new RegExp(`(${escaped})`, "ig"), "<mark>$1</mark>");
|
||||
}
|
||||
function allFilterText(row) {
|
||||
return ["oil_fuel_filters", "air_cooling_filters", "hydraulic_other_filters"].map((key) => row[key]).join(" ");
|
||||
}
|
||||
function filterParts(value) {
|
||||
const parts = value.split(/\s*\|\s*|(?=(?:Olej|Paliwo|Powietrze|Hydrauliczny|Odpowietrznik|Płyn|Kabinowy)\b)/i)
|
||||
.map((part) => part.replace(/\s*\|\s*$/, "").trim()).filter(Boolean);
|
||||
return parts.length ? parts : [value];
|
||||
}
|
||||
function suggestionData(query) {
|
||||
const needle = normalized(query);
|
||||
if (state.mode === "machine") {
|
||||
return [...new Set(state.rows.map((row) => row.model).filter(Boolean))]
|
||||
.filter((value) => normalized(value).includes(needle))
|
||||
.sort((a, b) => a.localeCompare(b, "pl"))
|
||||
.slice(0, 8)
|
||||
.map((value) => ({ value, meta: "model maszyny" }));
|
||||
}
|
||||
const values = new Map();
|
||||
Object.entries(labels).forEach(([key, label]) => {
|
||||
state.rows.forEach((row) => {
|
||||
const value = row[key];
|
||||
if (!value) return;
|
||||
filterParts(value).forEach((part) => {
|
||||
if (normalized(part).includes(needle) && !values.has(part)) values.set(part, label);
|
||||
});
|
||||
});
|
||||
});
|
||||
return [...values.entries()].sort(([a], [b]) => a.localeCompare(b, "pl")).slice(0, 8)
|
||||
.map(([value, meta]) => ({ value, meta }));
|
||||
}
|
||||
function renderSuggestions() {
|
||||
const input = $("#searchInput");
|
||||
const query = input.value.trim();
|
||||
const container = $("#suggestions");
|
||||
state.suggestionIndex = -1;
|
||||
if (query.length < 2 || !state.rows.length) {
|
||||
container.classList.remove("visible");
|
||||
container.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const suggestions = suggestionData(query);
|
||||
container.innerHTML = suggestions.map((item, index) =>
|
||||
`<button class="suggestion" type="button" role="option" data-index="${index}" data-value="${escapeHtml(item.value)}">
|
||||
<span class="suggestion-value">${highlighted(item.value, query)}</span><span class="suggestion-meta">${item.meta}</span>
|
||||
</button>`).join("");
|
||||
container.classList.toggle("visible", suggestions.length > 0);
|
||||
container.querySelectorAll(".suggestion").forEach((button) => button.addEventListener("mousedown", (event) => {
|
||||
event.preventDefault();
|
||||
input.value = button.dataset.value;
|
||||
container.classList.remove("visible");
|
||||
$("#clearButton").classList.add("visible");
|
||||
render();
|
||||
}));
|
||||
}
|
||||
function render() {
|
||||
const query = $("#searchInput").value.trim();
|
||||
const needle = normalized(query);
|
||||
const results = state.rows.filter((row) => {
|
||||
const haystack = state.mode === "machine"
|
||||
? [row.model, row.machine_type, row.engine, row.year, row.notes].join(" ")
|
||||
: allFilterText(row);
|
||||
return !needle || normalized(haystack).includes(needle);
|
||||
});
|
||||
$("#resultsTitle").textContent = state.mode === "machine" ? (query ? "Pasujące maszyny" : "Maszyny w bazie") : (query ? "Maszyny pasujące do filtra" : "Wyszukaj numer filtra");
|
||||
$("#resultCount").textContent = query ? `${results.length} wyników` : `${state.rows.length} rekordów`;
|
||||
const container = $("#results");
|
||||
container.innerHTML = "";
|
||||
if (!results.length) {
|
||||
container.innerHTML = `<div class="empty">Nie znaleziono wyników. Spróbuj krótszego numeru filtra lub nazwy modelu.</div>`;
|
||||
return;
|
||||
}
|
||||
results.forEach((row) => {
|
||||
const card = $("#machineTemplate").content.cloneNode(true);
|
||||
card.querySelector(".card-kicker").textContent = row.machine_type || "Maszyna";
|
||||
card.querySelector(".machine-name").innerHTML = highlighted(row.model, query);
|
||||
card.querySelector(".year").textContent = row.year ? `Rok ${row.year.replace(".0", "")}` : "";
|
||||
card.querySelector(".engine").textContent = row.engine ? `Silnik: ${row.engine}` : "Silnik: brak danych";
|
||||
const grid = card.querySelector(".filter-grid");
|
||||
Object.entries(labels).forEach(([key, label]) => {
|
||||
if (!row[key]) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "filter";
|
||||
const values = filterParts(row[key])
|
||||
.map((value) => `<span class="filter-line">${highlighted(value, query)}</span>`)
|
||||
.join("");
|
||||
item.innerHTML = `<span class="filter-label">${label}</span><span class="filter-value">${values}</span>`;
|
||||
grid.appendChild(item);
|
||||
});
|
||||
card.querySelector(".source").textContent = row.source_page ? `Źródło: strona ${row.source_page} PDF` : "";
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
function setLoaded(rows, source) {
|
||||
state.rows = rows;
|
||||
$("#statusDot").classList.add("ready");
|
||||
$("#statusTitle").textContent = "Baza załadowana";
|
||||
$("#statusText").textContent = source;
|
||||
$("#stats").textContent = `${rows.length} rekordów · ${new Set(rows.map((row) => row.model)).size} modeli`;
|
||||
render();
|
||||
renderSuggestions();
|
||||
}
|
||||
async function loadDefault() {
|
||||
try {
|
||||
const response = await fetch("atut_baza.csv");
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
setLoaded(parseCsv(await response.text()), "Źródło: atut_baza.csv");
|
||||
} catch (error) {
|
||||
$("#statusTitle").textContent = "Nie udało się wczytać CSV";
|
||||
$("#statusText").textContent = "Uruchom aplikację przez lokalny serwer, aby wczytać bazę danych.";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab").forEach((item) => item.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
state.mode = tab.dataset.mode;
|
||||
$("#searchLabel").textContent = state.mode === "machine" ? "Model, typ lub silnik maszyny" : "Numer lub oznaczenie filtra";
|
||||
$("#searchInput").placeholder = state.mode === "machine" ? "np. QES 20, AMMANN albo KUBOTA" : "np. C 14 200, WK 1020X albo FF00758";
|
||||
$("#searchHint").textContent = state.mode === "machine" ? "Wpisz minimum 2 znaki. Wyniki aktualizują się automatycznie." : "Wpisz fragment oznaczenia, np. C 14 200 albo WK 1020X.";
|
||||
render();
|
||||
renderSuggestions();
|
||||
}));
|
||||
$("#searchInput").addEventListener("input", () => { $("#clearButton").classList.toggle("visible", Boolean($("#searchInput").value)); render(); renderSuggestions(); });
|
||||
$("#searchInput").addEventListener("keydown", (event) => {
|
||||
const items = [...document.querySelectorAll(".suggestion")];
|
||||
if (!items.length) return;
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
state.suggestionIndex = (state.suggestionIndex + (event.key === "ArrowDown" ? 1 : items.length - 1)) % items.length;
|
||||
items.forEach((item, index) => item.classList.toggle("selected", index === state.suggestionIndex));
|
||||
} else if (event.key === "Enter" && state.suggestionIndex >= 0) {
|
||||
event.preventDefault();
|
||||
items[state.suggestionIndex].dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
} else if (event.key === "Escape") {
|
||||
$("#suggestions").classList.remove("visible");
|
||||
}
|
||||
});
|
||||
$("#searchInput").addEventListener("focus", renderSuggestions);
|
||||
$("#searchInput").addEventListener("blur", () => setTimeout(() => $("#suggestions").classList.remove("visible"), 120));
|
||||
$("#clearButton").addEventListener("click", () => { $("#searchInput").value = ""; $("#clearButton").classList.remove("visible"); render(); renderSuggestions(); $("#searchInput").focus(); });
|
||||
loadDefault();
|
||||
Reference in New Issue
Block a user