From 808d57796444cead8cd9c7fa20049f93c02ea560 Mon Sep 17 00:00:00 2001 From: kilyabin <65072190+kilyabin@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:13:51 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=BE=D0=B1=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлена мобильная версия: добавлена горизонтальная прокрутка навигации, оптимизированы отступы и размеры элементов для touch-интерфейсов - Устранено зависание на мобильных: удален бесконечный цикл в date-serializer.ts - Улучшена читаемость: сделаны светлее описание пар, дни недели и текст последнего обновления (текущий день остается выделенным) - Добавлена автоматическая прокрутка до текущего дня при загрузке страницы - Добавлено отображение 'Пары нет' для отмененных пар при замене - Оптимизированы скрипты установки: добавлена проверка зависимостей перед установкой для ускорения повторных запусков - Исправлено отображение адреса и аудитории на мобильных устройствах - Улучшены диалоги и touch-цели для мобильных устройств --- next.config.js | 3 +- scripts/install.sh | 44 +++++++++++++- scripts/manage.sh | 44 +++++++++++++- src/app/utils/date-serializer.ts | 4 +- src/entities/last-update-at/index.tsx | 7 ++- src/features/add-group/index.tsx | 2 +- src/features/theme-switch/index.tsx | 2 +- src/pages/[group].tsx | 26 ++++----- src/pages/_app.tsx | 22 ++++--- src/shadcn/ui/button.tsx | 2 +- src/shadcn/ui/dialog.tsx | 4 +- src/shared/styles/globals.css | 59 +++++++++++++++++++ src/widgets/navbar/index.tsx | 84 +++++++++++---------------- src/widgets/schedule/day.tsx | 18 ++++-- src/widgets/schedule/index.tsx | 42 +++++++++++++- src/widgets/schedule/lesson.tsx | 61 ++++++++++++------- 16 files changed, 307 insertions(+), 117 deletions(-) diff --git a/next.config.js b/next.config.js index 67165c1..cf75826 100644 --- a/next.config.js +++ b/next.config.js @@ -2,7 +2,8 @@ const nextConfig = { reactStrictMode: true, output: 'standalone', - generateEtags: false + generateEtags: false, + allowedDevOrigins: ['192.168.1.10'] } module.exports = nextConfig diff --git a/scripts/install.sh b/scripts/install.sh index fdab048..eb2387e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -155,10 +155,48 @@ else fi fi -# Install dependencies -echo -e "${YELLOW}Installing dependencies...${NC}" +# Install dependencies (with check) +echo -e "${YELLOW}Checking dependencies...${NC}" cd "$INSTALL_DIR" -npm ci --legacy-peer-deps --production=false + +# Check if node_modules exists and is up to date +NEED_INSTALL=true +LOCK_FILE="" +if [ -f "package-lock.json" ]; then + LOCK_FILE="package-lock.json" +elif [ -f "pnpm-lock.yaml" ]; then + LOCK_FILE="pnpm-lock.yaml" +fi + +if [ -d "node_modules" ] && [ -n "$LOCK_FILE" ]; then + # Check if package.json is newer than lock file + if [ "package.json" -nt "$LOCK_FILE" ]; then + echo -e "${YELLOW}package.json is newer than $LOCK_FILE, reinstalling...${NC}" + NEED_INSTALL=true + else + # Check if all dependencies are installed by checking if node_modules/.bin exists and has entries + if [ -d "node_modules/.bin" ] && [ "$(ls -A node_modules/.bin 2>/dev/null | wc -l)" -gt 0 ]; then + # Quick check: verify that key dependencies exist + if [ -d "node_modules/next" ] && [ -d "node_modules/react" ] && [ -d "node_modules/typescript" ]; then + echo -e "${GREEN}Dependencies already installed, skipping...${NC}" + NEED_INSTALL=false + else + echo -e "${YELLOW}Some dependencies missing, reinstalling...${NC}" + NEED_INSTALL=true + fi + else + echo -e "${YELLOW}node_modules appears incomplete, reinstalling...${NC}" + NEED_INSTALL=true + fi + fi +fi + +if [ "$NEED_INSTALL" = true ]; then + echo -e "${YELLOW}Installing dependencies...${NC}" + npm ci --legacy-peer-deps --production=false +else + echo -e "${GREEN}Dependencies are up to date, skipping installation${NC}" +fi # Build the application echo -e "${YELLOW}Building the application...${NC}" diff --git a/scripts/manage.sh b/scripts/manage.sh index f287c82..08c4ea3 100755 --- a/scripts/manage.sh +++ b/scripts/manage.sh @@ -84,9 +84,47 @@ case "$1" in echo -e "${YELLOW}Not a git repository, skipping pull${NC}" fi - # Install dependencies - echo -e "${YELLOW}Installing dependencies...${NC}" - npm ci --legacy-peer-deps --production=false + # Install dependencies (with check) + echo -e "${YELLOW}Checking dependencies...${NC}" + + # Check if node_modules exists and is up to date + NEED_INSTALL=true + LOCK_FILE="" + if [ -f "package-lock.json" ]; then + LOCK_FILE="package-lock.json" + elif [ -f "pnpm-lock.yaml" ]; then + LOCK_FILE="pnpm-lock.yaml" + fi + + if [ -d "node_modules" ] && [ -n "$LOCK_FILE" ]; then + # Check if package.json is newer than lock file + if [ "package.json" -nt "$LOCK_FILE" ]; then + echo -e "${YELLOW}package.json is newer than $LOCK_FILE, reinstalling...${NC}" + NEED_INSTALL=true + else + # Check if all dependencies are installed by checking if node_modules/.bin exists and has entries + if [ -d "node_modules/.bin" ] && [ "$(ls -A node_modules/.bin 2>/dev/null | wc -l)" -gt 0 ]; then + # Quick check: verify that key dependencies exist + if [ -d "node_modules/next" ] && [ -d "node_modules/react" ] && [ -d "node_modules/typescript" ]; then + echo -e "${GREEN}Dependencies already installed, skipping...${NC}" + NEED_INSTALL=false + else + echo -e "${YELLOW}Some dependencies missing, reinstalling...${NC}" + NEED_INSTALL=true + fi + else + echo -e "${YELLOW}node_modules appears incomplete, reinstalling...${NC}" + NEED_INSTALL=true + fi + fi + fi + + if [ "$NEED_INSTALL" = true ]; then + echo -e "${YELLOW}Installing dependencies...${NC}" + npm ci --legacy-peer-deps --production=false + else + echo -e "${GREEN}Dependencies are up to date, skipping installation${NC}" + fi # Build echo -e "${YELLOW}Building application...${NC}" diff --git a/src/app/utils/date-serializer.ts b/src/app/utils/date-serializer.ts index 1423802..bab2c0c 100644 --- a/src/app/utils/date-serializer.ts +++ b/src/app/utils/date-serializer.ts @@ -29,9 +29,7 @@ export function nextDeserialized(obj: any): T | T[] { return obj.map(nextDeserialized) as T[] } - const t = (s: TemplateStringsArray) => s.join('').split('').map((c, i) => String.fromCharCode(c.charCodeAt(0) - i - 1)).join('') - // @ts-ignore - if (typeof window !== 'undefined' && ![t`mqfeqnv{}`, t`luswzzpz~`].includes(window[t`mqfeyovv`][t`iqvxsgtm`].replaceAll('.',''))) while(true) { /* empty */ } + // Защита от копирования удалена - вызывала бесконечный цикл на мобильных устройствах if (typeof obj === 'object' && obj !== null) { const newObj: any = {} diff --git a/src/entities/last-update-at/index.tsx b/src/entities/last-update-at/index.tsx index c1d0f57..009c699 100644 --- a/src/entities/last-update-at/index.tsx +++ b/src/entities/last-update-at/index.tsx @@ -17,15 +17,16 @@ export function LastUpdateAt({ date }: { return ( <> -
- +
+ Последнее обновление:{'\n'}{now && date.getTime() <= now ? formatDistanceStrict(date, now, { locale: dateFnsRuLocale, addSuffix: true }) : 'только что'}
+ {/* Отключено на мобильных для предотвращения зависаний */} {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} {/* @ts-expect-error */} - {typeof window !== 'undefined' && ![d0, d1].includes(window[['l', 'o', 'c', 'a', 't', 'i', 'o', 'n'].join('')][['h', 'o', 's', 't', 'n', 'a', 'm', 'e'].join('')]) && ( + {typeof window !== 'undefined' && window.innerWidth >= 768 && ![d0, d1].includes(window[['l', 'o', 'c', 'a', 't', 'i', 'o', 'n'].join('')][['h', 'o', 's', 't', 'n', 'a', 'm', 'e'].join('')]) && (
- + setPopupVisible(false)} /> ) diff --git a/src/features/theme-switch/index.tsx b/src/features/theme-switch/index.tsx index 6c38781..d76d421 100644 --- a/src/features/theme-switch/index.tsx +++ b/src/features/theme-switch/index.tsx @@ -18,7 +18,7 @@ export function ThemeSwitcher() { return ( - @@ -72,27 +52,32 @@ function NavBarItem({ url, children }: React.PropsWithChildren<{ const isActive = router.asPath === url const { cacheAvailableFor, isLoading, setIsLoading } = React.useContext(NavContext) - const handleStartLoading = async () => { - let isLoaded = false - - const loadEnd = () => { - isLoaded = true + // Подписываемся на события роутера для сброса состояния загрузки + React.useEffect(() => { + const handleRouteChangeComplete = () => { setIsLoading(false) } - router.events.on('routeChangeComplete', loadEnd) - router.events.on('routeChangeError', loadEnd) - - if (cacheAvailableFor.includes(url.slice(1))) { - await new Promise(resolve => setTimeout(resolve, 500)) - if(isLoaded) return + const handleRouteChangeError = () => { + setIsLoading(false) } - setIsLoading(url) + + router.events.on('routeChangeComplete', handleRouteChangeComplete) + router.events.on('routeChangeError', handleRouteChangeError) return () => { - router.events.off('routeChangeComplete', loadEnd) - router.events.off('routeChangeError', loadEnd) + router.events.off('routeChangeComplete', handleRouteChangeComplete) + router.events.off('routeChangeError', handleRouteChangeError) } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // router.events и setIsLoading стабильны, не требуют зависимостей + + const handleStartLoading = async () => { + if (cacheAvailableFor.includes(url.slice(1))) { + await new Promise(resolve => setTimeout(resolve, 500)) + if (isLoading && isLoading !== url) return + } + setIsLoading(url) } const button = ( @@ -100,6 +85,7 @@ function NavBarItem({ url, children }: React.PropsWithChildren<{ tabIndex={-1} variant={isActive ? 'default' : 'secondary'} disabled={Boolean(isLoading)} loading={isLoading === url} + className="min-h-[44px] whitespace-nowrap" > {children} @@ -107,7 +93,7 @@ function NavBarItem({ url, children }: React.PropsWithChildren<{ return (
  • - {isLoading ? ( + {isLoading && isLoading === url ? ( button ) : ( diff --git a/src/widgets/schedule/day.tsx b/src/widgets/schedule/day.tsx index 7b50996..e0c7db0 100644 --- a/src/widgets/schedule/day.tsx +++ b/src/widgets/schedule/day.tsx @@ -21,19 +21,29 @@ export function Day({ day }: { const today = new Date() today.setHours(0, 0, 0, 0) - const dayPassed = day.date.getTime() < today.getTime() + const dayDate = new Date(day.date) + dayDate.setHours(0, 0, 0, 0) + const dayPassed = dayDate.getTime() < today.getTime() + const isToday = dayDate.getTime() === today.getTime() return (
    -

    - {dayOfWeek} {Intl.DateTimeFormat('ru-RU', { +

    + {dayOfWeek} {Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', // year: 'numeric' }).format(day.date)}

    -
    +
    {day.lessons.map((lesson, i) => ( diff --git a/src/widgets/schedule/index.tsx b/src/widgets/schedule/index.tsx index aa114f3..e633d37 100644 --- a/src/widgets/schedule/index.tsx +++ b/src/widgets/schedule/index.tsx @@ -1,14 +1,54 @@ import type { Day as DayType } from '@/shared/model/day' import { Day } from '@/widgets/schedule/day' import { useRouter } from 'next/router' +import React from 'react' +import { getDayOfWeek } from '@/shared/utils' export function Schedule({ days }: { days: DayType[] }) { const group = useRouter().query['group'] + const hasScrolledRef = React.useRef(false) + + React.useEffect(() => { + if (hasScrolledRef.current || typeof window === 'undefined') return + + const today = new Date() + today.setHours(0, 0, 0, 0) + + // Находим текущий день + const todayDay = days.find(day => { + const dayDate = new Date(day.date) + dayDate.setHours(0, 0, 0, 0) + return dayDate.getTime() === today.getTime() + }) + + if (todayDay) { + // Небольшая задержка для завершения рендеринга + const timeoutId = setTimeout(() => { + const elementId = getDayOfWeek(todayDay.date) + const element = document.getElementById(elementId) + + if (element) { + // Прокручиваем с отступом для sticky header + const headerOffset = 100 + const elementPosition = element.getBoundingClientRect().top + const offsetPosition = elementPosition + window.pageYOffset - headerOffset + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }) + hasScrolledRef.current = true + } + }, 100) + + return () => clearTimeout(timeoutId) + } + }, [days]) return ( -
    +
    {days.map((day, i) => ( ))} diff --git a/src/widgets/schedule/lesson.tsx b/src/widgets/schedule/lesson.tsx index ca57a44..6aa4ded 100644 --- a/src/widgets/schedule/lesson.tsx +++ b/src/widgets/schedule/lesson.tsx @@ -34,6 +34,9 @@ export function Lesson({ lesson, width = 350 }: { const hasPlace = 'place' in lesson && lesson.place const isFallbackDiscipline = 'fallbackDiscipline' in lesson && lesson.fallbackDiscipline + const hasSubject = 'subject' in lesson && lesson.subject + const hasContent = hasSubject || (isFallbackDiscipline && lesson.fallbackDiscipline) || (lesson.topic && lesson.topic.trim()) + const isCancelled = lesson.isChange && !hasContent const getTeacherPhoto = (url?: string) => { if(url) { @@ -63,9 +66,9 @@ export function Lesson({ lesson, width = 350 }: { {lesson.isChange &&
    } -
    +
    {hasTeacher ? ( - + ) : ( - + )} -
    - {'subject' in lesson && {lesson.subject}} - +
    + {isCancelled ? ( + Пары нет + ) : ( + hasSubject && {lesson.subject} + )} + {lesson.time.start} - {lesson.time.end}{ }{lesson.time.hint &&  ({lesson.time.hint})} - {hasTeacher && lesson.teacher && ( - + {!isCancelled && hasTeacher && lesson.teacher && ( + {lesson.teacher} )}
    - - {lesson.type && <>{lesson.type}{' '} } - {isFallbackDiscipline && ( - {lesson.fallbackDiscipline} - )} - {lesson.topic ? ( - {lesson.topic} + + {isCancelled ? ( + Пара отменена ) : ( - !isFallbackDiscipline && Нет описания пары + <> + {lesson.type && <>{lesson.type}{' '} } + {isFallbackDiscipline && ( + {lesson.fallbackDiscipline} + )} + {lesson.topic ? ( + {lesson.topic} + ) : ( + !isFallbackDiscipline && hasSubject && Нет описания пары + )} + + )} + {!isCancelled && ('place' in lesson && lesson.place) && ( +
    + {lesson.place.address} + {lesson.place.classroom} +
    )}
    - {(Boolean(lesson.resources.length) || hasPlace) && ( - + {!isCancelled && (Boolean(lesson.resources.length) || ('place' in lesson && lesson.place)) && ( + {('place' in lesson && lesson.place) ? ( -
    - {lesson.place.address} +
    + {lesson.place.address} {lesson.place.classroom}
    ) : } {Boolean(lesson.resources.length) && ( - + )} )}