Add dates, discipline fallbacks

This commit is contained in:
VityaSchel
2023-10-02 01:04:55 +04:00
parent de4208337e
commit f6daee6201
15 changed files with 592 additions and 219 deletions

View File

@@ -14,7 +14,7 @@ Reskin of https://lk.ks.psuti.ru/ since it lacks mobile support and is generally
- node-html-parser for scraping, rehydration strategy for cache
- TypeScript with types for each package
Built in 1 day. Tools used: pnpm, eslint.
Built in 1 day. Tools used: pnpm, eslint, react-icons.
## Hire me!

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
@@ -26,6 +27,7 @@
"node-html-parser": "^6.1.10",
"react": "latest",
"react-dom": "latest",
"react-icons": "^4.11.0",
"sharp": "^0.32.6",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7"

48
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ dependencies:
'@radix-ui/react-avatar':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
@@ -56,6 +59,9 @@ dependencies:
react-dom:
specifier: latest
version: 18.2.0(react@18.2.0)
react-icons:
specifier: ^4.11.0
version: 4.11.0(react@18.2.0)
sharp:
specifier: ^0.32.6
version: 0.32.6
@@ -446,6 +452,40 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.24)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.24)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.24)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.24)(react@18.2.0)
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.24)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.24)(react@18.2.0)
'@types/react': 18.2.24
'@types/react-dom': 18.2.8
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.2.24)(react@18.2.0)
dev: false
/@radix-ui/react-direction@1.0.1(@types/react@18.2.24)(react@18.2.0):
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
peerDependencies:
@@ -3241,6 +3281,14 @@ packages:
scheduler: 0.23.0
dev: false
/react-icons@4.11.0(react@18.2.0):
resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==}
peerDependencies:
react: '*'
dependencies:
react: 18.2.0
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true

View File

@@ -6,15 +6,15 @@ import { JSDOM } from 'jsdom'
import { content as mockContent } from './mock'
// ПС-7: 146
export async function getSchedule(groupID: number): Promise<Day[]> {
// const page = await fetch(`${process.env.PROXY_URL ?? 'https://lk.ks.psuti.ru'}/?mn=2&obj=${groupID}`)
const page = { text: async () => mockContent, status: 200, headers: { get: (s: string) => s && 'text/html' } }
export async function getSchedule(groupID: number, groupName: string): Promise<Day[]> {
const page = await fetch(`${process.env.PROXY_URL ?? 'https://lk.ks.psuti.ru'}/?mn=2&obj=${groupID}`)
// const page = { text: async () => mockContent, status: 200, headers: { get: (s: string) => s && 'text/html' } }
const content = await page.text()
const contentType = page.headers.get('content-type')
if (page.status === 200 && contentType && contentTypeParser.parse(contentType).type === 'text/html') {
try {
const root = new JSDOM(content).window.document
return parsePage(root)
return parsePage(root, groupName)
} catch(e) {
console.error('Error while parsing lk.ks.psuti.ru')
throw e

View File

@@ -10,67 +10,69 @@ const dayTitleParser = (text: string) => {
}
const parseLesson = (row: Element): Lesson | null => {
const cells = Array.from(row.querySelectorAll(':scope > td'))
if (cells[3].textContent!.trim() === 'Свободное время') return null
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const lesson: LessonObject = {}
const isChange = cells.every(td => td.getAttribute('bgcolor') === 'ffffbb')
try {
const cells = Array.from(row.querySelectorAll(':scope > td'))
if (cells[3].textContent!.trim() === 'Свободное время') return null
const timeCell = cells[1].childNodes
const [startTime, endTime] = timeCell[0].textContent!.trim().split(' ')
const time: Lesson['time'] = {
start: startTime ?? '',
end: endTime ?? ''
}
if (timeCell[2]) {
time.hint = timeCell[2].textContent!.trim()
}
lesson.isChange = cells.every(td => td.getAttribute('bgcolor') === 'ffffbb')
const subject = cells[3].childNodes[0].textContent!.trim()
let teacher: Lesson['teacher']
const teacherCell = cells[3].childNodes[2]
if (teacherCell) {
teacher = teacherCell.textContent!.trim()
}
const placeCell = cells[3].childNodes[3]
let place: Lesson['place']
if (placeCell) {
place = {
address: placeCell.childNodes[1].textContent!.trim(),
classroom: Number(placeCell.childNodes[3].textContent!.trim().match(/^Кабинет: (\d+)(-2)?$/)![1])
const timeCell = cells[1].childNodes
const [startTime, endTime] = timeCell[0].textContent!.trim().split(' ')
lesson.time = {
start: startTime ?? '',
end: endTime ?? ''
}
if (timeCell[2]) {
lesson.time.hint = timeCell[2].textContent!.trim()
}
}
const topic: Lesson['topic'] = cells[4].textContent!.trim()
try {
lesson.subject = cells[3].childNodes[0].textContent!.trim()
const resources: Lesson['resources'] = []
Array.from(cells[5].querySelectorAll('a'))
.forEach(a => {
resources.push({
type: 'link',
title: a.textContent!.trim(),
url: a.getAttribute('href')!
const teacherCell = cells[3].childNodes[2]
if (teacherCell) {
lesson.teacher = teacherCell.textContent!.trim()
}
const placeCell = cells[3].childNodes[3]
if (placeCell) {
lesson.place = {
address: placeCell.childNodes[1].textContent!.trim(),
classroom: placeCell.childNodes[3].textContent!.trim().match(/^Кабинет: ([^ ]+)(-2)?$/)![1]
}
}
} catch(e) {
console.error('Error while parsing discipline', e, cells[3].textContent?.trim())
lesson.fallbackDiscipline = cells[3].textContent?.trim()
}
lesson.topic = cells[4].textContent!.trim()
lesson.resources = []
Array.from(cells[5].querySelectorAll('a'))
.forEach(a => {
lesson.resources.push({
type: 'link',
title: a.textContent!.trim(),
url: a.getAttribute('href')!
})
})
})
return {
isChange,
time,
type: cells[2].textContent!.trim(),
subject,
...(teacher && { teacher }),
...(place && { place }),
...(topic && { topic }),
resources,
homework: cells[6].textContent!.trim()
return lesson
} catch(e) {
console.error('Error while parsing lesson in table', e, row.textContent?.trim())
return null
}
}
export function parsePage(document: Document): Day[] {
export function parsePage(document: Document, groupName: string): Day[] {
const tables = Array.from(document.querySelectorAll('body > table'))
const table = tables.find(table => table.querySelector(':scope > tbody > tr:first-child')?.textContent?.trim() === 'ПС-7')
const table = tables.find(table => table.querySelector(':scope > tbody > tr:first-child')?.textContent?.trim() === groupName)
const rows = Array.from(table!.children[0].children).filter(el => el.tagName === 'TR').slice(2)
const days = []

View File

@@ -1,6 +1,6 @@
import { Schedule } from '@/widgets/schedule'
import { Day } from '@/shared/model/day'
import { GetServerSidePropsResult } from 'next'
import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'
import { getSchedule } from '@/app/agregator/schedule'
import { NextSerialized, nextDeserializer, nextSerialized } from '@/app/utils/date-serializer'
import { NavBar } from '@/widgets/navbar'
@@ -20,12 +20,22 @@ export default function HomePage(props: PageProps) {
)
}
export async function getServerSideProps(): Promise<GetServerSidePropsResult<PageProps>> {
const schedule = await getSchedule(146)
return {
props: {
schedule: nextSerialized(schedule)
export async function getServerSideProps(context: GetServerSidePropsContext<{ group: string }>): Promise<GetServerSidePropsResult<PageProps>> {
const groups: { [group: string]: [number, string] } = {
ps7: [146, 'ПС-7'],
pks35k: [78, 'ПКС-35к']
}
const group = context.params?.group
if (group && Object.hasOwn(groups, group) && group in groups) {
const schedule = await getSchedule(...groups[group])
return {
props: {
schedule: nextSerialized(schedule)
}
}
} else {
return {
notFound: true
}
}
}

View File

@@ -2,7 +2,7 @@ import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Html lang="ru">
<Head />
<body>
<Main />

36
src/shadcn/ui/badge.tsx Normal file
View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/shared/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

121
src/shadcn/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,121 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/shared/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props} />
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -1,247 +1,258 @@
// https://gist.github.com/VityaSchel/28f1a360ee7798511765910b39c6086c
export const teachers = [
{
'name': 'Абалымова Людмила Павловна',
'picture': 'https://ks.psuti.ru/images/stories/emp/abalimova-l-.jpg'
name: 'Абалымова Людмила Павловна',
picture: 'https://ks.psuti.ru/images/stories/emp/abalimova-l-.jpg'
},
{
'name': 'Абрамова Светлана Геннадьевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/qwefdsfsd.jpg'
name: 'Абрамова Светлана Геннадьевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/qwefdsfsd.jpg'
},
{
'name': 'Алехин Иван Николаевич',
'picture': 'https://ks.psuti.ru/images/stories/emp/alehin-i-n.jpg'
name: 'Алехин Иван Николаевич',
picture: 'https://ks.psuti.ru/images/stories/emp/alehin-i-n.jpg'
},
{
'name': 'Амукова Светлана Николаевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/'
name: 'Амукова Светлана Николаевна',
picture: 'https://ks.psuti.ru/images/stories/emp/',
pronouns: 'she'
},
{
'name': 'Андреева Елена Сергеевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/-i-n.jpg'
name: 'Тарасова Таисия Евгеньевна',
picture: '',
pronouns: 'she'
},
{
'name': 'Андреевская Наталья Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2021/IMG_5419.jpg'
name: 'Андреева Елена Сергеевна',
picture: 'https://ks.psuti.ru/images/stories/emp/-i-n.jpg'
},
{
'name': 'Андрющенко Анна Вячеславовна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/andriushenko.jpg'
name: 'Андреевская Наталья Владимировна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2021/IMG_5419.jpg'
},
{
'name': 'Арефьев Андрей Андреевич',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/%20class='
name: 'Андрющенко Анна Вячеславовна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/andriushenko.jpg'
},
{
'name': 'Бондаренко Анастасия Вячеславовна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgCmhk.jpg'
name: 'Арефьев Андрей Андреевич',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/%20class=',
pronouns: 'he'
},
{
'name': 'Горшенина Ольга Николаевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/gorwenina-o-n.jpg'
name: 'Бондаренко Анастасия Вячеславовна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgCmhk.jpg'
},
{
'name': 'Дмитриева Наталья Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/dmitrieva-n-v.jpg'
name: 'Горшенина Ольга Николаевна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/gorwenina-o-n.jpg'
},
{
'name': 'Елисеева Эмиля Владиславовна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgC.jpg'
name: 'Дмитриева Наталья Владимировна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/dmitrieva-n-v.jpg'
},
{
'name': 'Ермолаева Галина Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/ermolaeva-g-v.jpg'
name: 'Елисеева Эмиля Владиславовна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgC.jpg'
},
{
'name': 'Жабборова Светлана Сергеевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/tryaskina-s-s.jpg'
name: 'Ермолаева Галина Владимировна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/ermolaeva-g-v.jpg'
},
{
'name': илина Елена Николаевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/jilina.jpg'
name: абборова Светлана Сергеевна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/tryaskina-s-s.jpg'
},
{
'name': 'Иванова Мария Сергеевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/konovalova.jpg'
name: 'Жилина Елена Николаевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/jilina.jpg'
},
{
'name': 'Карпеева Александра Сергеевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-karpeeva-a-s.jpg'
name: 'Иванова Мария Сергеевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/konovalova.jpg'
},
{
'name': 'Карпова Ирина Васильевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/karpova-i-v.jpg'
name: 'Карпеева Александра Сергеевна',
picture: 'https://ks.psuti.ru/images/stories/emp/2014-karpeeva-a-s.jpg',
pronouns: 'she'
},
{
'name': 'Козько Диана Игоревна',
'picture': 'https://ks.psuti.ru/images/stories/emp/kozko-d-i.jpg'
name: 'Карпова Ирина Васильевна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/karpova-i-v.jpg'
},
{
'name': 'Корнилова Светлана Александровна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/kornilova-s-a.jpg'
name: 'Козько Диана Игоревна',
picture: 'https://ks.psuti.ru/images/stories/emp/kozko-d-i.jpg'
},
{
'name': 'Краюшкина Ольга Борисовна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fsdadsd.jpg'
name: 'Корнилова Светлана Александровна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/kornilova-s-a.jpg'
},
{
'name': 'Крынкина Анна Андреевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/%D0%A7%D0%B0%D0%B4%D0%B5%D0%BD%D0%BA%D0%BE%D0%B2%D0%B0%20DSC06721.JPG'
name: 'Краюшкина Ольга Борисовна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fsdadsd.jpg'
},
{
'name': 'Кукарская Людмила Петровна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/kukarskaya-l-p.jpg'
name: 'Крынкина Анна Андреевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/%D0%A7%D0%B0%D0%B4%D0%B5%D0%BD%D0%BA%D0%BE%D0%B2%D0%B0%20DSC06721.JPG'
},
{
'name': 'Кусаева Зарина Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2022/%D0%B0%D0%B2%D0%BF%D0%BA%D1%83%D0%BF%D1%8B%D0%BF.jpg'
name: 'Кукарская Людмила Петровна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/kukarskaya-l-p.jpg'
},
{
'name': 'Ларионова Софья Николаевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
name: 'Кусаева Зарина Владимировна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2022/%D0%B0%D0%B2%D0%BF%D0%BA%D1%83%D0%BF%D1%8B%D0%BF.jpg',
pronouns: 'she'
},
{
'name': изунова Елена Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/lizunova.jpg'
name: арионова Софья Николаевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg',
pronouns: 'she'
},
{
'name': обачева Милана Евгеньевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/lobacheva-m-e.jpg'
name: изунова Елена Владимировна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/lizunova.jpg'
},
{
'name': 'Логвинов Александр Владимирович',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/logvinov_a_v.jpg'
name: 'Лобачева Милана Евгеньевна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/lobacheva-m-e.jpg'
},
{
'name': 'Малбасарова Галия Худанбаевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-kuntaeva.jpg'
name: 'Логвинов Александр Владимирович',
picture: 'https://ks.psuti.ru/images/stories/emp/new/logvinov_a_v.jpg'
},
{
'name': 'Матулина Татьяна Сергеевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/matulina.jpg'
name: 'Малбасарова Галия Худанбаевна',
picture: 'https://ks.psuti.ru/images/stories/emp/2014-kuntaeva.jpg'
},
{
'name': 'Михалькова Ирина Евгеньевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/2019-mihalkova.jpg'
name: 'Матулина Татьяна Сергеевна',
picture: 'https://ks.psuti.ru/images/stories/emp/matulina.jpg'
},
{
'name': 'Назарова Елена Федоровна',
'picture': 'https://ks.psuti.ru/images/stories/emp/nazarova.jpg'
name: 'Михалькова Ирина Евгеньевна',
picture: 'https://ks.psuti.ru/images/stories/emp/2019-mihalkova.jpg'
},
{
'name': 'Негина Айгуль Зинуловна',
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-aitasova-a-z.jpg'
name: 'Назарова Елена Федоровна',
picture: 'https://ks.psuti.ru/images/stories/emp/nazarova.jpg',
pronouns: 'bitch'
},
{
'name': 'Некрылова Татьяна Борисовна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/nekrylova-t-b.jpg'
name: 'Негина Айгуль Зинуловна',
picture: 'https://ks.psuti.ru/images/stories/emp/2014-aitasova-a-z.jpg'
},
{
'name': 'Никифоров Михаил Михайлович',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/nikiforov-m-m.jpg'
name: 'Некрылова Татьяна Борисовна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/nekrylova-t-b.jpg'
},
{
'name': 'Першина Елена Викторовна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/pershina-e-v.jpg'
name: 'Никифоров Михаил Михайлович',
picture: 'https://ks.psuti.ru/images/stories/emp/new/nikiforov-m-m.jpg'
},
{
'name': отяйкин Роман Владимирович',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
name: ершина Елена Викторовна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/pershina-e-v.jpg'
},
{
'name': 'Рзаева Алина Игоревна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/DSC06789.JPG'
name: 'Потяйкин Роман Владимирович',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
},
{
'name': 'Савич Мария Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/asdasad.jpg'
name: 'Рзаева Алина Игоревна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/DSC06789.JPG'
},
{
'name': 'Самойлова Наталья Николаевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2022/samoylova.jpg'
name: 'Савич Мария Владимировна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/asdasad.jpg'
},
{
'name': 'Семенов Антон Сергеевич',
'picture': 'https://ks.psuti.ru/images/stories/emp/antonov-a-s.jpg'
name: 'Самойлова Наталья Николаевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2022/samoylova.jpg'
},
{
'name': 'Сергеев Роман Алексеевич',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
name: 'Семенов Антон Сергеевич',
picture: 'https://ks.psuti.ru/images/stories/emp/antonov-a-s.jpg'
},
{
'name': 'Сиднина Юлия Валерьевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/%D1%81%D0%B8%D0%B4%D0%BD%D0%B8%D0%BD%D0%B0.jpg'
name: 'Сергеев Роман Алексеевич',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
},
{
'name': 'Синекопова Лариса Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/sinekopova-l-v.jpg'
name: 'Сиднина Юлия Валерьевна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/%D1%81%D0%B8%D0%B4%D0%BD%D0%B8%D0%BD%D0%B0.jpg'
},
{
'name': 'Сироткина Ольга Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/sirotkina-o-v.jpg'
name: 'Синекопова Лариса Владимировна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/sinekopova-l-v.jpg'
},
{
'name': 'Ситникова Людмила Геннадьевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/sitnikova-l-.jpg'
name: 'Сироткина Ольга Владимировна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/sirotkina-o-v.jpg'
},
{
'name': 'Славкина Татьяна Анатольевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/slavkina-t-a.jpg'
name: 'Ситникова Людмила Геннадьевна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/sitnikova-l-.jpg'
},
{
'name': 'Сорокина Надежда Леонидовна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/sorokina-n.jpg'
name: 'Славкина Татьяна Анатольевна',
picture: 'https://ks.psuti.ru/images/stories/emp/slavkina-t-a.jpg'
},
{
'name': 'Странник Дмитрий Христианович',
'picture': 'https://ks.psuti.ru/images/stories/emp/stranik.jpg'
name: 'Сорокина Надежда Леонидовна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/sorokina-n.jpg'
},
{
'name': 'Тананыхина Надежда Воалимировна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/IMG-f8eeb02.jpg'
name: 'Странник Дмитрий Христианович',
picture: 'https://ks.psuti.ru/images/stories/emp/stranik.jpg'
},
{
'name': 'Терёхин Дмитрий Вячеславович',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/terexin.jpg'
name: 'Тананыхина Надежда Воалимировна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/IMG-f8eeb02.jpg'
},
{
'name': 'Упанова Анастасия Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
name: 'Терёхин Дмитрий Вячеславович',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/terexin.jpg'
},
{
'name': 'Утыбаева Светлана Михайловна',
'picture': 'https://ks.psuti.ru/images/stories/emp/2019-utibaeva-s-m.jpg'
name: 'Упанова Анастасия Владимировна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
},
{
'name': 'Федотова Елена Дмитриевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fedotova-e-.jpg'
name: 'Утыбаева Светлана Михайловна',
picture: 'https://ks.psuti.ru/images/stories/emp/2019-utibaeva-s-m.jpg'
},
{
'name': омин Александр Васильевич',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/fomin-a-v.jpg'
name: едотова Елена Дмитриевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fedotova-e-.jpg'
},
{
'name': 'Ходотова Евгения Андреевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/hodotova.jpg'
name: 'Фомин Александр Васильевич',
picture: 'https://ks.psuti.ru/images/stories/emp/new/fomin-a-v.jpg'
},
{
'name': 'Черненкова Наталья Владимировна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/chernenkova-n-v.jpg'
name: 'Ходотова Евгения Андреевна',
picture: 'https://ks.psuti.ru/images/stories/emp/hodotova.jpg'
},
{
'name': 'Шамбер Лола Низамовна',
'picture': 'https://ks.psuti.ru/images/stories/emp/shamber-l-n.jpg'
name: 'Черненкова Наталья Владимировна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/chernenkova-n-v.jpg'
},
{
'name': омас Елена Александровна',
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-shomas.jpg'
name: амбер Лола Низамовна',
picture: 'https://ks.psuti.ru/images/stories/emp/shamber-l-n.jpg'
},
{
'name': укова Марина Геннадьевна',
'picture': 'https://ks.psuti.ru/images/stories/emp/new/shykova.jpg'
name: омас Елена Александровна',
picture: 'https://ks.psuti.ru/images/stories/emp/2014-shomas.jpg'
},
{
'name': 'Щербакова Надежда Юрьевна',
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/andreeva.jpg'
name: 'Шукова Марина Геннадьевна',
picture: 'https://ks.psuti.ru/images/stories/emp/new/shykova.jpg'
},
{
name: 'Щербакова Надежда Юрьевна',
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/andreeva.jpg'
}
]

View File

@@ -6,12 +6,6 @@ export type Lesson = {
hint?: string
}
type: string
subject: string
teacher?: string
place?: {
address: string
classroom: number
}
topic?: string
resources: {
type: 'link'
@@ -19,4 +13,13 @@ export type Lesson = {
url: string
}[]
homework: string
}
} & (
{
subject: string
teacher?: string
place?: {
address: string
classroom: string
}
} | { fallbackDiscipline?: string }
)

View File

@@ -6,11 +6,11 @@ import { useRouter } from 'next/router'
import cx from 'classnames'
export function NavBar() {
const { theme } = useTheme()
const { resolvedTheme } = useTheme()
return (
<header className="w-full p-2">
<nav className={cx('rounded-lg p-2 w-full flex justify-between', { 'bg-slate-200': theme === 'light', 'bg-slate-900': theme === 'dark' })}>
<nav className={cx('rounded-lg p-2 w-full flex justify-between', { 'bg-slate-200': resolvedTheme === 'light', 'bg-slate-900': resolvedTheme === 'dark' })}>
<ul className="flex gap-2">
<NavBarItem url="/ps7">ПС-7</NavBarItem>
<NavBarItem url="/pks35k">ПКС-35к</NavBarItem>

View File

@@ -14,15 +14,30 @@ export function Day({ day }: {
'Воскресенье'
][day.date.getDay()-1]
const longNames = day.lessons
.some(lesson => 'subject' in lesson && lesson.subject.length > 20)
return (
<div className="flex flex-col gap-5">
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
{dayOfWeek}
{dayOfWeek} <span className='text-muted ml-3'>{Intl.DateTimeFormat('ru-RU', {
day: 'numeric',
month: 'long',
// year: 'numeric'
}).format(day.date)}</span>
</h1>
<div className="flex flex-row gap-4">
{day.lessons.map((lesson, i) => (
<Lesson lesson={lesson} key={i} />
))}
<div className='overflow-hidden'>
<div className='overflow-auto'>
<div className="flex flex-row gap-4 w-max">
{day.lessons.map((lesson, i) => (
<Lesson
width={longNames ? 450 : 350}
lesson={lesson}
key={i}
/>
))}
</div>
</div>
</div>
</div>
)

View File

@@ -12,13 +12,28 @@ import {
AvatarFallback,
AvatarImage,
} from '@/shadcn/ui/avatar'
import { Badge } from '@/shadcn/ui/badge'
import { teachers } from '@/shared/data/teachers'
import { Lesson as LessonType } from '@/shared/model/lesson'
import React from 'react'
import { MdSchool } from 'react-icons/md'
import { AiOutlineFolderView } from 'react-icons/ai'
import { BsFillGeoAltFill } from 'react-icons/bs'
import { RiGroup2Fill } from 'react-icons/ri'
import { ResourcesDialog } from '@/widgets/schedule/resources-dialog'
export function Lesson({ lesson }: {
export function Lesson({ lesson, width = 350 }: {
lesson: LessonType
width: number
}) {
const teacherObj = lesson.teacher ? teachers.find(t => t.name === lesson.teacher) : null
const [resourcesDialogOpened, setResourcesDialogOpened] = React.useState(false)
const hasTeacher = 'teacher' in lesson && lesson.teacher
const teacherObj = hasTeacher ? teachers.find(t => t.name === lesson.teacher) : null
const hasPlace = 'place' in lesson && lesson.place
const isFallbackDiscipline = 'fallbackDiscipline' in lesson && lesson.fallbackDiscipline
const getTeacherPhoto = (url?: string) => {
if(url) {
@@ -34,27 +49,76 @@ export function Lesson({ lesson }: {
}
}
return (
<Card className="w-[350px]">
<div>
<Avatar>
<AvatarImage src={getTeacherPhoto(teacherObj?.picture)} alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar></Avatar>
<CardHeader>
<CardTitle>{lesson.subject}</CardTitle>
<CardDescription>{lesson.teacher}</CardDescription>
<CardDescription>{lesson.place?.classroom}</CardDescription>
</CardHeader>
</div>
<CardContent>
const fallbackTeacherName = () => {
if (!hasTeacher || !lesson.teacher) return ''
const [, firstName, middleName] = lesson.teacher.split(' ')
return firstName.at(0)! + middleName.at(0)!
}
const handleOpenResources = () => {
setResourcesDialogOpened(true)
}
return (
<Card className={`w-[${width}px] min-w-[${width}px] max-w-[${width}px] flex flex-col relative overflow-hidden`} style={{ minWidth: width, maxWidth: width }}>
{lesson.isChange && <div className='absolute top-0 left-0 w-full h-full bg-gradient-to-br from-[#ffc60026] to-[#95620026]'></div>}
<CardHeader>
<div className='flex gap-4'>
{hasTeacher ? (
<Avatar>
<AvatarImage
src={getTeacherPhoto(teacherObj?.picture)!}
alt={lesson.teacher}
title={lesson.teacher}
/>
<AvatarFallback title={lesson.teacher}>
{fallbackTeacherName()}
</AvatarFallback>
</Avatar>
) : (
<Avatar>
<AvatarFallback><MdSchool /></AvatarFallback>
</Avatar>
)}
<div className='flex flex-col gap-1'>
{'subject' in lesson && <CardTitle className='hyphens-auto'>{lesson.subject}</CardTitle>}
<CardDescription>
{lesson.time.start} - {lesson.time.end}{
}{lesson.time.hint && <span className='font-bold'>&nbsp;({lesson.time.hint})</span>}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{lesson.type && <><Badge>{lesson.type}</Badge>{' '}&nbsp;</>}
{isFallbackDiscipline && (
<span className='leading-relaxed hyphens-auto block'>{lesson.fallbackDiscipline}</span>
)}
{lesson.topic ? (
<span className='leading-relaxed hyphens-auto'>{lesson.topic}</span>
) : (
!isFallbackDiscipline && <span className='text-muted font-semibold'>Нет описания пары</span>
)}
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
{(Boolean(lesson.resources.length) || hasPlace) && (
<CardFooter className="flex justify-between mt-auto">
{('place' in lesson && lesson.place) ? (
<div className='flex flex-col text-muted-foreground text-xs'>
<span className='flex items-center gap-2'><BsFillGeoAltFill /> {lesson.place.address}</span>
<span className='font-bold flex items-center gap-2'><RiGroup2Fill /> {lesson.place.classroom}</span>
</div>
) : <span />}
{Boolean(lesson.resources.length) && (
<Button onClick={handleOpenResources}><AiOutlineFolderView />&nbsp;Материалы</Button>
)}
</CardFooter>
)}
<ResourcesDialog
open={resourcesDialogOpened}
onClose={() => setResourcesDialogOpened(false)}
teacherName={('teacher' in lesson && lesson.teacher) ? lesson.teacher : undefined}
resources={lesson.resources}
/>
</Card>
)
}

View File

@@ -0,0 +1,61 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/shadcn/ui/dialog'
import { teachers } from '@/shared/data/teachers'
import { Lesson } from '@/shared/model/lesson'
import Link from 'next/link'
import { BiLink } from 'react-icons/bi'
export function ResourcesDialog({ open, onClose, teacherName, resources }: {
open: boolean
onClose: () => any
teacherName?: string
resources: Lesson['resources']
}) {
const teacherPronouns = teachers.find(t => t.name === teacherName)?.pronouns
return (
<Dialog open={open} onOpenChange={isOpen => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Материалы к уроку</DialogTitle>
{teacherName && (
<DialogDescription>
{teacherName} {
teacherPronouns === 'she'
? 'поделилась'
: teacherPronouns === 'he'
? 'поделился'
: teacherPronouns === 'bitch'
? '(тварь) поделилась'
: 'поделилась(-ся)'
} материалами к этому уроку.
</DialogDescription>
)}
</DialogHeader>
<div className="grid gap-4 py-4">
{resources.map((resource, i) => <Resource resource={resource} key={i} />)}
</div>
</DialogContent>
</Dialog>
)
}
function Resource({ resource }: {
resource: Lesson['resources'][number]
}) {
if(resource.type === 'link') {
return (
<div className="flex items-center gap-4">
<BiLink />
<Link href={resource.url} className='whitespace-pre-wrap' target='_blank' rel='nofollower noreferrer'>
{resource.title}
</Link>
</div>
)
}
}