diff --git a/index.html b/index.html new file mode 100644 index 00000000..0c5fe9ec --- /dev/null +++ b/index.html @@ -0,0 +1,42 @@ + + + + + + Расписание SSAU + + + +
+
+

Расписание Самарского университета

+
+ +
+ + +
+ +
+
+

+

+

+
+
+ +
+ + + +
+ +
+

Загрузка расписания...

+
+
+ + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..83cdf698 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "1. Получить задание\r 2. Сделать форк данного репозитория\r 3. Выполнить задание согласно полученному варианту\r 4. Сделать PR (pull request) в данный репозиторий \r 6. Исправить замечания после code review\r 7. Получить approve \r 8. Прийти на занятие и защитить работу", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "axios": "^1.14.0", + "cheerio": "^1.2.0", + "cors": "^2.8.6", + "express": "^5.2.1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/GooDJopik/websec-2.git" + }, + "bugs": { + "url": "https://github.com/GooDJopik/websec-2/issues" + }, + "homepage": "https://github.com/GooDJopik/websec-2#readme" +} diff --git a/script.js b/script.js new file mode 100644 index 00000000..eeb92636 --- /dev/null +++ b/script.js @@ -0,0 +1,240 @@ +$(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 Информационная безопасность автоматизированных систем (Очная)', year:'01.09.2025' }, + { id:'1282690279', name:'6412-100503D', details:'10.05.03 Информационная безопасность автоматизированных систем (Очная)', year:'01.09.2025' }, + { id:'1213641978', name:'6413-100503D', details:'10.05.03 Информационная безопасность автоматизированных систем (Очная)', year:'01.09.2025' } + ]; + + const teachers = [ + { id: '664017039', name: 'Борисов А.Н.' }, + { id: '335824546', name: 'Максимов А.И.' }, + { id: '432837452', name: 'Юзькив Р.Р.' }, + { id: '333991624', name: 'Веричев А.В.' }, + { id: '62061001', name: 'Мясников В.В.' }, + { id: '364272302', name: 'Агафонов А.А.' }, + { id: '544973937', name: 'Шапиро Д.А.' }, + { id: '651422674', name: 'Позднякова Д.С.' }, + { id: '147619112', name: 'Кузнецов А.В.' }, + ]; + + let weeks = [32,33,34,35,36]; + + let currentWeekIndex = Math.floor((new Date() - new Date('2025-09-01')) / (7*24*60*60*1000)); + if(currentWeekIndex < 0) currentWeekIndex = 0; + if(currentWeekIndex >= weeks.length) currentWeekIndex = weeks.length - 1; + + let currentGroupId = null; + let currentTeacherId = null; + + let lastData = null; + + function updateWeekDisplay(){ + $('#currentWeek').text(`Неделя ${weeks[currentWeekIndex]}`); + $('#prevWeek').prop('disabled', currentWeekIndex === 0); + $('#nextWeek').prop('disabled', currentWeekIndex === weeks.length - 1); + } + + function clearInfo(){ + $('#groupName').text(''); + $('#groupDetails').text(''); + $('#groupYear').text(''); + } + + function renderSchedule(days, dates, matrix){ + + if(window.matchMedia("(min-width: 769px)").matches){ + + let html = ''; + + days.forEach((d,i)=>{ + html += ``; + }); + + html += ''; + + matrix.forEach((row)=>{ + html += ``; + + row.cells.forEach(cell=>{ + if(cell.length){ + let inner = ''; + + cell.forEach(l=>{ + let cls = l.type.toLowerCase().includes('лекция') ? 'lesson-lecture' : + l.type.toLowerCase().includes('практика') ? 'lesson-practice' : + l.type.toLowerCase().includes('лабораторная') ? 'lesson-lab' : 'lesson-other'; + + inner += `
+ ${l.subject}
+ ${l.teacher}
+ ${l.place} +
`; + }); + + html += ``; + } else { + html += ''; + } + }); + + html += ''; + }); + + html += '
Время${d}
${dates[i]}
${row.time || '—'}${inner}
'; + $('#scheduleContainer').html(html); + + } else { + + let html = ''; + + days.forEach((day, i)=>{ + html += `

${day} - ${dates[i]}

`; + + matrix.forEach((row)=>{ + let lessons = row.cells[i]; + + if(lessons.length){ + lessons.forEach(l=>{ + html += `
+ ${l.subject} | ${l.teacher} | ${l.place} | ${row.time} +
`; + }); + } else { + html += `
Нет занятий | ${row.time}
`; + } + }); + + html += '
'; + }); + + $('#scheduleContainer').html(html); + } + } + + function renderSaved(){ + if(lastData){ + renderSchedule(lastData.days, lastData.dates, lastData.scheduleMatrix); + } + } + + function updateGroupInfo(group){ + $('#groupName').text(group.name || ''); + $('#groupDetails').text(group.details || ''); + $('#groupYear').text(group.year ? `Начало: ${group.year}` : ''); + } + + function updateTeacherInfo(teacher){ + $('#groupName').text(teacher.name || ''); + $('#groupDetails').text('Преподаватель'); + $('#groupYear').text(''); + } + + function loadGroup(id){ + currentGroupId = id; + currentTeacherId = null; + + clearInfo(); + + const group = groups.find(g => g.id == id); + if(group) updateGroupInfo(group); + + $('#scheduleContainer').html('Загрузка...'); + + $.get(API_URL, {groupId: id, week: weeks[currentWeekIndex]}, function(res){ + lastData = res; + renderSchedule(res.days, res.dates, res.scheduleMatrix); + }); + } + + function loadTeacher(id){ + currentTeacherId = id; + currentGroupId = null; + + clearInfo(); + + const teacher = teachers.find(t => t.id == id); + if(teacher) updateTeacherInfo(teacher); + + $('#scheduleContainer').html('Загрузка...'); + + $.get(TEACHER_API_URL, {staffId: id, week: weeks[currentWeekIndex]}, function(res){ + lastData = res; + renderSchedule(res.days, res.dates, res.scheduleMatrix); + }); + } + + function autocomplete(input){ + let val = input.val().toLowerCase(); + let ul = $('#suggestions').empty(); + + let groupSuggestions = groups + .filter(g => g.name.toLowerCase().includes(val)) + .map(g => ({...g, type: 'group'})); + + let teacherSuggestions = teachers + .filter(t => t.name.toLowerCase().includes(val)) + .map(t => ({...t, type: 'teacher'})); + + let suggestions = [...groupSuggestions, ...teacherSuggestions]; + + if(suggestions.length){ + suggestions.forEach(s=>{ + ul.append(`
  • ${s.name}
  • `); + }); + ul.show(); + } else { + ul.hide(); + } + } + + $('#searchInput').on('keyup', function(){ + autocomplete($(this)); + }); + + $(document).on('click', '#suggestions li', function(){ + let id = $(this).data('id'); + let type = $(this).data('type'); + + $('#searchInput').val(''); + $('#suggestions').hide(); + + if(type === 'teacher'){ + loadTeacher(id); + } else { + loadGroup(id); + } + }); + + $('#prevWeek').click(function(){ + if(currentWeekIndex > 0){ + currentWeekIndex--; + updateWeekDisplay(); + + if(currentGroupId) loadGroup(currentGroupId); + else if(currentTeacherId) loadTeacher(currentTeacherId); + } + }); + + $('#nextWeek').click(function(){ + if(currentWeekIndex < weeks.length - 1){ + currentWeekIndex++; + updateWeekDisplay(); + + if(currentGroupId) loadGroup(currentGroupId); + else if(currentTeacherId) loadTeacher(currentTeacherId); + } + }); + + $(window).on('resize', function(){ + renderSaved(); + }); + + updateWeekDisplay(); + + let defaultGroup = groups.find(g => g.name === '6413-100503D'); + if(defaultGroup) loadGroup(defaultGroup.id); +}); \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 00000000..393a4128 --- /dev/null +++ b/server.js @@ -0,0 +1,154 @@ +const express = require('express'); +const axios = require('axios'); +const cheerio = require('cheerio'); +const cors = require('cors'); + +const app = express(); +app.use(cors()); + +function parseSchedule(html) { + const $ = cheerio.load(html); + + const days = []; + $('.schedule__head-weekday').each((i, e) => { + days.push($(e).text().trim()); + }); + + const dates = []; + $('.schedule__head-date').each((i, e) => { + dates.push($(e).text().trim()); + }); + + const matrix = []; + const children = $('.schedule__items').children(); + + for (let i = 0; i < children.length; i++) { + const el = children.eq(i); + + if (el.hasClass('schedule__time')) { + + const items = el.find('.schedule__time-item'); + + let time = ''; + if (items.length >= 2) { + const start = items.eq(0).text().trim(); + const end = items.eq(1).text().trim(); + time = `${start} – ${end}`; + } else { + time = el.text().trim(); + } + + 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 l = $(el); + + const subject = l.find('.schedule__discipline').text().trim(); + if (!subject) return; + + const place = l.find('.schedule__place').text().trim(); + + const teacher = l.find('.schedule__teacher').text().trim(); + + const type = l.find('.schedule__lesson-type-chip').text().trim(); + + let groupsInfo = ''; + const g = l.find('.schedule__groups'); + + if (g.length) { + const gl = g.find('a'); + groupsInfo = gl.length + ? gl.map((i, e) => $(e).text().trim()).get().join(', ') + : g.text().trim(); + } + + lessons.push({ + subject, + teacher: teacher || '—', + place: place || '—', + type: type || '—', + groups: groupsInfo || null + }); + }); + } + + cells.push(lessons); + } + + matrix.push({ + time: time || '—', + cells + }); + + i += days.length; + } + } + + 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: 'Не указана группа' }); + } + + try { + const response = await axios.get( + `https://ssau.ru/rasp?groupId=${groupId}&selectedWeek=${week}`, + { headers: { 'User-Agent': 'Mozilla/5.0' } } + ); + + res.json({ + type: 'group', + id: groupId, + week, + ...parseSchedule(response.data) + }); + + } catch (e) { + console.error(e.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: 'Не указан преподаватель' }); + } + + try { + const response = await axios.get( + `https://ssau.ru/rasp?staffId=${staffId}&selectedWeek=${week}`, + { headers: { 'User-Agent': 'Mozilla/5.0' } } + ); + + res.json({ + type: 'teacher', + id: staffId, + week, + ...parseSchedule(response.data) + }); + + } catch (e) { + console.error(e.message); + res.status(500).json({ error: 'Ошибка загрузки преподавателя' }); + } +}); + +app.listen(3000, () => { + console.log('Server running on http://localhost:3000'); +}); \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 00000000..e68da1fc --- /dev/null +++ b/styles.css @@ -0,0 +1,190 @@ +* { + margin:0; + padding:0; + box-sizing:border-box; +} + +body { + font-family: 'Segoe UI', sans-serif; + background:#f0f2f5; + color:#222; +} + +.main-container { + max-width: 1100px; + margin: 20px auto; + padding: 0 15px; +} + +.header { + text-align:center; + margin-bottom:20px; +} + +.header h1 { + font-size:2rem; + color:#1a3d7c; +} + +.search-section { + position:relative; + margin-bottom:20px; +} + +#searchInput { + width:100%; + padding:10px; + border:1px solid #1a3d7c; + border-radius:6px; + font-size:1rem; +} + +.suggestions { + list-style:none; + position:absolute; + top:100%; + left:0; + right:0; + background:#fff; + border:1px solid #ccc; + max-height:200px; + overflow-y:auto; + display:none; + z-index:10; +} + +.suggestions li { + padding:8px; + cursor:pointer; +} + +.suggestions li:hover { + background:#1a3d7c; + color:#fff; +} + +.info-section { + margin-bottom:20px; +} + +.group-card { + background:#1a3d7c; + color:#fff; + padding:15px; + border-radius:8px; +} + +.group-card h2 { + font-size:1.5rem; + margin-bottom:5px; +} + +.group-card p { + font-size:0.95rem; + margin-bottom:3px; +} + +.week-selector { + display:flex; + justify-content:center; + align-items:center; + gap:15px; + margin-bottom:20px; +} + +.week-selector button { + padding:5px 12px; + font-size:1rem; + border:none; + background:#1a3d7c; + color:#fff; + border-radius:4px; + cursor:pointer; +} + +.week-selector button:disabled { + opacity:0.5; + cursor:not-allowed; +} + +#currentWeek { + font-weight:bold; + font-size:1.1rem; + color:#1a3d7c; +} + +.schedule-container { + background:#fff; + border-radius:6px; + box-shadow:0 2px 8px rgba(0,0,0,0.1); + overflow-x:auto; + padding:10px; +} + +.schedule-table { + width:100%; + border-collapse:collapse; +} + +.schedule-table th, .schedule-table td { + border:1px solid #ddd; + padding:8px; + text-align:center; + vertical-align:top; +} + +.schedule-table th { + background:#e0e5ec; + font-weight:600; +} + +.lesson-cell { + text-align:left; + padding:5px; + font-size:0.9rem; +} + +.lesson-lecture { + border-left:4px solid #16A086; + padding-left:6px; +} + +.lesson-practice { + border-left:4px solid #64B5FF; + padding-left:6px; +} + +.lesson-lab { + border-left:4px solid #DF5FFF; + padding-left:6px; +} + +.lesson-other { + border-left:4px solid #F19236; + padding-left:6px; +} + +@media (max-width:768px){ + .schedule-table { + display:none; + } + + .mobile-day { + background:#f8f8f8; + margin-bottom:10px; + padding:10px; + border-radius:8px; + } + + .mobile-day h3 { + color:#1a3d7c; + font-size:1rem; + margin-bottom:5px; + } + + .mobile-lesson { + padding:5px; + margin-bottom:4px; + border-left:4px solid; + } +} \ No newline at end of file