feat(schedule): auto-parsing groups from target site
This commit is contained in:
@@ -154,7 +154,7 @@ function cleanupCache() {
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext<{ group: string }>): Promise<GetServerSidePropsResult<NextSerialized<PageProps>>> {
|
||||
// Используем кеш (обновляется каждую минуту автоматически)
|
||||
const groups = loadGroups()
|
||||
const groups = await loadGroups()
|
||||
const settings = loadSettings()
|
||||
const group = context.params?.group
|
||||
const wkParam = context.query.wk
|
||||
|
||||
@@ -23,11 +23,13 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/shadcn/ui/accordion'
|
||||
import { SCHED_MODE } from '@/shared/constants/urls'
|
||||
|
||||
type AdminPageProps = {
|
||||
groups: GroupsData
|
||||
settings: AppSettings
|
||||
isDefaultPassword: boolean
|
||||
isKspsutiMode: boolean
|
||||
}
|
||||
|
||||
// Компонент Toggle Switch
|
||||
@@ -94,7 +96,7 @@ function DialogFooterButtons({ onCancel, onSubmit, submitLabel, loading, submitV
|
||||
)
|
||||
}
|
||||
|
||||
export default function AdminPage({ groups: initialGroups, settings: initialSettings, isDefaultPassword: initialIsDefaultPassword }: AdminPageProps) {
|
||||
export default function AdminPage({ groups: initialGroups, settings: initialSettings, isDefaultPassword: initialIsDefaultPassword, isKspsutiMode }: AdminPageProps) {
|
||||
const [authenticated, setAuthenticated] = React.useState<boolean | null>(null)
|
||||
const [password, setPassword] = React.useState('')
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
@@ -590,9 +592,11 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
<CardTitle>Группы</CardTitle>
|
||||
<CardDescription>Управление группами для расписания</CardDescription>
|
||||
</div>
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
Добавить группу
|
||||
</Button>
|
||||
{!isKspsutiMode && (
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
Добавить группу
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -610,23 +614,30 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
<div className="text-sm text-muted-foreground">
|
||||
ID: {id} | Parse ID: {group.parseId} | Курс: {group.course}
|
||||
</div>
|
||||
{isKspsutiMode && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Группа получена автоматически с lk.ks.psuti.ru. Редактирование отключено.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openEditDialog(id)}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => openDeleteDialog(id)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div>
|
||||
{!isKspsutiMode && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openEditDialog(id)}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => openDeleteDialog(id)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1124,7 +1135,7 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<AdminPageProps> = async () => {
|
||||
const groups = loadGroups()
|
||||
const groups = await loadGroups()
|
||||
const settings = loadSettings()
|
||||
|
||||
// Проверяем, используется ли дефолтный пароль
|
||||
@@ -1135,7 +1146,8 @@ export const getServerSideProps: GetServerSideProps<AdminPageProps> = async () =
|
||||
props: {
|
||||
groups,
|
||||
settings,
|
||||
isDefaultPassword: isDefault
|
||||
isDefaultPassword: isDefault,
|
||||
isKspsutiMode: SCHED_MODE === 'kspsuti',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
|
||||
import { loadGroups, saveGroups, clearGroupsCache, GroupsData } from '@/shared/data/groups-loader'
|
||||
import { validateGroupId, validateCourse } from '@/shared/utils/validation'
|
||||
import { SCHED_MODE } from '@/shared/constants/urls'
|
||||
|
||||
type ResponseData = ApiResponse<{
|
||||
groups?: GroupsData
|
||||
@@ -11,10 +12,15 @@ async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData>
|
||||
) {
|
||||
if (SCHED_MODE === 'kspsuti' && req.method !== 'GET') {
|
||||
res.status(403).json({ error: 'Groups are managed automatically from lk.ks.psuti.ru in this mode' })
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
// Получение списка групп (всегда свежие данные для админ-панели)
|
||||
clearGroupsCache()
|
||||
const groups = loadGroups(true)
|
||||
const groups = await loadGroups(true)
|
||||
res.status(200).json({ groups })
|
||||
return
|
||||
}
|
||||
@@ -50,7 +56,7 @@ async function handler(
|
||||
return
|
||||
}
|
||||
|
||||
const groups = loadGroups()
|
||||
const groups = await loadGroups()
|
||||
|
||||
// Проверка на дубликат
|
||||
if (groups[id]) {
|
||||
@@ -68,7 +74,7 @@ async function handler(
|
||||
saveGroups(groups)
|
||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
||||
clearGroupsCache()
|
||||
const updatedGroups = loadGroups(true)
|
||||
const updatedGroups = await loadGroups(true)
|
||||
res.status(200).json({ success: true, groups: updatedGroups })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
|
||||
import { loadGroups, saveGroups, clearGroupsCache, GroupsData } from '@/shared/data/groups-loader'
|
||||
import { validateCourse } from '@/shared/utils/validation'
|
||||
import { SCHED_MODE } from '@/shared/constants/urls'
|
||||
|
||||
type ResponseData = ApiResponse<{
|
||||
groups?: GroupsData
|
||||
@@ -18,8 +19,13 @@ async function handler(
|
||||
return
|
||||
}
|
||||
|
||||
if (SCHED_MODE === 'kspsuti') {
|
||||
res.status(403).json({ error: 'Groups are managed automatically from lk.ks.psuti.ru in this mode' })
|
||||
return
|
||||
}
|
||||
|
||||
// Загружаем группы с проверкой кеша
|
||||
let groups = loadGroups()
|
||||
let groups = await loadGroups()
|
||||
|
||||
if (req.method === 'PUT') {
|
||||
// Редактирование группы
|
||||
@@ -56,7 +62,7 @@ async function handler(
|
||||
saveGroups(groups)
|
||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
||||
clearGroupsCache()
|
||||
const updatedGroups = loadGroups(true)
|
||||
const updatedGroups = await loadGroups(true)
|
||||
res.status(200).json({ success: true, groups: updatedGroups })
|
||||
return
|
||||
}
|
||||
@@ -73,7 +79,7 @@ async function handler(
|
||||
saveGroups(groups)
|
||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
||||
clearGroupsCache()
|
||||
const updatedGroups = loadGroups(true)
|
||||
const updatedGroups = await loadGroups(true)
|
||||
res.status(200).json({ success: true, groups: updatedGroups })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,17 +118,6 @@ export default function HomePage(props: HomePageProps) {
|
||||
const [openCourses, setOpenCourses] = React.useState<Set<number>>(new Set())
|
||||
const [addGroupDialogOpen, setAddGroupDialogOpen] = React.useState(false)
|
||||
|
||||
// Подсчитываем смещения для каждого курса для последовательной анимации
|
||||
const courseOffsets = React.useMemo(() => {
|
||||
const offsets: { [course: number]: number } = {}
|
||||
let totalGroups = 0
|
||||
for (const course of [1, 2, 3, 4, 5]) {
|
||||
offsets[course] = totalGroups
|
||||
totalGroups += (groupsByCourse[course] || []).length
|
||||
}
|
||||
return { offsets, totalGroups }
|
||||
}, [groupsByCourse])
|
||||
|
||||
const toggleCourse = (course: number) => {
|
||||
setOpenCourses(prev => {
|
||||
const next = new Set(prev)
|
||||
@@ -158,7 +147,6 @@ export default function HomePage(props: HomePageProps) {
|
||||
{[1, 2, 3, 4, 5].map((course, courseIndex) => {
|
||||
const courseGroups = groupsByCourse[course] || []
|
||||
const isOpen = openCourses.has(course)
|
||||
const courseOffset = courseOffsets.offsets[course]
|
||||
|
||||
if (courseGroups.length === 0) {
|
||||
return null
|
||||
@@ -193,19 +181,10 @@ export default function HomePage(props: HomePageProps) {
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{courseGroups.map(({ id, name }, groupIndex) => {
|
||||
// Последовательная анимация: каждый следующий элемент с задержкой
|
||||
// courseOffset - это количество групп во всех предыдущих курсах
|
||||
// groupIndex - это индекс в текущем курсе
|
||||
// Итого: последовательный счетчик для всех групп подряд
|
||||
const globalIndex = courseOffset + groupIndex
|
||||
const delay = 0.15 + globalIndex * 0.04
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="stagger-card"
|
||||
style={{
|
||||
animationDelay: `${delay}s`,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Link href={`/${id}`}>
|
||||
<Button
|
||||
@@ -239,7 +218,6 @@ export default function HomePage(props: HomePageProps) {
|
||||
{showTeachersButton && (
|
||||
<div
|
||||
className="stagger-card mt-6"
|
||||
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.05}s` } as React.CSSProperties}
|
||||
>
|
||||
<Link href="/teachers" className="block">
|
||||
<Button variant="default" className="w-full h-auto py-4 text-base font-semibold">
|
||||
@@ -253,7 +231,6 @@ export default function HomePage(props: HomePageProps) {
|
||||
{showAddGroupButton && (
|
||||
<div
|
||||
className="stagger-card"
|
||||
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.08}s` } as React.CSSProperties}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -267,13 +244,11 @@ export default function HomePage(props: HomePageProps) {
|
||||
)}
|
||||
<div
|
||||
className="stagger-card"
|
||||
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + (showAddGroupButton ? 0.11 : 0.08)}s` } as React.CSSProperties}
|
||||
>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
<div
|
||||
className="stagger-card"
|
||||
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + (showAddGroupButton ? 0.14 : 0.11)}s` } as React.CSSProperties}
|
||||
>
|
||||
<Link href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Button variant="outline" className="gap-2">
|
||||
@@ -329,7 +304,7 @@ export const getServerSideProps: GetServerSideProps<HomePageProps> = async () =>
|
||||
}
|
||||
|
||||
// Если режим каникул выключен, загружаем группы и обрабатываем их
|
||||
const groups = loadGroups()
|
||||
const groups = await loadGroups()
|
||||
|
||||
// Группируем группы по курсам
|
||||
const groupsByCourse: { [course: number]: Array<{ id: string; name: string }> } = {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SITEMAP_SITE_URL } from '@/shared/constants/urls'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
// Используем кеш (обновляется каждую минуту автоматически)
|
||||
const groups = loadGroups()
|
||||
const groups = await loadGroups()
|
||||
const fields = Object.keys(groups).map<ISitemapField>(group => (
|
||||
{
|
||||
loc: `${SITEMAP_SITE_URL}/${group}`,
|
||||
|
||||
@@ -166,7 +166,7 @@ function cleanupCache() {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext<{ teacher: string }>): Promise<GetServerSidePropsResult<NextSerialized<PageProps>>> {
|
||||
const groups = loadGroups()
|
||||
const groups = await loadGroups()
|
||||
const settings = loadSettings()
|
||||
const teacherParam = context.params?.teacher
|
||||
const wkParam = context.query.wk
|
||||
|
||||
Reference in New Issue
Block a user