diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b74310c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +package-lock.json +npm-debug.log +.env +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md index 163d41b9..9f71cb83 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,36 @@ -# Безопасность веб-приложений. Лабораторка №2 +# Безопасность веб-приложений. Лабораторка №2. Расписание -## Схема сдачи +Удалось разработать завершенное целое веб-приложение, с клиентской и серверной частями. -1. Получить задание -2. Сделать форк данного репозитория -3. Выполнить задание согласно полученному варианту -4. Сделать PR (pull request) в данный репозиторий -6. Исправить замечания после code review -7. Получить approve -8. Прийти на занятие и защитить работу +Серверная часть была реализована с помощью Node.js и Express, что позволило: -Что нужно проявить в работе: -- умение разработать завершенное целое веб-приложение, с клиентской и серверной частями (допустимы открытые АПИ) -- навыки верстки на html в объеме 200-300 тегов -- навыки применения css для лейаута и стилизации, желательно с адаптацией к мобилке -- использование jQuery или аналогичных JS-фреймворков -- динамическая подгрузка контента -- динамическое изменение DOM и CSSOM +- Парсить реальные данные с официального сайта +- Предоставлять данные клиенту в формате JSON по API +- Поддерживать запросы расписания как для групп, так и для преподавателей -Если у вас своя идея по заданию, то расскажите, обсудим и подкорректирую. +Клиентская часть сделана с использованием HTML, CSS, jQuery. С помощью нее возможно: -## Вариант 1. Расписания +- Осуществлять поиск по номеру группы (6411, 6412, 6413) или фамилии преподавателя +- Переключать учебные недели с 32 по 36 +- Использовать приложение в мобильной версии (адаптивность) +- Динамически подгружать данные без перезагрузки страницы -Сделать аналог раздела https://ssau.ru/rasp?groupId=531030143 +Также соблюдено условие верстки в объеме 200-300 тегов. Визуальная составляющая была повторена с официального сайта, но с некоторыми огрешностями. -Какие нужны возможности: -- справочники групп, табличные данные по расписаниям добывать с настоящего сайта на серверной стороне приложения -- в клиентскую часть подгружать эти сведения динамически по JSON-API -- обеспечить возможность смотреть расписания в разрезе группы или препода -- обеспечить возможность выбора учебной недели (по умолчанию выбирается автоматически) +### Структура проекта -## Вариант 2. Аналог Прибывалки для электричек + `schedule.html` - разметка страницы + `schedule.css` - стили + `script.js` - клиентская логика + `server.js` - сервер + `result.png` - готовое расписание + `package.json` - зависимости -Сделать веб-версию Прибывалки, только для электричек +### Инструкция по запуску -Какие нужны возможности: -- находить желаемую ЖД-станцию поиском по названию и по карте -- отображать расписания всех проходящих поездов через выбранную станцию -- отображать расписания для поездов между двумя станциями -- работа через АПИ Яндекс.Расписаний https://yandex.ru/dev/rasp/doc/ru/ (доступ получите сами) -- хорошая работа в условиях экрана смартфона -- бонус: функция "любимых остановок" - -## Вариант 3. Прогноз погоды - -Сделать одностраничный сайт с картой, на которой можно выбрать населенный пункт и получить прогноз погоды на несколько дней по нему. - -Какие нужны возможности: - - увидеть на карте точки с населенными пунктами. Координаты населенных пунктов взять из https://tochno.st/datasets/allsettlements - но все 150 тысяч не нужно, выберите 1 тысячу с самым большим населением. - - при нажатии на точку получить всплывающее окошко с графиками изменения температуры, осадков, силы ветра. API для прогнозов возьмите с https://projecteol.ru/ru/ с соблюдением правил. - - графики рисовать каким-нибудь приличным компонентом, например, https://www.chartjs.org/ - - находить населенный пункт по названию - - можете реализовать с собственным серверным компонентом или придумать, как обойтись без него +- Установить Node.js +- Запустить сервер `node server.js` +- Открыть `schedule.html`. diff --git a/package.json b/package.json new file mode 100644 index 00000000..86ec8f68 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "axios": "^1.14.0", + "cheerio": "^1.2.0", + "cors": "^2.8.6", + "express": "^5.2.1" + } +} diff --git a/result.png b/result.png new file mode 100644 index 00000000..2e96a389 Binary files /dev/null and b/result.png differ diff --git a/schedule.css b/schedule.css new file mode 100644 index 00000000..4b19c15e --- /dev/null +++ b/schedule.css @@ -0,0 +1,540 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f5f5f5; + color: #333; + line-height: 1.4; +} + +.container { + max-width: 1200px; + margin: 20px auto; + padding: 0 15px; +} + +.schedule-header { + margin-bottom: 25px; +} + +#groupTitle { + font-size: 2.2rem; + font-weight: 600; + margin: 0 0 10px 0; + text-align: left; + color: #2c3e50; +} + +.divider { + height: 2px; + background: linear-gradient(to right, #107ac0 0%, #107ac0 15%, #ccc 15%, #ccc 100%); + width: 100%; +} + +.search-trigger { + width: 100%; + padding: 8px 15px; + font-size: 1rem; + border: 1px solid #ddd; + border-radius: 5px; + background-color: #fff; + display: flex; + justify-content: space-between; + align-items: center; + border-color: #107ac0; + cursor: pointer; + margin-bottom: 0; + transition: all 0.2s ease; +} + +.search-trigger:hover { + border-color: #bbb; +} + +.trigger-text { + color: #888; +} + +.search-arrow { + font-size: 12px; + color: #999; + transition: transform 0.2s; +} + +.search-trigger.open .search-arrow { + transform: rotate(180deg); +} + +.search-dropdown { + display: none; + margin-top: 2px; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + overflow: hidden; +} + +.search-dropdown.show { + display: block; +} + +.dropdown-inner { + padding: 8px; +} + +.dropdown-input { + width: 100%; + padding: 8px 12px; + font-size: 1rem; + border: 1px solid #ccc; + border-radius: 4px; + margin-bottom: 10px; +} + +.dropdown-input:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(52,152,219,0.2); +} + +.dropdown-hint { + font-size: 1rem; + color: #202020; + text-align: left; +} + +.group-info { + background: linear-gradient(135deg, #2884E2 0%, #443FA2 100%); + border-radius: 4px; + padding: 10px 15px 15px 15px; + margin: 35px 0; + box-shadow: 0 1px 3px rgba(0,0,0,1); +} + +.group-name { + font-size: 1.7rem; + font-weight: 350; + margin-bottom: 6px; + color: #ffffff; +} + +.group-details { + font-size: 1rem; + color: #e6e6e6; + margin-bottom: 6px; +} + +.group-start-year { + font-size: 1rem; + color: #e6e6e6; +} + +.legend { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 20px 0; +} + +.legend-item { + display: flex; + align-items: center; + padding: 6px 12px; + border-radius: 4px; + font-size: 0.78rem; + font-weight: 700; + background-color: transparent; +} + +.legend-color { + width: 9px; + height: 9px; + border-radius: 50%; + margin-right: 6px; + display: inline-block; +} + +.legend-dash { + margin: 0 4px; + color: #666; +} + +.legend-text { + color: #66727F; +} + +.lecture-color { background-color: #16A086; } +.practice-color { background-color: #64B5FF; } +.lab-color { background-color: #DF5FFF; } +.exam-color { background-color: #0B40B3; } +.test-color { background-color: #FFDE7B; } +.consultation-color { background-color: #0BB4BF; } +.other-color { background-color: #F19236; } + +.lecture-bg { background-color: #d9f3e3; } +.practice-bg { background-color: #d3e7ff; } +.lab-bg { background-color: #ffdafe; } +.exam-bg { background-color: #DAE2F4; } +.test-bg { background-color: #faf9d9; } +.consultation-bg { background-color: #cef7fc; } +.other-bg { background-color: #ffeed8; } + +.schedule-container { + background-color: #fff; + border-radius: 5px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + overflow-x: auto; + margin-top: 15px; + border: 1px solid #ddd; +} + +.schedule-header-block { + padding: 10px 15px; + border-bottom: 1px solid #A6D5FA; +} + +.week-nav { + display: flex; + justify-content: space-between; + align-items: center; + gap: 15px; + flex-wrap: nowrap; +} + +.week-nav-prev, .week-nav-next { + background: none; + font-size: 2rem; + border: none; + cursor: pointer; + color: #135291; + display: flex; + align-items: center; + justify-content: center; +} + +.week-nav-prev:disabled, .week-nav-next:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.week-number { + font-size: 1.2rem; + font-weight: bold; + display: inline-block; + padding: 5px 20px; + color: #135291; +} + +.schedule-wrapper { + overflow-x: auto; + padding: 0; +} + +.schedule-table { + width: 100%; + border-collapse: collapse; +} + +.schedule-table th, +.schedule-table td { + padding: 8px 6px; + vertical-align: top; + border: 2px solid #A6D5FA; +} + +.schedule-table th { + background-color: #f8f9fa; + font-weight: 600; + text-align: center; + vertical-align: middle; + font-size: 1rem; +} + +.time-col { + width: 70px; + text-align: center; + vertical-align: middle; +} + +.time-cell { + text-align: center; + font-weight: 500; + vertical-align: middle; +} + +.day-col { + min-width: 130px; +} + +.date { + font-size: 0.8rem; + font-weight: normal; + color: #000000; +} + +.lesson-cell { + background-color: #fff; + vertical-align: top; + padding: 8px 6px; +} + +.lesson-subject { + font-weight: bold; + margin-bottom: 4px; +} + +.lesson-place { + font-size: 0.9rem; + color: #555; + margin-bottom: 2px; +} + +.lesson-teacher { + font-size: 0.8rem; + color: #1a89e4; + margin-bottom: 2px; +} + +.lesson-groups { + font-size: 0.8rem; + color: #1a89e4; + margin-top: 3px; +} + +.empty { + background-color: #fefefe; + color: #999; +} + +.lesson-divider { + height: 1px; + background-color: #A6D5FA; + margin: 8px 0; +} + +.lesson-item { + margin-bottom: 4px; +} + +.lesson-item:last-child { + margin-bottom: 0; +} + +.lecture.lesson-item { + position: relative; + padding-left: 12px; +} + +.lecture.lesson-item::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background-color: #16A086; +} + +.practice.lesson-item { + position: relative; + padding-left: 12px; +} + +.practice.lesson-item::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background-color: #64B5FF; +} + +.lab.lesson-item { + position: relative; + padding-left: 12px; +} + +.lab.lesson-item::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background-color: #DF5FFF; +} + +.exam.lesson-item { + position: relative; + padding-left: 12px; +} + +.exam.lesson-item::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background-color: #0B40B3; +} + +.test.lesson-item { + position: relative; + padding-left: 12px; +} + +.test.lesson-item::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background-color: #FFDE7B; +} + +.consultation.lesson-item { + position: relative; + padding-left: 12px; +} + +.consultation.lesson-item::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background-color: #0BB4BF; +} + +.other.lesson-item { + position: relative; + padding-left: 12px; +} + +.other.lesson-item::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background-color: #F19236; +} + +.mobile-schedule { + display: flex; + flex-direction: column; + gap: 20px; + padding: 5px 0; +} + +.mobile-day-card { + background: white; + border: 1px solid #A6D5FA; + border-radius: 12px; + overflow: hidden; +} + +.mobile-day-header { + background: #e8f4f8; + padding: 12px 15px; + border-bottom: 2px solid #A6D5FA; + display: flex; + justify-content: space-between; + align-items: baseline; + flex-wrap: wrap; + gap: 5px; +} + +.mobile-day-name { + font-size: 1.1rem; + font-weight: bold; + color: #135291; +} + +.mobile-day-date { + font-size: 0.8rem; + color: #555; +} + +.mobile-day-lessons { + display: flex; + flex-direction: column; +} + +.mobile-lesson-item { + display: flex; + border-bottom: 1px solid #eee; + padding: 10px 12px; + gap: 12px; +} + +.mobile-lesson-item:last-child { + border-bottom: none; +} + +.mobile-lesson-time { + width: 70px; + flex-shrink: 0; + font-weight: 500; + font-size: 0.8rem; + color: #135291; + padding-top: 2px; +} + +.mobile-lesson-content { + flex: 1; +} + +.mobile-lesson-item.lecture { border-left: 4px solid #16A086; } +.mobile-lesson-item.practice { border-left: 4px solid #64B5FF; } +.mobile-lesson-item.lab { border-left: 4px solid #DF5FFF; } +.mobile-lesson-item.exam { border-left: 4px solid #0B40B3; } +.mobile-lesson-item.test { border-left: 4px solid #FFDE7B; } +.mobile-lesson-item.other { border-left: 4px solid #F19236; } + +.mobile-lesson-item.empty { + color: #888; + font-style: italic; + justify-content: center; +} + +@media (max-width: 768px) { + .schedule-wrapper { + display: none; + } +} + +@media (min-width: 769px) { + .mobile-schedule { + display: none; + } + + .week-nav { + justify-content: center; + gap: 20px; + } +} + +.loading { + text-align: center; + padding: 40px; + font-size: 1.2rem; + color: #3498db; +} + +.error { + text-align: center; + padding: 40px; + color: #e74c3c; + background-color: #fef2f2; + border-radius: 8px; +} \ No newline at end of file diff --git a/schedule.html b/schedule.html new file mode 100644 index 00000000..8f36f892 --- /dev/null +++ b/schedule.html @@ -0,0 +1,50 @@ + + + + + + Расписание Самарского университета + + + +
+
+

Расписание

+
+
+ +
+ Поиск... + +
+ +
+ +
+ +
+
+
+
+
+ +
+
ЛЕКЦИЯ
+
ПРАКТИКА
+
ЛАБОРАТОРНАЯ
+
ЭКЗАМЕН
+
ЗАЧЁТ
+
КОНСУЛЬТАЦИЯ
+
ДРУГОЕ
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 00000000..ed14c0e2 --- /dev/null +++ b/script.js @@ -0,0 +1,339 @@ +$(document).ready(function() { + const API_URL = 'http://localhost:3000/api/schedule'; + const TEACHER_API_URL = 'http://localhost:3000/api/teacher-schedule'; + + const groups = [ + { id: '1282690301', name: '6411-100503D', + details: '10.05.03 Информационная безопасность автоматизированных систем
Специалист (Очная форма обучения)', + startYear: 'Начало учебного года: 01.09.2025' }, + { id: '1282690279', name: '6412-100503D', + details: '10.05.03 Информационная безопасность автоматизированных систем
Специалист (Очная форма обучения)', + startYear: 'Начало учебного года: 01.09.2025' }, + { id: '1213641978', name: '6413-100503D', + details: '10.05.03 Информационная безопасность автоматизированных систем
Специалист (Очная форма обучения)', + startYear: 'Начало учебного года: 01.09.2025' } + ]; + + const teachers = [ + { id: '335824546', name: 'Максимов А.И.' }, + { id: '664017039', name: 'Борисов А.Н.' }, + { id: '333991624', name: 'Веричев А.В.' }, + { id: '364272302', name: 'Агафонов А.А.' }, + { id: '651422674', name: 'Позднякова Д.С.' }, + { id: '147619112', name: 'Кузнецов А.В.' }, + { id: '432837452', name: 'Юзькив Р.Р.' }, + { id: '544973937', name: 'Шапиро Д.А.' }, + { id: '62061001', name: 'Мясников В.В.' } + ]; + + const weeks = [32, 33, 34, 35, 36]; + let currentWeekIndex = 0; + + const searchTrigger = $('#searchTrigger'); + const searchDropdown = $('#searchDropdown'); + const groupInput = $('#groupInput'); + const container = $('#scheduleContainer'); + const groupTitle = $('#groupTitle'); + const groupNameDisplay = $('#groupNameDisplay'); + const groupDetailsDisplay = $('#groupDetailsDisplay'); + const startYearDisplay = $('#startYearDisplay'); + + let currentData = null; + + function setContent(content) { + const header = `
+
+ +
${weeks[currentWeekIndex]} неделя
+ +
+
`; + container.html(header + content); + updateWeekButtons(); + } + + function toggleSearch(open) { + if (open) { + searchDropdown.addClass('show'); + searchTrigger.addClass('open'); + groupInput.focus(); + } else { + searchDropdown.removeClass('show'); + searchTrigger.removeClass('open'); + } + } + + searchTrigger.on('click', function(e) { + e.stopPropagation(); + toggleSearch(!searchDropdown.hasClass('show')); + }); + + $(document).on('click', function(e) { + if (!$(e.target).closest('.search-trigger, .search-dropdown').length) { + toggleSearch(false); + } + }); + + function getTypeClass(type) { + let t = type.toLowerCase(); + if (t.includes('лекция')) return 'lecture'; + if (t.includes('практика')) return 'practice'; + if (t.includes('лабораторная')) return 'lab'; + if (t.includes('экзамен')) return 'exam'; + if (t.includes('зачёт')) return 'test'; + return 'other'; + } + + function desktopView(days, dates, matrix) { + let html = '
'; + html += ''; + for (let i = 0; i < days.length; i++) { + html += ``; + } + html += ''; + + for (let row of matrix) { + html += ''; + let time = row.time; + if (time.includes('–')) { + let parts = time.split('–'); + time = parts[0] + '
' + parts[1]; + } + html += ``; + + for (let cell of row.cells) { + if (cell && cell.length) { + let cellHtml = ''; + html += cellHtml; + } else { + html += ''; + } + } + html += ''; + } + html += '
Время${days[i]}
${dates[i] || ''}
${time}'; + for (let i = 0; i < cell.length; i++) { + let l = cell[i]; + let groups = l.groupsInfo ? `
${l.groupsInfo.split(',').map(g => g.trim()).join('
')}
` : ''; + cellHtml += `
+
${l.subject}
+
${l.place}
+
${l.teacher}
+ ${groups} +
`; + if (i < cell.length - 1) cellHtml += '
'; + } + cellHtml += '
'; + return html; + } + + function mobileView(days, dates, matrix) { + let daysData = []; + for (let i = 0; i < days.length; i++) { + let lessons = []; + for (let row of matrix) { + let time = row.time; + if (time.includes('–')) { + let parts = time.split('–'); + time = parts[0] + '
' + parts[1]; + } + let cell = row.cells[i]; + if (cell && cell.length) { + for (let l of cell) { + let groups = l.groupsInfo ? `
${l.groupsInfo.split(',').map(g => g.trim()).join('
')}
` : ''; + lessons.push({ + time: time, + subject: l.subject, + place: l.place, + teacher: l.teacher, + groups: groups, + type: getTypeClass(l.type) + }); + } + } + } + daysData.push({ name: days[i], date: dates[i] || '', lessons: lessons }); + } + + let html = '
'; + for (let day of daysData) { + html += `
+
+ ${day.name} + ${day.date} +
+
`; + if (day.lessons.length === 0) { + html += '
Нет занятий
'; + } else { + for (let l of day.lessons) { + html += `
+
${l.time}
+
+
${l.subject}
+
${l.place}
+
${l.teacher}
+ ${l.groups} +
+
`; + } + } + html += `
`; + } + html += '
'; + return html; + } + + function render(days, dates, matrix) { + if (!matrix || !matrix.length) { + setContent('

Расписание не найдено

'); + return; + } + let isMobile = window.innerWidth <= 768; + let content = isMobile ? mobileView(days, dates, matrix) : desktopView(days, dates, matrix); + setContent(content); + currentData = { days, dates, matrix }; + } + + $(window).on('resize', function() { + if (currentData) { + let isMobile = window.innerWidth <= 768; + let content = isMobile ? mobileView(currentData.days, currentData.dates, currentData.matrix) : desktopView(currentData.days, currentData.dates, currentData.matrix); + setContent(content); + } + }); + + function loadGroup(id, week) { + if (!id) return; + setContent('
Загрузка расписания...
'); + $.ajax({ + url: API_URL, + data: { groupId: id, week: week }, + success: function(res) { + if (res.scheduleMatrix && res.scheduleMatrix.length) { + render(res.days, res.dates, res.scheduleMatrix); + } else { + setContent('

Расписание пустое или не найдено

'); + } + }, + error: function() { + setContent('

Ошибка загрузки. Проверьте сервер.

'); + } + }); + } + + function loadTeacher(id, week) { + if (!id) return; + setContent('
Загрузка расписания преподавателя...
'); + $.ajax({ + url: TEACHER_API_URL, + data: { staffId: id, week: week }, + success: function(res) { + if (res.scheduleMatrix && res.scheduleMatrix.length) { + render(res.days, res.dates, res.scheduleMatrix); + } else { + setContent('

Расписание преподавателя не найдено

'); + } + }, + error: function() { + setContent('

Ошибка загрузки. Проверьте сервер.

'); + } + }); + } + + function updateGroupInfo(name) { + let group = groups.find(g => g.name === name); + if (group) { + groupNameDisplay.text(group.name); + groupDetailsDisplay.html(group.details); + startYearDisplay.text(group.startYear); + } else { + groupNameDisplay.text(''); + groupDetailsDisplay.text(''); + startYearDisplay.text(''); + } + } + + function findGroup(query) { + let q = query.trim().toLowerCase(); + if (!q) return null; + return groups.find(g => g.name.toLowerCase() === q || g.id === q || g.name.toLowerCase().includes(q)); + } + + function findTeacher(query) { + let q = query.trim().toLowerCase(); + if (!q) return null; + return teachers.find(t => t.name.toLowerCase().includes(q)); + } + + function updateWeekButtons() { + $('#weekNumber').text(`${weeks[currentWeekIndex]} неделя`); + $('#prevWeekBtn').prop('disabled', currentWeekIndex === 0); + $('#nextWeekBtn').prop('disabled', currentWeekIndex === weeks.length - 1); + } + + $(document).on('click', '#prevWeekBtn', function() { + if (currentWeekIndex > 0) { + currentWeekIndex--; + let week = weeks[currentWeekIndex]; + let groupId = $('#group').val(); + if (groupId) { + loadGroup(groupId, week); + } else { + loadGroup('1213641978', week); + } + updateWeekButtons(); + } + }); + + $(document).on('click', '#nextWeekBtn', function() { + if (currentWeekIndex < weeks.length - 1) { + currentWeekIndex++; + let week = weeks[currentWeekIndex]; + let groupId = $('#group').val(); + if (groupId) { + loadGroup(groupId, week); + } else { + loadGroup('1213641978', week); + } + updateWeekButtons(); + } + }); + + groupInput.on('keypress', function(e) { + if (e.which === 13) { + let query = groupInput.val(); + let group = findGroup(query); + if (group) { + groupTitle.text(`Расписание, ${group.name}`); + updateGroupInfo(group.name); + loadGroup(group.id, weeks[currentWeekIndex]); + toggleSearch(false); + groupInput.val(''); + return; + } + let teacher = findTeacher(query); + if (teacher) { + groupTitle.text(`Расписание преподавателя: ${teacher.name}`); + groupNameDisplay.text(''); + groupDetailsDisplay.text(''); + startYearDisplay.text(''); + loadTeacher(teacher.id, weeks[currentWeekIndex]); + toggleSearch(false); + groupInput.val(''); + return; + } + setContent('

Ни группа, ни преподаватель не найдены. Попробуйте: 6411, 6412, 6413 или фамилию преподавателя (например, Максимов)

'); + } + }); + + let defaultGroup = groups.find(g => g.name === '6413-100503D'); + if (defaultGroup) { + groupTitle.text(`Расписание, ${defaultGroup.name}`); + updateGroupInfo(defaultGroup.name); + loadGroup(defaultGroup.id, weeks[currentWeekIndex]); + } else { + groupTitle.text('Расписание'); + setContent('

Нажмите "Поиск..." и введите номер группы или фамилию преподавателя

'); + } +}); \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 00000000..89497817 --- /dev/null +++ b/server.js @@ -0,0 +1,121 @@ +const express = require('express'); +const axios = require('axios'); +const cheerio = require('cheerio'); +const cors = require('cors'); + +const app = express(); +const PORT = 3000; + +app.use(cors()); +app.use(express.json()); + +function parseSchedule(html) { + const $ = cheerio.load(html); + + const days = []; + $('.schedule__head-weekday').each((i, el) => days.push($(el).text().trim())); + + const dates = []; + $('.schedule__head-date').each((i, el) => dates.push($(el).text().trim())); + + const timeSlots = []; + $('.schedule__time').each((i, el) => { + const items = $(el).find('.schedule__time-item'); + if (items.length >= 2) { + const start = items.eq(0).text().trim().split(' ')[0]; + const end = items.eq(1).text().trim().split(' ')[0]; + timeSlots.push(`${start}–${end}`); + } else { + timeSlots.push($(el).text().trim()); + } + }); + + const matrix = []; + const children = $('.schedule__items').children(); + let timeIndex = 0; + + for (let i = 0; i < children.length; i++) { + const child = children.eq(i); + if (child.hasClass('schedule__time')) { + const currentTime = timeSlots[timeIndex] || ''; + const cells = []; + + for (let d = 0; d < days.length; d++) { + const cell = children.eq(i + 1 + d); + const lessons = []; + + if (cell.length && (cell.hasClass('schedule__item') || cell.hasClass('schedule__item_show'))) { + cell.find('.schedule__lesson-wrapper').each((idx, el) => { + const lesson = $(el); + const subject = lesson.find('.schedule__discipline').text().trim(); + if (subject) { + const place = lesson.find('.schedule__place').text().trim(); + const teacher = lesson.find('.schedule__teacher a').text().trim(); + const type = lesson.find('.schedule__lesson-type-chip').text().trim(); + let groupsInfo = ''; + const groupsElem = lesson.find('.schedule__groups'); + if (groupsElem.length) { + const groupLinks = groupsElem.find('a'); + if (groupLinks.length) { + groupsInfo = groupLinks.map((idx, el) => $(el).text().trim()).get().join(', '); + } else { + groupsInfo = groupsElem.text().trim(); + } + } + lessons.push({ subject, place, teacher, type, groupsInfo }); + } + }); + } + cells.push(lessons); + } + + matrix.push({ time: currentTime, cells: cells }); + i += days.length; + timeIndex++; + } + } + + return { days, dates, scheduleMatrix: matrix }; +} + +app.get('/api/schedule', async (req, res) => { + const { groupId, week = '32' } = req.query; + if (!groupId) return res.status(400).json({ error: 'Не указана группа' }); + + const url = `https://ssau.ru/rasp?groupId=${groupId}&selectedWeek=${week}`; + console.log(`Группа: ${url}`); + + try { + const response = await axios.get(url, { + headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } + }); + const data = parseSchedule(response.data); + res.json({ groupId, week, ...data }); + } catch (error) { + console.error('Ошибка группы:', error.message); + res.status(500).json({ error: 'Не удалось загрузить расписание группы' }); + } +}); + +app.get('/api/teacher-schedule', async (req, res) => { + const { staffId, week = '32' } = req.query; + if (!staffId) return res.status(400).json({ error: 'Не указан ID преподавателя' }); + + const url = `https://ssau.ru/rasp?staffId=${staffId}&selectedWeek=${week}`; + console.log(`Преподаватель: ${url}`); + + try { + const response = await axios.get(url, { + headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } + }); + const data = parseSchedule(response.data); + res.json({ staffId, week, ...data }); + } catch (error) { + console.error('Ошибка преподавателя:', error.message); + res.status(500).json({ error: 'Не удалось загрузить расписание преподавателя' }); + } +}); + +app.listen(PORT, () => { + console.log(`Сервер запущен на http://localhost:3000`); +}); \ No newline at end of file