From 28c7d6d0f8dea9c9e9aa3918d61159f46d1be7ab Mon Sep 17 00:00:00 2001 From: Tatyana Date: Fri, 5 Sep 2025 12:05:24 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D0=B0=20=D0=B4=D0=BD=D1=8F=20=D0=B2=20=D1=88?= =?UTF-8?q?=D0=B0=D0=BF=D0=BA=D0=B5=20=D0=BD=D0=B0=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=86=D0=B5=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=83=D0=BF=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AppRoutes.tsx | 33 +- src/pages/CourseExercises.tsx | 196 ++++------ src/pages/Exercise.tsx | 654 ++++++++++++++++------------------ src/shared/consts/router.ts | 6 + 4 files changed, 412 insertions(+), 477 deletions(-) diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index fa93bf1..efc3eec 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -1,23 +1,23 @@ import { Switch, Route, Redirect } from "react-router-dom" -import Welcome from "./pages/Welcome"; -import Login from "./pages/Login"; -import Home from "./pages/Home"; -import ForgotPasword from "./pages/ForgotPassword"; -import { Courses } from "./pages/Courses"; -import { CourseExercises } from "./pages/CourseExercises"; -import { Exercise } from "./pages/Exercise"; +import Welcome from "./pages/Welcome" +import Login from "./pages/Login" +import Home from "./pages/Home" +import ForgotPasword from "./pages/ForgotPassword" +import { Courses } from "./pages/Courses" +import { CourseExercises } from "./pages/CourseExercises" +import { Exercise } from "./pages/Exercise" -import Settings from "./pages/Settings"; -import CourseComplete from "./pages/CourseComplete"; +import Settings from "./pages/Settings" +import CourseComplete from "./pages/CourseComplete" -import { getRouteWelcome } from "./shared/consts/router"; -import { getRouteLogin } from "./shared/consts/router"; -import { getRouteHome } from "./shared/consts/router"; -import { getRouteForgotPassword } from "./shared/consts/router"; -import { getRouteCourses } from "./shared/consts/router"; -import { getRouteSettings } from "./shared/consts/router"; -import { getRouteCourseComplete } from "./shared/consts/router"; +import { getRouteWelcome } from "./shared/consts/router" +import { getRouteLogin } from "./shared/consts/router" +import { getRouteHome } from "./shared/consts/router" +import { getRouteForgotPassword } from "./shared/consts/router" +import { getRouteCourses } from "./shared/consts/router" +import { getRouteSettings } from "./shared/consts/router" +import { getRouteCourseComplete } from "./shared/consts/router" const AppRoutes = () => ( @@ -31,6 +31,7 @@ const AppRoutes = () => ( + diff --git a/src/pages/CourseExercises.tsx b/src/pages/CourseExercises.tsx index e02d630..196565a 100644 --- a/src/pages/CourseExercises.tsx +++ b/src/pages/CourseExercises.tsx @@ -1,142 +1,123 @@ "use client" -import { useState, useEffect } from "react"; -import { useParams, useHistory, useLocation } from "react-router-dom"; +import { useState, useEffect } from "react" +import { useParams, useHistory, useLocation } from "react-router-dom" -import HeaderNav from "../components/HeaderNav"; -import BottomNavigation from "../components/BottomNavigation"; +import HeaderNav from "../components/HeaderNav" +import BottomNavigation from "../components/BottomNavigation" -import { connect } from '../confconnect'; +import { connect } from "../confconnect" -import { getRouteExercise } from "../shared/consts/router"; -import { ArrowIcon } from "../components/icons/ArrowIcon"; +import { getRouteExerciseByIndex } from "../shared/consts/router" +import { ArrowIcon } from "../components/icons/ArrowIcon" -import type { Course } from "../pages/Courses"; -// import { Exercise } from "./Exercise"; +import type { Course } from "../pages/Courses" export interface CourseExercises { - id: number; - id_course: number; - id_exercise: number; - exercise: Exercise; - day: number; - position: number; - repeats: number; - time: string; - DeletedAt: string | null; + id: number + id_course: number + id_exercise: number + exercise: Exercise + day: number + position: number + repeats: number + time: string + DeletedAt: string | null } interface Exercise { - id: number; - title: string; + id: number + title: string } - export const CourseExercises = () => { - const history = useHistory(); - const { id } = useParams<{ id: string }>(); + const history = useHistory() + const { id } = useParams<{ id: string }>() - const location = useLocation<{ course?: Course }>(); - const course = location.state?.course; + const location = useLocation<{ course?: Course }>() + const course = location.state?.course + const [course_exercises, setExercises] = useState([]) + const [selectedDay, setSelectedDay] = useState(null) - const [course_exercises, setExercises] = useState([]); - const [selectedDay, setSelectedDay] = useState(null); - - - - const token = localStorage.getItem('authToken'); + const token = localStorage.getItem("authToken") useEffect(() => { console.log(token) if (!token) { - console.log('Токен не найден'); - return; + console.log("Токен не найден") + return } - connect.get(`/pacient/${id}`, { - - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - - }) - .then(response => { - console.log('Данные упражнения курса:', response.data.course_exercises); - - setExercises(response.data.course_exercises); - + connect + .get(`/pacient/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, }) - .catch(error => { + .then((response) => { + console.log("Данные упражнения курса:", response.data.course_exercises) + setExercises(response.data.course_exercises) + }) + .catch((error) => { if (error.response) { - console.error('Ошибка ответа сервера:', error.response.status, error.response.data); - + console.error("Ошибка ответа сервера:", error.response.status, error.response.data) } else if (error.request) { - console.error('Нет ответа от сервера:', error.request); - + console.error("Нет ответа от сервера:", error.request) } else { - console.error('Ошибка при настройке запроса:', error.message); + console.error("Ошибка при настройке запроса:", error.message) } - }); - }, []); - + }) + }, []) useEffect(() => { if (course_exercises.length > 0 && selectedDay === null) { - setSelectedDay(course_exercises[0].day); + setSelectedDay(course_exercises[0].day) } - }, [course_exercises]); + }, [course_exercises]) - const uniqueDays = Array.from(new Set(course_exercises.map(ex => ex.day))).sort((a, b) => a - b); + const uniqueDays = Array.from(new Set(course_exercises.map((ex) => ex.day))).sort((a, b) => a - b) - const dayMap: { [key: number]: number } = {}; + const dayMap: { [key: number]: number } = {} uniqueDays.forEach((day, index) => { - dayMap[day] = index + 1; - }); + dayMap[day] = index + 1 + }) - const daysNav = uniqueDays.map(day => dayMap[day]); + const daysNav = uniqueDays.map((day) => dayMap[day]) - const days = Array.from(new Set(course_exercises.map(ex => ex.day))).sort((a, b) => a - b); + const days = Array.from(new Set(course_exercises.map((ex) => ex.day))).sort((a, b) => a - b) - const filteredExercises = selectedDay !== null - ? course_exercises.filter(ex => ex.day === selectedDay) - : course_exercises; + const filteredExercises = + selectedDay !== null ? course_exercises.filter((ex) => ex.day === selectedDay) : course_exercises -console.log('отфильтрованный список по дням',filteredExercises) + console.log("отфильтрованный список по дням", filteredExercises) return (
- +
- {/* Заголовок секции */}

Упражнения

Количество упражнений: {course_exercises.length}
- {/*

{JSON.stringify(course_exercises)}

*/} - - - {/* Кнопки выбора дня */} {days.length > 1 && (
{days.map((day, index) => ( - - @@ -145,41 +126,34 @@ console.log('отфильтрованный список по дням',filtered
)} - -
- { - - - filteredExercises.length > 0 ? ( + {filteredExercises.length > 0 ? ( filteredExercises.map((item, index) => (
{ - - // console.log(course_exercises.map(ex => ex.id_exercise)) - - - // Передаем id_course и индекс из полного массива - history.push(getRouteExercise(item.id_course.toString(), item.id_exercise.toString())); - }} - className="p-4 mb-4 cursor-pointer hover:scale-105 transition duration-300 glass-morphism rounded-3xl border border-white/50 shadow-2xl overflow-hidden backdrop-blur-2xl relative"> + history.push( + getRouteExerciseByIndex( + item.id_course.toString(), + index, // Используем индекс из отфильтрованного массива + selectedDay || undefined, // Передаем выбранный день для контекста + ), + ) + }} + className="p-4 mb-4 cursor-pointer hover:scale-105 transition duration-300 glass-morphism rounded-3xl border border-white/50 shadow-2xl overflow-hidden backdrop-blur-2xl relative" + >
-

Упражнение {++index}

+

Упражнение {index + 1}

{item.exercise.title}

- - -

Повторений: {item.repeats}

-

Время выполнения: {item.time}

@@ -192,27 +166,5 @@ console.log('отфильтрованный список по дням',filtered
- ) } - -// type ButtonTheme = 'primary' | 'secondary' | 'contrast'; - -// interface ButtonProps { -// onClick: (event: MouseEvent) => -// theme: ButtonTheme -// } - -// const Button = (props: ButtonProps) => { -// const {theme} = props; -// return -// } - -// const Page = () => { -// const t: ButtonTheme = 'contrast' -// return ( -//
-//
-// ) -// } \ No newline at end of file diff --git a/src/pages/Exercise.tsx b/src/pages/Exercise.tsx index 086f1c8..4fb05db 100644 --- a/src/pages/Exercise.tsx +++ b/src/pages/Exercise.tsx @@ -3,7 +3,7 @@ // Импортируем необходимые функции из React для создания интерактивного компонента import { useState, useEffect } from "react" // Импортируем функции для навигации между страницами -import { useHistory, useParams } from "react-router-dom" +import { useHistory, useParams, useLocation } from "react-router-dom" // Импортируем иконки и компоненты интерфейса import { RefreshIcon } from "../components/icons/RefreshIcon" @@ -28,127 +28,114 @@ export interface Exercise { sessionname: string // Название тренировочной сессии } - interface RouteParams { - courseId: string // ID курса (программы тренировок) - exerciseId: string // ID конкретного упражнения + courseId: string + exerciseId?: string // Made optional since we might have exerciseIndex instead + exerciseIndex?: string // Added exerciseIndex parameter } export const Exercise = () => { - // Получаем функции для навигации и параметры из URL const history = useHistory() - const { courseId, exerciseId } = useParams() + const { courseId, exerciseId, exerciseIndex } = useParams() + const location = useLocation() + + const [courseExercises, setCourseExercises] = useState([]) + const [actualExerciseId, setActualExerciseId] = useState("") + const [currentDay, setCurrentDay] = useState(null) + const [exercise, setExercise] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState("") // ========== СОСТОЯНИЯ ТАЙМЕРУ УПРАЖНЕНИЯ ========== - const [isPlaying, setIsPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) const [totalTime, setTotalTime] = useState(900) // ========== СОСТОЯНИЯ ПОДХОДОВ (СЕТОВ) ========== - // Номер текущего подхода (начинаем с 1-го подхода) const [currentSet, setCurrentSet] = useState(1) - - // Общее количество подходов в упражнении (по умолчанию 3) const [totalSets, setTotalSets] = useState(3) - - // Массив номеров завершенных подходов (например: [1, 2] = завершены 1-й и 2-й подходы) const [completedSets, setCompletedSets] = useState([]) // ========== СОСТОЯНИЯ ИНТЕРФЕЙСА ========== - // Крутится ли иконка обновления (для анимации) const [isRotating, setIsRotating] = useState(false) - - // Сохранен ли прогресс в памяти браузера (true = есть сохраненный прогресс) const [hasSavedProgress, setHasSavedProgress] = useState(false) - - // Завершено ли упражнение полностью (true = все подходы выполнены) const [isCompleted, setIsCompleted] = useState(false) // ========== СОСТОЯНИЯ ОТДЫХА МЕЖДУ ПОДХОДАМИ ========== - // Идет ли сейчас отдых между подходами (true = отдыхаем, false = выполняем упражнение) const [isResting, setIsResting] = useState(false) - - // Оставшееся время отдыха в секундах (уменьшается каждую секунду) const [restTime, setRestTime] = useState(0) - - // Общее время отдыха между подходами (60 секунд = 1 минута) const [totalRestTime] = useState(60) - - // На паузе ли таймер отдыха (true = отдых приостановлен, false = отдых идет) const [isRestPaused, setIsRestPaused] = useState(false) - // ========== ВОССТАНОВЛЕНИЕ ПРОГРЕССА ПРИ ЗАГРУЗКЕ СТРАНИЦЫ ========== - // Этот код выполняется каждый раз при загрузке страницы или изменении упражнения useEffect(() => { - const loadProgress = async () => { - const serverLoaded = await loadProgressFromServer() + const loadCourseExercises = async () => { + if (!courseId) return - // async — говорит, что функция работает асинхронно и возвращает промис. - // await — заставляет функцию ждать завершения промиса, не блокируя при этом весь поток выполнения. + try { + const token = localStorage.getItem("authToken") + const response = await connect.get(`/pacient/${courseId}`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }) + const exercises = response.data.course_exercises || [] + setCourseExercises(exercises) - if (!serverLoaded) { - // Fallback to localStorage if server loading failed - console.log("Пытаемся загрузить прогресс из localStorage как резервный вариант") + const selectedDay = new URLSearchParams(location.search).get("day") + const fixedDay = selectedDay ? Number.parseInt(selectedDay) : 1 + setCurrentDay(fixedDay) + console.log("[v0] FIXED currentDay to:", fixedDay, "from URL parameter") - const savedProgress = localStorage.getItem(`exerciseProgress_${courseId}_${exerciseId}`) + if (exerciseIndex !== undefined) { + let filteredExercises = exercises - if (savedProgress) { - // Если прогресс найден, преобразуем его из текста в объект - const progress = JSON.parse(savedProgress) - - // Проверяем статус сохраненного прогресса - if (progress.status === 1) { - // СТАТУС 1 = упражнение было полностью завершено ранее - console.log("Найден завершенный прогресс упражнения для курса", courseId) - setIsCompleted(true) // Помечаем как завершенное - setCurrentTime(progress.totalTime || totalTime) // Устанавливаем время на максимум - setCompletedSets(progress.completedSets || []) // Восстанавливаем завершенные подходы - setCurrentSet(progress.set || totalSets) // Устанавливаем текущий подход на последний - setHasSavedProgress(false) // Сбрасываем флаг сохранения - } else { - // СТАТУС 0 = упражнение было на паузе или в процессе выполнения - console.log("Найден незавершенный прогресс упражнения для курса", courseId) - setCurrentTime(progress.position) // Восстанавливаем время выполнения - setCurrentSet(progress.set) // Восстанавливаем номер подхода - setCompletedSets(progress.completedSets || []) // Восстанавливаем завершенные подходы - setIsResting(progress.isResting || false) // Восстанавливаем состояние отдыха - setRestTime(progress.restTime || 0) // Восстанавливаем время отдыха - setIsRestPaused(progress.isRestPaused || false) // Восстанавливаем паузу отдыха - setHasSavedProgress(true) // Помечаем что есть сохраненный прогресс - setIsCompleted(false) // Упражнение не завершено + if (selectedDay) { + filteredExercises = exercises.filter((ex: any) => ex.day === Number.parseInt(selectedDay)) } + + const exerciseIndexNum = Number.parseInt(exerciseIndex) + if (exerciseIndexNum >= 0 && exerciseIndexNum < filteredExercises.length) { + const targetExercise = filteredExercises[exerciseIndexNum] + setActualExerciseId(targetExercise.id_exercise.toString()) + console.log("[v0] Using exercise:", targetExercise.id_exercise, "but keeping fixed day:", fixedDay) + } else { + setError("Упражнение не найдено") + setLoading(false) + return + } + } else if (exerciseId) { + setActualExerciseId(exerciseId) + console.log("[v0] Using exerciseId:", exerciseId, "but keeping fixed day:", fixedDay) + } else { + setError("ID упражнения не найден") + setLoading(false) + return } + } catch (error) { + console.error("Ошибка при загрузке упражнений курса:", error) + setError("Ошибка при загрузке упражнений") + setLoading(false) } } - loadProgress() - }, [courseId, exerciseId, totalTime, totalSets]) // Зависимости для повторного выполнения + loadCourseExercises() + }, [courseId, exerciseId, exerciseIndex, location.search]) // ========== ЗАГРУЗКА ДАННЫХ УПРАЖНЕНИЯ С СЕРВЕРА ========== useEffect(() => { - console.log("Загружаем упражнение. ID курса:", courseId, "ID упражнения:", exerciseId) + if (!actualExerciseId || !courseId) return - // Проверяем, что у нас есть необходимые ID - if (!courseId || !exerciseId) { - setError("ID курса или упражнения не найден") - setLoading(false) - return - } + console.log("Загружаем упражнение. ID курса:", courseId, "ID упражнения:", actualExerciseId) - // Отправляем запрос на сервер для получения данных упражнения connect - .get(`pacient/${courseId}/${exerciseId}`) + .get(`pacient/${courseId}/${actualExerciseId}`) .then((response) => { - // Если запрос успешен, получаем данные упражнения const exerciseData = response.data console.log("Данные упражнения получены:", exerciseData) - // Сохраняем данные упражнения в состояние компонента setExercise({ id: exerciseData.id, title: exerciseData.title, @@ -163,147 +150,138 @@ export const Exercise = () => { sessionname: exerciseData.sessionname, }) - // Если есть время упражнения, конвертируем минуты в секунды if (exerciseData.time) { const timeInSeconds = Number.parseInt(exerciseData.time) * 60 setTotalTime(timeInSeconds) console.log("Установлено время упражнения:", timeInSeconds, "секунд") } - // Если есть количество подходов, устанавливаем его if (exerciseData.count) { setTotalSets(exerciseData.count) console.log("Установлено количество подходов:", exerciseData.count) } - // Загрузка завершена успешно setLoading(false) }) .catch((error) => { - // Если произошла ошибка при загрузке console.error("Ошибка при получении упражнения:", error) - // Определяем тип ошибки и показываем соответствующее сообщение if (error.response) { - // Сервер ответил с ошибкой (например, 404, 500) console.error("Ошибка ответа сервера:", error.response.status, error.response.data) setError(`Ошибка сервера: ${error.response.status}`) } else if (error.request) { - // Запрос был отправлен, но ответа не получено (нет интернета) console.error("Нет ответа от сервера:", error.request) setError("Нет ответа от сервера") } else { - // Другая ошибка при настройке запроса console.error("Ошибка при настройке запроса:", error.message) setError(`Ошибка: ${error.message}`) } setLoading(false) }) - }, [courseId, exerciseId]) // Код выполняется при изменении ID курса или упражнения - - + }, [courseId, actualExerciseId]) // ========== ФУНКЦИЯ ЗАГРУЗКИ ПРОГРЕССА С СЕРВЕРА ========== - // Эта функция получает сохраненный прогресс упражнения с бэкенда -const loadProgressFromServer = async () => { - try { - console.log(`Загружаем прогресс для курса ${courseId} и упражнения ${exerciseId}`); + const loadProgressFromServer = async () => { + try { + console.log(`Загружаем прогресс для курса ${courseId}, упражнения ${actualExerciseId}, день ${currentDay}`) - const token = localStorage.getItem('authToken'); + const token = localStorage.getItem("authToken") - // Выполняем GET-запрос на сервер - const response = await connect.get(`pacient/${courseId}/${exerciseId}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const dayParam = currentDay ? `?day=${currentDay}` : "" + const response = await connect.get(`pacient/${courseId}/${actualExerciseId}${dayParam}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) - // Логируем ответ - console.log("Ответ сервера с прогрессом:", response.data); + console.log("Ответ сервера с прогрессом:", response.data) - // Проверяем наличие user_progress - if ( - response.data && - response.data.user_progress && - Array.isArray(response.data.user_progress) && - response.data.user_progress.length > 0 - ) { - const progressArray = response.data.user_progress; + if ( + response.data && + response.data.user_progress && + Array.isArray(response.data.user_progress) && + response.data.user_progress.length > 0 + ) { + const progressArray = response.data.user_progress - // Фильтруем прогресс по текущему курсу - const filteredProgress = progressArray.filter((record: any) => { - return record.course_id === Number.parseInt(courseId); - }); + const filteredProgress = progressArray.filter((record: any) => { + const matchesCourse = record.course_id === Number.parseInt(courseId) + const matchesDay = currentDay ? record.day === currentDay : true + console.log( + "[v0] Filtering progress - Course match:", + matchesCourse, + "Day match:", + matchesDay, + "Record day:", + record.day, + "Current day:", + currentDay, + ) + return matchesCourse && matchesDay + }) - console.log("Отфильтрованный прогресс:", filteredProgress); + console.log("Отфильтрованный прогресс по дню:", filteredProgress) - const completedSetsFromServer: number[] = []; - let lastIncompleteSet: any = null; - let lastSavedTime = "00:00"; + const completedSetsFromServer: number[] = [] + let lastIncompleteSet: any = null + let lastSavedTime = "00:00" - // Обрабатываем каждый прогресс - filteredProgress.forEach((record: any) => { - console.log(`Подход ${record.set}, статус ${record.status}, время ${record.time_users}`); + filteredProgress.forEach((record: any) => { + console.log(`Подход ${record.set}, статус ${record.status}, время ${record.time_users}`) - if (record.status === 1) { - // Завершённый подход - completedSetsFromServer.push(record.set); - } else if (record.status === 0) { - // Незавершённый подход - if (!lastIncompleteSet || record.set > lastIncompleteSet.set) { - lastIncompleteSet = record; - lastSavedTime = record.time_users || "00:00"; + if (record.status === 1) { + completedSetsFromServer.push(record.set) + } else if (record.status === 0) { + if (!lastIncompleteSet || record.set > lastIncompleteSet.set) { + lastIncompleteSet = record + lastSavedTime = record.time_users || "00:00" + } } + }) + + if (completedSetsFromServer.length > 0) { + setCompletedSets(completedSetsFromServer) } - }); - // Восстановление завершённых подходов - if (completedSetsFromServer.length > 0) { - setCompletedSets(completedSetsFromServer); + if (lastIncompleteSet) { + const timeInSeconds = convertTimeToSeconds(lastSavedTime) + + setCurrentSet(lastIncompleteSet.set) + setCurrentTime(timeInSeconds) + setHasSavedProgress(true) + setIsCompleted(false) + + console.log(`Восстановлен незавершённый подход ${lastIncompleteSet.set} с временем ${timeInSeconds} секунд`) + } + + if (completedSetsFromServer.length >= totalSets) { + setIsCompleted(true) + setCurrentTime(totalTime) + setHasSavedProgress(false) + console.log("Все подходы завершены") + } + + return true + } else { + console.log("Прогресс не найден, начинаем с чистого листа") + return false } - - // Восстановление незавершённого подхода - if (lastIncompleteSet) { - const timeInSeconds = convertTimeToSeconds(lastSavedTime); - - setCurrentSet(lastIncompleteSet.set); - setCurrentTime(timeInSeconds); - setHasSavedProgress(true); - setIsCompleted(false); - - console.log(`Восстановлен незавершённый подход ${lastIncompleteSet.set} с временем ${timeInSeconds} секунд`); - } - - // Проверка, завершены ли все подходы - if (completedSetsFromServer.length >= totalSets) { - setIsCompleted(true); - setCurrentTime(totalTime); - setHasSavedProgress(false); - console.log("Все подходы завершены"); - } - - return true; - } else { - console.log("Прогресс не найден, начинаем с чистого листа"); - return false; + } catch (error) { + console.error("Ошибка при загрузке прогресса с сервера:", error) + return false } - } catch (error) { - console.error("Ошибка при загрузке прогресса с сервера:", error); - return false; } -}; + // ========== ФУНКЦИЯ КОНВЕРТАЦИИ ВРЕМЕНИ ИЗ MM:SS В СЕКУНДЫ ========== - // Преобразует время из формата "MM:SS" в секунды (например: "02:30" = 150 секунд) const convertTimeToSeconds = (timeString: string): number => { try { - // Разделяем строку по двоеточию const parts = timeString.split(":") if (parts.length === 2) { - const minutes = Number.parseInt(parts[0]) || 0 // Получаем минуты - const seconds = Number.parseInt(parts[1]) || 0 // Получаем секунды - const totalSeconds = minutes * 60 + seconds // Конвертируем в секунды + const minutes = Number.parseInt(parts[0]) || 0 + const seconds = Number.parseInt(parts[1]) || 0 + const totalSeconds = minutes * 60 + seconds console.log(`Конвертируем время ${timeString} в ${totalSeconds} секунд`) return totalSeconds @@ -316,74 +294,73 @@ const loadProgressFromServer = async () => { return 0 } } + // ========== ФУНКЦИЯ СОХРАНЕНИЯ ПРОГРЕССА ПОДХОДА НА СЕРВЕР ========== const saveSetProgress = async (setNumber: number, timeString: string, status: number) => { try { - console.log(`Сохраняем прогресс подхода ${setNumber} со статусом ${status}`) + const dayToSave = currentDay || 1 + console.log(`[v0] SAVING PROGRESS: Set ${setNumber}, Status ${status}, Day ${dayToSave}`) + console.log(`[v0] Current state - currentDay: ${currentDay}, dayToSave: ${dayToSave}`) + console.log(`[v0] URL search params:`, location.search) const progressData = { - time_users: timeString, // Время в формате MM:SS - set: setNumber, // Номер подхода (1, 2, 3...) - status: status, // 0 = в процессе, 1 = завершен - course_id: Number.parseInt(courseId), // ВАЖНО: добавляем ID курса для правильной фильтрации + time_users: timeString, + set: setNumber, + status: status, + course_id: Number.parseInt(courseId), + day: dayToSave, } - console.log("Отправляем данные на сервер:", progressData) + console.log("[v0] FINAL DATA TO SERVER:", progressData) - // Отправляем POST запрос на сервер для сохранения прогресса - const response = await connect.post(`pacient/${courseId}/${exerciseId}`, progressData) + const response = await connect.post(`pacient/${courseId}/${actualExerciseId}`, progressData) - if (response.status === 200) { - console.log("Прогресс успешно сохранен на сервере") + if (response.status === 200 || response.status === 201) { + console.log("[v0] SUCCESS: Progress saved to server for day", dayToSave) } } catch (error) { console.error("Ошибка при сохранении прогресса на сервер:", error) } } - // ========== ФУНКЦИЯ ПЕРЕХОДА К СЛЕДУЮЩЕМУ УПРАЖНЕНИЮ ========== const goToNextExercise = () => { console.log("Переходим к следующему упражнению") - // Получаем текущий номер упражнения и увеличиваем на 1 - const currentExerciseNum = Number.parseInt(exerciseId); - const nextExerciseId = currentExerciseNum; + if (exerciseIndex !== undefined) { + const currentIndex = Number.parseInt(exerciseIndex) + const nextIndex = currentIndex + 1 + const selectedDay = new URLSearchParams(location.search).get("day") + const dayParam = selectedDay ? `?day=${selectedDay}` : "" - console.log(`!!!!Текущее упражнение: ${currentExerciseNum}, следующее: ${nextExerciseId}`) - - console.log({ courseId }, { nextExerciseId }) - // Переходим к следующему упражнению в том же курсе - history.push(`${courseId}/${nextExerciseId}`) + history.push(`/course/${courseId}/exercise/${nextIndex}${dayParam}`) + } else { + const currentExerciseNum = Number.parseInt(actualExerciseId) + const nextExerciseId = currentExerciseNum + 1 + history.push(`/course/${courseId}/${nextExerciseId}`) + } } // ========== ФУНКЦИЯ ЗАВЕРШЕНИЯ ТЕКУЩЕГО ПОДХОДА ========== const handleCompleteSet = async () => { console.log("Пользователь завершает подход", currentSet, "из", totalSets) - // Сохраняем завершенный подход на сервер со статусом 1 (завершен) await saveSetProgress(currentSet, formatTime(currentTime), 1) - // Добавляем текущий подход в список завершенных const newCompletedSets = [...completedSets, currentSet] setCompletedSets(newCompletedSets) - // Останавливаем таймер упражнения setIsPlaying(false) - // Проверяем, завершены ли все подходы if (newCompletedSets.length >= totalSets) { console.log("ВСЕ ПОДХОДЫ ЗАВЕРШЕНЫ! Упражнение выполнено полностью.") - // Все подходы завершены - завершаем упражнение полностью await handleComplete(newCompletedSets) } else { console.log("Начинаем отдых перед подходом", currentSet + 1) - // Еще есть подходы - начинаем отдых перед следующим подходом - setIsResting(true) // Включаем режим отдыха - setIsRestPaused(false) // Отдых не на паузе - setRestTime(totalRestTime) // Устанавливаем полное время отдыха - setCurrentTime(0) // Сбрасываем время упражнения + setIsResting(true) + setIsRestPaused(false) + setRestTime(totalRestTime) + setCurrentTime(0) - // Сохраняем прогресс с информацией об отдыхе await handlePause(newCompletedSets, currentSet, true, totalRestTime, false) } } @@ -392,29 +369,32 @@ const loadProgressFromServer = async () => { const handleComplete = async (completedSetsArray = completedSets) => { console.log("УПРАЖНЕНИЕ ПОЛНОСТЬЮ ЗАВЕРШЕНО! Все подходы выполнены.") - // Сохраняем в память браузера как завершенное (СТАТУС 1 = ЗАВЕРШЕНО) + const storageKey = currentDay + ? `exerciseProgress_${courseId}_${actualExerciseId}_day_${currentDay}` + : `exerciseProgress_${courseId}_${actualExerciseId}` + localStorage.setItem( - `exerciseProgress_${courseId}_${exerciseId}`, + storageKey, JSON.stringify({ - exerciseId: exercise?.id, // ID упражнения - courseId: courseId, // ID курса для дополнительной проверки - position: totalTime, // Позиция на максимальном времени - set: totalSets, // Все подходы завершены - status: 1, // СТАТУС 1 = ВСЕ УПРАЖНЕНИЕ ЗАВЕРШЕНО - totalTime: totalTime, // Общее время упражнения - completedSets: completedSetsArray, // Список завершенных подходов - completedAt: new Date().toISOString(), // Время завершения + exerciseId: exercise?.id, + courseId: courseId, + day: currentDay, + position: totalTime, + set: totalSets, + status: 1, + totalTime: totalTime, + completedSets: completedSetsArray, + completedAt: new Date().toISOString(), }), ) - // Обновляем состояние компонента - setIsCompleted(true) // Помечаем как завершенное - setCurrentTime(totalTime) // Устанавливаем время на максимум - setIsPlaying(false) // Останавливаем таймер - setIsResting(false) // Выключаем режим отдыха - setIsRestPaused(false) // Убираем паузу отдыха - setHasSavedProgress(false) // Сбрасываем флаг сохранения - setCompletedSets(completedSetsArray) // Обновляем завершенные подходы + setIsCompleted(true) + setCurrentTime(totalTime) + setIsPlaying(false) + setIsResting(false) + setIsRestPaused(false) + setHasSavedProgress(false) + setCompletedSets(completedSetsArray) } // ========== ФУНКЦИЯ ПАУЗЫ И СОХРАНЕНИЯ ПРОГРЕССА ========== @@ -425,40 +405,90 @@ const loadProgressFromServer = async () => { currentRestTime = restTime, restPaused = isRestPaused, ) => { - console.log("Сохраняем прогресс на паузе для курса", courseId, "упражнения", exerciseId) + console.log("Сохраняем прогресс на паузе для курса", courseId, "упражнения", actualExerciseId, "день", currentDay) - // Сохраняем как незавершенное (СТАТУС 0 = В ПРОЦЕССЕ/НА ПАУЗЕ) await saveSetProgress(setNumber, formatTime(currentTime), 0) - // Сохраняем прогресс в память браузера + const storageKey = currentDay + ? `exerciseProgress_${courseId}_${actualExerciseId}_day_${currentDay}` + : `exerciseProgress_${courseId}_${actualExerciseId}` + localStorage.setItem( - `exerciseProgress_${courseId}_${exerciseId}`, + storageKey, JSON.stringify({ - exerciseId: exercise?.id, // ID упражнения - courseId: courseId, // ID курса для дополнительной проверки - position: currentTime, // Текущая позиция времени - set: setNumber, // Текущий подход - status: 0, // СТАТУС 0 = УПРАЖНЕНИЕ НА ПАУЗЕ ИЛИ В ПРОЦЕССЕ - completedSets: completedSetsArray, // Завершенные подходы - isResting: resting, // Идет ли отдых - restTime: currentRestTime, // Оставшееся время отдыха - isRestPaused: restPaused, // На паузе ли отдых + exerciseId: exercise?.id, + courseId: courseId, + day: currentDay, + position: currentTime, + set: setNumber, + status: 0, + completedSets: completedSetsArray, + isResting: resting, + restTime: currentRestTime, + isRestPaused: restPaused, }), ) - // Помечаем что прогресс сохранен setHasSavedProgress(true) } + // ========== ВОССТАНОВЛЕНИЕ ПРОГРЕССА ПРИ ЗАГРУЗКЕ СТРАНИЦЫ ========== + useEffect(() => { + const loadProgress = async () => { + if (!actualExerciseId) return + + const serverLoaded = await loadProgressFromServer() + + if (!serverLoaded) { + console.log("Пытаемся загрузить прогресс из localStorage как резервный вариант") + + const storageKey = currentDay + ? `exerciseProgress_${courseId}_${actualExerciseId}_day_${currentDay}` + : `exerciseProgress_${courseId}_${actualExerciseId}` + + const savedProgress = localStorage.getItem(storageKey) + + if (savedProgress) { + const progress = JSON.parse(savedProgress) + + if (currentDay && progress.day && progress.day !== currentDay) { + console.log("Прогресс для другого дня, игнорируем") + return + } + + setExercise({ + id: progress.exerciseId, + title: "", + desc: "", + url_file: "", + url_file_img: "", + time: "", + repeats: 0, + count: 0, + position: progress.position, + day: progress.day, + sessionname: "", + }) + + setCurrentTime(progress.position) + setCurrentSet(progress.set) + setCompletedSets(progress.completedSets || []) + setIsResting(progress.isResting || false) + setRestTime(progress.restTime || 0) + setIsRestPaused(progress.isRestPaused || false) + setHasSavedProgress(true) + setIsCompleted(progress.status === 1) + } + } + } + + loadProgress() + }, [courseId, actualExerciseId, currentDay, totalTime, totalSets]) + // ========== ОСНОВНОЙ ТАЙМЕР ДЛЯ УПРАЖНЕНИЯ ========== - // Этот код выполняется каждую секунду когда упражнение активно useEffect(() => { let interval: NodeJS.Timeout | undefined - // Таймер работает только если: - // - упражнение запущено (isPlaying = true) - // - упражнение не завершено (isCompleted = false) - // - не идет отдых (isResting = false) if (isPlaying && !isCompleted && !isResting) { console.log("Запускаем таймер упражнения") @@ -467,12 +497,10 @@ const loadProgressFromServer = async () => { const newTime = prev + 1 console.log(`Таймер упражнения: ${newTime}/${totalTime} секунд`) - // Проверяем, достигли ли мы конца времени для текущего подхода if (newTime >= totalTime) { console.log("Время подхода истекло, автоматически завершаем подход") setIsPlaying(false) - // Используем setTimeout чтобы избежать проблем с состоянием в useEffect setTimeout(() => { handleCompleteSet() }, 100) @@ -480,10 +508,9 @@ const loadProgressFromServer = async () => { } return newTime }) - }, 1000) // Выполняется каждую секунду (1000 миллисекунд) + }, 1000) } - // Очищаем таймер при размонтировании компонента или изменении зависимостей return () => { if (interval) { console.log("Останавливаем таймер упражнения") @@ -493,14 +520,9 @@ const loadProgressFromServer = async () => { }, [isPlaying, totalTime, isCompleted, isResting, currentSet, completedSets]) // ========== ТАЙМЕР ДЛЯ ОТДЫХА МЕЖДУ ПОДХОДАМИ ========== - // Этот код выполняется каждую секунду во время отдыха useEffect(() => { let restInterval: NodeJS.Timeout | undefined - // Таймер отдыха работает только если: - // - идет отдых (isResting = true) - // - есть оставшееся время отдыха (restTime > 0) - // - отдых не на паузе (isRestPaused = false) if (isResting && restTime > 0 && !isRestPaused) { console.log("Запускаем таймер отдыха") @@ -509,23 +531,20 @@ const loadProgressFromServer = async () => { const newRestTime = prev - 1 console.log(`Таймер отдыха: ${newRestTime} секунд осталось`) - // Если время отдыха закончилось if (newRestTime <= 0) { console.log("Отдых закончен, переходим к следующему подходу") - // Выключаем режим отдыха и переходим к следующему подходу - setIsResting(false) // Выключаем отдых - setIsRestPaused(false) // Убираем паузу - setCurrentSet(currentSet + 1) // Переходим к следующему подходу - setCurrentTime(0) // Сбрасываем время упражнения + setIsResting(false) + setIsRestPaused(false) + setCurrentSet(currentSet + 1) + setCurrentTime(0) return 0 } return newRestTime }) - }, 1000) // Выполняется каждую секунду + }, 1000) } - // Очищаем таймер при размонтировании компонента или изменении зависимостей return () => { if (restInterval) { console.log("Останавливаем таймер отдыха") @@ -546,43 +565,34 @@ const loadProgressFromServer = async () => { } // ========== ФУНКЦИЯ ФОРМАТИРОВАНИЯ ВРЕМЕНИ ========== - // Преобразует секунды в формат MM:SS (например: 125 секунд = "02:05") const formatTime = (seconds: number) => { - const mins = Math.floor(seconds / 60) // Получаем минуты - const secs = seconds % 60 // Получаем оставшиеся секунды - // Добавляем ведущий ноль если число меньше 10 (например: 5 -> "05") + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}` } // ========== РАСЧЕТ ПРОГРЕССА ДЛЯ ПОЛОСКИ ========== - // Вычисляем процент заполнения полоски прогресса (от 0 до 100) - const progress = isResting - ? ((totalRestTime - restTime) / totalRestTime) * 100 // Прогресс отдыха - : (currentTime / totalTime) * 100 // Прогресс упражнения + const progress = isResting ? ((totalRestTime - restTime) / totalRestTime) * 100 : (currentTime / totalTime) * 100 // ========== ИКОНКИ ДЛЯ КНОПОК ========== - // SVG иконка "Играть" (треугольник) const PlayIcon = () => ( ) - // SVG иконка "Пауза" (две вертикальные полоски) const PauseIcon = () => ( ) - // SVG иконка "Галочка" (завершено) const CheckIcon = () => ( ) - // SVG иконка "Часы" (для отдыха) const RestIcon = () => ( { ) // ========== ИНФОРМАЦИЯ ОБ УПРАЖНЕНИИ ДЛЯ ОТОБРАЖЕНИЯ ========== - // Массив с описанием шагов упражнения const exerciseSteps = [ { title: "Описание упражнения", @@ -618,8 +627,6 @@ const loadProgressFromServer = async () => { ] // ========== ЭКРАНЫ ЗАГРУЗКИ И ОШИБОК ========== - - // Если данные еще загружаются, показываем экран загрузки if (loading) { return (
@@ -633,7 +640,6 @@ const loadProgressFromServer = async () => { ) } - // Если произошла ошибка, показываем экран ошибки if (error) { return (
@@ -655,7 +661,6 @@ const loadProgressFromServer = async () => { ) } - // Если упражнение не найдено, показываем соответствующее сообщение if (!exercise) { return (
@@ -681,55 +686,46 @@ const loadProgressFromServer = async () => { return (
- {/* Заголовок страницы с названием упражнения */} - +
- {/* Основная карточка с изображением и кнопкой воспроизведения */}
- {/* Изображение упражнения */} {exercise.title} { - // Если изображение не загрузилось, показываем заглушку const target = e.target as HTMLImageElement target.src = "/placeholder.svg?height=300&width=400&text=Упражнение" }} /> - {/* Градиент поверх изображения для лучшей читаемости */}
- {/* Центральная кнопка управления */}
{isCompleted ? ( - // Если упражнение завершено - показываем галочку
) : isResting ? ( - // Если идет отдых - показываем иконку часов
) : ( - // Если упражнение активно - показываем кнопку воспроизведения/паузы )}
- {/* Индикаторы статуса в левом верхнем углу */} {isCompleted && (
@@ -757,7 +753,6 @@ const loadProgressFromServer = async () => {
)} - {/* Таймер в правом верхнем углу */}
{isResting ? formatTime(restTime) : formatTime(currentTime)} @@ -767,31 +762,27 @@ const loadProgressFromServer = async () => {
- {/* Индикатор прогресса подходов */}

Прогресс подходов

- {/* Создаем полоски для каждого подхода */} {Array.from({ length: totalSets }, (_, index) => { - const setNumber = index + 1 // Номер подхода (начинаем с 1) - const isSetCompleted = completedSets.includes(setNumber) // Завершен ли этот подход - const isCurrent = setNumber === currentSet && !isSetCompleted // Текущий ли это подход + const setNumber = index + 1 + const isSetCompleted = completedSets.includes(setNumber) + const isCurrent = setNumber === currentSet && !isSetCompleted return (
- {/* Полоска прогресса для подхода */}
- {/* Номер подхода под полоской */}
{setNumber}
) })}
- {/* Общая статистика */}
Завершено: {completedSets.length} Всего: {totalSets} @@ -799,7 +790,6 @@ const loadProgressFromServer = async () => {
- {/* Информационные карточки об упражнении */}
{exerciseSteps.map((step, index) => (
{ ))}
- {/* Фиксированная панель управления внизу экрана */}
- {/* Информация о текущем состоянии */}
- {/* Индикатор активности */}
- {/* Текст состояния */} {isCompleted ? `Все подходы завершены!` @@ -840,35 +827,31 @@ const loadProgressFromServer = async () => { : `Подход ${currentSet} из ${totalSets}`}
- {/* Отображение времени */} {isResting ? `${formatTime(restTime)} отдых` : `${formatTime(currentTime)} / ${formatTime(totalTime)}`}
- {/* Полоска прогресса */}
- {/* Кнопки управления */}
{isCompleted ? ( - // Если упражнение завершено - показываем кнопки перехода и статуса <>
@@ -877,19 +860,17 @@ const loadProgressFromServer = async () => {
) : isResting ? ( - // Если идет отдых - показываем кнопки управления отдыхом <> ) : ( - // Если упражнение активно - показываем основные кнопки управления <> - {/* Кнопка завершения текущего подхода */}
- {/* Нижняя навигация */}
diff --git a/src/shared/consts/router.ts b/src/shared/consts/router.ts index dc53b5e..f354848 100644 --- a/src/shared/consts/router.ts +++ b/src/shared/consts/router.ts @@ -6,5 +6,11 @@ export const getRouteCourses = () => `/courses` export const getRouteCourseExercises = (id: number | string) => `/course/${id}` export const getRouteExercise = (courseId: number | string, exerciseId: number | string) => `/course/${courseId}/${exerciseId}` + +export const getRouteExerciseByIndex = (courseId: number | string, exerciseIndex: number, day?: number) => { + const dayParam = day ? `?day=${day}` : "" + return `/course/${courseId}/exercise/${exerciseIndex}${dayParam}` +} + export const getRouteSettings = () => `/settings` export const getRouteCourseComplete = () => `/course-complete`