385 lines
14 KiB
TypeScript
385 lines
14 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { useHistory, useParams } from "react-router-dom"
|
||
|
||
import BottomNavigation from "../components/BottomNavigation"
|
||
import { getRouteCourseComplete } from "../shared/consts/router"
|
||
import HeaderNav from "../components/HeaderNav"
|
||
import video from "../assets/img/video.mov"
|
||
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
|
||
}
|
||
|
||
export const Exercise = () => {
|
||
const history = useHistory()
|
||
const { courseId, exerciseId } = useParams<RouteParams>()
|
||
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) // Default 15 minutes
|
||
const [currentSet, setCurrentSet] = useState(1)
|
||
const [totalSets, setTotalSets] = useState(3)
|
||
|
||
useEffect(() => {
|
||
console.log("Course ID:", courseId)
|
||
console.log("Exercise ID:", exerciseId)
|
||
|
||
if (!courseId || !exerciseId) {
|
||
setError("ID курса или упражнения не найден")
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
// Получаем данные упражнения через API: GET /pacient/:course_id/:exercise_id
|
||
connect
|
||
.get(`pacient/${courseId}/${exerciseId}`)
|
||
.then((response) => {
|
||
console.log("Response status:", response.status)
|
||
console.log("Response data:", response.data)
|
||
|
||
const exerciseData = response.data
|
||
|
||
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) {
|
||
setTotalTime(Number.parseInt(exerciseData.time) * 60) // Конвертируем минуты в секунды
|
||
}
|
||
if (exerciseData.count) {
|
||
setTotalSets(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, exerciseId])
|
||
|
||
useEffect(() => {
|
||
let interval: NodeJS.Timeout | undefined
|
||
if (isPlaying) {
|
||
interval = setInterval(() => {
|
||
setCurrentTime((prev) => {
|
||
if (prev >= totalTime) {
|
||
setIsPlaying(false)
|
||
// Отправляем результат на сервер при завершении
|
||
submitProgress()
|
||
// Show completion animation
|
||
history.push(getRouteCourseComplete())
|
||
return totalTime
|
||
}
|
||
return prev + 1
|
||
})
|
||
}, 1000)
|
||
}
|
||
return () => {
|
||
if (interval) clearInterval(interval)
|
||
}
|
||
}, [isPlaying, totalTime, history])
|
||
|
||
// Функция для отправки прогресса на сервер
|
||
const submitProgress = async () => {
|
||
if (!courseId || !exerciseId) return
|
||
|
||
try {
|
||
const timeUsers = formatTime(currentTime) // Отправляем время в формате MM:SS
|
||
|
||
await connect.post(`pacient/${courseId}/${exerciseId}`, {
|
||
time_users: timeUsers,
|
||
})
|
||
|
||
console.log("Прогресс отправлен на сервер:", timeUsers)
|
||
} catch (error) {
|
||
console.error("Ошибка при отправке прогресса:", error)
|
||
}
|
||
}
|
||
|
||
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 = (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 RefreshIcon = () => (
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="20"
|
||
height="20"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="#6F6F6F"
|
||
strokeWidth="3"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
>
|
||
<path d="M2.5 2v6h6M21.5 22v-6h-6" />
|
||
<path d="M22 11.5A10 10 0 0 0 3.2 7.2M2 12.5a10 10 0 0 0 18.8 4.2" />
|
||
</svg>
|
||
)
|
||
|
||
const CheckIcon = () => (
|
||
<svg className="w-6 h-6 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||
</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 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 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.day}, позиция ${exercise.position}`} />
|
||
|
||
<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 || video} alt={exercise.title} className="w-full h-120 object-cover" />
|
||
<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">
|
||
<button
|
||
onClick={() => setIsPlaying(!isPlaying)}
|
||
className={
|
||
isPlaying
|
||
? "w-20 h-20 bg-transparent backdrop-blur-xl rounded-full flex items-center justify-center shadow-2xl transition-all duration-300 transform hover:scale-110 border border-cyan-50 opacity-40"
|
||
: "rounded-full w-20 h-20 flex items-center justify-center backdrop-blur-xl opacity-70"
|
||
}
|
||
>
|
||
{isPlaying ? <PauseIcon /> : <PlayIcon />}
|
||
</button>
|
||
</div>
|
||
|
||
{isPlaying && (
|
||
<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">{formatTime(currentTime)}</span>
|
||
</div>
|
||
</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>
|
||
|
||
{/* Fixed Timer at Bottom */}
|
||
<div className="fixed bottom-36 left-0 right-0 bg-white opacity-95 border-t border-white/20 px-4 sm:px-6 py-4 shadow-2xl 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 bg-green-400 rounded-full animate-pulse"></div>
|
||
<span className="text-sm font-bold text-gray-700">
|
||
Подход {currentSet} из {totalSets}
|
||
</span>
|
||
</div>
|
||
<span className="text-sm font-black text-gray-700">
|
||
{formatTime(currentTime)} / {formatTime(totalTime)}
|
||
</span>
|
||
</div>
|
||
<div className="bg-white/30 rounded-full h-3 mb-4 overflow-hidden">
|
||
<div
|
||
className="bg-gradient-to-r from-[#2BACBE] via-cyan-500 to-cyan-700 h-3 rounded-full transition-all duration-1000 shadow-sm"
|
||
style={{ width: `${progress}%` }}
|
||
></div>
|
||
</div>
|
||
<div className="flex space-x-3">
|
||
<button
|
||
onClick={() => setIsPlaying(!isPlaying)}
|
||
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 ${
|
||
isPlaying
|
||
? "bg-gray-400 text-white shadow-lg"
|
||
: "bg-[#2BACBE] hover:bg-[#2099A8] text-white shadow-lg"
|
||
}`}
|
||
>
|
||
{isPlaying ? (
|
||
<>
|
||
<PauseIcon /> <span>Пауза</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<PlayIcon /> <span className="backdrop-blur-xl">Начать</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setCurrentTime(0)
|
||
setIsPlaying(false)
|
||
}}
|
||
className="px-6 py-3 bg-white text-gray-800 font-bold rounded-xl transition-all duration-300 hover:shadow-lg border border-gray-200 flex items-center justify-center"
|
||
>
|
||
<RefreshIcon />
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setCurrentSet((prev) => Math.min(prev + 1, totalSets))
|
||
submitProgress()
|
||
}}
|
||
className="px-6 py-3 bg-white text-gray-800 font-bold rounded-xl transition-all duration-300 hover:shadow-lg border border-gray-200 flex items-center justify-center"
|
||
>
|
||
<CheckIcon />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<BottomNavigation />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|