refactor: optimize project structure, migrate to SQLite, and add new features
fix:
- Fix TypeScript type errors in api-wrapper.ts (ApiResponse type)
- Fix backward compatibility in database.ts getSettings() for missing fields
- Fix default value for weekNavigationEnabled (changed from true to false)
- Fix API routes error handling with unified wrapper
- Fix duplicate toggle switch code in admin.tsx (6 instances)
- Fix inconsistent authentication check in API routes (unified with withAuth)
- Fix error message text in loading-context.tsx (improved user experience)
add:
- Add database.ts: SQLite database layer with better-sqlite3 for persistent storage
* Groups management (CRUD operations)
* Settings management with caching
* Admin password hashing with bcrypt
* Automatic database initialization and migration
- Add api-wrapper.ts utility for unified API route handling
* withAuth wrapper for protected routes
* withMethods wrapper for public routes
* Consistent error handling and method validation
- Add validation.ts utility with centralized validation functions
* validateCourse - course validation (1-5)
* validateGroupId - group ID format validation
* validatePassword - password strength validation
- Add showAddGroupButton setting to control visibility of 'Add Group' button on homepage
- Add toggle switch component in admin.tsx for reusable UI (replaces 6 duplicate instances)
- Add CourseSelect component in admin.tsx for reusable course selection
- Add DialogFooterButtons component in admin.tsx for reusable dialog footer
- Add unified loadData function in admin.tsx to reduce code duplication
- Add change-password.ts API endpoint for admin password management
- Add logs.ts API endpoint for viewing error logs in admin panel
- Add logErrorToFile function in logger.ts for persistent error logging
- Add comprehensive error logging in schedule.ts (parsing, fetch, timeout, network errors)
- Add comprehensive project structure documentation in README.md
- Add architecture and code organization section in README.md
- Add database information section in README.md
- Add SQLite and bcrypt to tech stack documentation
- Add better-sqlite3 and bcrypt dependencies to package.json
- Add .gitignore rules for error.log and database files (data/, *.db, *.db-shm, *.db-wal)
refactor:
- Refactor admin.tsx: extract reusable components (toggle, select, dialog footer)
- Refactor API routes to use withAuth wrapper for consistent authentication
- Refactor API routes to use validation utilities instead of inline validation
- Refactor groups.ts and groups.json: move to old/data/ directory (deprecated, now using SQLite)
- Refactor settings-loader.ts: migrate from JSON to SQLite database
- Refactor groups-loader.ts: migrate from JSON to SQLite database
- Refactor database.ts: improve backward compatibility for settings migration
- Refactor admin.tsx: unify data loading functions (loadGroupsList, loadSettingsList)
- Refactor index.tsx: add showAddGroupButton prop and conditional rendering
- Refactor API routes: consistent error handling and method validation
- Refactor README.md: update tech stack, project structure, and admin panel documentation
- Refactor auth.ts: improve session management and cookie handling
- Refactor schedule.ts: improve error handling with detailed logging and error types
- Refactor logger.ts: add file-based error logging functionality
- Refactor loading-context.tsx: improve error message clarity
remove:
- Remove hello.ts test API endpoint
- Remove groups.ts and groups.json (moved to old/data/, replaced by SQLite)
update:
- Update .gitignore to exclude old data files, database files, and error logs
- Update package.json: add better-sqlite3, bcrypt and their type definitions
- Update README.md with new features, architecture, and database information
- Update all API routes to use new wrapper system
- Update admin panel with new settings and improved UI
- Update sitemap.xml with cache usage comment
This commit is contained in:
@@ -275,10 +275,101 @@ const parseLesson = (row: Element): Lesson | null => {
|
||||
const isFreeTimeReplacement = lesson.isChange &&
|
||||
(cellText.includes('Свободное время') && cellText.includes('Замена') && cellText.includes('на:'))
|
||||
|
||||
// Проверяем, является ли это заменой предмета на предмет
|
||||
const isSubjectReplacement = lesson.isChange &&
|
||||
!isFreeTimeReplacement &&
|
||||
cellText.includes('Замена') &&
|
||||
cellText.includes('на:')
|
||||
|
||||
if (isFreeTimeReplacement) {
|
||||
// Для замены "свободное время" на пару нужно парсить данные после "на:"
|
||||
// Структура: "Замена Свободное время на:</a><br> название <br> преподаватель <font> адрес <br> кабинет </font>
|
||||
|
||||
// Используем HTML парсинг для извлечения данных после "на:"
|
||||
const afterOnIndex = cellHTML.indexOf('на:')
|
||||
if (afterOnIndex !== -1) {
|
||||
const afterOn = cellHTML.substring(afterOnIndex + 3) // +3 для "на:"
|
||||
|
||||
// Пропускаем первый <br> (он идет сразу после "на:")
|
||||
const firstBrIndex = afterOn.indexOf('<br')
|
||||
if (firstBrIndex !== -1) {
|
||||
// Находим конец первого <br> тега
|
||||
const firstBrEnd = afterOn.indexOf('>', firstBrIndex) + 1
|
||||
const afterFirstBr = afterOn.substring(firstBrEnd)
|
||||
|
||||
// Извлекаем название предмета (текст до следующего <br>)
|
||||
const secondBrIndex = afterFirstBr.indexOf('<br')
|
||||
if (secondBrIndex !== -1) {
|
||||
const subjectHTML = afterFirstBr.substring(0, secondBrIndex)
|
||||
lesson.subject = subjectHTML.replace(/<[^>]+>/g, '').trim()
|
||||
|
||||
// Извлекаем преподавателя (текст между вторым <br> и <font> или следующим <br>)
|
||||
const secondBrEnd = afterFirstBr.indexOf('>', secondBrIndex) + 1
|
||||
const afterSecondBr = afterFirstBr.substring(secondBrEnd)
|
||||
|
||||
const fontIndex = afterSecondBr.indexOf('<font')
|
||||
if (fontIndex !== -1) {
|
||||
const teacherHTML = afterSecondBr.substring(0, fontIndex)
|
||||
lesson.teacher = teacherHTML.replace(/<[^>]+>/g, '').trim()
|
||||
} else {
|
||||
// Если нет <font>, преподаватель может быть до следующего <br> или до конца
|
||||
const thirdBrIndex = afterSecondBr.indexOf('<br')
|
||||
if (thirdBrIndex !== -1) {
|
||||
const teacherHTML = afterSecondBr.substring(0, thirdBrIndex)
|
||||
lesson.teacher = teacherHTML.replace(/<[^>]+>/g, '').trim()
|
||||
} else {
|
||||
lesson.teacher = afterSecondBr.replace(/<[^>]+>/g, '').trim()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Если нет второго <br>, название предмета может быть до <font> или до конца
|
||||
const fontIndex = afterFirstBr.indexOf('<font')
|
||||
if (fontIndex !== -1) {
|
||||
const subjectHTML = afterFirstBr.substring(0, fontIndex)
|
||||
lesson.subject = subjectHTML.replace(/<[^>]+>/g, '').trim()
|
||||
} else {
|
||||
lesson.subject = afterFirstBr.replace(/<[^>]+>/g, '').trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем адрес и кабинет внутри <font>
|
||||
const fontMatch = afterOn.match(/<font[^>]*>([\s\S]*?)<\/font>/i)
|
||||
if (fontMatch) {
|
||||
const fontContent = fontMatch[1]
|
||||
// Ищем паттерн: <br> адрес <br> Кабинет: номер
|
||||
// Сначала убираем все теги и разбиваем по <br>
|
||||
const cleanContent = fontContent.replace(/<[^>]+>/g, '|').split('|').filter(p => p.trim())
|
||||
// Ищем адрес (первая непустая часть) и кабинет (часть с "Кабинет:")
|
||||
for (let i = 0; i < cleanContent.length; i++) {
|
||||
const part = cleanContent[i].trim()
|
||||
if (part && !part.includes('Кабинет:')) {
|
||||
const nextPart = cleanContent[i + 1]?.trim() || ''
|
||||
const classroomMatch = nextPart.match(/Кабинет:\s*([^\s]+)/i)
|
||||
if (classroomMatch) {
|
||||
lesson.place = {
|
||||
address: part,
|
||||
classroom: classroomMatch[1]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Если нет <font>, ищем адрес и кабинет напрямую в тексте после "на:"
|
||||
const addressMatch = afterOn.match(/([^<]+?)(?:<br[^>]*>|\s+)Кабинет:\s*([^<\s]+)/i)
|
||||
if (addressMatch) {
|
||||
lesson.place = {
|
||||
address: addressMatch[1].replace(/<[^>]+>/g, '').trim(),
|
||||
classroom: addressMatch[2].trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isSubjectReplacement) {
|
||||
// Для замены предмета на предмет нужно парсить данные после "на:"
|
||||
// Структура: "Замена [старый предмет] на:</a><br> [новый предмет] <br> [преподаватель] <font> [адрес] <br> Кабинет: [номер] </font>
|
||||
|
||||
// Используем HTML парсинг для извлечения данных после "на:"
|
||||
const afterOnIndex = cellHTML.indexOf('на:')
|
||||
if (afterOnIndex !== -1) {
|
||||
|
||||
Reference in New Issue
Block a user