feat: добавлена админ-панель и главная страница с навигацией по курсам
Основные изменения: Админ-панель: - Создана защищенная паролем админ-панель по пути /admin - Реализована система авторизации с сессионными куками - Добавлен CRUD для управления группами (создание, редактирование, удаление) - Добавлено поле "курс" (1-5) для каждой группы с возможностью редактирования Структура данных: - Миграция групп из TypeScript файла в JSON формат (groups.json) - Обновлена структура данных: добавлено поле course - Реализована автоматическая миграция старых данных в новый формат - Создан groups-loader для работы с JSON файлом Главная страница: - Создана главная страница с аккордеоном по курсам (1-5) - Группы сгруппированы по курсам для удобной навигации - Добавлены кнопки: "Добавить группу", переключение темы и GitHub - Убрана верхняя навигация с главной страницы Навигация: - Добавлена кнопка "К группам" в начало навигации на страницах расписания - На мобильных устройствах скрыты кнопки групп, оставлена только кнопка возврата - Улучшена адаптивность навигации Технические улучшения: - Исправлена проблема с tailwind-scrollbar-hide (заменен плагин на CSS класс) - Обновлены все компоненты для работы с новой структурой данных групп - Добавлена поддержка переменных окружения ADMIN_PASSWORD и ADMIN_SESSION_SECRET
This commit is contained in:
128
src/shared/data/groups-loader.ts
Normal file
128
src/shared/data/groups-loader.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export type GroupInfo = {
|
||||
parseId: number
|
||||
name: string
|
||||
course: number
|
||||
}
|
||||
|
||||
export type GroupsData = { [group: string]: GroupInfo }
|
||||
|
||||
// Старый формат для миграции
|
||||
type OldGroupsData = { [group: string]: [number, string] | GroupInfo }
|
||||
|
||||
let cachedGroups: GroupsData | null = null
|
||||
|
||||
/**
|
||||
* Мигрирует старый формат данных в новый
|
||||
*/
|
||||
function migrateGroups(oldGroups: OldGroupsData): GroupsData {
|
||||
const migrated: GroupsData = {}
|
||||
|
||||
for (const [id, data] of Object.entries(oldGroups)) {
|
||||
// Проверяем, является ли это старым форматом [parseId, name]
|
||||
if (Array.isArray(data) && data.length === 2 && typeof data[0] === 'number' && typeof data[1] === 'string') {
|
||||
// Старый формат - мигрируем
|
||||
migrated[id] = {
|
||||
parseId: data[0],
|
||||
name: data[1],
|
||||
course: 1 // По умолчанию курс 1
|
||||
}
|
||||
} else if (typeof data === 'object' && 'parseId' in data && 'name' in data) {
|
||||
// Уже новый формат
|
||||
migrated[id] = data as GroupInfo
|
||||
}
|
||||
}
|
||||
|
||||
return migrated
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает группы из JSON файла
|
||||
* Использует кеш для оптимизации в production
|
||||
* Автоматически мигрирует старый формат в новый
|
||||
*/
|
||||
export function loadGroups(): GroupsData {
|
||||
if (cachedGroups) {
|
||||
return cachedGroups
|
||||
}
|
||||
|
||||
// В production Next.js может использовать другую структуру директорий
|
||||
// Пробуем несколько путей
|
||||
const possiblePaths = [
|
||||
path.join(process.cwd(), 'src/shared/data/groups.json'),
|
||||
path.join(process.cwd(), '.next/standalone/src/shared/data/groups.json'),
|
||||
path.join(process.cwd(), 'groups.json'),
|
||||
]
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const fileContents = fs.readFileSync(filePath, 'utf8')
|
||||
const rawGroups = JSON.parse(fileContents) as OldGroupsData
|
||||
|
||||
// Проверяем, нужна ли миграция
|
||||
const needsMigration = Object.values(rawGroups).some(
|
||||
data => Array.isArray(data) && data.length === 2
|
||||
)
|
||||
|
||||
let groups: GroupsData
|
||||
if (needsMigration) {
|
||||
// Мигрируем старый формат
|
||||
groups = migrateGroups(rawGroups)
|
||||
// Сохраняем мигрированные данные
|
||||
const mainPath = path.join(process.cwd(), 'src/shared/data/groups.json')
|
||||
if (filePath === mainPath) {
|
||||
// Сохраняем только если это основной файл
|
||||
fs.writeFileSync(mainPath, JSON.stringify(groups, null, 2), 'utf8')
|
||||
console.log('Groups data migrated to new format')
|
||||
}
|
||||
} else {
|
||||
groups = rawGroups as GroupsData
|
||||
}
|
||||
|
||||
cachedGroups = groups
|
||||
return groups
|
||||
}
|
||||
} catch (error) {
|
||||
// Пробуем следующий путь
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Error loading groups.json: file not found in any of the expected locations')
|
||||
// Fallback к пустому объекту
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет группы в JSON файл
|
||||
*/
|
||||
export function saveGroups(groups: GroupsData): void {
|
||||
// Всегда сохраняем в основной путь
|
||||
const filePath = path.join(process.cwd(), 'src/shared/data/groups.json')
|
||||
|
||||
try {
|
||||
// Создаем директорию, если её нет
|
||||
const dir = path.dirname(filePath)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(groups, null, 2), 'utf8')
|
||||
// Сбрасываем кеш
|
||||
cachedGroups = null
|
||||
} catch (error) {
|
||||
console.error('Error saving groups.json:', error)
|
||||
throw new Error('Failed to save groups')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сбрасывает кеш групп (полезно после обновления файла)
|
||||
*/
|
||||
export function clearGroupsCache(): void {
|
||||
cachedGroups = null
|
||||
}
|
||||
|
||||
22
src/shared/data/groups.json
Normal file
22
src/shared/data/groups.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ib4k": {
|
||||
"parseId": 138,
|
||||
"name": "ИБ-4к",
|
||||
"course": 4
|
||||
},
|
||||
"ib5": {
|
||||
"parseId": 144,
|
||||
"name": "ИБ-5",
|
||||
"course": 3
|
||||
},
|
||||
"ib6": {
|
||||
"parseId": 145,
|
||||
"name": "ИБ-6",
|
||||
"course": 3
|
||||
},
|
||||
"ib7k": {
|
||||
"parseId": 172,
|
||||
"name": "ИБ-7к",
|
||||
"course": 3
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
export const groups: { [group: string]: [number, string] } = {
|
||||
ib4k: [138, 'ИБ-4к'],
|
||||
ib5: [144, 'ИБ-5'],
|
||||
ib6: [145, 'ИБ-6'],
|
||||
ib7k: [172, 'ИБ-7к']
|
||||
// Загружаем группы из JSON файла только на сервере
|
||||
// На клиенте будет пустой объект, группы должны передаваться через props
|
||||
let groups: { [group: string]: [number, string] } = {}
|
||||
|
||||
// Используем условный require только на сервере для избежания включения fs в клиентскую сборку
|
||||
if (typeof window === 'undefined') {
|
||||
// Серверная сторона
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const groupsLoader = require('./groups-loader')
|
||||
groups = groupsLoader.loadGroups()
|
||||
} catch (error) {
|
||||
console.error('Error loading groups:', error)
|
||||
groups = {}
|
||||
}
|
||||
}
|
||||
|
||||
export { groups }
|
||||
|
||||
Reference in New Issue
Block a user