/
SakhCollART
/
ski-first-year-guide
Обзор
Документация
Войти
/
SakhCollART
/
ski-first-year-guide
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
script.js
281 строка
13 KB
SakhCollART
Улучшенная навигация и маршрут первокурсника
03 авг 2026, 07:40
Верифицирован
03 авг 2026, 07:40
23c1f0d
Код
Авторство
О чём код?
const storageKey = "ski-first-year-steps"; const menuButton = document.querySelector(".menuButton"); const nav = document.querySelector(".nav"); const topButton = document.querySelector(".toTop"); const checks = [...document.querySelectorAll("#checklist input")]; const boxes = [...document.querySelectorAll("#checklist .box")]; function loadProgress() { try { const saved = JSON.parse(localStorage.getItem(storageKey) || "[]"); checks.forEach((check, index) => { check.checked = Boolean(saved[index]); }); } catch (_) {} updateProgress(false); } function updateProgress(save = true) { const state = checks.map((check) => check.checked); if (save) localStorage.setItem(storageKey, JSON.stringify(state)); checks.forEach((check, index) => { check.closest(".check").classList.toggle("done", check.checked); boxes[index].textContent = check.checked ? "✓" : String(index + 1).padStart(2, "0"); }); const count = state.filter(Boolean).length; const percent = Math.round(count / checks.length * 100); document.querySelector("#progress-label").textContent = `${percent}% готово`; document.querySelector("#progress-count").textContent = `${count} из ${checks.length}`; document.querySelector("#progress-bar").style.width = `${percent}%`; document.querySelector("#success").hidden = percent !== 100; } checks.forEach((check) => check.addEventListener("change", () => updateProgress())); menuButton.addEventListener("click", () => { const open = nav.classList.toggle("open"); menuButton.setAttribute("aria-expanded", String(open)); menuButton.textContent = open ? "Закрыть" : "Меню"; }); nav.querySelectorAll("a").forEach((link) => link.addEventListener("click", () => { nav.classList.remove("open"); menuButton.setAttribute("aria-expanded", "false"); menuButton.textContent = "Меню"; })); window.addEventListener("scroll", () => topButton.classList.toggle("visible", window.scrollY > 600)); topButton.addEventListener("click", () => window.scrollTo({top: 0, behavior: "smooth"})); const routeTabs = [...document.querySelectorAll("[data-route]")]; const routePanels = [...document.querySelectorAll("[data-route-panel]")]; function selectRoute(route, moveFocus = false) { routeTabs.forEach((tab) => { const selected = tab.dataset.route === route; tab.setAttribute("aria-selected", String(selected)); tab.tabIndex = selected ? 0 : -1; if (selected && moveFocus) tab.focus(); }); routePanels.forEach((panel) => { panel.hidden = panel.dataset.routePanel !== route; }); } routeTabs.forEach((tab, index) => { tab.addEventListener("click", () => selectRoute(tab.dataset.route)); tab.addEventListener("keydown", (event) => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; event.preventDefault(); let next = index; if (event.key === 'ArrowRight') next = (index + 1) % routeTabs.length; if (event.key === 'ArrowLeft') next = (index - 1 + routeTabs.length) % routeTabs.length; if (event.key === 'Home') next = 0; if (event.key === 'End') next = routeTabs.length - 1; selectRoute(routeTabs[next].dataset.route, true); }); }); const trackedNavLinks = [...document.querySelectorAll('.nav a[href^="#"], .mobileDock a[href^="#"]')]; const trackedTargets = [...new Set(trackedNavLinks.map((link) => document.querySelector(link.getAttribute('href'))).filter(Boolean))]; function setActiveNav(targetId) { trackedNavLinks.forEach((link) => { link.classList.toggle('active', link.getAttribute('href') === `#${targetId}`); }); } if ('IntersectionObserver' in window) { const visibleSections = new Map(); const navObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) visibleSections.set(entry.target.id, entry.intersectionRatio); else visibleSections.delete(entry.target.id); }); const current = [...visibleSections.entries()].sort((a, b) => b[1] - a[1])[0]; if (current) setActiveNav(current[0]); }, {rootMargin: '-18% 0px -62% 0px', threshold: [0, .15, .35, .65]}); trackedTargets.forEach((target) => navObserver.observe(target)); } const faqSearch = document.querySelector('#faq-search'); const faqClear = document.querySelector('#faq-clear'); const faqResult = document.querySelector('#faq-result'); function filterFaq() { if (!faqSearch) return; const query = faqSearch.value.trim().toLocaleLowerCase('ru'); const items = [...document.querySelectorAll('#faq-list details')]; let visible = 0; items.forEach((item) => { const matches = !query || item.textContent.toLocaleLowerCase('ru').includes(query); item.hidden = !matches; if (matches) visible += 1; }); faqClear.hidden = !query; faqResult.textContent = query ? (visible ? `Найдено ответов: ${visible}` : 'Пока такого ответа нет. Спроси куратора или адаптера — и мы позже добавим его сюда.') : ''; } faqSearch?.addEventListener('input', filterFaq); faqClear?.addEventListener('click', () => { faqSearch.value = ''; filterFaq(); faqSearch.focus(); }); const groupDirectory = { nh: {code: "НХ", title: "Народный хор (сольное и хоровое народное пение)", curator: "Алешко Елена Ивановна", emoji: "🎶", tone: "music"}, mzm: {code: "МЗМ", title: "Музыкальное звукооператорское мастерство", curator: "Забродин Константин Валерьевич", emoji: "🎧", tone: "music"}, di: {code: "ДИ", title: "Духовые инструменты", curator: "Шпакова Ирина Владимировна", emoji: "🎺", tone: "music"}, ni: {code: "НИ", title: "Народные инструменты", curator: "Шпакова Ирина Владимировна", emoji: "🪗", tone: "music"}, fi: {code: "ФИ", title: "Фортепиано", curator: "Гололобова Мария Геннадьевна", emoji: "🎹", tone: "music"}, si: {code: "СИ", title: "Струнные инструменты", curator: "Гололобова Мария Геннадьевна", emoji: "🎻", tone: "music"}, tm: {code: "ТМ", title: "Теория музыки", curator: "Гололобова Мария Геннадьевна", emoji: "🎼", tone: "music"}, ht: {code: "ХТ", title: "Хореографическое творчество", curator: "Самусева Елизавета Николаевна", emoji: "💃", tone: "dance"}, skd: {code: "СКД", title: "Социально-культурная деятельность", curator: "Горячкин Семен Витальевич", emoji: "🎉", tone: "skd"}, vi: {code: "ВИ", title: "Вокальное искусство", curator: "Поршнева Юлия Юрьевна", emoji: "🎤", tone: "music"}, hd: {code: "ХД", title: "Хоровое дирижирование", curator: "Поршнева Юлия Юрьевна", emoji: "🎵", tone: "music"}, "mie-ep": {code: "МИЭ ЭП", title: "Музыкальное искусство эстрады: эстрадное пение", curator: "Железняк Антонина Анатольевна", emoji: "🎙️", tone: "music"}, "mie-ieo": {code: "МИЭ ИЭО", title: "Музыкальное искусство эстрады: инструменты эстрадного оркестра", curator: "Ли Данил Антонович", emoji: "🎸", tone: "music"}, zhiv: {code: "ЖИВ", title: "Живопись", curator: "Троегубова Надежда Викторовна", emoji: "🎨", tone: "art"} }; const groupSelect = document.querySelector("#group-select"); const groupCard = document.querySelector("#group-card"); const groupEmpty = document.querySelector("#group-empty"); const adapterSelect = document.querySelector("#adapter-group-select"); const adapterCard = document.querySelector("#adapter-card"); const adapterEmpty = document.querySelector("#adapter-empty"); let adapterRecords = []; let curatorRecords = []; function safePhoto(value) { return typeof value === "string" && value.startsWith("data:image/") ? value : ""; } function setDirectoryAvatar(element, photo, fallback) { if (!element) return; element.replaceChildren(); const safe = safePhoto(photo); if (safe) { const image = document.createElement("img"); image.src = safe; image.alt = ""; element.append(image); } else { element.textContent = fallback; } } async function loadPeopleData() { try { const [adaptersResponse, curatorsResponse, contentResponse] = await Promise.all([ fetch("./adapters.json", {cache:"no-store"}), fetch("./curators.json", {cache:"no-store"}), fetch("./site-content.json", {cache:"no-store"}) ]); if (adaptersResponse.ok) { const data = await adaptersResponse.json(); adapterRecords = Array.isArray(data.adapters) ? data.adapters : []; } if (curatorsResponse.ok) { const data = await curatorsResponse.json(); curatorRecords = Array.isArray(data.curators) ? data.curators : []; } if (contentResponse.ok) { renderSiteContent(await contentResponse.json()); } if (groupSelect?.value) groupSelect.dispatchEvent(new Event("change")); if (adapterSelect?.value) adapterSelect.dispatchEvent(new Event("change")); } catch (_) {} } const contentTones = new Set(["mint", "violet", "blue", "coral", "yellow"]); function contentTone(value) { return contentTones.has(value) ? value : "mint"; } function addTextElement(parent, tag, className, value) { const element = document.createElement(tag); if (className) element.className = className; element.textContent = String(value || ""); parent.append(element); return element; } function renderHelpers(items) { const grid = document.querySelector("#helper-grid"); if (!grid || !Array.isArray(items) || !items.length) return; grid.replaceChildren(); items.forEach((item, index) => { const article = document.createElement("article"); article.className = `tone-${contentTone(item.tone)}${item.featured ? " helperFeature" : ""}`; addTextElement(article, "span", "", String(index + 1).padStart(2, "0")); addTextElement(article, "h3", "", item.title); addTextElement(article, "p", "", item.description); addTextElement(article, "small", "", item.contact); grid.append(article); }); } function renderImportant(items) { const grid = document.querySelector("#important-grid"); if (!grid || !Array.isArray(items) || !items.length) return; grid.replaceChildren(); items.forEach((item, index) => { const article = document.createElement("article"); article.className = `tone-${contentTone(item.tone)}`; addTextElement(article, "span", "", String(index + 1).padStart(2, "0")); addTextElement(article, "h3", "", item.title); addTextElement(article, "p", "", item.description); addTextElement(article, "b", "", item.status); grid.append(article); }); } function renderFaq(items) { const list = document.querySelector("#faq-list"); if (!list || !Array.isArray(items) || !items.length) return; list.replaceChildren(); items.forEach((item, index) => { const details = document.createElement("details"); const summary = document.createElement("summary"); addTextElement(summary, "span", "", String(index + 1).padStart(2, "0")); summary.append(document.createTextNode(item.question || "")); addTextElement(summary, "i", "", "+"); details.append(summary); addTextElement(details, "p", "", item.answer); list.append(details); }); filterFaq(); } function renderSiteContent(data) { renderHelpers(data.helpers); renderImportant(data.important); renderFaq(data.faq); } groupSelect?.addEventListener("change", () => { const group = groupDirectory[groupSelect.value]; groupCard.hidden = !group; groupEmpty.hidden = Boolean(group); if (!group) return; groupCard.className = `groupCard ${group.tone}`; document.querySelector("#group-avatar").textContent = group.emoji; document.querySelector("#group-code").textContent = group.code; document.querySelector("#group-title").textContent = group.title; const curator = curatorRecords.find((item) => item.groupId === groupSelect.value); document.querySelector("#group-curator").textContent = curator?.fullName || group.curator; document.querySelector("#group-curator-contact").textContent = curator?.phone || "Добавим после подтверждения"; setDirectoryAvatar(document.querySelector("#group-avatar"), curator?.photo, group.emoji); }); adapterSelect?.addEventListener("change", () => { const group = groupDirectory[adapterSelect.value]; adapterCard.hidden = !group; adapterEmpty.hidden = Boolean(group); if (!group) return; adapterCard.className = `adapterCard ${group.tone}`; const adapter = adapterRecords.find((item) => item.groupId === adapterSelect.value); document.querySelector("#adapter-label").textContent = `Адаптер группы ${group.code}`; document.querySelector("#adapter-title").textContent = group.title; document.querySelector("#adapter-name").textContent = adapter?.fullName || "Имя скоро появится"; document.querySelector("#adapter-contact").textContent = adapter?.phone || "Контакт добавим"; document.querySelector("#adapter-course").textContent = adapter?.course || "Добавим после подтверждения"; setDirectoryAvatar(document.querySelector("#adapter-avatar"), adapter?.photo, group.emoji); }); loadPeopleData(); loadProgress(); filterFaq();