995 lines
40 KiB
TypeScript
995 lines
40 KiB
TypeScript
"use client"
|
||
|
||
// Импортируем необходимые функции из React для создания интерактивного компонента
|
||
import { useState, useEffect } from "react"
|
||
// Импортируем функции для навигации между страницами
|
||
import { useHistory, useParams, useLocation } from "react-router-dom"
|
||
|
||
// Импортируем иконки и компоненты интерфейса
|
||
import { RefreshIcon } from "../components/icons/RefreshIcon"
|
||
import HeaderNav from "../components/HeaderNav"
|
||
import BottomNavigation from "../components/BottomNavigation"
|
||
|
||
import type { CourseExercises } from "./CourseExercises"
|
||
|
||
// Импортируем функцию для связи с сервером (бэкендом)
|
||
import { connect } from "../confconnect"
|
||
|
||
// Описываем структуру данных упражнения - что должно содержать каждое упражнение
|
||
export interface Exercise {
|
||
id: number // Уникальный номер упражнения
|
||
title: string // Название упражнения
|
||
desc: string // Описание упражнения
|
||
url_file: string // Ссылка на видео упражнения
|
||
url_file_img: string // Ссылка на картинку упражнения
|
||
time: string // Время выполнения упражнения в минутах
|
||
repeats: number // Количество повторений в одном подходе
|
||
count: number // Количество подходов (сетов)
|
||
position: number // Порядковый номер упражнения в программе
|
||
day: number // День программы
|
||
sessionname: string // Название тренировочной сессии
|
||
}
|
||
|
||
interface RouteParams {
|
||
courseId: string
|
||
exerciseId?: string // Made optional since we might have exerciseIndex instead
|
||
exerciseIndex?: string // Added exerciseIndex parameter
|
||
}
|
||
|
||
export const Exercise = () => {
|
||
const history = useHistory()
|
||
const { courseId, exerciseId, exerciseIndex } = useParams<RouteParams>()
|
||
const location = useLocation()
|
||
|
||
|
||
const [course_exercises, setCourseExercises] = useState<CourseExercises[]>([])
|
||
const [actualExerciseId, setActualExerciseId] = useState<string>("")
|
||
const [currentDay, setCurrentDay] = useState<number | null>(null)
|
||
|
||
const [exercise, setExercise] = useState<Exercise | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string>("")
|
||
|
||
// ========== СОСТОЯНИЯ ТАЙМЕРУ УПРАЖНЕНИЯ ==========
|
||
const [isPlaying, setIsPlaying] = useState(false)
|
||
const [currentTime, setCurrentTime] = useState(0)
|
||
const [totalTime, setTotalTime] = useState(900)
|
||
|
||
// ========== СОСТОЯНИЯ ПОДХОДОВ (СЕТОВ) ==========
|
||
const [currentSet, setCurrentSet] = useState(1)
|
||
const [totalSets, setTotalSets] = useState(3)
|
||
const [completedSets, setCompletedSets] = useState<number[]>([])
|
||
|
||
// ========== СОСТОЯНИЯ ИНТЕРФЕЙСА ==========
|
||
const [isRotating, setIsRotating] = useState(false)
|
||
const [hasSavedProgress, setHasSavedProgress] = useState(false)
|
||
const [isCompleted, setIsCompleted] = useState(false)
|
||
|
||
// ========== СОСТОЯНИЯ ОТДЫХА МЕЖДУ ПОДХОДАМИ ==========
|
||
const [isResting, setIsResting] = useState(false)
|
||
const [restTime, setRestTime] = useState(0)
|
||
const [totalRestTime] = useState(60)
|
||
const [isRestPaused, setIsRestPaused] = useState(false)
|
||
|
||
useEffect(() => {
|
||
const loadCourseExercises = async () => {
|
||
if (!courseId) return
|
||
|
||
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)
|
||
|
||
const selectedDay = new URLSearchParams(location.search).get("day")
|
||
const fixedDay = selectedDay ? Number.parseInt(selectedDay) : 1
|
||
setCurrentDay(fixedDay)
|
||
console.log(" FIXED currentDay to:", fixedDay, "from URL parameter")
|
||
|
||
if (exerciseIndex !== undefined) {
|
||
let filteredExercises = exercises
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
loadCourseExercises()
|
||
}, [courseId, exerciseId, exerciseIndex, location.search])
|
||
|
||
// ========== ЗАГРУЗКА ДАННЫХ УПРАЖНЕНИЯ С СЕРВЕРА ==========
|
||
useEffect(() => {
|
||
if (!actualExerciseId || !courseId) return
|
||
|
||
console.log("Загружаем упражнение. ID курса:", courseId, "ID упражнения:", actualExerciseId)
|
||
|
||
connect
|
||
.get(`pacient/${courseId}/${actualExerciseId}`)
|
||
.then((response) => {
|
||
const exerciseData = response.data
|
||
console.log("Данные упражнения получены:", exerciseData)
|
||
|
||
setExercise({
|
||
id: exerciseData.id,
|
||
title: exerciseData.title,
|
||
desc: exerciseData.desc,
|
||
url_file: exerciseData.url_file,
|
||
url_file_img: exerciseData.url_file_img,
|
||
time: exerciseData.time,
|
||
repeats: exerciseData.repeats,
|
||
count: exerciseData.count,
|
||
position: exerciseData.position,
|
||
day: exerciseData.day,
|
||
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) {
|
||
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, actualExerciseId])
|
||
|
||
// ========== ФУНКЦИЯ ЗАГРУЗКИ ПРОГРЕССА С СЕРВЕРА ==========
|
||
const loadProgressFromServer = async () => {
|
||
try {
|
||
console.log(`Загружаем прогресс для курса ${courseId}, упражнения ${actualExerciseId}, день ${currentDay}`)
|
||
|
||
const token = localStorage.getItem("authToken")
|
||
|
||
const dayParam = currentDay ? `?day=${currentDay}` : ""
|
||
const response = await connect.get(`pacient/${courseId}/${actualExerciseId}${dayParam}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
})
|
||
|
||
|
||
console.log("Ответ сервера с прогрессом:", response.data)
|
||
|
||
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) => {
|
||
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)
|
||
|
||
const completedSetsFromServer: number[] = []
|
||
let lastIncompleteSet: any = null
|
||
let lastSavedTime = "00:00"
|
||
|
||
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 (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
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка при загрузке прогресса с сервера:", error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ========== ФУНКЦИЯ КОНВЕРТАЦИИ ВРЕМЕНИ ИЗ MM:SS В СЕКУНДЫ ==========
|
||
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
|
||
|
||
console.log(`Конвертируем время ${timeString} в ${totalSeconds} секунд`)
|
||
return totalSeconds
|
||
} else {
|
||
console.warn("Неверный формат времени:", timeString)
|
||
return 0
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка при конвертации времени:", error)
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// ========== ФУНКЦИЯ СОХРАНЕНИЯ ПРОГРЕССА ПОДХОДА НА СЕРВЕР ==========
|
||
const saveSetProgress = async (setNumber: number, timeString: string, status: number) => {
|
||
try {
|
||
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,
|
||
set: setNumber,
|
||
status: status,
|
||
course_id: Number.parseInt(courseId),
|
||
day: dayToSave,
|
||
}
|
||
|
||
console.log("FINAL DATA TO SERVER:", progressData)
|
||
|
||
const response = await connect.post(`pacient/${courseId}/${actualExerciseId}`, progressData)
|
||
|
||
if (response.status === 200 || response.status === 201) {
|
||
console.log("SUCCESS: Progress saved to server for day", dayToSave)
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка при сохранении прогресса на сервер:", error)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
console.log("TEST DAY COMPLETE", course_exercises)
|
||
console.log("Всего упражнений в курсе", course_exercises.length)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// ========== ФУНКЦИЯ перехода к след упражнению ==========
|
||
const goToNextExercise = () => {
|
||
console.log("Переходим к следующему упражнению")
|
||
|
||
|
||
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}` : ""
|
||
|
||
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)
|
||
|
||
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)
|
||
|
||
await handlePause(newCompletedSets, currentSet, true, totalRestTime, false)
|
||
}
|
||
}
|
||
|
||
// ========== ФУНКЦИЯ ПОЛНОГО ЗАВЕРШЕНИЯ УПРАЖНЕНИЯ ==========
|
||
const handleComplete = async (completedSetsArray = completedSets) => {
|
||
console.log("УПРАЖНЕНИЕ ПОЛНОСТЬЮ ЗАВЕРШЕНО! Все подходы выполнены.")
|
||
|
||
const storageKey = currentDay
|
||
? `exerciseProgress_${courseId}_${actualExerciseId}_day_${currentDay}`
|
||
: `exerciseProgress_${courseId}_${actualExerciseId}`
|
||
|
||
localStorage.setItem(
|
||
storageKey,
|
||
JSON.stringify({
|
||
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)
|
||
}
|
||
|
||
// ========== ФУНКЦИЯ ПАУЗЫ И СОХРАНЕНИЯ ПРОГРЕССА ==========
|
||
const handlePause = async (
|
||
completedSetsArray = completedSets,
|
||
setNumber = currentSet,
|
||
resting = isResting,
|
||
currentRestTime = restTime,
|
||
restPaused = isRestPaused,
|
||
) => {
|
||
console.log("Сохраняем прогресс на паузе для курса", courseId, "упражнения", actualExerciseId, "день", currentDay)
|
||
|
||
await saveSetProgress(setNumber, formatTime(currentTime), 0)
|
||
|
||
const storageKey = currentDay
|
||
? `exerciseProgress_${courseId}_${actualExerciseId}_day_${currentDay}`
|
||
: `exerciseProgress_${courseId}_${actualExerciseId}`
|
||
|
||
localStorage.setItem(
|
||
storageKey,
|
||
JSON.stringify({
|
||
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
|
||
|
||
if (isPlaying && !isCompleted && !isResting) {
|
||
console.log("Запускаем таймер упражнения")
|
||
|
||
interval = setInterval(() => {
|
||
setCurrentTime((prev) => {
|
||
const newTime = prev + 1
|
||
console.log(`Таймер упражнения: ${newTime}/${totalTime} секунд`)
|
||
|
||
if (newTime >= totalTime) {
|
||
console.log("Время подхода истекло, автоматически завершаем подход")
|
||
setIsPlaying(false)
|
||
|
||
setTimeout(() => {
|
||
handleCompleteSet()
|
||
}, 100)
|
||
return totalTime
|
||
}
|
||
return newTime
|
||
})
|
||
}, 1000)
|
||
}
|
||
|
||
return () => {
|
||
if (interval) {
|
||
console.log("Останавливаем таймер упражнения")
|
||
clearInterval(interval)
|
||
}
|
||
}
|
||
}, [isPlaying, totalTime, isCompleted, isResting, currentSet, completedSets])
|
||
|
||
// ========== ТАЙМЕР ДЛЯ ОТДЫХА МЕЖДУ ПОДХОДАМИ ==========
|
||
useEffect(() => {
|
||
let restInterval: NodeJS.Timeout | undefined
|
||
|
||
if (isResting && restTime > 0 && !isRestPaused) {
|
||
console.log("Запускаем таймер отдыха")
|
||
|
||
restInterval = setInterval(() => {
|
||
setRestTime((prev) => {
|
||
const newRestTime = prev - 1
|
||
console.log(`Таймер отдыха: ${newRestTime} секунд осталось`)
|
||
|
||
if (newRestTime <= 0) {
|
||
console.log("Отдых закончен, переходим к следующему подходу")
|
||
|
||
setIsResting(false)
|
||
setIsRestPaused(false)
|
||
setCurrentSet(currentSet + 1)
|
||
setCurrentTime(0)
|
||
return 0
|
||
}
|
||
return newRestTime
|
||
})
|
||
}, 1000)
|
||
}
|
||
|
||
return () => {
|
||
if (restInterval) {
|
||
console.log("Останавливаем таймер отдыха")
|
||
clearInterval(restInterval)
|
||
}
|
||
}
|
||
}, [isResting, restTime, isRestPaused, currentSet])
|
||
|
||
// ========== ФУНКЦИИ ДЛЯ АНИМАЦИИ КНОПКИ СБРОСА ==========
|
||
const handleClick = () => {
|
||
console.log("Запускаем анимацию кнопки сброса")
|
||
setIsRotating(true)
|
||
}
|
||
|
||
const handleTransitionEnd = () => {
|
||
console.log("Анимация кнопки сброса завершена")
|
||
setIsRotating(false)
|
||
}
|
||
|
||
// ========== ФУНКЦИЯ ФОРМАТИРОВАНИЯ ВРЕМЕНИ ==========
|
||
const formatTime = (seconds: number) => {
|
||
const mins = Math.floor(seconds / 60)
|
||
const secs = seconds % 60
|
||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||
}
|
||
|
||
// ========== РАСЧЕТ ПРОГРЕССА ДЛЯ ПОЛОСКИ ==========
|
||
const progress = isResting ? ((totalRestTime - restTime) / totalRestTime) * 100 : (currentTime / totalTime) * 100
|
||
|
||
// ========== ИКОНКИ ДЛЯ КНОПОК ==========
|
||
const PlayIcon = () => (
|
||
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M8 5v14l11-7z" />
|
||
</svg>
|
||
)
|
||
|
||
const PauseIcon = () => (
|
||
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
||
</svg>
|
||
)
|
||
|
||
const CheckIcon = () => (
|
||
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||
</svg>
|
||
)
|
||
|
||
const RestIcon = () => (
|
||
<svg width="40" height="40" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||
<path
|
||
d="M12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2ZM12 6C11.7348 6 11.4804 6.10536 11.2929 6.29289C11.1054 6.48043 11 6.73478 11 7V12C11.0001 12.2652 11.1055 12.5195 11.293 12.707L14.293 15.707C14.4816 15.8892 14.7342 15.99 14.9964 15.9877C15.2586 15.9854 15.5094 15.8802 15.6948 15.6948C15.8802 15.5094 15.9854 15.2586 15.9877 14.9964C15.99 14.7342 14.8892 14.4816 14.293 14.293L13 11.586V7C13 6.73478 12.8946 6.48043 12.7071 6.29289C12.5196 6.10536 12.2652 6 12 6Z"
|
||
fill="#ffffff95"
|
||
/>
|
||
</svg>
|
||
)
|
||
|
||
// ========== ИНФОРМАЦИЯ ОБ УПРАЖНЕНИИ ДЛЯ ОТОБРАЖЕНИЯ ==========
|
||
const exerciseSteps = [
|
||
{
|
||
title: "Описание упражнения",
|
||
description: exercise?.desc || "Выполните упражнение согласно инструкции.",
|
||
},
|
||
{
|
||
title: "Продолжительность",
|
||
description: `Время выполнения: ${exercise?.time || 15} минут`,
|
||
},
|
||
{
|
||
title: "Подходы и повторения",
|
||
description: `Выполните ${exercise?.count || 1} подход по ${exercise?.repeats || 12} повторений с отдыхом 60 секунд между подходами.`,
|
||
},
|
||
{
|
||
title: "Позиция в программе",
|
||
description: `Это упражнение №${exercise?.position || 1} в программе дня ${exercise?.day || 1}.`,
|
||
},
|
||
{
|
||
title: "Техника безопасности",
|
||
description: "Следите за правильной техникой выполнения. При появлении боли немедленно прекратите упражнение.",
|
||
},
|
||
]
|
||
|
||
// ========== ЭКРАНЫ ЗАГРУЗКИ И ОШИБОК ==========
|
||
if (loading) {
|
||
return (
|
||
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||
<div className="mt-36 min-h-screen max-w-4xl mx-auto flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#2BACBE] mx-auto mb-4"></div>
|
||
<p className="text-gray-600">Загрузка упражнения...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||
<div className="mt-36 mb-90 min-h-screen max-w-4xl mx-auto">
|
||
<HeaderNav item="Ошибка" text="Не удалось загрузить" />
|
||
<div className="px-6 mt-8">
|
||
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
|
||
<p className="text-red-600 font-medium mb-4">{error}</p>
|
||
<button
|
||
onClick={() => history.goBack()}
|
||
className="bg-[#2BACBE] text-white px-4 py-2 rounded-lg hover:bg-[#2A9FB8] transition-colors"
|
||
>
|
||
Вернуться назад
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!exercise) {
|
||
return (
|
||
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||
<div className="mt-36 mb-90 min-h-screen max-w-4xl mx-auto">
|
||
<HeaderNav item="Упражнение" text="Не найдено" />
|
||
<div className="px-6 mt-8">
|
||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
|
||
<p className="text-gray-600 mb-4">Упражнение не найдено</p>
|
||
<button
|
||
onClick={() => history.goBack()}
|
||
className="bg-[#2BACBE] text-white px-4 py-2 rounded-lg hover:bg-[#2A9FB8] transition-colors"
|
||
>
|
||
Вернуться назад
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ========== ОСНОВНОЙ ИНТЕРФЕЙС УПРАЖНЕНИЯ ==========
|
||
return (
|
||
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||
<div className="mt-36 mb-90 min-h-screen max-w-4xl mx-auto">
|
||
<HeaderNav item={exercise.title} text={`упражнение ${exercise.position} / день ${currentDay}`} />
|
||
|
||
<div className="px-4 sm:px-6 mt-10 mb-6">
|
||
<div className="glass-morphism rounded-3xl overflow-hidden shadow-2xl border border-white/20 backdrop-blur-2xl">
|
||
<div className="relative">
|
||
<img
|
||
src={exercise.url_file_img || "/placeholder.svg?height=300&width=400&text=Упражнение"}
|
||
alt={exercise.title}
|
||
className="w-full h-96 object-cover"
|
||
onError={(e) => {
|
||
const target = e.target as HTMLImageElement
|
||
target.src = "/placeholder.svg?height=300&width=400&text=Упражнение"
|
||
}}
|
||
/>
|
||
<div className="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-black/10"></div>
|
||
|
||
<div className="absolute inset-0 flex items-center justify-center">
|
||
{isCompleted ? (
|
||
<div className="w-20 h-20 bg-cyan-500 rounded-full flex items-center justify-center shadow-2xl">
|
||
<CheckIcon />
|
||
</div>
|
||
) : isResting ? (
|
||
<div className="w-20 h-20 bg-cyan-500 opacity-80 rounded-full flex items-center justify-center shadow-2xl">
|
||
<RestIcon />
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={() => !isCompleted && !isResting && setIsPlaying(!isPlaying)}
|
||
disabled={isCompleted || isResting}
|
||
className={`w-20 h-20 rounded-full flex items-center justify-center shadow-2xl transition-all duration-300 transform hover:scale-110 ${
|
||
isPlaying
|
||
? "bg-white/20 backdrop-blur-xl border border-white/30"
|
||
: "bg-white/30 backdrop-blur-xl border border-white/50"
|
||
} ${isCompleted || isResting ? "opacity-50 cursor-not-allowed" : ""}`}
|
||
>
|
||
{isPlaying ? <PauseIcon /> : <PlayIcon />}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{isCompleted && (
|
||
<div className="absolute top-4 left-4 flex items-center space-x-2">
|
||
<div className="w-3 h-3 bg-cyan-500 rounded-full"></div>
|
||
<span className="text-white text-sm font-bold bg-cyan-600/80 px-3 py-1 rounded-full backdrop-blur-sm">
|
||
Выполнено
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{isResting && (
|
||
<div className="absolute top-4 left-4 flex items-center space-x-2">
|
||
<div className={`w-3 h-3 bg-cyan-500 rounded-full ${!isRestPaused ? "animate-pulse" : ""}`}></div>
|
||
<span className="text-white text-sm font-bold bg-cyan-600/80 px-3 py-1 rounded-full backdrop-blur-sm">
|
||
{isRestPaused ? "Отдых на паузе" : "Отдых"}
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{isPlaying && !isCompleted && !isResting && (
|
||
<div className="absolute top-4 left-4 flex items-center space-x-2">
|
||
<div className="w-3 h-3 bg-orange-500 rounded-full animate-pulse"></div>
|
||
<span className="text-white text-sm font-bold bg-black/30 px-3 py-1 rounded-full backdrop-blur-sm">
|
||
Выполнение
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
<div className="absolute top-4 right-4 bg-cyan-50 backdrop-blur-sm px-3 py-1 rounded-xl">
|
||
<span className="text-gray-800 text-sm font-bold">
|
||
{isResting ? formatTime(restTime) : formatTime(currentTime)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-4 sm:px-6 mb-6">
|
||
<div className="bg-white rounded-2xl p-4 border border-gray-200 shadow-lg">
|
||
<h3 className="text-lg font-bold text-gray-800 mb-3">Прогресс подходов</h3>
|
||
<div className="flex space-x-2">
|
||
{Array.from({ length: totalSets }, (_, index) => {
|
||
const setNumber = index + 1
|
||
const isSetCompleted = completedSets.includes(setNumber)
|
||
const isCurrent = setNumber === currentSet && !isSetCompleted
|
||
|
||
return (
|
||
<div key={setNumber} className="flex-1 text-center">
|
||
<div
|
||
className={`h-3 rounded-full transition-all duration-300 ${
|
||
isSetCompleted ? "bg-cyan-500" : isCurrent ? "bg-cyan-500" : "bg-gray-200"
|
||
}`}
|
||
/>
|
||
<div className="text-xs font-bold mt-1 text-gray-600">{setNumber}</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
<div className="flex justify-between text-xs text-gray-600 mt-2">
|
||
<span>Завершено: {completedSets.length}</span>
|
||
<span>Всего: {totalSets}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-4 sm:px-6 space-y-4 mb-6">
|
||
{exerciseSteps.map((step, index) => (
|
||
<div
|
||
key={index}
|
||
className="bg-white rounded-2xl p-5 border border-gray-200 shadow-lg hover:shadow-xl transition-all duration-300"
|
||
>
|
||
<div className="flex items-start space-x-4">
|
||
<div>
|
||
<h3 className="text-lg font-black text-gray-800 mb-2">{step.title}</h3>
|
||
<p className="text-gray-600 leading-relaxed text-sm sm:text-base">{step.description}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="fixed bottom-36 left-0 right-0 bg-white/95 backdrop-blur-sm border-t border-gray-200 px-4 sm:px-6 py-4 shadow-xl z-30">
|
||
<div className="max-w-md mx-auto">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center space-x-2">
|
||
<div
|
||
className={`w-2 h-2 rounded-full ${
|
||
isCompleted
|
||
? "bg-cyan-400"
|
||
: isResting
|
||
? `bg-cyan-400 ${!isRestPaused ? "animate-pulse" : ""}`
|
||
: "bg-cyan-400 animate-pulse"
|
||
}`}
|
||
></div>
|
||
<span className="text-sm font-bold text-gray-700">
|
||
{isCompleted
|
||
? `Все подходы завершены!`
|
||
: isResting
|
||
? `Отдых перед подходом ${currentSet + 1}${isRestPaused ? " (пауза)" : ""}`
|
||
: `Подход ${currentSet} из ${totalSets}`}
|
||
</span>
|
||
</div>
|
||
<span className="text-sm font-black text-gray-700">
|
||
{isResting ? `${formatTime(restTime)} отдых` : `${formatTime(currentTime)} / ${formatTime(totalTime)}`}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="bg-gray-200 rounded-full h-2 mb-4 overflow-hidden">
|
||
<div
|
||
className={`h-2 rounded-full transition-all duration-1000 shadow-sm ${
|
||
isCompleted
|
||
? "bg-gradient-to-r from-cyan-400 via-cyan-500 to-cyan-600"
|
||
: isResting
|
||
? "bg-gradient-to-r from-cyan-400 via-cyan-500 to-cyan-600"
|
||
: "bg-gradient-to-r from-[#2BACBE] via-cyan-500 to-cyan-700"
|
||
}`}
|
||
style={{ width: `${progress}%` }}
|
||
></div>
|
||
</div>
|
||
|
||
<div className="flex space-x-3">
|
||
{isCompleted ? (
|
||
<>
|
||
<button
|
||
onClick={goToNextExercise}
|
||
className="flex-1 font-bold py-3 px-4 rounded-xl bg-orange-400 hover:bg-yellow-500 hover:scale-105 text-white flex items-center justify-center space-x-2 transition-all duration-300"
|
||
>
|
||
<span>Следующее упражнение</span>
|
||
</button>
|
||
|
||
|
||
<div className="px-4 py-3 bg-cyan-500 text-white font-bold rounded-xl flex items-center justify-center space-x-2">
|
||
<CheckIcon />
|
||
<span>Завершено</span>
|
||
</div>
|
||
</>
|
||
) : isResting ? (
|
||
<>
|
||
<button
|
||
onClick={() => {
|
||
setIsRestPaused(!isRestPaused)
|
||
handlePause(completedSets, currentSet, true, restTime, !isRestPaused)
|
||
}}
|
||
className={`flex-1 font-bold py-3 px-4 rounded-xl transition-all duration-300 flex items-center justify-center space-x-2 ${
|
||
isRestPaused
|
||
? "bg-yellow-500 hover:bg-yellow-600 text-white"
|
||
: "bg-[#2BACBE] hover:bg-[#2099A8] text-white"
|
||
}`}
|
||
>
|
||
{isRestPaused ? (
|
||
<>
|
||
<PlayIcon />
|
||
<span>Продолжить отдых</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<PauseIcon />
|
||
<span>Пауза отдыха</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
console.log("Пропускаем отдых, переходим к следующему подходу")
|
||
setIsResting(false)
|
||
setIsRestPaused(false)
|
||
setCurrentSet(currentSet + 1)
|
||
setCurrentTime(0)
|
||
}}
|
||
className="px-6 py-3 bg-gray-500 hover:bg-gray-600 text-white font-bold rounded-xl transition-all duration-300 flex items-center justify-center"
|
||
>
|
||
Пропустить
|
||
</button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<button
|
||
onClick={() => {
|
||
if (!isPlaying) {
|
||
console.log("Запускаем упражнение")
|
||
setIsPlaying(true)
|
||
} else {
|
||
console.log("Ставим упражнение на паузу")
|
||
handlePause()
|
||
setIsPlaying(false)
|
||
}
|
||
}}
|
||
className={`flex-1 font-bold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 flex items-center justify-center space-x-2 cursor-pointer ${
|
||
isPlaying
|
||
? "bg-yellow-500 hover:bg-yellow-600 text-white shadow-lg"
|
||
: "bg-[#2BACBE] hover:bg-[#2099A8] text-white shadow-lg"
|
||
}`}
|
||
>
|
||
{isPlaying ? (
|
||
<>
|
||
<PauseIcon /> <span>Пауза</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<PlayIcon />
|
||
<span className="backdrop-blur-xl">{hasSavedProgress ? "Продолжить" : "Начать"}</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleCompleteSet}
|
||
className="px-6 py-3 bg-cyan-500 hover:bg-cyan-600 hover:scale-110 text-white font-bold rounded-xl transition-all duration-300 flex items-center justify-center"
|
||
>
|
||
<CheckIcon />
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => {
|
||
console.log("Сбрасываем все состояния упражнения")
|
||
setCurrentTime(0)
|
||
setIsPlaying(false)
|
||
setHasSavedProgress(false)
|
||
setIsCompleted(false)
|
||
setCompletedSets([])
|
||
setCurrentSet(1)
|
||
setIsResting(false)
|
||
setIsRestPaused(false)
|
||
setRestTime(0)
|
||
handleClick()
|
||
const storageKey = currentDay
|
||
? `exerciseProgress_${courseId}_${actualExerciseId}_day_${currentDay}`
|
||
: `exerciseProgress_${courseId}_${actualExerciseId}`
|
||
localStorage.removeItem(storageKey)
|
||
}}
|
||
className="cursor-pointer px-6 py-3 bg-gray-300 hover:bg-gray-200 text-white font-bold rounded-xl transition-all duration-300 hover:shadow-lg hover:scale-110 flex items-center justify-center"
|
||
>
|
||
<div onTransitionEnd={handleTransitionEnd} className="inline-block">
|
||
<RefreshIcon
|
||
className={`transition-transform duration-400 ease-in-out ${isRotating ? "rotate-360" : ""}`}
|
||
/>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<BottomNavigation />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|