Загрузка данных
const STORAGE_KEY = "casefilter_cases";
let cases = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
let currentFilter = "all";
// ==========================================
// DOM
// ==========================================
const nameInput = document.getElementById("case-name");
const storeInput = document.getElementById("case-store");
const specsInput = document.getElementById("case-specs");
const tableBody = document.getElementById("cases-table");
const emptyState = document.getElementById("empty-state");
// ==========================================
// REQUIREMENTS
// ==========================================
function getRequirements() {
return {
price: Number(document.getElementById("req-price").value) || 10000,
gpu: Number(document.getElementById("req-gpu").value) || 360,
hdd: Number(document.getElementById("req-hdd").value) || 1,
rating: Number(document.getElementById("req-rating").value) || 4.5,
atx: document.getElementById("req-atx").checked,
top360: document.getElementById("req-top360").checked,
argb: document.getElementById("req-argb").checked,
psu: document.getElementById("req-psu").checked
};
}
// ==========================================
// PARSER
// ==========================================
function parseSpecs(text) {
const raw = text.toLowerCase();
const result = {
price: extractPrice(raw),
gpu: extractGpu(raw),
hdd: extractHdd(raw),
rating: extractRating(raw),
atx: detectATX(raw),
top360: detectTop360(raw),
argb: detectARGB(raw),
psu: detectPSU(raw),
fans: extractFans(raw),
raw: text
};
return result;
}
// ==========================================
// PRICE
// ==========================================
function extractPrice(text) {
const patterns = [
/цена[^0-9]{0,20}([0-9\s]{3,10})\s*(?:₽|руб|р\.)?/i,
/([0-9\s]{3,10})\s*(?:₽|руб|р\.)/i,
/price[^0-9]{0,20}([0-9\s]{3,10})/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
const value = parseInt(
match[1].replace(/\s/g, ""),
10
);
if (value > 500) {
return value;
}
}
}
return null;
}
// ==========================================
// GPU
// ==========================================
function extractGpu(text) {
const patterns = [
/длин[а-я]*\s*(?:видеокарты|gpu)[^0-9]{0,30}(\d{3,4})\s*мм/i,
/видеокарт[а-я]*[^0-9]{0,30}(\d{3,4})\s*мм/i,
/gpu[^0-9]{0,30}(\d{3,4})\s*мм/i,
/(\d{3,4})\s*мм[^a-zа-я]{0,10}(?:gpu|видеокарт)/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
return Number(match[1]);
}
}
return null;
}
// ==========================================
// HDD
// ==========================================
function extractHdd(text) {
const patterns = [
/(?:3\.5|3,5)[^0-9]{0,20}(\d+)\s*(?:шт|мест|отсек|накоп)/i,
/(\d+)\s*(?:x|×)\s*(?:3\.5|3,5)/i,
/(?:hdd|3\.5)[^0-9]{0,20}(\d+)/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
return Number(match[1]);
}
}
return 0;
}
// ==========================================
// RATING
// ==========================================
function extractRating(text) {
const patterns = [
/рейтинг[^0-9]{0,20}([0-5](?:[.,]\d+)?)/i,
/оценка[^0-9]{0,20}([0-5](?:[.,]\d+)?)/i,
/([0-5][.,]\d+)\s*(?:\/\s*5|из\s*5)/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
return Number(
match[1].replace(",", ".")
);
}
}
return null;
}
// ==========================================
// ATX
// ==========================================
function detectATX(text) {
return /\batx\b/i.test(text);
}
// ==========================================
// 360 TOP
// ==========================================
function detectTop360(text) {
const topWords =
/(верх|сверху|top)/i;
const radiator =
/(360|360мм|360 мм)/i;
if (!radiator.test(text)) {
return false;
}
/*
Сначала ищем прямые сочетания:
"360 мм сверху"
"верх: 360 мм"
"top 360"
*/
const directPatterns = [
/360\s*мм?[^.\n]{0,50}(верх|сверху|top)/i,
/(верх|сверху|top)[^.\n]{0,50}360\s*мм?/i,
/радиатор[^.\n]{0,80}(верх|top)[^.\n]{0,50}360/i,
/(верх|top)[^.\n]{0,80}радиатор[^.\n]{0,50}360/i
];
return directPatterns.some(pattern => pattern.test(text));
}
// ==========================================
// ARGB
// ==========================================
function detectARGB(text) {
return /argb|a-rgb|rgb|подсветк/i.test(text);
}
// ==========================================
// PSU
// ==========================================
function detectPSU(text) {
const lower = text.toLowerCase();
const bottom =
/нижн(ее|ий|ем)|bottom/i.test(lower);
const hidden =
/за\s*(?:материн|поддон)|скрыт.*бп|бп.*скрыт/i.test(lower);
const front =
/передн.*располож.*бп|бп.*передн/i.test(lower);
const top =
/верхн.*располож.*бп|бп.*верхн/i.test(lower);
if (front || top) {
return false;
}
if (bottom || hidden) {
return true;
}
/*
Если магазин просто пишет
"Расположение БП: нижнее"
— поймается выше.
*/
return false;
}
// ==========================================
// FANS
// ==========================================
function extractFans(text) {
const patterns = [
/(\d+)\s*(?:x|×)\s*(?:120|140)\s*мм/i,
/(\d+)\s*(?:шт|шт\.)[^.\n]{0,20}вентил/i,
/вентилятор[^0-9]{0,20}(\d+)/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
return Number(match[1]);
}
}
return null;
}
// ==========================================
// CHECK CASE
// ==========================================
function checkCase(specs) {
const req = getRequirements();
const checks = {
price:
specs.price !== null &&
specs.price <= req.price,
gpu:
specs.gpu !== null &&
specs.gpu >= req.gpu,
hdd:
specs.hdd >= req.hdd,
rating:
specs.rating !== null &&
specs.rating >= req.rating,
atx:
!req.atx || specs.atx,
top360:
!req.top360 || specs.top360,
argb:
!req.argb || specs.argb,
psu:
!req.psu || specs.psu
};
const requiredChecks = Object.entries(checks);
const passed = requiredChecks.every(
([, value]) => value === true
);
return {
checks,
passed
};
}
// ==========================================
// ADD CASE
// ==========================================
document
.getElementById("add-case")
.addEventListener("click", () => {
const name = nameInput.value.trim();
const store = storeInput.value.trim() || "Не указан";
const text = specsInput.value.trim();
if (!name) {
alert("Укажи название корпуса.");
return;
}
if (!text) {
alert("Вставь характеристики корпуса.");
return;
}
const specs = parseSpecs(text);
const newCase = {
id: Date.now(),
name,
store,
specs,
createdAt: new Date().toISOString()
};
cases.push(newCase);
saveCases();
render();
nameInput.value = "";
storeInput.value = "";
specsInput.value = "";
});
// ==========================================
// SAVE
// ==========================================
function saveCases() {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(cases)
);
}
// ==========================================
// RENDER
// ==========================================
function render() {
const sortedCases = sortCases([...cases]);
const filteredCases = sortedCases.filter(caseItem => {
const result = checkCase(caseItem.specs);
if (currentFilter === "passed") {
return result.passed;
}
if (currentFilter === "failed") {
return !result.passed;
}
return true;
});
tableBody.innerHTML = "";
emptyState.style.display =
filteredCases.length === 0
? "block"
: "none";
filteredCases.forEach(caseItem => {
const result =
checkCase(caseItem.specs);
const tr =
document.createElement("tr");
tr.innerHTML = `
<td class="case-name-cell">
<div class="case-name">
${escapeHTML(caseItem.name)}
</div>
<div class="case-store">
${escapeHTML(caseItem.store)}
</div>
</td>
<td>
${caseItem.specs.price !== null
? formatNumber(caseItem.specs.price) + " ₽"
: "—"}
</td>
<td>
${statusIcon(result.checks.atx)}
</td>
<td>
${statusIcon(result.checks.top360)}
</td>
<td>
${caseItem.specs.gpu !== null
? caseItem.specs.gpu + " мм"
: "—"}
</td>
<td>
${caseItem.specs.hdd}
</td>
<td>
${statusIcon(result.checks.argb)}
</td>
<td>
${statusIcon(result.checks.psu)}
</td>
<td>
${caseItem.specs.rating !== null
? caseItem.specs.rating.toFixed(1)
: "—"}
</td>
<td class="result ${result.passed ? "good" : "bad"}">
${result.passed
? "ПОДХОДИТ"
: "НЕ ПОДХОДИТ"}
</td>
<td>
<button
class="delete-btn"
onclick="deleteCase(${caseItem.id})"
title="Удалить"
>
×
</button>
</td>
`;
tableBody.appendChild(tr);
});
updateStats();
}
// ==========================================
// ICON
// ==========================================
function statusIcon(value) {
return value
? `<span class="status pass">✓</span>`
: `<span class="status fail">×</span>`;
}
// ==========================================
// DELETE
// ==========================================
function deleteCase(id) {
cases =
cases.filter(item => item.id !== id);
saveCases();
render();
}
// ==========================================
// CLEAR FORM
// ==========================================
document
.getElementById("clear-form")
.addEventListener("click", () => {
nameInput.value = "";
storeInput.value = "";
specsInput.value = "";
});
// ==========================================
// CLEAR ALL
// ==========================================
document
.getElementById("clear-all")
.addEventListener("click", () => {
if (!cases.length) {
return;
}
if (!confirm("Удалить все корпуса?")) {
return;
}
cases = [];
saveCases();
render();
});
// ==========================================
// FILTERS
// ==========================================
document
.querySelectorAll(".filter")
.forEach(button => {
button.addEventListener("click", () => {
document
.querySelectorAll(".filter")
.forEach(btn =>
btn.classList.remove("active")
);
button.classList.add("active");
currentFilter =
button.dataset.filter;
render();
});
});
// ==========================================
// REQUIREMENTS
// ==========================================
document
.getElementById("apply-requirements")
.addEventListener("click", () => {
render();
});
// ==========================================
// SORT
// ==========================================
document
.getElementById("sort-select")
.addEventListener("change", render);
function sortCases(list) {
const sort =
document.getElementById("sort-select").value;
if (sort === "price-asc") {
return list.sort(
(a, b) =>
(a.specs.price ?? Infinity) -
(b.specs.price ?? Infinity)
);
}
if (sort === "price-desc") {
return list.sort(
(a, b) =>
(b.specs.price ?? -Infinity) -
(a.specs.price ?? -Infinity)
);
}
if (sort === "rating-desc") {
return list.sort(
(a, b) =>
(b.specs.rating ?? -Infinity) -
(a.specs.rating ?? -Infinity)
);
}
if (sort === "gpu-desc") {
return list.sort(
(a, b) =>
(b.specs.gpu ?? -Infinity) -
(a.specs.gpu ?? -Infinity)
);
}
if (sort === "name") {
return list.sort(
(a, b) =>
a.name.localeCompare(
b.name,
"ru"
)
);
}
return list;
}
// ==========================================
// STATS
// ==========================================
function updateStats() {
let passed = 0;
let failed = 0;
cases.forEach(item => {
if (checkCase(item.specs).passed) {
passed++;
} else {
failed++;
}
});
document.getElementById("total-count")
.textContent = cases.length;
document.getElementById("passed-count")
.textContent = passed;
document.getElementById("failed-count")
.textContent = failed;
document.getElementById("filter-all-count")
.textContent = cases.length;
document.getElementById("filter-passed-count")
.textContent = passed;
document.getElementById("filter-failed-count")
.textContent = failed;
}
// ==========================================
// CSV EXPORT
// ==========================================
document
.getElementById("export-csv")
.addEventListener("click", exportCSV);
function exportCSV() {
if (!cases.length) {
alert("Нет корпусов для экспорта.");
return;
}
const headers = [
"Корпус",
"Магазин",
"Цена",
"ATX",
"360 TOP",
"GPU",
"HDD 3.5",
"ARGB",
"БП",
"Рейтинг",
"Вентиляторы",
"Итог"
];
const rows = cases.map(item => {
const result =
checkCase(item.specs);
return [
item.name,
item.store,
item.specs.price ?? "",
result.checks.atx ? "Да" : "Нет",
result.checks.top360 ? "Да" : "Нет",
item.specs.gpu ?? "",
item.specs.hdd,
result.checks.argb ? "Да" : "Нет",
result.checks.psu ? "Да" : "Нет",
item.specs.rating ?? "",
item.specs.fans ?? "",
result.passed
? "ПОДХОДИТ"
: "НЕ ПОДХОДИТ"
];
});
const csv = [
headers,
...rows
]
.map(row =>
row
.map(value =>
`"${String(value).replace(/"/g, '""')}"`
)
.join(";")
)
.join("\n");
const blob =
new Blob(
["\ufeff" + csv],
{
type: "text/csv;charset=utf-8;"
}
);
const url =
URL.createObjectURL(blob);
const link =
document.createElement("a");
link.href = url;
link.download =
"casefilter.csv";
link.click();
URL.revokeObjectURL(url);
}
// ==========================================
// HELPERS
// ==========================================
function formatNumber(number) {
return number
.toLocaleString("ru-RU");
}
function escapeHTML(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// ==========================================
// START
// ==========================================
render();