настроены маршруты перехода с курсов на упражнения - с упражнений на 1 упражнение
This commit is contained in:
parent
0955571cbf
commit
4814d2d310
@ -1,24 +1,23 @@
|
|||||||
import { Switch, Route, Redirect } from "react-router-dom";
|
import { Switch, Route, Redirect } from "react-router-dom"
|
||||||
|
|
||||||
import Welcome from "./pages/Welcome";
|
import Welcome from "./pages/Welcome"
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login"
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home"
|
||||||
import ForgotPasword from "./pages/ForgotPassword";
|
import ForgotPasword from "./pages/ForgotPassword"
|
||||||
import { Courses } from "./pages/Courses";
|
import { Courses } from "./pages/Courses"
|
||||||
import CourseExercises from "./pages/CourseExercises";
|
import CourseExercises from "./pages/CourseExercises"
|
||||||
|
import { Exercise } from "./pages/Exercise"
|
||||||
|
|
||||||
import Settings from "./pages/Settings";
|
import Settings from "./pages/Settings"
|
||||||
import CourseComplete from "./pages/CourseComplete";
|
import CourseComplete from "./pages/CourseComplete"
|
||||||
|
|
||||||
import { getRouteWelcome } from "./shared/consts/router";
|
import { getRouteWelcome } from "./shared/consts/router"
|
||||||
import { getRouteLogin } from "./shared/consts/router";
|
import { getRouteLogin } from "./shared/consts/router"
|
||||||
import { getRouteHome } from "./shared/consts/router";
|
import { getRouteHome } from "./shared/consts/router"
|
||||||
import { getRouteForgotPassword } from "./shared/consts/router";
|
import { getRouteForgotPassword } from "./shared/consts/router"
|
||||||
import { getRouteCourses } from "./shared/consts/router";
|
import { getRouteCourses } from "./shared/consts/router"
|
||||||
|
import { getRouteSettings } from "./shared/consts/router"
|
||||||
|
import { getRouteCourseComplete } from "./shared/consts/router"
|
||||||
import { getRouteSettings } from "./shared/consts/router";
|
|
||||||
import { getRouteCourseComplete } from "./shared/consts/router";
|
|
||||||
|
|
||||||
const AppRoutes = () => (
|
const AppRoutes = () => (
|
||||||
<Switch>
|
<Switch>
|
||||||
@ -30,16 +29,12 @@ const AppRoutes = () => (
|
|||||||
<Route path={getRouteLogin()} component={Login} />
|
<Route path={getRouteLogin()} component={Login} />
|
||||||
<Route path={getRouteHome()} component={Home} />
|
<Route path={getRouteHome()} component={Home} />
|
||||||
<Route path={getRouteForgotPassword()} component={ForgotPasword} />
|
<Route path={getRouteForgotPassword()} component={ForgotPasword} />
|
||||||
<Route path={getRouteCourses()} component={Courses} />
|
<Route exact path={getRouteCourses()} component={Courses} />
|
||||||
|
<Route exact path="/course/:id" component={CourseExercises} />
|
||||||
|
<Route exact path="/course/:courseId/exercise/:exerciseId" component={Exercise} />
|
||||||
<Route path={getRouteSettings()} component={Settings} />
|
<Route path={getRouteSettings()} component={Settings} />
|
||||||
<Route path={getRouteCourseComplete()} component={CourseComplete} />
|
<Route path={getRouteCourseComplete()} component={CourseComplete} />
|
||||||
|
|
||||||
|
|
||||||
<Route path="/course/:id" component={CourseExercises} />
|
|
||||||
|
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default AppRoutes;
|
export default AppRoutes
|
||||||
|
@ -1,41 +1,36 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios"
|
||||||
|
|
||||||
import { AUTH_TOKEN, USER_KEY } from '@/shared/consts/localStorage';
|
|
||||||
import { getRouteLogin } from '@/shared/consts/router';
|
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: 'http://localhost:8093/',
|
baseURL: "http://localhost:8093/",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-type': 'application/json',
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
instance.interceptors.response.use(undefined, async (error) => {
|
instance.interceptors.response.use(undefined, async (error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem(AUTH_TOKEN);
|
localStorage.removeItem("authToken")
|
||||||
localStorage.removeItem(USER_KEY);
|
localStorage.removeItem("token")
|
||||||
|
localStorage.removeItem("userId")
|
||||||
|
localStorage.removeItem("userName")
|
||||||
|
|
||||||
// перехватывает запросы и брал пути из функции
|
window.location.href = "/login"
|
||||||
|
|
||||||
window.location.href = getRouteLogin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error)
|
||||||
});
|
})
|
||||||
|
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = localStorage.getItem(AUTH_TOKEN);
|
const token = localStorage.getItem("authToken") || localStorage.getItem("token")
|
||||||
const c = config;
|
const c = config
|
||||||
|
|
||||||
if (token && config) {
|
if (token && config) {
|
||||||
c.headers.Authorization = `Bearer ${token}`;
|
c.headers.Authorization = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
return config;
|
return config
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error),
|
(error) => Promise.reject(error),
|
||||||
);
|
)
|
||||||
|
|
||||||
export { instance as connect };
|
|
||||||
|
|
||||||
|
|
||||||
|
export { instance as connect }
|
||||||
|
@ -3,104 +3,423 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { useHistory, useParams } from "react-router-dom"
|
import { useHistory, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import HeaderNav from "../components/HeaderNav"
|
import HeaderNav from "../components/HeaderNav"
|
||||||
import BottomNavigation from "../components/BottomNavigation"
|
import BottomNavigation from "../components/BottomNavigation"
|
||||||
|
import { connect } from "../confconnect"
|
||||||
|
import { getRouteExercise } from "../shared/consts/router"
|
||||||
import video from "../assets/img/video.mov"
|
import video from "../assets/img/video.mov"
|
||||||
|
|
||||||
import { connect } from '../confconnect';
|
|
||||||
import { getRouteExercise } from "../shared/consts/router"
|
|
||||||
import type { Exercise } from "./Exercise"
|
|
||||||
|
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
id: string;
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const CourseExercises: React.FC =() => {
|
interface Exercise {
|
||||||
const history = useHistory()
|
id: number
|
||||||
const { id } = useParams<RouteParams>(); // получаем ID курса из URL
|
title: string
|
||||||
const [currentSlide, setCurrentSlide] = useState(0);
|
desc: string
|
||||||
const token = localStorage.getItem('authToken');
|
url_file_img: string
|
||||||
|
url_file: string
|
||||||
|
time: string
|
||||||
|
repeats: number
|
||||||
|
count: number
|
||||||
|
position: number
|
||||||
|
day: number
|
||||||
|
sessionname: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Course {
|
||||||
|
ID: number
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
url_file_img: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExercisesByDay {
|
||||||
|
[day: number]: Exercise[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const CourseExercises: React.FC = () => {
|
||||||
|
const history = useHistory()
|
||||||
|
const { id } = useParams<RouteParams>()
|
||||||
|
|
||||||
|
const [exercises, setExercises] = useState<Exercise[]>([])
|
||||||
|
const [exercisesByDay, setExercisesByDay] = useState<ExercisesByDay>({})
|
||||||
|
const [course, setCourse] = useState<Course | null>(null)
|
||||||
|
const [error, setError] = useState<string>("")
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [currentSlide, setCurrentSlide] = useState(0)
|
||||||
|
const [selectedDay, setSelectedDay] = useState<number>(1)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(token)
|
console.log("Course ID:", id)
|
||||||
|
|
||||||
|
// Проверяем токен перед запросом
|
||||||
|
const token = localStorage.getItem("authToken") || localStorage.getItem("token")
|
||||||
|
console.log("Token exists:", !!token)
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setError('Токен не найден');
|
console.log("No token found, redirecting to login")
|
||||||
return;
|
history.push("/login")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
connect.get(`/pacient/${id}`, {
|
if (!id) {
|
||||||
headers: {
|
setError("ID курса не найден")
|
||||||
Authorization: `Bearer ${token}`,
|
setLoading(false)
|
||||||
},
|
return
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
console.log('Response status:', response.status);
|
|
||||||
|
|
||||||
// Предполагаемая структура:
|
|
||||||
// response.data.courses — массив пользователей
|
|
||||||
const exercises = response.data.exercises || [];
|
|
||||||
|
|
||||||
// Собираем все курсы из всех пользователей
|
|
||||||
const allCourse: Exercises[] = [];
|
|
||||||
|
|
||||||
users.forEach(user => {
|
|
||||||
if (user.Courses && Array.isArray(user.Courses)) {
|
|
||||||
user.Courses.forEach(course => {
|
|
||||||
// Можно добавить проверку или преобразование
|
|
||||||
allCourses.push({
|
|
||||||
ID: course.id, // или course.ID, зависит от структуры
|
|
||||||
title: course.title,
|
|
||||||
desc: course.desc,
|
|
||||||
url_file_img: course.url_file_img,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
setCourses(allCourses);
|
// Получаем все упражнения курса
|
||||||
})
|
const fetchAllExercises = async () => {
|
||||||
.catch(error => {
|
try {
|
||||||
if (error.response) {
|
const allExercises: Exercise[] = []
|
||||||
console.error('Ошибка ответа сервера:', error.response.status, error.response.data);
|
let exerciseId = 1
|
||||||
setError(`Ошибка сервера: ${error.response.status}`);
|
let hasMoreExercises = true
|
||||||
} else if (error.request) {
|
let consecutiveErrors = 0
|
||||||
console.error('Нет ответа от сервера:', error.request);
|
|
||||||
setError('Нет ответа от сервера');
|
// Получаем упражнения по одному, пока не закончатся
|
||||||
|
while (hasMoreExercises && exerciseId <= 20 && consecutiveErrors < 3) {
|
||||||
|
try {
|
||||||
|
console.log(`Fetching exercise ${exerciseId} for course ${id}`)
|
||||||
|
const response = await connect.get(`pacient/${id}/${exerciseId}`)
|
||||||
|
console.log(`Exercise ${exerciseId} response:`, response.data)
|
||||||
|
|
||||||
|
if (response.data && response.data.id) {
|
||||||
|
allExercises.push(response.data)
|
||||||
|
exerciseId++
|
||||||
|
consecutiveErrors = 0 // Сбрасываем счетчик ошибок
|
||||||
} else {
|
} else {
|
||||||
console.error('Ошибка при настройке запроса:', error.message);
|
hasMoreExercises = false
|
||||||
setError(`Ошибка: ${error.message}`);
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(`Error fetching exercise ${exerciseId}:`, error.response?.status)
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
consecutiveErrors++
|
||||||
|
exerciseId++
|
||||||
|
} else if (error.response?.status === 401) {
|
||||||
|
// Токен недействителен
|
||||||
|
localStorage.removeItem("authToken")
|
||||||
|
localStorage.removeItem("token")
|
||||||
|
history.push("/login")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
consecutiveErrors++
|
||||||
|
exerciseId++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
|
console.log("All exercises fetched:", allExercises)
|
||||||
|
|
||||||
|
if (allExercises.length > 0) {
|
||||||
|
// Сортируем упражнения по дням и позиции
|
||||||
|
const sortedExercises = allExercises.sort((a, b) => {
|
||||||
|
if (a.day !== b.day) {
|
||||||
|
return a.day - b.day
|
||||||
|
}
|
||||||
|
return a.position - b.position
|
||||||
|
})
|
||||||
|
|
||||||
|
// Группируем упражнения по дням
|
||||||
|
const groupedByDay: ExercisesByDay = {}
|
||||||
|
sortedExercises.forEach((exercise) => {
|
||||||
|
if (!groupedByDay[exercise.day]) {
|
||||||
|
groupedByDay[exercise.day] = []
|
||||||
|
}
|
||||||
|
groupedByDay[exercise.day].push(exercise)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Exercises grouped by day:", groupedByDay)
|
||||||
|
|
||||||
|
setExercises(sortedExercises)
|
||||||
|
setExercisesByDay(groupedByDay)
|
||||||
|
|
||||||
|
// Устанавливаем первый доступный день
|
||||||
|
const firstDay = Math.min(...Object.keys(groupedByDay).map(Number))
|
||||||
|
setSelectedDay(firstDay)
|
||||||
|
|
||||||
|
// Устанавливаем информацию о курсе
|
||||||
|
setCourse({
|
||||||
|
ID: Number.parseInt(id),
|
||||||
|
title: `Курс ${id}`,
|
||||||
|
desc: "Упражнения для реабилитации",
|
||||||
|
url_file_img: "",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setError("Упражнения не найдены")
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Ошибка при получении упражнений:", error)
|
||||||
|
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem("authToken")
|
||||||
|
localStorage.removeItem("token")
|
||||||
|
history.push("/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAllExercises()
|
||||||
|
}, [id, history])
|
||||||
|
|
||||||
|
// Получаем упражнения для выбранного дня
|
||||||
|
const currentDayExercises = exercisesByDay[selectedDay] || []
|
||||||
|
const currentExercise = currentDayExercises[currentSlide] || exercises[0]
|
||||||
|
|
||||||
|
// Функции для переключения на следующее/предыдущее упражнение
|
||||||
|
const nextExercise = () => {
|
||||||
|
if (currentDayExercises.length > 0) {
|
||||||
|
setCurrentSlide((prev) => (prev + 1) % currentDayExercises.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevExercise = () => {
|
||||||
|
if (currentDayExercises.length > 0) {
|
||||||
|
setCurrentSlide((prev) => (prev - 1 + currentDayExercises.length) % currentDayExercises.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDifficultyColor = (difficulty: string) => {
|
||||||
|
switch (difficulty) {
|
||||||
|
case "Легкий":
|
||||||
|
return "bg-gradient-to-r from-emerald-400 to-green-500"
|
||||||
|
case "Средний":
|
||||||
|
return "bg-gradient-to-r from-amber-400 to-orange-500"
|
||||||
|
case "Сложный":
|
||||||
|
return "bg-gradient-to-r from-red-400 to-pink-500"
|
||||||
|
default:
|
||||||
|
return "bg-gradient-to-r from-cyan-400 to-cyan-600"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||||||
|
<div className="my-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="bg-gray-50 w-full h-full overflow-auto">
|
||||||
<div className="my-36 min-h-screen max-w-4xl mx-auto">
|
<div className="my-36 min-h-screen max-w-4xl mx-auto">
|
||||||
<HeaderNav item={'Курс:'} text={'все упражнения курса'} />
|
<HeaderNav item="Ошибка" text="Не удалось загрузить" />
|
||||||
<div>
|
<div className="px-6 mt-8">
|
||||||
<h1>Курс ID: {id}</h1>
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<p></p>
|
if (!course || exercises.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||||||
|
<div className="my-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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* тут можете использовать id для загрузки данных или другого */}
|
const availableDays = Object.keys(exercisesByDay)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 w-full h-full overflow-auto">
|
||||||
|
<div className="my-36 min-h-screen max-w-4xl mx-auto">
|
||||||
|
<HeaderNav item={course.title} text={course.desc} />
|
||||||
|
|
||||||
|
{/* Day Selector */}
|
||||||
|
{availableDays.length > 1 && (
|
||||||
|
<div className="px-4 sm:px-6 mb-6">
|
||||||
|
<div className="flex space-x-2 overflow-x-auto pb-2">
|
||||||
|
{availableDays.map((day) => (
|
||||||
|
<button
|
||||||
|
key={day}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDay(day)
|
||||||
|
setCurrentSlide(0)
|
||||||
|
}}
|
||||||
|
className={`flex-shrink-0 px-4 py-2 rounded-full text-sm font-semibold transition-all duration-300 ${
|
||||||
|
selectedDay === day
|
||||||
|
? "bg-[#2BACBE] text-white shadow-lg"
|
||||||
|
: "bg-white text-gray-600 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
День {day}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Exercise Slider */}
|
||||||
|
{currentDayExercises.length > 0 && currentExercise && (
|
||||||
|
<div className="px-4 sm:px-6 mt-34 mb-10">
|
||||||
|
<div className="glass-morphism rounded-3xl border border-white/50 shadow-2xl overflow-hidden backdrop-blur-2xl relative">
|
||||||
|
{/* Exercise Image */}
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={currentExercise.url_file_img || video}
|
||||||
|
alt={currentExercise.title}
|
||||||
|
className="w-full h-120 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-black/10"></div>
|
||||||
|
|
||||||
|
{/* Time Badge */}
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<div
|
||||||
|
className={`px-4 py-2 rounded-full text-xs font-bold text-white shadow-lg backdrop-blur-sm ${getDifficultyColor("Средний")}`}
|
||||||
|
>
|
||||||
|
{currentExercise.time} мин
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation arrows */}
|
||||||
|
{currentDayExercises.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={prevExercise}
|
||||||
|
className="z-50 absolute left-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/20 backdrop-blur-xl rounded-full flex items-center justify-center text-white hover:bg-black/30 transition-all duration-300 shadow-lg border border-white/20"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextExercise}
|
||||||
|
className="z-50 absolute right-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/20 backdrop-blur-xl rounded-full flex items-center justify-center text-white hover:bg-black/30 transition-all duration-300 shadow-lg border border-white/20"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Exercise Info */}
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-xl font-semibold text-gray-600 mb-3">{currentExercise.title}</h3>
|
||||||
|
<p className="text-gray-500 mb-4">{currentExercise.desc}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => history.push(getRouteExercise(id!, currentExercise.id.toString()))}
|
||||||
|
className="w-full bg-gradient-to-r from-orange-400 to-orange-500 text-white font-semibold py-4 px-6 rounded-2xl hover:shadow-2xl transition-all duration-300 transform hover:scale-105 shadow-lg backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
Начать упражнение
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slide indicators */}
|
||||||
|
{currentDayExercises.length > 1 && (
|
||||||
|
<div className="flex justify-center mt-6 space-x-2">
|
||||||
|
{currentDayExercises.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setCurrentSlide(index)}
|
||||||
|
className={`h-4 rounded-full transition-all duration-300 ${
|
||||||
|
index === currentSlide
|
||||||
|
? "bg-gradient-to-r from-[#2BACBE] to-cyan-600 w-8 shadow-lg"
|
||||||
|
: "bg-gray-300 w-4 hover:bg-gray-400"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Exercise List for Selected Day */}
|
||||||
|
<div className="px-4 sm:px-6 mb-50">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-semibold text-gray-800 mb-6">
|
||||||
|
{availableDays.length > 1 ? `Упражнения на день ${selectedDay}` : "Все упражнения курса"}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentDayExercises.map((exercise, index) => (
|
||||||
|
<div
|
||||||
|
key={exercise.id}
|
||||||
|
onClick={() => history.push(getRouteExercise(id!, exercise.id.toString()))}
|
||||||
|
className={`glass-morphism rounded-2xl p-4 sm:p-6 border border-white/50 shadow-lg cursor-pointer transition-all duration-300 hover:shadow-2xl transform hover:scale-[1.02] backdrop-blur-xl ${
|
||||||
|
index === currentSlide ? "ring-2 ring-[#2BACBE] bg-cyan-50/20" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-14 h-14 sm:w-16 sm:h-16 border-2 border-cyan-500 rounded-2xl flex items-center justify-center text-cyan-500 font-semibold text-lg sm:text-xl shadow-xl">
|
||||||
|
{exercise.position}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-gray-800 text-lg sm:text-xl truncate">{exercise.title}</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-2 line-clamp-2">{exercise.desc}</p>
|
||||||
|
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
||||||
|
<span>{exercise.time} мин</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{exercise.count} подходов</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{exercise.repeats} повторений</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1">
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BottomNavigation />
|
<BottomNavigation />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default CourseExercises
|
||||||
export default CourseExercises;
|
|
||||||
|
240
src/pages/Exercise old.tsx
Normal file
240
src/pages/Exercise old.tsx
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useHistory } 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"
|
||||||
|
|
||||||
|
|
||||||
|
export interface Exercise {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
url_file: string;
|
||||||
|
url_file_img: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const Exercise = () => {
|
||||||
|
const history = useHistory()
|
||||||
|
// const { id } = useParams<{ id: string }>()
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
|
const [totalTime] = useState(900) // 15 minutes in seconds
|
||||||
|
const [currentSet, setCurrentSet] = useState(1)
|
||||||
|
const [totalSets] = useState(3)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout | undefined
|
||||||
|
if (isPlaying) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setCurrentTime((prev) => {
|
||||||
|
if (prev >= totalTime) {
|
||||||
|
setIsPlaying(false)
|
||||||
|
// Show completion animation
|
||||||
|
history.push(getRouteCourseComplete())
|
||||||
|
return totalTime
|
||||||
|
}
|
||||||
|
return prev + 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (interval) clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [isPlaying, totalTime, history])
|
||||||
|
|
||||||
|
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" stroke-width="3" stroke-linecap="round" stroke-linejoin="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:
|
||||||
|
"Лягте на спирку на коврик. Руки вдоль тела, ладони прижаты к полу. Ноги выпрямлены, носки направлены вверх. Поясница плотно прижата к полу.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Задание",
|
||||||
|
description:
|
||||||
|
"Медленно поднимите прямые ноги до угла 90 градусов. Задержитесь на 2 секунды, затем медленно опустите ноги, не касаясь пола. Повторите движение плавно и контролируемо.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Подходы",
|
||||||
|
description: "Выполните 3 подхода по 12 повторений с отдыхом 60 секунд между подходами.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Перерыв",
|
||||||
|
description: "Отдыхайте 60 секунд между подходами. Дышите спокойно и расслабьте мышцы.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Динамика и статика",
|
||||||
|
description:
|
||||||
|
"Динамическая фаза: подъем и опускание ног выполняется плавно, 2 секунды вверх, 2 секунды вниз. Статическая фаза: удержание ног в верхней точке на 2 секунды.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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-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">
|
||||||
|
<video
|
||||||
|
src={video}
|
||||||
|
className="w-full h-120 object-cover"
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
/>
|
||||||
|
<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))}
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,34 +1,105 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { useHistory } from "react-router-dom"
|
import { useHistory, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import BottomNavigation from "../components/BottomNavigation"
|
import BottomNavigation from "../components/BottomNavigation"
|
||||||
import { getRouteCourseComplete } from "../shared/consts/router"
|
import { getRouteCourseComplete } from "../shared/consts/router"
|
||||||
import HeaderNav from "../components/HeaderNav"
|
import HeaderNav from "../components/HeaderNav"
|
||||||
import video from "../assets/img/video.mov"
|
import video from "../assets/img/video.mov"
|
||||||
|
import { connect } from "../confconnect"
|
||||||
|
|
||||||
export interface Exercise {
|
export interface Exercise {
|
||||||
id: number;
|
id: number
|
||||||
title: string;
|
title: string
|
||||||
desc: string;
|
desc: string
|
||||||
url_file: string;
|
url_file: string
|
||||||
url_file_img: 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 = () => {
|
export const Exercise = () => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
// const { id } = useParams<{ id: string }>()
|
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 [isPlaying, setIsPlaying] = useState(false)
|
||||||
const [currentTime, setCurrentTime] = useState(0)
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
const [totalTime] = useState(900) // 15 minutes in seconds
|
const [totalTime, setTotalTime] = useState(900) // Default 15 minutes
|
||||||
const [currentSet, setCurrentSet] = useState(1)
|
const [currentSet, setCurrentSet] = useState(1)
|
||||||
const [totalSets] = useState(3)
|
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(() => {
|
useEffect(() => {
|
||||||
let interval: NodeJS.Timeout | undefined
|
let interval: NodeJS.Timeout | undefined
|
||||||
@ -37,6 +108,8 @@ export const Exercise = () => {
|
|||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
if (prev >= totalTime) {
|
if (prev >= totalTime) {
|
||||||
setIsPlaying(false)
|
setIsPlaying(false)
|
||||||
|
// Отправляем результат на сервер при завершении
|
||||||
|
submitProgress()
|
||||||
// Show completion animation
|
// Show completion animation
|
||||||
history.push(getRouteCourseComplete())
|
history.push(getRouteCourseComplete())
|
||||||
return totalTime
|
return totalTime
|
||||||
@ -50,6 +123,23 @@ export const Exercise = () => {
|
|||||||
}
|
}
|
||||||
}, [isPlaying, totalTime, history])
|
}, [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 formatTime = (seconds: number) => {
|
||||||
const mins = Math.floor(seconds / 60)
|
const mins = Math.floor(seconds / 60)
|
||||||
const secs = seconds % 60
|
const secs = seconds % 60
|
||||||
@ -58,8 +148,6 @@ export const Exercise = () => {
|
|||||||
|
|
||||||
const progress = (currentTime / totalTime) * 100
|
const progress = (currentTime / totalTime) * 100
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const PlayIcon = () => (
|
const PlayIcon = () => (
|
||||||
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
|
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M8 5v14l11-7z" />
|
<path d="M8 5v14l11-7z" />
|
||||||
@ -73,7 +161,20 @@ export const Exercise = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const RefreshIcon = () => (
|
const RefreshIcon = () => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#6F6F6F" stroke-width="3" stroke-linecap="round" stroke-linejoin="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>
|
<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 = () => (
|
const CheckIcon = () => (
|
||||||
@ -82,52 +183,94 @@ export const Exercise = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Генерируем инструкции на основе реальных данных упражнения
|
||||||
const exerciseSteps = [
|
const exerciseSteps = [
|
||||||
{
|
{
|
||||||
title: "Исходное положение",
|
title: "Описание упражнения",
|
||||||
description:
|
description: exercise?.desc || "Выполните упражнение согласно инструкции.",
|
||||||
"Лягте на спирку на коврик. Руки вдоль тела, ладони прижаты к полу. Ноги выпрямлены, носки направлены вверх. Поясница плотно прижата к полу.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Задание",
|
title: "Продолжительность",
|
||||||
description:
|
description: `Время выполнения: ${exercise?.time || 15} минут`,
|
||||||
"Медленно поднимите прямые ноги до угла 90 градусов. Задержитесь на 2 секунды, затем медленно опустите ноги, не касаясь пола. Повторите движение плавно и контролируемо.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Подходы",
|
title: "Подходы и повторения",
|
||||||
description: "Выполните 3 подхода по 12 повторений с отдыхом 60 секунд между подходами.",
|
description: `Выполните ${exercise?.count || 1} подход по ${exercise?.repeats || 12} повторений с отдыхом 60 секунд между подходами.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Перерыв",
|
title: "Позиция в программе",
|
||||||
description: "Отдыхайте 60 секунд между подходами. Дышите спокойно и расслабьте мышцы.",
|
description: `Это упражнение №${exercise?.position || 1} в программе дня ${exercise?.day || 1}.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Динамика и статика",
|
title: "Техника безопасности",
|
||||||
description:
|
description: "Следите за правильной техникой выполнения. При появлении боли немедленно прекратите упражнение.",
|
||||||
"Динамическая фаза: подъем и опускание ног выполняется плавно, 2 секунды вверх, 2 секунды вниз. Статическая фаза: удержание ног в верхней точке на 2 секунды.",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
|
|
||||||
<div className="bg-gray-50 w-full h-full overflow-auto">
|
<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">
|
<div className="mt-36 mb-90 min-h-screen max-w-4xl mx-auto">
|
||||||
|
<HeaderNav item={exercise.title} text={`День ${exercise.day}, позиция ${exercise.position}`} />
|
||||||
<HeaderNav item='Надо разобраться?' text='упражнение' />
|
|
||||||
|
|
||||||
|
|
||||||
<div className="px-4 sm:px-6 mt-10 mb-6">
|
<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="glass-morphism rounded-3xl overflow-hidden shadow-2xl border border-white/20 backdrop-blur-2xl">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<video
|
<img src={exercise.url_file_img || video} alt={exercise.title} className="w-full h-120 object-cover" />
|
||||||
src={video}
|
|
||||||
className="w-full h-120 object-cover"
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-black/10"></div>
|
<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">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<button
|
<button
|
||||||
@ -144,7 +287,7 @@ export const Exercise = () => {
|
|||||||
|
|
||||||
{isPlaying && (
|
{isPlaying && (
|
||||||
<div className="absolute top-4 left-4 flex items-center space-x-2">
|
<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>
|
<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 className="text-white text-sm font-bold bg-black/30 px-3 py-1 rounded-full backdrop-blur-sm">
|
||||||
Выполнение
|
Выполнение
|
||||||
</span>
|
</span>
|
||||||
@ -165,7 +308,6 @@ export const Exercise = () => {
|
|||||||
className="bg-white rounded-2xl p-5 border border-gray-200 shadow-lg hover:shadow-xl transition-all duration-300"
|
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 className="flex items-start space-x-4">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-black text-gray-800 mb-2">{step.title}</h3>
|
<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>
|
<p className="text-gray-600 leading-relaxed text-sm sm:text-base">{step.description}</p>
|
||||||
@ -198,7 +340,8 @@ export const Exercise = () => {
|
|||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsPlaying(!isPlaying)}
|
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
|
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-gray-400 text-white shadow-lg"
|
||||||
: "bg-[#2BACBE] hover:bg-[#2099A8] text-white shadow-lg"
|
: "bg-[#2BACBE] hover:bg-[#2099A8] text-white shadow-lg"
|
||||||
}`}
|
}`}
|
||||||
@ -223,7 +366,10 @@ export const Exercise = () => {
|
|||||||
<RefreshIcon />
|
<RefreshIcon />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentSet((prev) => Math.min(prev + 1, totalSets))}
|
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"
|
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 />
|
<CheckIcon />
|
||||||
@ -236,5 +382,3 @@ export const Exercise = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export const USER_KEY = 'user';
|
export const AUTH_TOKEN = "authToken"
|
||||||
export const AUTH_TOKEN = 'token';
|
export const USER_KEY = "userId"
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
export const getRouteWelcome = () => `/welcome`;
|
export const getRouteWelcome = () => `/welcome`
|
||||||
export const getRouteLogin = () => `/login`;
|
export const getRouteLogin = () => `/login`
|
||||||
export const getRouteHome = () => `/home`;
|
export const getRouteHome = () => `/home`
|
||||||
export const getRouteForgotPassword = () => `/forgot-password`;
|
export const getRouteForgotPassword = () => `/forgot-password`
|
||||||
|
export const getRouteCourses = () => `/courses`
|
||||||
export const getRouteCourses = () => `/courses`;
|
export const getRouteCourseExercises = (id: number | string) => `/course/${id}`
|
||||||
export const getRouteCourseExercises = (id: number | string) => `/course/${id}`;
|
export const getRouteExercise = (courseId: number | string, exerciseId: number | string) =>
|
||||||
export const getRouteExercise = (id: number | string) => `/exercise/${id}`;
|
`/course/${courseId}/exercise/${exerciseId}`
|
||||||
|
export const getRouteSettings = () => `/settings`
|
||||||
export const getRouteSettings = () => `/settings`;
|
export const getRouteCourseComplete = () => `/course-complete`
|
||||||
export const getRouteCourseComplete = () => `/course-complete`;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user