{"id":4488,"date":"2025-12-25T18:51:12","date_gmt":"2025-12-25T16:51:12","guid":{"rendered":"https:\/\/science.knu.ua\/new\/?page_id=4488"},"modified":"2026-01-05T14:58:43","modified_gmt":"2026-01-05T12:58:43","slug":"aktyvni-proyekty","status":"publish","type":"page","link":"https:\/\/science.knu.ua\/new\/?page_id=4488","title":{"rendered":"\u0410\u043a\u0442\u0438\u0432\u043d\u0456 \u043f\u0440\u043e\u0454\u043a\u0442\u0438"},"content":{"rendered":"<!-- FONTS & LIBRARIES -->\r\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Montserrat:ital,wght@0,400;0,600;0,700;1,400&display=swap\" rel=\"stylesheet\">\r\n<!-- Lucide Icons -->\r\n<script src=\"https:\/\/unpkg.com\/lucide@latest\"><\/script>\r\n<!-- QRCode.js -->\r\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/qrcodejs\/1.0.0\/qrcode.min.js\"><\/script>\r\n<!-- DOCX & FileSaver (For Word Export) -->\r\n<script src=\"https:\/\/unpkg.com\/docx@7.1.0\/build\/index.js\"><\/script>\r\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/FileSaver.js\/2.0.5\/FileSaver.min.js\"><\/script>\r\n\r\n<style>\r\n  \/* MAIN CONTAINER & RESET *\/\r\n  #knu-projects-app {\r\n    font-family: 'Montserrat', sans-serif;\r\n    color: #333;\r\n    max-width: 1200px;\r\n    margin: 0 auto;\r\n    padding: 20px 10px;\r\n    box-sizing: border-box;\r\n    font-size: 16px; \r\n    line-height: 1.5;\r\n  }\r\n\r\n  #knu-projects-app *, \r\n  #knu-projects-app *::before, \r\n  #knu-projects-app *::after {\r\n    box-sizing: border-box;\r\n  }\r\n\r\n  \/* VARIABLES *\/\r\n  :root {\r\n    --knu-blue: #0b5394;\r\n    --knu-red: #b80000;\r\n    --knu-green: #2e7d32;\r\n    --knu-bg: #f9f9f9;\r\n    --knu-card-bg: #ffffff;\r\n    --knu-border: #e0e0e0;\r\n  }\r\n\r\n  \/* FILTERS SECTION *\/\r\n  .knu-filters {\r\n    display: flex;\r\n    flex-wrap: wrap;\r\n    align-items: flex-end;\r\n    gap: 8px; \r\n    margin-bottom: 20px;\r\n    background: var(--knu-card-bg);\r\n    padding: 15px;\r\n    border-radius: 8px;\r\n    box-shadow: 0 4px 15px rgba(0,0,0,0.05);\r\n    border: 1px solid var(--knu-border);\r\n  }\r\n\r\n  .knu-input-group {\r\n    flex: 1;\r\n    min-width: 110px; \r\n    display: flex;\r\n    flex-direction: column;\r\n    gap: 4px;\r\n  }\r\n  \r\n  .knu-input-group.search-group {\r\n    flex: 1.3; \r\n    min-width: 140px; \r\n  }\r\n\r\n  .knu-input-group.btn-group {\r\n    flex: 0 0 auto; \r\n    min-width: auto;\r\n    align-self: flex-end;\r\n  }\r\n\r\n  .knu-input-group label {\r\n    font-size: 0.75em;\r\n    font-weight: 700;\r\n    color: var(--knu-blue);\r\n    text-transform: uppercase;\r\n    letter-spacing: 0.5px;\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 5px;\r\n    white-space: nowrap;\r\n  }\r\n\r\n  .knu-select, .knu-input {\r\n    padding: 5px 10px;\r\n    border: 1px solid #ccc;\r\n    border-radius: 4px;\r\n    font-family: 'Montserrat', sans-serif;\r\n    font-size: 0.9em;\r\n    width: 100%;\r\n    transition: border-color 0.3s;\r\n    height: 38px;\r\n    background-color: #fff;\r\n  }\r\n\r\n  .knu-select:focus, .knu-input:focus {\r\n    border-color: var(--knu-blue);\r\n    outline: none;\r\n  }\r\n\r\n  \/* Buttons Style *\/\r\n  .knu-btn-icon {\r\n    height: 38px;\r\n    width: 38px;\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    border: 1px solid #ccc;\r\n    background: #fff;\r\n    border-radius: 4px;\r\n    cursor: pointer;\r\n    color: var(--knu-blue);\r\n    transition: all 0.2s;\r\n    padding: 0;\r\n  }\r\n\r\n  .knu-btn-icon:hover {\r\n    background: #f0f8ff;\r\n    border-color: var(--knu-blue);\r\n    transform: scale(1.05);\r\n  }\r\n  \r\n  .knu-btn-icon.active {\r\n    background-color: var(--knu-blue);\r\n    color: #fff;\r\n    border-color: var(--knu-blue);\r\n  }\r\n\r\n  \/* COUNTER STYLES *\/\r\n  .knu-counter-bar {\r\n    display: flex;\r\n    justify-content: space-between;\r\n    align-items: center;\r\n    padding: 0 5px 15px 5px;\r\n    font-size: 0.9em;\r\n    color: #555;\r\n    font-weight: 600;\r\n  }\r\n\r\n  .knu-counter-number {\r\n    color: var(--knu-blue);\r\n    font-weight: 700;\r\n  }\r\n  \r\n  \/* Cache Status Indicator *\/\r\n  #knu-cache-status {\r\n    font-size: 0.85em;\r\n    color: #e65100;\r\n    background: #fff3e0;\r\n    padding: 2px 8px;\r\n    border-radius: 4px;\r\n    display: none;\r\n    animation: fadeIn 0.5s;\r\n  }\r\n\r\n  \/* GRID LAYOUT (Cards) *\/\r\n  .knu-grid-view {\r\n    display: grid;\r\n    gap: 20px;\r\n    align-items: stretch;\r\n    grid-template-columns: 1fr;\r\n  }\r\n\r\n  @media (min-width: 700px) {\r\n    .knu-grid-view { grid-template-columns: repeat(2, 1fr); }\r\n  }\r\n\r\n  @media (min-width: 1100px) {\r\n    .knu-grid-view { grid-template-columns: repeat(3, 1fr); }\r\n  }\r\n\r\n  \/* TABLE LAYOUT *\/\r\n  .knu-table-wrapper {\r\n    overflow-x: auto;\r\n    background: #fff;\r\n    border-radius: 8px;\r\n    border: 1px solid var(--knu-border);\r\n    box-shadow: 0 4px 15px rgba(0,0,0,0.05);\r\n  }\r\n  \r\n  .knu-table {\r\n    width: 100%;\r\n    border-collapse: collapse;\r\n    font-size: 0.9em;\r\n    min-width: 800px; \r\n  }\r\n  \r\n  .knu-table th, .knu-table td {\r\n    padding: 12px 15px;\r\n    text-align: left;\r\n    border-bottom: 1px solid #eee;\r\n    vertical-align: middle;\r\n  }\r\n  \r\n  .knu-table th {\r\n    background-color: #f4f6f8;\r\n    color: var(--knu-blue);\r\n    font-weight: 700;\r\n    text-transform: uppercase;\r\n    font-size: 0.8em;\r\n    position: sticky;\r\n    top: 0;\r\n  }\r\n  \r\n  .knu-table tr:hover {\r\n    background-color: #fafafa;\r\n  }\r\n  \r\n  .knu-table-title {\r\n    font-weight: 600;\r\n    color: #222;\r\n    margin-bottom: 4px;\r\n    display: block;\r\n  }\r\n  \r\n  .knu-table-id {\r\n    font-size: 0.8em;\r\n    color: #888;\r\n    display: block;\r\n  }\r\n  \r\n  .knu-table-sup {\r\n    font-style: italic;\r\n    color: #444;\r\n  }\r\n\r\n  \/* CARD STYLES *\/\r\n  .knu-card {\r\n    background: var(--knu-card-bg);\r\n    border: 1px solid var(--knu-border);\r\n    border-radius: 8px;\r\n    display: flex;\r\n    flex-direction: column;\r\n    transition: transform 0.3s, box-shadow 0.3s;\r\n    overflow: hidden;\r\n    position: relative;\r\n    height: 100%;\r\n  }\r\n\r\n  .knu-card:hover {\r\n    transform: translateY(-4px);\r\n    box-shadow: 0 8px 20px rgba(0,0,0,0.08);\r\n    border-color: var(--knu-blue);\r\n  }\r\n\r\n  \/* --- COMPACT HEADER --- *\/\r\n  .knu-card-header {\r\n    padding: 8px 15px;\r\n    display: flex;\r\n    justify-content: space-between;\r\n    align-items: center;\r\n    border-bottom: 1px solid #f0f0f0;\r\n    background: #fafafa;\r\n    flex-shrink: 0;\r\n    height: 44px;\r\n  }\r\n\r\n  .knu-type-box {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    width: 32px;\r\n    height: 32px;\r\n    border-radius: 6px;\r\n  }\r\n  .knu-type-box.domestic { background-color: #e3f2fd; color: var(--knu-blue); }\r\n  .knu-type-box.foreign { background-color: #ffebee; color: var(--knu-red); }\r\n\r\n  .knu-header-year {\r\n    font-weight: 700;\r\n    font-size: 1.1em;\r\n    color: #444;\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 6px;\r\n  }\r\n\r\n  .knu-status-icon {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n  }\r\n\r\n  \/* --- CARD BODY --- *\/\r\n  .knu-card-body {\r\n    padding: 15px;\r\n    flex-grow: 1;\r\n    display: flex;\r\n    flex-direction: column;\r\n    gap: 6px;\r\n    position: relative;\r\n  }\r\n\r\n  .knu-meta-row {\r\n    font-size: 0.8em;\r\n    color: #777;\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 5px;\r\n    margin-bottom: 2px;\r\n  }\r\n\r\n  .knu-faculty {\r\n    font-size: 0.8em;\r\n    color: var(--knu-blue);\r\n    font-weight: 600;\r\n    text-transform: uppercase;\r\n    line-height: 1.2;\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 6px;\r\n  }\r\n\r\n  .knu-title {\r\n    font-size: 1em;\r\n    font-weight: 700;\r\n    line-height: 1.3;\r\n    color: #222;\r\n    margin: 4px 0 8px 0;\r\n  }\r\n\r\n  .knu-supervisor {\r\n    font-size: 0.9em;\r\n    color: #444;\r\n    display: flex;\r\n    align-items: flex-start;\r\n    gap: 6px;\r\n    font-style: italic;\r\n    margin-top: auto;\r\n    padding-top: 5px;\r\n    line-height: 1.3;\r\n  }\r\n\r\n  .knu-id {\r\n    font-size: 0.75em;\r\n    color: #999;\r\n    align-self: flex-start;\r\n    margin-top: 4px;\r\n  }\r\n\r\n  \/* --- CARD FOOTER --- *\/\r\n  .knu-card-footer {\r\n    padding: 10px 15px;\r\n    background: #fff;\r\n    border-top: 1px solid #f0f0f0;\r\n    display: flex;\r\n    justify-content: space-between;\r\n    gap: 8px;\r\n    flex-wrap: wrap;\r\n    flex-shrink: 0;\r\n    min-height: 55px;\r\n  }\r\n\r\n  .knu-btn {\r\n    flex: 1;\r\n    display: inline-flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    gap: 6px;\r\n    padding: 6px 10px;\r\n    border-radius: 4px;\r\n    font-size: 0.85em;\r\n    font-weight: 600;\r\n    text-decoration: none !important;\r\n    transition: all 0.2s;\r\n    cursor: pointer;\r\n    border: none;\r\n    white-space: nowrap;\r\n    height: 34px;\r\n  }\r\n\r\n  .knu-btn-repo {\r\n    background-color: var(--knu-blue);\r\n    color: #fff !important;\r\n  }\r\n  .knu-btn-repo:hover { background-color: #084175; }\r\n\r\n  .knu-btn-web {\r\n    background-color: #fff;\r\n    border: 1px solid var(--knu-blue);\r\n    color: var(--knu-blue) !important;\r\n  }\r\n  .knu-btn-web:hover { background-color: #f0f8ff; }\r\n\r\n  .knu-btn-qr {\r\n    background-color: #fff;\r\n    border: 1px solid #ccc;\r\n    color: #555 !important;\r\n    max-width: 34px;\r\n    padding: 0;\r\n  }\r\n  .knu-btn-qr:hover { border-color: #333; color: #333 !important; }\r\n  \r\n  \/* Small Link Button for Table *\/\r\n  .knu-link-sm {\r\n    display: inline-flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    width: 30px;\r\n    height: 30px;\r\n    border-radius: 4px;\r\n    color: var(--knu-blue);\r\n    border: 1px solid #ddd;\r\n    background: #fff;\r\n    margin-right: 4px;\r\n    transition: 0.2s;\r\n  }\r\n  .knu-link-sm:hover {\r\n    background-color: var(--knu-blue);\r\n    color: #fff;\r\n    border-color: var(--knu-blue);\r\n  }\r\n\r\n  \/* QR OVERLAY *\/\r\n  .knu-qr-container {\r\n    display: none;\r\n    position: absolute;\r\n    top: 0;\r\n    left: 0;\r\n    width: 100%;\r\n    height: 100%;\r\n    background: rgba(255, 255, 255, 0.98);\r\n    backdrop-filter: blur(4px);\r\n    z-index: 10;\r\n    flex-direction: column;\r\n    justify-content: center;\r\n    align-items: center;\r\n    text-align: center;\r\n    animation: fadeIn 0.2s ease;\r\n    padding: 20px;\r\n  }\r\n  \r\n  .knu-qr-canvas img { \r\n    margin: 0 auto 15px auto; \r\n    display: block; \r\n    cursor: pointer; \r\n    padding: 10px;\r\n    background: #fff;\r\n    border: 1px solid #eee;\r\n    border-radius: 8px;\r\n    box-shadow: 0 4px 10px rgba(0,0,0,0.1);\r\n  }\r\n\r\n  .knu-qr-title {\r\n    font-size: 0.9rem;\r\n    font-weight: 700;\r\n    color: var(--knu-blue);\r\n    margin-bottom: 5px;\r\n    line-height: 1.3;\r\n    max-height: 60px;\r\n    overflow: hidden;\r\n  }\r\n  \r\n  .knu-qr-hint { \r\n    font-size: 0.8em; \r\n    color: #666; \r\n  }\r\n\r\n  .knu-no-results {\r\n    width: 100%;\r\n    text-align: center;\r\n    padding: 40px;\r\n    color: #777;\r\n    font-size: 1.2em;\r\n    grid-column: 1 \/ -1;\r\n  }\r\n  \r\n  .knu-loading {\r\n    display: flex;\r\n    flex-direction: column;\r\n    align-items: center;\r\n    justify-content: center;\r\n    gap: 15px;\r\n    padding: 40px;\r\n    grid-column: 1 \/ -1;\r\n    color: var(--knu-blue);\r\n    font-weight: 600;\r\n  }\r\n  \r\n  #knu-loading-details {\r\n    font-size: 0.85em;\r\n    color: #666;\r\n    background: #f0f0f0;\r\n    padding: 4px 12px;\r\n    border-radius: 20px;\r\n  }\r\n\r\n  @keyframes fadeIn {\r\n    from { opacity: 0; }\r\n    to { opacity: 1; }\r\n  }\r\n\r\n  @media (max-width: 768px) {\r\n    .knu-filters { flex-direction: column; gap: 10px; }\r\n    .knu-input-group { width: 100%; min-width: 100%; }\r\n    .knu-input-group.btn-group { align-self: flex-end; width: auto; flex: 0; min-width: auto; }\r\n    .knu-counter-bar { justify-content: flex-start; flex-wrap: wrap; gap:10px; }\r\n    \r\n    .knu-btn-row {\r\n      display: flex;\r\n      width: 100%;\r\n      justify-content: flex-end;\r\n      gap: 10px;\r\n    }\r\n  }\r\n  \r\n  .animate-spin {\r\n    animation: spin 1s linear infinite;\r\n  }\r\n  @keyframes spin {\r\n    from { transform: rotate(0deg); }\r\n    to { transform: rotate(360deg); }\r\n  }\r\n<\/style>\r\n\r\n<div id=\"knu-projects-app\">\r\n  <!-- Search & Filters -->\r\n  <div class=\"knu-filters\">\r\n    <div class=\"knu-input-group search-group\">\r\n      <label for=\"knu-search\">\r\n        <i data-lucide=\"search\" size=\"14\"><\/i> \u041f\u043e\u0448\u0443\u043a\r\n      <\/label>\r\n      <div style=\"position:relative;\">\r\n        <input type=\"text\" id=\"knu-search\" class=\"knu-input\" placeholder=\"\u041d\u0430\u0437\u0432\u0430, \u041a\u0435\u0440\u0456\u0432\u043d\u0438\u043a, ID...\">\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"knu-input-group\">\r\n      <label for=\"knu-faculty-filter\">\r\n        <i data-lucide=\"building-2\" size=\"14\"><\/i> \u041f\u0456\u0434\u0440\u043e\u0437\u0434\u0456\u043b\u0438\r\n      <\/label>\r\n      <select id=\"knu-faculty-filter\" class=\"knu-select\">\r\n        <option value=\"\">\u0412\u0441\u0456<\/option>\r\n      <\/select>\r\n    <\/div>\r\n\r\n    <div class=\"knu-input-group\">\r\n      <label for=\"knu-year-filter\">\r\n        <i data-lucide=\"calendar\" size=\"14\"><\/i> \u041f\u0435\u0440\u0456\u043e\u0434 \/ \u0420\u0456\u043a\r\n      <\/label>\r\n      <select id=\"knu-year-filter\" class=\"knu-select\">\r\n\t\t<option value=\"active\">\u0410\u043a\u0442\u0438\u0432\u043d\u0456<\/option>\r\n        <option value=\"done\">\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0456<\/option>\r\n        <option value=\"all\">\u0412\u0441\u0456 \u043f\u0440\u043e\u0454\u043a\u0442\u0438<\/option>\r\n        <option disabled>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<\/option>\r\n      <\/select>\r\n    <\/div>\r\n\r\n    <div class=\"knu-input-group\">\r\n      <label for=\"knu-type-filter\">\r\n        <i data-lucide=\"filter\" size=\"14\"><\/i> \u0422\u0438\u043f\r\n      <\/label>\r\n      <select id=\"knu-type-filter\" class=\"knu-select\">\r\n        <option value=\"\">\u0412\u0441\u0456<\/option>\r\n        <option value=\"\u0432\u0456\u0442\u0447\u0438\u0437\u043d\u044f\u043d\u0438\u0439\">\u0412\u0456\u0442\u0447\u0438\u0437\u043d\u044f\u043d\u0456<\/option>\r\n        <option value=\"\u0437\u0430\u043a\u043e\u0440\u0434\u043e\u043d\u043d\u0438\u0439\">\u0417\u0430\u043a\u043e\u0440\u0434\u043e\u043d\u043d\u0456<\/option>\r\n      <\/select>\r\n    <\/div>\r\n\r\n    <div class=\"knu-input-group\">\r\n      <label for=\"knu-sort-select\">\r\n        <i data-lucide=\"arrow-up-down\" size=\"14\"><\/i> \u0421\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438\r\n      <\/label>\r\n      <select id=\"knu-sort-select\" class=\"knu-select\">\r\n        <option value=\"default\">\u0411\u0430\u0437\u043e\u0432\u043e<\/option>\r\n        <option value=\"title_asc\">\u041d\u0430\u0437\u0432\u0430 &#9650;<\/option>\r\n        <option value=\"title_desc\">\u041d\u0430\u0437\u0432\u0430 &#9660;<\/option>\r\n        <option value=\"supervisor_asc\">\u041a\u0435\u0440\u0456\u0432\u043d\u0438\u043a &#9650;<\/option>\r\n        <option value=\"supervisor_desc\">\u041a\u0435\u0440\u0456\u0432\u043d\u0438\u043a &#9660;<\/option>\r\n      <\/select>\r\n    <\/div>\r\n\r\n    <!-- Buttons Row -->\r\n    <div class=\"knu-input-group btn-group\">\r\n       <div style=\"display:flex; gap:5px;\">\r\n          <button id=\"knu-download-btn\" class=\"knu-btn-icon\" title=\"\u0417\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0441\u043f\u0438\u0441\u043e\u043a (DOCX)\">\r\n             <i data-lucide=\"download\" size=\"18\"><\/i>\r\n          <\/button>\r\n          <button id=\"knu-view-toggle\" class=\"knu-btn-icon\" title=\"\u041f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447 \u0432\u0438\u0434\u0443 (\u0422\u0430\u0431\u043b\u0438\u0446\u044f\/\u041a\u0430\u0440\u0442\u043a\u0438)\">\r\n             <i data-lucide=\"table\" size=\"18\"><\/i>\r\n          <\/button>\r\n          <button id=\"knu-reload-btn\" class=\"knu-btn-icon\" title=\"\u041e\u043d\u043e\u0432\u0438\u0442\u0438 \u0434\u0430\u043d\u0456\">\r\n            <i data-lucide=\"rotate-cw\" size=\"18\"><\/i>\r\n          <\/button>\r\n       <\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <!-- Counter Section -->\r\n  <div class=\"knu-counter-bar\">\r\n    <span>\u041f\u043e\u043a\u0430\u0437\u0430\u043d\u043e <span id=\"knu-shown-count\" class=\"knu-counter-number\">0<\/span> \u0456\u0437 <span id=\"knu-total-count\" class=\"knu-counter-number\">0<\/span> \u043f\u0440\u043e\u0454\u043a\u0442\u0456\u0432<\/span>\r\n    <span id=\"knu-cache-status\"><i data-lucide=\"cloud-download\" size=\"12\"><\/i> \u041e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f...<\/span>\r\n  <\/div>\r\n\r\n  <!-- Content Container (Grid or Table) -->\r\n  <div id=\"knu-content-area\">\r\n    <div class=\"knu-loading\">\r\n      <i data-lucide=\"loader-2\" class=\"animate-spin\" size=\"40\"><\/i>\r\n      <span id=\"knu-loading-status\">\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0431\u0430\u0437\u0438 \u0434\u0430\u043d\u0438\u0445...<\/span>\r\n      <span id=\"knu-loading-details\">\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f...<\/span>\r\n      <!-- ADDED BUTTON HERE -->\r\n      <a href=\"https:\/\/science.knu.ua\/new\/?p=4799\" target=\"_blank\" class=\"knu-btn knu-btn-web\" style=\"margin-top:15px; border-color:#ccc; color:#555!important; width:auto; padding: 0 15px;\">\r\n         <i data-lucide=\"external-link\" size=\"16\"><\/i> \u0412\u0456\u0434\u043a\u0440\u0438\u0442\u0438 \u043e\u0440\u0438\u0433\u0456\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u0442\u0456\r\n      <\/a>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<script>\r\n\/**\r\n * DATA STORAGE\r\n *\/\r\nlet projectsData = [];\r\nlet currentFilteredData = [];\r\nlet currentViewMode = 'grid'; \/\/ 'grid' or 'table'\r\n\r\n\/\/ CONSTANTS\r\nconst INVALID_REPO = \"https:\/\/ir.library.knu.ua\";\r\nconst INVALID_WEB = \"https:\/\/google.com\";\r\nconst SOURCE_URL = \"https:\/\/science.knu.ua\/new\/?p=4799\";\r\nconst CACHE_KEY = \"knu_projects_list_v2\"; \/\/ LocalStorage key\r\nconst FETCH_TIMEOUT_MS = 1500; \/\/ Reduced for faster failover\r\n\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n  const container = document.getElementById('knu-content-area');\r\n  const searchInput = document.getElementById('knu-search');\r\n  const facultyFilter = document.getElementById('knu-faculty-filter');\r\n  const typeFilter = document.getElementById('knu-type-filter');\r\n  const yearFilter = document.getElementById('knu-year-filter');\r\n  const sortSelect = document.getElementById('knu-sort-select');\r\n  const reloadBtn = document.getElementById('knu-reload-btn');\r\n  const viewToggleBtn = document.getElementById('knu-view-toggle');\r\n  const downloadBtn = document.getElementById('knu-download-btn');\r\n  const shownCountEl = document.getElementById('knu-shown-count');\r\n  const totalCountEl = document.getElementById('knu-total-count');\r\n  const cacheStatusEl = document.getElementById('knu-cache-status');\r\n  \r\n  const currentYear = new Date().getFullYear(); \r\n  if (window.lucide) { lucide.createIcons(); }\r\n\r\n  \/\/ Update loading text helper\r\n  function updateLoadingUI(text) {\r\n    const statusEl = document.getElementById('knu-loading-status');\r\n    const detailsEl = document.getElementById('knu-loading-details');\r\n    if(statusEl) statusEl.textContent = text || \"\u0417\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445...\";\r\n    if(detailsEl) detailsEl.textContent = \"\u041e\u0431\u0440\u043e\u0431\u043a\u0430...\";\r\n  }\r\n\r\n  \/\/ --- LOCAL STORAGE HELPERS ---\r\n  function saveToCache(data) {\r\n    try {\r\n        const payload = {\r\n            timestamp: new Date().getTime(),\r\n            data: data\r\n        };\r\n        localStorage.setItem(CACHE_KEY, JSON.stringify(payload));\r\n    } catch (e) {\r\n        console.warn(\"Local storage limit or error\", e);\r\n    }\r\n  }\r\n\r\n  function loadFromCache() {\r\n    try {\r\n        const raw = localStorage.getItem(CACHE_KEY);\r\n        if (!raw) return null;\r\n        return JSON.parse(raw);\r\n    } catch (e) {\r\n        return null;\r\n    }\r\n  }\r\n\r\n  \/\/ --- URL STATE MANAGEMENT ---\r\n  function applyUrlState() {\r\n     const params = new URLSearchParams(window.location.search);\r\n     \r\n     if (params.has('q')) searchInput.value = params.get('q');\r\n     if (params.has('fac')) facultyFilter.value = params.get('fac');\r\n     if (params.has('type')) typeFilter.value = params.get('type');\r\n     if (params.has('year')) yearFilter.value = params.get('year');\r\n     if (params.has('sort')) sortSelect.value = params.get('sort');\r\n\r\n     if (params.has('view')) {\r\n         const mode = params.get('view');\r\n         if (mode === 'table') {\r\n             currentViewMode = 'table';\r\n             viewToggleBtn.classList.add('active');\r\n             viewToggleBtn.innerHTML = '<i data-lucide=\"layout-grid\" size=\"18\"><\/i>';\r\n         } else {\r\n             currentViewMode = 'grid';\r\n             viewToggleBtn.classList.remove('active');\r\n             viewToggleBtn.innerHTML = '<i data-lucide=\"table\" size=\"18\"><\/i>';\r\n         }\r\n     }\r\n  }\r\n\r\n  function updateUrlState() {\r\n     const params = new URLSearchParams(window.location.search);\r\n     \r\n     const setOrDelete = (key, value) => {\r\n         if (value) params.set(key, value);\r\n         else params.delete(key);\r\n     };\r\n\r\n     setOrDelete('q', searchInput.value);\r\n     setOrDelete('fac', facultyFilter.value);\r\n     setOrDelete('type', typeFilter.value);\r\n     setOrDelete('year', yearFilter.value); \/\/ explicitly set whatever is selected (active, done, all, or year)\r\n\r\n     if (sortSelect.value && sortSelect.value !== 'default') {\r\n         params.set('sort', sortSelect.value);\r\n     } else {\r\n         params.delete('sort');\r\n     }\r\n\r\n     if (currentViewMode === 'table') {\r\n         params.set('view', 'table');\r\n     } else {\r\n         params.delete('view');\r\n     }\r\n\r\n     const newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '');\r\n     window.history.replaceState({}, '', newUrl);\r\n  }\r\n\r\n  \/\/ Timeout wrapper for fetch\r\n  async function fetchWithTimeout(url, options = {}, timeout = FETCH_TIMEOUT_MS) {\r\n      const controller = new AbortController();\r\n      const id = setTimeout(() => controller.abort(), timeout);\r\n      try {\r\n          const response = await fetch(url, { ...options, signal: controller.signal });\r\n          clearTimeout(id);\r\n          return response;\r\n      } catch (error) {\r\n          clearTimeout(id);\r\n          throw error;\r\n      }\r\n  }\r\n\r\n  \/**\r\n   * ROBUST FETCH WITH DIRECT TRY + STREAMING FALLBACK\r\n   *\/\r\n  async function fetchWithFallback() {\r\n    const t = new Date().getTime();\r\n    const encodedTarget = encodeURIComponent(`${SOURCE_URL}&_t=${t}`);\r\n\r\n    \/\/ 1. ATTEMPT DIRECT FETCH FIRST (FASTEST)\r\n    try {\r\n        const response = await fetchWithTimeout(`${SOURCE_URL}&_t=${t}`, { \r\n            headers: { 'Cache-Control': 'no-cache' } \r\n        }, 1500); \/\/ 1.5s timeout for direct\r\n        \r\n        if (response.ok) {\r\n            const text = await response.text();\r\n            if (text && text.length > 50) {\r\n                console.log(\"Loaded directly (Fastest)\");\r\n                return text;\r\n            }\r\n        }\r\n    } catch (e) {\r\n        \/\/ Direct fetch failed, proceed to proxies silently\r\n    }\r\n\r\n    \/\/ 2. FALLBACK PROXIES\r\n    const proxies = [\r\n      { name: 'CorsProxy', url: `https:\/\/corsproxy.io\/?${encodedTarget}`, type: 'text' },\r\n      { name: 'AllOrigins', url: `https:\/\/api.allorigins.win\/get?url=${encodedTarget}`, type: 'json' },\r\n      { name: 'CodeTabs', url: `https:\/\/api.codetabs.com\/v1\/proxy?quest=${encodedTarget}`, type: 'text' }\r\n    ];\r\n\r\n    let lastError = null;\r\n\r\n    for (const proxy of proxies) {\r\n      try {\r\n        \/\/ Reduced timeout for proxies (2s)\r\n        const response = await fetchWithTimeout(proxy.url, { cache: \"no-store\" }, 2000);\r\n        if (!response.ok) throw new Error(`Status ${response.status}`);\r\n        \r\n        let htmlContent = \"\";\r\n        if (proxy.type === 'json') {\r\n          const data = await response.json();\r\n          htmlContent = data.contents;\r\n        } else {\r\n          htmlContent = await response.text();\r\n        }\r\n\r\n        if (!htmlContent || htmlContent.length < 50) throw new Error(\"Empty content\");\r\n        return htmlContent; \r\n        \r\n      } catch (err) {\r\n        console.warn(`${proxy.name} failed:`, err);\r\n        lastError = err;\r\n      }\r\n    }\r\n    throw lastError || new Error(\"All proxies failed\");\r\n  }\r\n\r\n  function parseHtmlToProjects(htmlContent) {\r\n      \/\/ Use DOMParser for safer extracting\r\n      const parser = new DOMParser();\r\n      const doc = parser.parseFromString(htmlContent, \"text\/html\");\r\n      const contentArea = doc.querySelector('.entry-content') || doc.body;\r\n      const cleanText = contentArea.innerText;\r\n\r\n      \/\/ Split blocks based on the specific WordPress separator: -\/-\/-\/-\/-...\r\n      const rawBlocks = cleanText.split(\/(?:-\\\/){5,}\/);\r\n\r\n      return rawBlocks.map(block => {\r\n        if (!block.trim()) return null;\r\n\r\n        const proj = {\r\n            type: \"\",\r\n            receiveYear: 0,\r\n            endYear: 0,\r\n            executionYears: \"\",\r\n            faculty: \"\",\r\n            id: \"\",\r\n            supervisor: \"\",\r\n            title: \"\",\r\n            repoUrl: \"\",\r\n            webUrl: \"\"\r\n        };\r\n        \r\n        const lines = block.split(\/\\r?\\n\/);\r\n        \r\n        lines.forEach(line => {\r\n            const separatorIndex = line.indexOf(':');\r\n            if (separatorIndex === -1) return;\r\n\r\n            let key = line.substring(0, separatorIndex).trim().toLowerCase();\r\n            \r\n            \/\/ --- INVISIBLE CHAR FIX ---\r\n            if (key.includes('\u0442\u0438\u043f')) key = '\u0442\u0438\u043f';\r\n\r\n            const value = line.substring(separatorIndex + 1).trim();\r\n\r\n            switch(key) {\r\n                case '\u0442\u0438\u043f': proj.type = value; break;\r\n                case '\u043f\u043e\u0447\u0430\u0442\u043e\u043a': proj.receiveYear = parseInt(value, 10); break;\r\n                case '\u043a\u0456\u043d\u0435\u0446\u044c': proj.endYear = parseInt(value, 10); break;\r\n                case '\u043f\u0456\u0434\u0440\u043e\u0437\u0434\u0456\u043b': proj.faculty = value; break;\r\n                case '\u043d\u043e\u043c\u0435\u0440(\u0438)': \r\n                case '\u043d\u043e\u043c\u0435\u0440': proj.id = value; break;\r\n                case '\u043a\u0435\u0440\u0456\u0432\u043d\u0438\u043a': proj.supervisor = value; break;\r\n                case '\u043d\u0430\u0437\u0432\u0430': proj.title = value; break;\r\n                case '\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u0430\u0440\u0456\u0439': proj.repoUrl = value; break;\r\n                case '\u0432\u0435\u0431-\u0441\u0430\u0439\u0442': proj.webUrl = value; break;\r\n            }\r\n        });\r\n\r\n        if (!proj.title) return null;\r\n\r\n        if (proj.receiveYear && proj.endYear) {\r\n            proj.executionYears = `${proj.receiveYear}-${proj.endYear}`;\r\n        } else {\r\n            proj.executionYears = proj.receiveYear ? `${proj.receiveYear}` : \"\";\r\n        }\r\n\r\n        return proj;\r\n      }).filter(item => item !== null);\r\n  }\r\n\r\n  \/\/ --- MAIN ORCHESTRATOR ---\r\n  async function fetchProjects(forceReload = false) {\r\n    let isDisplayedFromCache = false;\r\n\r\n    \/\/ 1. Try Cache First (Sync)\r\n    if (!forceReload) {\r\n        const cached = loadFromCache();\r\n        if (cached && cached.data && cached.data.length > 0) {\r\n            projectsData = cached.data;\r\n            populateFilters();\r\n            applyUrlState();\r\n            filterData();\r\n            isDisplayedFromCache = true;\r\n            \/\/ Update UI to show we are checking in background\r\n            if(cacheStatusEl) {\r\n                cacheStatusEl.style.display = \"block\";\r\n                cacheStatusEl.innerHTML = '<i data-lucide=\"cloud\" size=\"12\"><\/i> \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c...';\r\n                if(window.lucide) lucide.createIcons();\r\n            }\r\n        }\r\n    }\r\n\r\n    \/\/ 2. Network Fetch (Async Background)\r\n    (async () => {\r\n        try {\r\n            if (!isDisplayedFromCache) {\r\n                updateLoadingUI(\"\u0417\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445...\");\r\n            }\r\n            \r\n            let htmlContent = await fetchWithFallback();\r\n            const freshData = parseHtmlToProjects(htmlContent);\r\n\r\n            if (freshData.length > 0) {\r\n                const jsonFresh = JSON.stringify(freshData);\r\n                const jsonOld = isDisplayedFromCache ? JSON.stringify(projectsData) : \"\";\r\n\r\n                if (jsonFresh !== jsonOld) {\r\n                    projectsData = freshData;\r\n                    saveToCache(projectsData);\r\n                    populateFilters();\r\n                    applyUrlState();\r\n                    filterData();\r\n                    if (cacheStatusEl) {\r\n                        cacheStatusEl.innerHTML = '<i data-lucide=\"check\" size=\"12\"><\/i> \u0414\u0430\u043d\u0456 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043e';\r\n                        cacheStatusEl.style.display = \"block\";\r\n                        setTimeout(() => { cacheStatusEl.style.display = \"none\"; }, 3000);\r\n                        if(window.lucide) lucide.createIcons();\r\n                    }\r\n                } else {\r\n                    \/\/ Data matches cache\r\n                    if (cacheStatusEl) {\r\n                        cacheStatusEl.innerHTML = '<i data-lucide=\"check\" size=\"12\"><\/i> \u0410\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e';\r\n                        setTimeout(() => { cacheStatusEl.style.display = \"none\"; }, 2000);\r\n                        if(window.lucide) lucide.createIcons();\r\n                    }\r\n                }\r\n            }\r\n        } catch (error) {\r\n            console.error(\"Critical Error:\", error);\r\n            if (!isDisplayedFromCache) {\r\n                container.innerHTML = `\r\n                    <div class=\"knu-no-results\">\r\n                    <i data-lucide=\"wifi-off\" size=\"40\" style=\"margin-bottom:10px; color:#b80000;\"><\/i><br>\r\n                    \u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u0430\u043d\u0456.<br>\r\n                    <small style=\"font-size:0.7em; color:#999;\">Error: ${error.message}<\/small><br>\r\n                    <div style=\"display:flex; flex-direction:column; gap:10px; align-items:center; margin-top:15px;\">\r\n                        <button onclick=\"location.reload()\" class=\"knu-btn\" style=\"max-width:200px; background:#eee;\">\u0421\u043f\u0440\u043e\u0431\u0443\u0432\u0430\u0442\u0438 \u0449\u0435 \u0440\u0430\u0437<\/button>\r\n                        <a href=\"https:\/\/science.knu.ua\/new\/?p=4799\" target=\"_blank\" class=\"knu-btn knu-btn-web\" style=\"max-width:200px; border-color:#ccc; color:#555!important;\">\r\n                            <i data-lucide=\"external-link\" size=\"16\"><\/i> \u0412\u0456\u0434\u043a\u0440\u0438\u0442\u0438 \u043e\u0440\u0438\u0433\u0456\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u0442\u0456\r\n                        <\/a>\r\n                    <\/div>\r\n                    <\/div>\r\n                `;\r\n                if (window.lucide) lucide.createIcons();\r\n            } else {\r\n                if (cacheStatusEl) {\r\n                    cacheStatusEl.innerHTML = '<i data-lucide=\"wifi-off\" size=\"12\"><\/i> \u041e\u0444\u043b\u0430\u0439\u043d \u0440\u0435\u0436\u0438\u043c';\r\n                    if(window.lucide) lucide.createIcons();\r\n                }\r\n            }\r\n        }\r\n    })();\r\n  }\r\n\r\n  function populateFilters() {\r\n    while (facultyFilter.options.length > 1) { facultyFilter.remove(1); }\r\n    while (yearFilter.options.length > 4) { yearFilter.remove(4); } \r\n\r\n    const uniqueFaculties = [...new Set(projectsData.map(p => p.faculty))].filter(f => f).sort();\r\n    uniqueFaculties.forEach(faculty => {\r\n      const option = document.createElement('option');\r\n      option.value = faculty;\r\n      option.textContent = faculty;\r\n      facultyFilter.appendChild(option);\r\n    });\r\n\r\n    const uniqueYears = [...new Set(projectsData.map(p => p.receiveYear))].filter(y => y).sort((a,b) => b-a);\r\n    uniqueYears.forEach(year => {\r\n      const option = document.createElement('option');\r\n      option.value = year;\r\n      option.textContent = year;\r\n      yearFilter.appendChild(option);\r\n    });\r\n  }\r\n\r\n  function renderProjects(projects) {\r\n    shownCountEl.textContent = projects.length;\r\n    totalCountEl.textContent = projectsData.length;\r\n    container.innerHTML = '';\r\n    \r\n    if (projects.length === 0) {\r\n      container.innerHTML = '<div class=\"knu-no-results\">\u041d\u0430 \u0436\u0430\u043b\u044c, \u0437\u0430 \u0432\u0430\u0448\u0438\u043c \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u043d\u0456\u0447\u043e\u0433\u043e \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.<\/div>';\r\n      return;\r\n    }\r\n\r\n    if (currentViewMode === 'table') {\r\n      renderTable(projects);\r\n    } else {\r\n      renderGrid(projects);\r\n    }\r\n    \r\n    if (window.lucide) { lucide.createIcons(); }\r\n  }\r\n\r\n  function renderGrid(projects) {\r\n    container.className = 'knu-grid-view';\r\n    \r\n    projects.forEach((proj, index) => {\r\n      const isDomestic = proj.type.toLowerCase().includes('\u0432\u0456\u0442\u0447\u0438\u0437\u043d\u044f\u043d\u0438\u0439');\r\n      const typeClass = isDomestic ? 'domestic' : 'foreign';\r\n      const typeIconName = isDomestic ? 'map-pin' : 'globe';\r\n      const uniqueQrId = `qr-${index}`;\r\n      const isActive = currentYear <= proj.endYear;\r\n      \r\n      const statusIconHtml = isActive \r\n        ? `<div title=\"\u041f\u0440\u043e\u0435\u043a\u0442 \u0432\u0438\u043a\u043e\u043d\u0443\u0454\u0442\u044c\u0441\u044f\"><i data-lucide=\"circle-check\" color=\"var(--knu-green)\" fill=\"#e8f5e9\" size=\"22\"><\/i><\/div>`\r\n        : `<div title=\"\u041f\u0440\u043e\u0435\u043a\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e\"><i data-lucide=\"circle-x\" color=\"var(--knu-red)\" fill=\"#ffebee\" size=\"22\"><\/i><\/div>`;\r\n\r\n      const isRepoValid = proj.repoUrl && proj.repoUrl !== INVALID_REPO && proj.repoUrl !== '#';\r\n      const isWebValid = proj.webUrl && proj.webUrl !== INVALID_WEB && proj.webUrl !== '#';\r\n      const qrTargetUrl = isRepoValid ? proj.repoUrl : (isWebValid ? proj.webUrl : null);\r\n\r\n      let buttonsHtml = '';\r\n      if (isRepoValid) {\r\n        buttonsHtml += `\r\n          <a href=\"${proj.repoUrl}\" target=\"_blank\" class=\"knu-btn knu-btn-repo\">\r\n            <i data-lucide=\"database\" size=\"16\"><\/i> \u041a\u0430\u0440\u0442\u043a\u0430\r\n          <\/a>`;\r\n      }\r\n      if (isWebValid) {\r\n        buttonsHtml += `\r\n          <a href=\"${proj.webUrl}\" target=\"_blank\" class=\"knu-btn knu-btn-web\">\r\n            <i data-lucide=\"globe\" size=\"16\"><\/i> \u0421\u0430\u0439\u0442\r\n          <\/a>`;\r\n      }\r\n      if (qrTargetUrl) {\r\n        buttonsHtml += `\r\n          <button class=\"knu-btn knu-btn-qr\" onclick=\"toggleQR('${uniqueQrId}', '${qrTargetUrl}')\" title=\"\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438\/\u041f\u0440\u0438\u0445\u043e\u0432\u0430\u0442\u0438 QR\">\r\n            <i data-lucide=\"qr-code\" size=\"18\"><\/i>\r\n          <\/button>`;\r\n      }\r\n\r\n      const card = document.createElement('div');\r\n      card.className = 'knu-card';\r\n      card.innerHTML = `\r\n        <div class=\"knu-card-header\">\r\n          <div class=\"knu-type-box ${typeClass}\" title=\"${proj.type}\">\r\n            <i data-lucide=\"${typeIconName}\" size=\"18\"><\/i>\r\n          <\/div>\r\n          <div class=\"knu-header-year\" title=\"\u0420\u0456\u043a \u043f\u043e\u0447\u0430\u0442\u043a\u0443\">\r\n            <i data-lucide=\"calendar\" size=\"18\"><\/i> ${proj.receiveYear}\r\n          <\/div>\r\n          <div class=\"knu-status-icon\">${statusIconHtml}<\/div>\r\n        <\/div>\r\n        \r\n        <div class=\"knu-card-body\">\r\n          <div class=\"knu-meta-row\">\r\n             <i data-lucide=\"hourglass\" size=\"14\"><\/i> <b>\u0422\u0435\u0440\u043c\u0456\u043d:<\/b> ${proj.executionYears}\r\n          <\/div>\r\n          <div class=\"knu-faculty\">\r\n            <i data-lucide=\"graduation-cap\" size=\"16\"><\/i>${proj.faculty}\r\n          <\/div>\r\n          <h3 class=\"knu-title\">${proj.title}<\/h3>\r\n          <div class=\"knu-supervisor\">\r\n            <i data-lucide=\"user\" size=\"16\" style=\"min-width:16px; margin-top:3px;\"><\/i>\r\n            <span>${proj.supervisor}<\/span>\r\n          <\/div>\r\n          <div class=\"knu-id\">ID: ${proj.id}<\/div>\r\n          \r\n          <div class=\"knu-qr-container\" id=\"${uniqueQrId}\">\r\n             <div class=\"knu-qr-canvas\"><\/div>\r\n             <div class=\"knu-qr-title\">${proj.title}<\/div>\r\n             <div class=\"knu-qr-hint\">\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 QR \u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0443<\/div>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"knu-card-footer\">${buttonsHtml}<\/div>\r\n      `;\r\n      container.appendChild(card);\r\n    });\r\n  }\r\n\r\n  function renderTable(projects) {\r\n    container.className = ''; \r\n    \r\n    let rowsHtml = '';\r\n    \r\n    projects.forEach((proj) => {\r\n      const isDomestic = proj.type.toLowerCase().includes('\u0432\u0456\u0442\u0447\u0438\u0437\u043d\u044f\u043d\u0438\u0439');\r\n      const isActive = currentYear <= proj.endYear;\r\n      \r\n      const statusIcon = isActive \r\n        ? `<i data-lucide=\"circle-check\" color=\"var(--knu-green)\" fill=\"#e8f5e9\" size=\"20\"><\/i>`\r\n        : `<i data-lucide=\"circle-x\" color=\"var(--knu-red)\" fill=\"#ffebee\" size=\"20\"><\/i>`;\r\n      \r\n      const typeIcon = isDomestic \r\n        ? `<i data-lucide=\"map-pin\" color=\"var(--knu-blue)\" size=\"20\" title=\"${proj.type}\"><\/i>` \r\n        : `<i data-lucide=\"globe\" color=\"var(--knu-red)\" size=\"20\" title=\"${proj.type}\"><\/i>`;\r\n\r\n      const isRepoValid = proj.repoUrl && proj.repoUrl !== INVALID_REPO && proj.repoUrl !== '#';\r\n      const isWebValid = proj.webUrl && proj.webUrl !== INVALID_WEB && proj.webUrl !== '#';\r\n      \r\n      let linksHtml = '';\r\n      if (isRepoValid) linksHtml += `<a href=\"${proj.repoUrl}\" target=\"_blank\" class=\"knu-link-sm\" title=\"\u041a\u0430\u0440\u0442\u043a\u0430\"><i data-lucide=\"database\" size=\"14\"><\/i><\/a>`;\r\n      if (isWebValid) linksHtml += `<a href=\"${proj.webUrl}\" target=\"_blank\" class=\"knu-link-sm\" title=\"\u0421\u0430\u0439\u0442\"><i data-lucide=\"globe\" size=\"14\"><\/i><\/a>`;\r\n\r\n      rowsHtml += `\r\n        <tr>\r\n          <td style=\"text-align:center;\">${statusIcon}<\/td>\r\n          <td style=\"text-align:center;\">${typeIcon}<\/td>\r\n          <td>\r\n            <span class=\"knu-table-title\">${proj.title}<\/span>\r\n            <span class=\"knu-table-id\">ID: ${proj.id}<\/span>\r\n          <\/td>\r\n          <td class=\"knu-table-sup\">${proj.supervisor}<\/td>\r\n          <td style=\"font-size:0.85em;\">${proj.faculty}<\/td>\r\n          <td style=\"white-space:nowrap; font-weight:600;\">${proj.executionYears}<\/td>\r\n          <td style=\"white-space:nowrap;\">${linksHtml}<\/td>\r\n        <\/tr>\r\n      `;\r\n    });\r\n\r\n    const tableHtml = `\r\n      <div class=\"knu-table-wrapper\">\r\n        <table class=\"knu-table\">\r\n          <thead>\r\n            <tr>\r\n              <th width=\"50\" style=\"text-align:center;\">\u0421\u0442.<\/th>\r\n              <th width=\"50\" style=\"text-align:center;\">\u0422\u0438\u043f<\/th>\r\n              <th>\u041f\u0440\u043e\u0454\u043a\u0442 \/ ID<\/th>\r\n              <th>\u041a\u0435\u0440\u0456\u0432\u043d\u0438\u043a<\/th>\r\n              <th>\u041f\u0456\u0434\u0440\u043e\u0437\u0434\u0456\u043b<\/th>\r\n              <th>\u0420\u043e\u043a\u0438<\/th>\r\n              <th>\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f<\/th>\r\n            <\/tr>\r\n          <\/thead>\r\n          <tbody>\r\n            ${rowsHtml}\r\n          <\/tbody>\r\n        <\/table>\r\n      <\/div>\r\n    `;\r\n    \r\n    container.innerHTML = tableHtml;\r\n  }\r\n\r\n  function filterData() {\r\n    const query = searchInput.value.toLowerCase();\r\n    const selectedFaculty = facultyFilter.value;\r\n    const selectedType = typeFilter.value;\r\n    const selectedYear = yearFilter.value;\r\n    const sortValue = sortSelect.value;\r\n    \r\n    \/\/ UPDATE URL\r\n    updateUrlState();\r\n\r\n    let filtered = projectsData.filter(item => {\r\n      const matchSearch = item.title.toLowerCase().includes(query) || \r\n                          item.supervisor.toLowerCase().includes(query) || \r\n                          item.id.toLowerCase().includes(query);\r\n      \r\n      const matchFaculty = selectedFaculty ? item.faculty === selectedFaculty : true;\r\n      const matchType = selectedType ? item.type.toLowerCase().includes(selectedType.toLowerCase()) : true;\r\n      \r\n      let matchYear = true;\r\n      if (selectedYear === 'active') {\r\n        matchYear = currentYear <= item.endYear;\r\n      } else if (selectedYear === 'done') {\r\n        matchYear = currentYear > item.endYear;\r\n      } else if (selectedYear === 'all') {\r\n        matchYear = true;\r\n      } else if (selectedYear) {\r\n        matchYear = item.receiveYear.toString() === selectedYear;\r\n      }\r\n      return matchSearch && matchFaculty && matchType && matchYear;\r\n    });\r\n\r\n    if (sortValue === 'title_asc') {\r\n      filtered.sort((a, b) => a.title.localeCompare(b.title, 'uk'));\r\n    } else if (sortValue === 'title_desc') {\r\n      filtered.sort((a, b) => b.title.localeCompare(a.title, 'uk'));\r\n    } else if (sortValue === 'supervisor_asc') {\r\n      filtered.sort((a, b) => a.supervisor.localeCompare(b.supervisor, 'uk'));\r\n    } else if (sortValue === 'supervisor_desc') {\r\n      filtered.sort((a, b) => b.supervisor.localeCompare(a.supervisor, 'uk'));\r\n    }\r\n    \r\n    \/\/ Store for download\r\n    currentFilteredData = filtered;\r\n    \r\n    renderProjects(filtered);\r\n  }\r\n\r\n  \/\/ --- DOCX GENERATOR ---\r\n  downloadBtn.addEventListener('click', () => {\r\n    if (!currentFilteredData || currentFilteredData.length === 0) {\r\n      alert(\"\u041d\u0435\u043c\u0430\u0454 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f\");\r\n      return;\r\n    }\r\n\r\n    if (!window.docx) {\r\n      alert(\"\u0411\u0456\u0431\u043b\u0456\u043e\u0442\u0435\u043a\u0430 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0456\u0432 \u0449\u0435 \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0443\u0454\u0442\u044c\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u043a\u0456\u043b\u044c\u043a\u0430 \u0441\u0435\u043a\u0443\u043d\u0434.\");\r\n      return;\r\n    }\r\n\r\n    const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, BorderStyle } = window.docx;\r\n\r\n    const children = [];\r\n\r\n    \/\/ Title\r\n    children.push(new Paragraph({\r\n        text: \"\u041f\u0435\u0440\u0435\u043b\u0456\u043a \u043d\u0430\u0443\u043a\u043e\u0432\u0438\u0445 \u043f\u0440\u043e\u0454\u043a\u0442\u0456\u0432\",\r\n        heading: HeadingLevel.HEADING_1,\r\n        alignment: AlignmentType.CENTER,\r\n        spacing: { after: 200 }\r\n    }));\r\n    \r\n    \/\/ Summary\r\n    children.push(new Paragraph({\r\n        text: `\u0417\u0433\u0435\u043d\u0435\u0440\u043e\u0432\u0430\u043d\u043e: ${new Date().toLocaleDateString()} | \u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c: ${currentFilteredData.length}`,\r\n        alignment: AlignmentType.CENTER,\r\n        spacing: { after: 400 }\r\n    }));\r\n\r\n    \/\/ Data Loop\r\n    currentFilteredData.forEach(p => {\r\n        const addField = (label, val) => {\r\n            children.push(new Paragraph({\r\n                children: [\r\n                    new TextRun({ text: label + \": \", bold: true }),\r\n                    new TextRun({ text: val || \"\" })\r\n                ],\r\n                spacing: { after: 50 }\r\n            }));\r\n        };\r\n\r\n        addField(\"\u0422\u0438\u043f\", p.type);\r\n        addField(\"\u041f\u043e\u0447\u0430\u0442\u043e\u043a\", p.receiveYear.toString());\r\n        addField(\"\u041a\u0456\u043d\u0435\u0446\u044c\", p.endYear.toString());\r\n        addField(\"\u041f\u0456\u0434\u0440\u043e\u0437\u0434\u0456\u043b\", p.faculty);\r\n        addField(\"\u041d\u043e\u043c\u0435\u0440(\u0438)\", p.id);\r\n        addField(\"\u041a\u0435\u0440\u0456\u0432\u043d\u0438\u043a\", p.supervisor);\r\n        addField(\"\u041d\u0430\u0437\u0432\u0430\", p.title);\r\n        addField(\"\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u0430\u0440\u0456\u0439\", p.repoUrl);\r\n        addField(\"\u0412\u0435\u0431-\u0441\u0430\u0439\u0442\", p.webUrl);\r\n\r\n        \/\/ Separator\r\n        children.push(new Paragraph({\r\n            text: \"--------------------------------------------------------------------------------------------\",\r\n            spacing: { before: 100, after: 100 },\r\n            color: \"999999\"\r\n        }));\r\n    });\r\n\r\n    const doc = new Document({\r\n        sections: [{ children: children }]\r\n    });\r\n\r\n    Packer.toBlob(doc).then(blob => {\r\n        saveAs(blob, \"projects_export.docx\");\r\n    });\r\n  });\r\n\r\n  searchInput.addEventListener('input', filterData);\r\n  facultyFilter.addEventListener('change', filterData);\r\n  typeFilter.addEventListener('change', filterData);\r\n  yearFilter.addEventListener('change', filterData);\r\n  sortSelect.addEventListener('change', filterData);\r\n\r\n  reloadBtn.addEventListener('click', () => {\r\n    container.innerHTML = `\r\n      <div class=\"knu-loading\">\r\n        <i data-lucide=\"loader-2\" class=\"animate-spin\" size=\"40\"><\/i>\r\n        <span id=\"knu-loading-status\">\u041f\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f...<\/span>\r\n        <span id=\"knu-loading-details\">\u041e\u0447\u0438\u0449\u0435\u043d\u043d\u044f \u043a\u0435\u0448\u0443...<\/span>\r\n        <!-- ADDED BUTTON HERE -->\r\n        <a href=\"https:\/\/science.knu.ua\/new\/?p=4799\" target=\"_blank\" class=\"knu-btn knu-btn-web\" style=\"margin-top:15px; border-color:#ccc; color:#555!important; width:auto; padding: 0 15px;\">\r\n           <i data-lucide=\"external-link\" size=\"16\"><\/i> \u0412\u0456\u0434\u043a\u0440\u0438\u0442\u0438 \u043e\u0440\u0438\u0433\u0456\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u0442\u0456\r\n        <\/a>\r\n      <\/div>\r\n    `;\r\n    if (window.lucide) lucide.createIcons();\r\n    projectsData = [];\r\n    currentFilteredData = [];\r\n    localStorage.removeItem(CACHE_KEY); \/\/ Clear Cache\r\n\r\n    \/\/ Clear only APP params on hard reload\r\n    const params = new URLSearchParams(window.location.search);\r\n    ['q', 'fac', 'type', 'year', 'sort', 'view'].forEach(k => params.delete(k));\r\n    const newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '');\r\n    window.history.replaceState({}, '', newUrl);\r\n\r\n    searchInput.value = '';\r\n    \r\n    fetchProjects(true); \/\/ Force network\r\n  });\r\n\r\n  viewToggleBtn.addEventListener('click', () => {\r\n    currentViewMode = currentViewMode === 'grid' ? 'table' : 'grid';\r\n    if(currentViewMode === 'table') {\r\n        viewToggleBtn.classList.add('active');\r\n        viewToggleBtn.innerHTML = '<i data-lucide=\"layout-grid\" size=\"18\"><\/i>';\r\n    } else {\r\n        viewToggleBtn.classList.remove('active');\r\n        viewToggleBtn.innerHTML = '<i data-lucide=\"table\" size=\"18\"><\/i>';\r\n    }\r\n    filterData(); \r\n  });\r\n\r\n  \/\/ Init\r\n  fetchProjects();\r\n});\r\n\r\nwindow.toggleQR = function(elementId, url) {\r\n  const container = document.getElementById(elementId);\r\n  if (!container) return;\r\n  const isHidden = getComputedStyle(container).display === 'none';\r\n  document.querySelectorAll('.knu-qr-container').forEach(el => el.style.display = 'none');\r\n\r\n  if (isHidden) {\r\n    container.style.display = 'flex';\r\n    const canvasDiv = container.querySelector('.knu-qr-canvas');\r\n    if (canvasDiv.innerHTML.trim() === '') {\r\n      try {\r\n        new QRCode(canvasDiv, {\r\n          text: url,\r\n          width: 150,\r\n          height: 150,\r\n          colorDark : \"#0b5394\",\r\n          colorLight : \"#ffffff\",\r\n          correctLevel : QRCode.CorrectLevel.H\r\n        });\r\n        canvasDiv.addEventListener('click', () => { window.open(url, '_blank'); });\r\n      } catch (e) {\r\n        console.error(\"QR Error\", e);\r\n      }\r\n    }\r\n  } else {\r\n    container.style.display = 'none';\r\n  }\r\n};\r\n<\/script>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":4,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-4488","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages\/4488","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4488"}],"version-history":[{"count":2,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages\/4488\/revisions"}],"predecessor-version":[{"id":4791,"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=\/wp\/v2\/pages\/4488\/revisions\/4791"}],"wp:attachment":[{"href":"https:\/\/science.knu.ua\/new\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4488"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}