Add dates, discipline fallbacks
This commit is contained in:
@@ -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
|
- node-html-parser for scraping, rehydration strategy for cache
|
||||||
- TypeScript with types for each package
|
- 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!
|
## Hire me!
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@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-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"node-html-parser": "^6.1.10",
|
"node-html-parser": "^6.1.10",
|
||||||
"react": "latest",
|
"react": "latest",
|
||||||
"react-dom": "latest",
|
"react-dom": "latest",
|
||||||
|
"react-icons": "^4.11.0",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
|||||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ dependencies:
|
|||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.0.4
|
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)
|
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':
|
'@radix-ui/react-dropdown-menu':
|
||||||
specifier: ^2.0.6
|
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)
|
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:
|
react-dom:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
react-icons:
|
||||||
|
specifier: ^4.11.0
|
||||||
|
version: 4.11.0(react@18.2.0)
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.32.6
|
specifier: ^0.32.6
|
||||||
version: 0.32.6
|
version: 0.32.6
|
||||||
@@ -446,6 +452,40 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
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):
|
/@radix-ui/react-direction@1.0.1(@types/react@18.2.24)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
|
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3241,6 +3281,14 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
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:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import { JSDOM } from 'jsdom'
|
|||||||
import { content as mockContent } from './mock'
|
import { content as mockContent } from './mock'
|
||||||
|
|
||||||
// ПС-7: 146
|
// ПС-7: 146
|
||||||
export async function getSchedule(groupID: number): Promise<Day[]> {
|
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 = 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 page = { text: async () => mockContent, status: 200, headers: { get: (s: string) => s && 'text/html' } }
|
||||||
const content = await page.text()
|
const content = await page.text()
|
||||||
const contentType = page.headers.get('content-type')
|
const contentType = page.headers.get('content-type')
|
||||||
if (page.status === 200 && contentType && contentTypeParser.parse(contentType).type === 'text/html') {
|
if (page.status === 200 && contentType && contentTypeParser.parse(contentType).type === 'text/html') {
|
||||||
try {
|
try {
|
||||||
const root = new JSDOM(content).window.document
|
const root = new JSDOM(content).window.document
|
||||||
return parsePage(root)
|
return parsePage(root, groupName)
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('Error while parsing lk.ks.psuti.ru')
|
console.error('Error while parsing lk.ks.psuti.ru')
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
@@ -10,67 +10,69 @@ const dayTitleParser = (text: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parseLesson = (row: Element): Lesson | null => {
|
const parseLesson = (row: Element): Lesson | null => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
const lesson: LessonObject = {}
|
||||||
|
|
||||||
|
try {
|
||||||
const cells = Array.from(row.querySelectorAll(':scope > td'))
|
const cells = Array.from(row.querySelectorAll(':scope > td'))
|
||||||
if (cells[3].textContent!.trim() === 'Свободное время') return null
|
if (cells[3].textContent!.trim() === 'Свободное время') return null
|
||||||
|
|
||||||
const isChange = cells.every(td => td.getAttribute('bgcolor') === 'ffffbb')
|
lesson.isChange = cells.every(td => td.getAttribute('bgcolor') === 'ffffbb')
|
||||||
|
|
||||||
const timeCell = cells[1].childNodes
|
const timeCell = cells[1].childNodes
|
||||||
const [startTime, endTime] = timeCell[0].textContent!.trim().split(' – ')
|
const [startTime, endTime] = timeCell[0].textContent!.trim().split(' – ')
|
||||||
const time: Lesson['time'] = {
|
lesson.time = {
|
||||||
start: startTime ?? '',
|
start: startTime ?? '',
|
||||||
end: endTime ?? ''
|
end: endTime ?? ''
|
||||||
}
|
}
|
||||||
if (timeCell[2]) {
|
if (timeCell[2]) {
|
||||||
time.hint = timeCell[2].textContent!.trim()
|
lesson.time.hint = timeCell[2].textContent!.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const subject = cells[3].childNodes[0].textContent!.trim()
|
try {
|
||||||
|
lesson.subject = cells[3].childNodes[0].textContent!.trim()
|
||||||
|
|
||||||
let teacher: Lesson['teacher']
|
|
||||||
const teacherCell = cells[3].childNodes[2]
|
const teacherCell = cells[3].childNodes[2]
|
||||||
if (teacherCell) {
|
if (teacherCell) {
|
||||||
teacher = teacherCell.textContent!.trim()
|
lesson.teacher = teacherCell.textContent!.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeCell = cells[3].childNodes[3]
|
const placeCell = cells[3].childNodes[3]
|
||||||
|
|
||||||
let place: Lesson['place']
|
|
||||||
if (placeCell) {
|
if (placeCell) {
|
||||||
place = {
|
lesson.place = {
|
||||||
address: placeCell.childNodes[1].textContent!.trim(),
|
address: placeCell.childNodes[1].textContent!.trim(),
|
||||||
classroom: Number(placeCell.childNodes[3].textContent!.trim().match(/^Кабинет: (\d+)(-2)?$/)![1])
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
const topic: Lesson['topic'] = cells[4].textContent!.trim()
|
lesson.topic = cells[4].textContent!.trim()
|
||||||
|
|
||||||
const resources: Lesson['resources'] = []
|
lesson.resources = []
|
||||||
Array.from(cells[5].querySelectorAll('a'))
|
Array.from(cells[5].querySelectorAll('a'))
|
||||||
.forEach(a => {
|
.forEach(a => {
|
||||||
resources.push({
|
lesson.resources.push({
|
||||||
type: 'link',
|
type: 'link',
|
||||||
title: a.textContent!.trim(),
|
title: a.textContent!.trim(),
|
||||||
url: a.getAttribute('href')!
|
url: a.getAttribute('href')!
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return lesson
|
||||||
isChange,
|
} catch(e) {
|
||||||
time,
|
console.error('Error while parsing lesson in table', e, row.textContent?.trim())
|
||||||
type: cells[2].textContent!.trim(),
|
return null
|
||||||
subject,
|
|
||||||
...(teacher && { teacher }),
|
|
||||||
...(place && { place }),
|
|
||||||
...(topic && { topic }),
|
|
||||||
resources,
|
|
||||||
homework: cells[6].textContent!.trim()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePage(document: Document): Day[] {
|
export function parsePage(document: Document, groupName: string): Day[] {
|
||||||
const tables = Array.from(document.querySelectorAll('body > table'))
|
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 rows = Array.from(table!.children[0].children).filter(el => el.tagName === 'TR').slice(2)
|
||||||
|
|
||||||
const days = []
|
const days = []
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Schedule } from '@/widgets/schedule'
|
import { Schedule } from '@/widgets/schedule'
|
||||||
import { Day } from '@/shared/model/day'
|
import { Day } from '@/shared/model/day'
|
||||||
import { GetServerSidePropsResult } from 'next'
|
import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'
|
||||||
import { getSchedule } from '@/app/agregator/schedule'
|
import { getSchedule } from '@/app/agregator/schedule'
|
||||||
import { NextSerialized, nextDeserializer, nextSerialized } from '@/app/utils/date-serializer'
|
import { NextSerialized, nextDeserializer, nextSerialized } from '@/app/utils/date-serializer'
|
||||||
import { NavBar } from '@/widgets/navbar'
|
import { NavBar } from '@/widgets/navbar'
|
||||||
@@ -20,12 +20,22 @@ export default function HomePage(props: PageProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(): Promise<GetServerSidePropsResult<PageProps>> {
|
export async function getServerSideProps(context: GetServerSidePropsContext<{ group: string }>): Promise<GetServerSidePropsResult<PageProps>> {
|
||||||
const schedule = await getSchedule(146)
|
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 {
|
return {
|
||||||
props: {
|
props: {
|
||||||
schedule: nextSerialized(schedule)
|
schedule: nextSerialized(schedule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
notFound: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import { Html, Head, Main, NextScript } from 'next/document'
|
|||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html lang="en">
|
<Html lang="ru">
|
||||||
<Head />
|
<Head />
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|||||||
36
src/shadcn/ui/badge.tsx
Normal file
36
src/shadcn/ui/badge.tsx
Normal 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
121
src/shadcn/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
||||||
@@ -1,247 +1,258 @@
|
|||||||
// https://gist.github.com/VityaSchel/28f1a360ee7798511765910b39c6086c
|
// https://gist.github.com/VityaSchel/28f1a360ee7798511765910b39c6086c
|
||||||
export const teachers = [
|
export const teachers = [
|
||||||
{
|
{
|
||||||
'name': 'Абалымова Людмила Павловна',
|
name: 'Абалымова Людмила Павловна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/abalimova-l-.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/abalimova-l-.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Абрамова Светлана Геннадьевна',
|
name: 'Абрамова Светлана Геннадьевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/qwefdsfsd.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/qwefdsfsd.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Алехин Иван Николаевич',
|
name: 'Алехин Иван Николаевич',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/alehin-i-n.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/alehin-i-n.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Амукова Светлана Николаевна',
|
name: 'Амукова Светлана Николаевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/'
|
picture: 'https://ks.psuti.ru/images/stories/emp/',
|
||||||
|
pronouns: 'she'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Андреева Елена Сергеевна',
|
name: 'Тарасова Таисия Евгеньевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/-i-n.jpg'
|
picture: '',
|
||||||
|
pronouns: 'she'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Андреевская Наталья Владимировна',
|
name: 'Андреева Елена Сергеевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2021/IMG_5419.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/-i-n.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Андрющенко Анна Вячеславовна',
|
name: 'Андреевская Наталья Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/andriushenko.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2021/IMG_5419.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Арефьев Андрей Андреевич',
|
name: 'Андрющенко Анна Вячеславовна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/%20class='
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/andriushenko.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Бондаренко Анастасия Вячеславовна',
|
name: 'Арефьев Андрей Андреевич',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgCmhk.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/%20class=',
|
||||||
|
pronouns: 'he'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Горшенина Ольга Николаевна',
|
name: 'Бондаренко Анастасия Вячеславовна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/gorwenina-o-n.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgCmhk.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Дмитриева Наталья Владимировна',
|
name: 'Горшенина Ольга Николаевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/dmitrieva-n-v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/gorwenina-o-n.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Елисеева Эмиля Владиславовна',
|
name: 'Дмитриева Наталья Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgC.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/dmitrieva-n-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Ермолаева Галина Владимировна',
|
name: 'Елисеева Эмиля Владиславовна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/ermolaeva-g-v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/9GLhbTgC.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Жабборова Светлана Сергеевна',
|
name: 'Ермолаева Галина Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/tryaskina-s-s.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/ermolaeva-g-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Жилина Елена Николаевна',
|
name: 'Жабборова Светлана Сергеевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/jilina.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/tryaskina-s-s.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Иванова Мария Сергеевна',
|
name: 'Жилина Елена Николаевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/konovalova.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/jilina.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Карпеева Александра Сергеевна',
|
name: 'Иванова Мария Сергеевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-karpeeva-a-s.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/konovalova.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Карпова Ирина Васильевна',
|
name: 'Карпеева Александра Сергеевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/karpova-i-v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/2014-karpeeva-a-s.jpg',
|
||||||
|
pronouns: 'she'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Козько Диана Игоревна',
|
name: 'Карпова Ирина Васильевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/kozko-d-i.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/karpova-i-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Корнилова Светлана Александровна',
|
name: 'Козько Диана Игоревна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/kornilova-s-a.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/kozko-d-i.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Краюшкина Ольга Борисовна',
|
name: 'Корнилова Светлана Александровна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fsdadsd.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/kornilova-s-a.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Крынкина Анна Андреевна',
|
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'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fsdadsd.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Кукарская Людмила Петровна',
|
name: 'Крынкина Анна Андреевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/kukarskaya-l-p.jpg'
|
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': 'Кусаева Зарина Владимировна',
|
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'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/kukarskaya-l-p.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Ларионова Софья Николаевна',
|
name: 'Кусаева Зарина Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
|
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': 'Лизунова Елена Владимировна',
|
name: 'Ларионова Софья Николаевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/lizunova.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg',
|
||||||
|
pronouns: 'she'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Лобачева Милана Евгеньевна',
|
name: 'Лизунова Елена Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/lobacheva-m-e.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/lizunova.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Логвинов Александр Владимирович',
|
name: 'Лобачева Милана Евгеньевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/logvinov_a_v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/lobacheva-m-e.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Малбасарова Галия Худанбаевна',
|
name: 'Логвинов Александр Владимирович',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-kuntaeva.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/logvinov_a_v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Матулина Татьяна Сергеевна',
|
name: 'Малбасарова Галия Худанбаевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/matulina.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/2014-kuntaeva.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Михалькова Ирина Евгеньевна',
|
name: 'Матулина Татьяна Сергеевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/2019-mihalkova.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/matulina.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Назарова Елена Федоровна',
|
name: 'Михалькова Ирина Евгеньевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/nazarova.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/2019-mihalkova.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Негина Айгуль Зинуловна',
|
name: 'Назарова Елена Федоровна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-aitasova-a-z.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/nazarova.jpg',
|
||||||
|
pronouns: 'bitch'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Некрылова Татьяна Борисовна',
|
name: 'Негина Айгуль Зинуловна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/nekrylova-t-b.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/2014-aitasova-a-z.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Никифоров Михаил Михайлович',
|
name: 'Некрылова Татьяна Борисовна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/nikiforov-m-m.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/nekrylova-t-b.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Першина Елена Викторовна',
|
name: 'Никифоров Михаил Михайлович',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/pershina-e-v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/nikiforov-m-m.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Потяйкин Роман Владимирович',
|
name: 'Першина Елена Викторовна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/pershina-e-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Рзаева Алина Игоревна',
|
name: 'Потяйкин Роман Владимирович',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/DSC06789.JPG'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Савич Мария Владимировна',
|
name: 'Рзаева Алина Игоревна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/asdasad.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/DSC06789.JPG'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Самойлова Наталья Николаевна',
|
name: 'Савич Мария Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2022/samoylova.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/asdasad.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Семенов Антон Сергеевич',
|
name: 'Самойлова Наталья Николаевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/antonov-a-s.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2022/samoylova.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Сергеев Роман Алексеевич',
|
name: 'Семенов Антон Сергеевич',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/antonov-a-s.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Сиднина Юлия Валерьевна',
|
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'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Синекопова Лариса Владимировна',
|
name: 'Сиднина Юлия Валерьевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/sinekopova-l-v.jpg'
|
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': 'Сироткина Ольга Владимировна',
|
name: 'Синекопова Лариса Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/sirotkina-o-v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/sinekopova-l-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Ситникова Людмила Геннадьевна',
|
name: 'Сироткина Ольга Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/sitnikova-l-.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/sirotkina-o-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Славкина Татьяна Анатольевна',
|
name: 'Ситникова Людмила Геннадьевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/slavkina-t-a.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/sitnikova-l-.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Сорокина Надежда Леонидовна',
|
name: 'Славкина Татьяна Анатольевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/sorokina-n.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/slavkina-t-a.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Странник Дмитрий Христианович',
|
name: 'Сорокина Надежда Леонидовна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/stranik.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/sorokina-n.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Тананыхина Надежда Воалимировна',
|
name: 'Странник Дмитрий Христианович',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/IMG-f8eeb02.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/stranik.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Терёхин Дмитрий Вячеславович',
|
name: 'Тананыхина Надежда Воалимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/terexin.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/IMG-f8eeb02.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Упанова Анастасия Владимировна',
|
name: 'Терёхин Дмитрий Вячеславович',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/terexin.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Утыбаева Светлана Михайловна',
|
name: 'Упанова Анастасия Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/2019-utibaeva-s-m.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Федотова Елена Дмитриевна',
|
name: 'Утыбаева Светлана Михайловна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fedotova-e-.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/2019-utibaeva-s-m.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Фомин Александр Васильевич',
|
name: 'Федотова Елена Дмитриевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/fomin-a-v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/fedotova-e-.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Ходотова Евгения Андреевна',
|
name: 'Фомин Александр Васильевич',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/hodotova.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/fomin-a-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Черненкова Наталья Владимировна',
|
name: 'Ходотова Евгения Андреевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/chernenkova-n-v.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/hodotova.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Шамбер Лола Низамовна',
|
name: 'Черненкова Наталья Владимировна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/shamber-l-n.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/new/chernenkova-n-v.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Шомас Елена Александровна',
|
name: 'Шамбер Лола Низамовна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/2014-shomas.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/shamber-l-n.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Шукова Марина Геннадьевна',
|
name: 'Шомас Елена Александровна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/emp/new/shykova.jpg'
|
picture: 'https://ks.psuti.ru/images/stories/emp/2014-shomas.jpg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Щербакова Надежда Юрьевна',
|
name: 'Шукова Марина Геннадьевна',
|
||||||
'picture': 'https://ks.psuti.ru/images/stories/ks-news/2016/prepodavateli-foto/andreeva.jpg'
|
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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -6,12 +6,6 @@ export type Lesson = {
|
|||||||
hint?: string
|
hint?: string
|
||||||
}
|
}
|
||||||
type: string
|
type: string
|
||||||
subject: string
|
|
||||||
teacher?: string
|
|
||||||
place?: {
|
|
||||||
address: string
|
|
||||||
classroom: number
|
|
||||||
}
|
|
||||||
topic?: string
|
topic?: string
|
||||||
resources: {
|
resources: {
|
||||||
type: 'link'
|
type: 'link'
|
||||||
@@ -19,4 +13,13 @@ export type Lesson = {
|
|||||||
url: string
|
url: string
|
||||||
}[]
|
}[]
|
||||||
homework: string
|
homework: string
|
||||||
|
} & (
|
||||||
|
{
|
||||||
|
subject: string
|
||||||
|
teacher?: string
|
||||||
|
place?: {
|
||||||
|
address: string
|
||||||
|
classroom: string
|
||||||
}
|
}
|
||||||
|
} | { fallbackDiscipline?: string }
|
||||||
|
)
|
||||||
@@ -6,11 +6,11 @@ import { useRouter } from 'next/router'
|
|||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
|
|
||||||
export function NavBar() {
|
export function NavBar() {
|
||||||
const { theme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="w-full p-2">
|
<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">
|
<ul className="flex gap-2">
|
||||||
<NavBarItem url="/ps7">ПС-7</NavBarItem>
|
<NavBarItem url="/ps7">ПС-7</NavBarItem>
|
||||||
<NavBarItem url="/pks35k">ПКС-35к</NavBarItem>
|
<NavBarItem url="/pks35k">ПКС-35к</NavBarItem>
|
||||||
|
|||||||
@@ -14,16 +14,31 @@ export function Day({ day }: {
|
|||||||
'Воскресенье'
|
'Воскресенье'
|
||||||
][day.date.getDay()-1]
|
][day.date.getDay()-1]
|
||||||
|
|
||||||
|
const longNames = day.lessons
|
||||||
|
.some(lesson => 'subject' in lesson && lesson.subject.length > 20)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
<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>
|
</h1>
|
||||||
<div className="flex flex-row gap-4">
|
<div className='overflow-hidden'>
|
||||||
|
<div className='overflow-auto'>
|
||||||
|
<div className="flex flex-row gap-4 w-max">
|
||||||
{day.lessons.map((lesson, i) => (
|
{day.lessons.map((lesson, i) => (
|
||||||
<Lesson lesson={lesson} key={i} />
|
<Lesson
|
||||||
|
width={longNames ? 450 : 350}
|
||||||
|
lesson={lesson}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -12,13 +12,28 @@ import {
|
|||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from '@/shadcn/ui/avatar'
|
} from '@/shadcn/ui/avatar'
|
||||||
|
import { Badge } from '@/shadcn/ui/badge'
|
||||||
import { teachers } from '@/shared/data/teachers'
|
import { teachers } from '@/shared/data/teachers'
|
||||||
import { Lesson as LessonType } from '@/shared/model/lesson'
|
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
|
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) => {
|
const getTeacherPhoto = (url?: string) => {
|
||||||
if(url) {
|
if(url) {
|
||||||
@@ -34,27 +49,76 @@ export function Lesson({ lesson }: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const fallbackTeacherName = () => {
|
||||||
<Card className="w-[350px]">
|
if (!hasTeacher || !lesson.teacher) return ''
|
||||||
<div>
|
const [, firstName, middleName] = lesson.teacher.split(' ')
|
||||||
<Avatar>
|
return firstName.at(0)! + middleName.at(0)!
|
||||||
<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 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'> ({lesson.time.hint})</span>}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{lesson.type && <><Badge>{lesson.type}</Badge>{' '} </>}
|
||||||
|
{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>
|
</CardContent>
|
||||||
<CardFooter className="flex justify-between">
|
{(Boolean(lesson.resources.length) || hasPlace) && (
|
||||||
<Button variant="outline">Cancel</Button>
|
<CardFooter className="flex justify-between mt-auto">
|
||||||
<Button>Deploy</Button>
|
{('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 /> Материалы</Button>
|
||||||
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
<ResourcesDialog
|
||||||
|
open={resourcesDialogOpened}
|
||||||
|
onClose={() => setResourcesDialogOpened(false)}
|
||||||
|
teacherName={('teacher' in lesson && lesson.teacher) ? lesson.teacher : undefined}
|
||||||
|
resources={lesson.resources}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
61
src/widgets/schedule/resources-dialog.tsx
Normal file
61
src/widgets/schedule/resources-dialog.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user