Files
filtry_www/wersja1/app.js
T
2026-09-19 22:16:00 +02:00

178 lines
8.3 KiB
JavaScript

const state = { rows: [], mode: "machine", suggestionIndex: -1 };
const labels = {
oil_filters: "Olej",
fuel_filters: "Paliwo",
air_filters: "Powietrze",
hydraulic_filters: "Hydrauliczny",
other_filters: "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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" }[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 Object.keys(labels).map((key) => row[key]).join(" ");
}
function filterParts(value) {
const parts = value.split(/\s*\|\s*/)
.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;
render();
renderSuggestions();
}
async function loadDefault() {
try {
const response = await fetch(`atut_baza.csv?v=${Date.now()}`, { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setLoaded(parseCsv(await response.text()), "Źródło: atut_baza.csv");
} catch (error) {
$("#results").innerHTML = `<div class="empty">Nie udało się wczytać bazy danych. Uruchom aplikację przez lokalny serwer.</div>`;
}
}
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();