Загрузка данных


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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}
// ==========================================
// START
// ==========================================
render();