корректное отображение времени в секундах

This commit is contained in:
Tatyana 2025-09-08 14:46:31 +03:00
parent a8b87fbade
commit df8cc1596c
8 changed files with 551 additions and 313 deletions

View File

@ -6,22 +6,26 @@ interface CircularProgressDisplayProps {
overallProgress: number overallProgress: number
totalCourses: number totalCourses: number
totalExercises: number totalExercises: number
completedCourses?: number
completedExercises?: number
} }
const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
overallProgress, overallProgress,
totalCourses, totalCourses,
totalExercises, totalExercises,
completedCourses = 0,
completedExercises = 0,
}) => { }) => {
const radius = 40 const radius = 40
const circumference = 2 * Math.PI * radius const circumference = 2 * Math.PI * radius
// For the "Courses" ring (cyan) // For the "Courses" ring (orange) - shows completed courses progress
const coursesProgress = (totalCourses / 5) * 100 // Assuming max 5 courses for visual representation const coursesProgress = totalCourses > 0 ? (completedCourses / totalCourses) * 100 : 0
const coursesStrokeDashoffset = circumference - (coursesProgress / 100) * circumference const coursesStrokeDashoffset = circumference - (coursesProgress / 100) * circumference
// For the "Exercises" ring (green) // For the "Exercises" ring (cyan) - shows completed exercises progress
const exercisesProgress = (totalExercises / 50) * 100 // Assuming max 50 exercises for visual representation const exercisesProgress = totalExercises > 0 ? (completedExercises / totalExercises) * 100 : 0
const exercisesStrokeDashoffset = circumference - (exercisesProgress / 100) * circumference const exercisesStrokeDashoffset = circumference - (exercisesProgress / 100) * circumference
return ( return (
@ -38,7 +42,6 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
cy="50" cy="50"
/> />
<circle <circle
className="text-orange-400" className="text-orange-400"
strokeWidth="8" strokeWidth="8"
@ -47,13 +50,13 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
strokeLinecap="round" strokeLinecap="round"
stroke="currentColor" stroke="currentColor"
fill="transparent" fill="transparent"
r={radius} r={radius - 10} // Slightly smaller radius for the inner ring
cx="50" cx="50"
cy="50" cy="50"
transform="rotate(-90 50 50)" transform="rotate(-90 50 50)"
/> />
<circle <circle
className="text-cyan-500" className="text-cyan-500"
strokeWidth="8" strokeWidth="8"
@ -62,7 +65,7 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
strokeLinecap="round" strokeLinecap="round"
stroke="currentColor" stroke="currentColor"
fill="transparent" fill="transparent"
r={radius - 10} // Slightly smaller radius for the inner ring r={radius}
cx="50" cx="50"
cy="50" cy="50"
transform="rotate(-90 50 50)" transform="rotate(-90 50 50)"
@ -70,7 +73,6 @@ const CircularProgressDisplay: React.FC<CircularProgressDisplayProps> = ({
</svg> </svg>
<div className="absolute text-center"> <div className="absolute text-center">
<div className="text-3xl font-black text-gray-600">{overallProgress}%</div> <div className="text-3xl font-black text-gray-600">{overallProgress}%</div>
</div> </div>
</div> </div>
) )

View File

@ -4,14 +4,13 @@
// Если он взаимодействует с браузерными API или DOM. // Если он взаимодействует с браузерными API или DOM.
// Если он содержит обработчики событий или управляет состоянием. // Если он содержит обработчики событий или управляет состоянием.
import type React from "react" import type React from "react"
interface StatCardProps { interface StatCardProps {
title: string title: string
subtitle: string subtitle: string
icon: React.ComponentType<{ size?: number; fill?: string; className?: string; style?: React.CSSProperties }> icon: React.ComponentType<{ size?: number; fill?: string; className?: string; style?: React.CSSProperties }>
fill:string fill: string
onClick: () => void onClick: () => void
} }
@ -26,33 +25,18 @@ interface StatCardProps {
// className?: string — необязственный CSS-класс. // className?: string — необязственный CSS-класс.
// style?: React.CSSProperties — необязательные inline-стили. // style?: React.CSSProperties — необязательные inline-стили.
export const StatCardHome: React.FC<StatCardProps> = ({ title, subtitle, icon: Icon, fill, onClick }) => {
export const StatCardHome: React.FC<StatCardProps> = ({
title,
subtitle,
icon: Icon,
fill,
onClick,
}) => {
return ( return (
<div <div
onClick={onClick} onClick={onClick}
className="glass-morphism rounded-3xl text-left border border-white/50 shadow-lg backdrop-blur-xl p-6 text-white transition-transform transform hover:scale-105 duration-300 overflow-hidden cursor-pointer" className="glass-morphism rounded-3xl text-left border border-white/50 shadow-lg backdrop-blur-xl p-6 text-white transition-transform transform hover:scale-105 duration-300 overflow-hidden cursor-pointer"
> >
{/* Фоновая иконка */} {/* Фоновая иконка */}
<Icon <Icon size={180} fill={"#E6E5E5"} className="absolute -right-8 -top-8 -rotate-45" />
size={180}
fill={"#E6E5E5"}
className="absolute -right-8 -top-8 -rotate-45"
/>
{/* Основная иконка */} {/* Основная иконка */}
<div className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center shadow-xl mb-4 z-20 relative"> <div className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center shadow-xl mb-4 z-20 relative">
<Icon size={40} <Icon size={40} fill={fill} />
fill={fill}
/>
</div> </div>
{/* Текст */} {/* Текст */}

View File

@ -2,14 +2,39 @@
import type React from "react" import type React from "react"
interface CurrentExercise {
courseId: string
exerciseId: string
exerciseIndex: number
day: number
title: string
position: number
isCompleted: boolean
}
interface WorkoutCardProps { interface WorkoutCardProps {
onBackClick: () => void onBackClick: () => void
onCardClick: () => void onCardClick: () => void
currentExercise?: CurrentExercise | null
loading?: boolean
} }
// Это означает, что компонент ожидает получить пропс onBackClick, который должен быть функцией, вызываемой при определенном событии (например, при нажатии кнопки "Назад"). export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCardClick, currentExercise, loading }) => {
const getExerciseStatus = () => {
if (loading) return "Загрузка..."
if (!currentExercise) return "Нет активных упражнений"
if (currentExercise.isCompleted) return "Все упражнения завершены"
return "В процессе"
}
const getExerciseDescription = () => {
if (loading) return "Поиск текущего упражнения..."
if (!currentExercise) return "Начните новый курс"
if (currentExercise.isCompleted) return "Отличная работа!"
return `Курс ${currentExercise.courseId} / День ${currentExercise.day}`
}
export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCardClick }) => {
return ( return (
<div <div
onClick={onCardClick} onClick={onCardClick}
@ -51,7 +76,11 @@ export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCar
</svg> </svg>
</div> </div>
{/* Пульсирующая точка */} {/* Пульсирующая точка */}
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-white rounded-full shadow-lg animate-pulse flex items-center justify-center"> <div
className={`absolute -bottom-1 -right-1 w-6 h-6 bg-white rounded-full shadow-lg flex items-center justify-center ${
loading ? "animate-pulse" : currentExercise && !currentExercise.isCompleted ? "animate-pulse" : ""
}`}
>
<svg className="w-3 h-3 text-[#2BACBE]" fill="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3 text-[#2BACBE]" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" /> <path d="M8 5v14l11-7z" />
</svg> </svg>
@ -60,8 +89,8 @@ export const WorkoutCardHome: React.FC<WorkoutCardProps> = ({ onBackClick, onCar
{/* Информация о упражнении */} {/* Информация о упражнении */}
<div className="flex flex-col justify-center flex-grow"> <div className="flex flex-col justify-center flex-grow">
<h3 className="font-extrabold text-lg mb-2">В процессе</h3> <h3 className="font-extrabold text-lg mb-2">{getExerciseStatus()}</h3>
<p className="text-white/70 font-medium mb-4 text-base">Текущее упражнение</p> <p className="text-white/70 font-medium mb-4 text-base">{getExerciseDescription()}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@
@font-face { @font-face {
font-family: 'Poppins'; font-family: 'Poppins';
src: url('../src/assets/fonts/Poppins-Semibold.woff2') format('woff2'); src: url('../src/assets/fonts/Poppins-SemiBold.woff2') format('woff2');
font-weight: 600; font-weight: 600;
font-style: normal; font-style: normal;
} }

View File

@ -1,100 +1,139 @@
"use client" "use client"
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react"
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom"
import BottomNavigation from "../components/BottomNavigation"; import BottomNavigation from "../components/BottomNavigation"
import HeaderNav from "../components/HeaderNav"; import HeaderNav from "../components/HeaderNav"
import { connect } from '../confconnect'; import { connect } from "../confconnect"
import { getRouteCourseExercises } from '../shared/consts/router'; import { getRouteCourseExercises } from "../shared/consts/router"
import type { CourseExercises } from './CourseExercises'; import type { CourseExercises } from "./CourseExercises"
export interface Course { export interface Course {
ID: number; ID: number
title: string; title: string
desc: string; desc: string
url_file_img: string; url_file_img: string
course_exercises: CourseExercises; course_exercises: CourseExercises
} }
interface ResponseData { interface ResponseData {
courses: User[]; courses: User[]
course_exercises: CourseExercises; course_exercises: CourseExercises
} }
interface User { interface User {
id: number; id: number
name: string; name: string
Courses?: Course[]; Courses?: Course[]
} }
const ProgressLine = ({ progress }: { progress: number }) => {
const ProgressLine = () => {
return ( return (
<div <div
className="h-full bg-gradient-to-r from-orange-300 to-orange-500 transition-all duration-500 absolute left-0 top-0 rounded-l-2xl" className="h-full bg-gradient-to-r from-orange-300 to-orange-500 transition-all duration-500 absolute left-0 top-0 rounded-l-2xl"
style={{ width: '85%' }} style={{ width: `${progress}%` }}
/> />
); )
} }
const AnalitcsCards = () => { const AnalitcsCards = ({ overallProgress }: { overallProgress: number }) => {
return ( return (
<div className="px-6 mb-8" > <div className="px-6 mb-8">
<div className="text-center relative "> <div className="text-center relative ">
<div className="w-full h-10 bg-gray-500 rounded-2xl relative flex items-center justify-center mb-4 shadow-xl"> <div className="w-full h-10 bg-gray-500 rounded-2xl relative flex items-center justify-center mb-4 shadow-xl">
{/* Прогрессная часть */} {/* Прогрессная часть */}
<ProgressLine /> <ProgressLine progress={overallProgress} />
{/* Текст поверх линии */} {/* Текст поверх линии */}
<div className="absolute text-white font-semibold text-sm sm:text-base">Вы прошли реабилитацию на {85}%</div> <div className="absolute text-white font-semibold text-sm sm:text-base">
Вы прошли реабилитацию на {overallProgress}%
</div>
</div> </div>
</div> </div>
</div> </div>
) )
} }
const calculateCourseProgress = async (course: Course, token: string): Promise<number> => {
try {
const response = await connect.get(`/pacient/${course.ID}`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
const exercises = response.data.course_exercises || []
if (exercises.length === 0) return 0
let completedExercises = 0
for (const exercise of exercises) {
const storageKey = `exerciseProgress_${course.ID}_${exercise.id_exercise}_day_${exercise.day}`
const savedProgress = localStorage.getItem(storageKey)
if (savedProgress) {
const progress = JSON.parse(savedProgress)
const isCompleted =
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
if (isCompleted) {
completedExercises++
}
}
}
return Math.round((completedExercises / exercises.length) * 100)
} catch (error) {
console.error(`Ошибка при расчете прогресса курса ${course.ID}:`, error)
return 0
}
}
// НАЧАЛО КОМПОНЕНТА // НАЧАЛО КОМПОНЕНТА
export const Courses = () => { export const Courses = () => {
const history = useHistory(); const history = useHistory()
const [courses, setCourses] = useState<Course[]>([]); const [courses, setCourses] = useState<Course[]>([])
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>("")
const token = localStorage.getItem('authToken'); const token = localStorage.getItem("authToken")
const [, setExercises] = useState<CourseExercises[]>([]); const [, setExercises] = useState<CourseExercises[]>([])
const [courseProgress, setCourseProgress] = useState<{ [key: number]: number }>({})
const [overallProgress, setOverallProgress] = useState<number>(0)
useEffect(() => { useEffect(() => {
console.log(token) console.log(token)
if (!token) { if (!token) {
setError('Токен не найден'); setError("Токен не найден")
return; return
} }
connect.get('/pacient/courses', { connect
headers: { .get("/pacient/courses", {
Authorization: `Bearer ${token}`, headers: {
}, Authorization: `Bearer ${token}`,
}) },
})
.then((response: { data: ResponseData }) => { .then((response: { data: ResponseData }) => {
console.log('Response status:', response.data); console.log("Response status:", response.data)
setExercises(response.data.courses.course_exercises); setExercises(response.data.courses.course_exercises)
// Предполагаемая структура: // Предполагаемая структура:
// response.data.courses — массив пользователей // response.data.courses — массив пользователей
const users = response.data.courses || []; const users = response.data.courses || []
// Собираем все курсы из всех пользователей // Собираем все курсы из всех пользователей
const allCourses: Course[] = []; const allCourses: Course[] = []
users.forEach(user => { users.forEach((user) => {
if (user.Courses && Array.isArray(user.Courses)) { if (user.Courses && Array.isArray(user.Courses)) {
user.Courses.forEach(course => { user.Courses.forEach((course) => {
// Можно добавить проверку или преобразование // Можно добавить проверку или преобразование
allCourses.push({ allCourses.push({
ID: course.id, // или course.ID, зависит от структуры ID: course.id, // или course.ID, зависит от структуры
@ -102,46 +141,56 @@ export const Courses = () => {
desc: course.desc, desc: course.desc,
url_file_img: course.url_file_img, url_file_img: course.url_file_img,
course_exercises: course.course_exercises, course_exercises: course.course_exercises,
}); })
}); })
} }
}); })
setCourses(allCourses)
setCourses(allCourses);
}) })
.catch(error => { .catch((error) => {
if (error.response) { if (error.response) {
console.error('Ошибка ответа сервера:', error.response.status, error.response.data); console.error("Ошибка ответа сервера:", error.response.status, error.response.data)
setError(`Ошибка сервера: ${error.response.status}`); setError(`Ошибка сервера: ${error.response.status}`)
} else if (error.request) { } else if (error.request) {
console.error('Нет ответа от сервера:', error.request); console.error("Нет ответа от сервера:", error.request)
setError('Нет ответа от сервера'); setError("Нет ответа от сервера")
} else { } else {
console.error('Ошибка при настройке запроса:', error.message); console.error("Ошибка при настройке запроса:", error.message)
setError(`Ошибка: ${error.message}`); setError(`Ошибка: ${error.message}`)
} }
}); })
}, [token]); }, [token])
useEffect(() => {
if (courses.length > 0 && token) {
const calculateAllProgress = async () => {
const progressMap: { [key: number]: number } = {}
let totalProgress = 0
for (const course of courses) {
const progress = await calculateCourseProgress(course, token)
progressMap[course.ID] = progress
totalProgress += progress
}
// Генерируем случайный прогресс для каждого курса setCourseProgress(progressMap)
const getRandomProgress = () => Math.floor(Math.random() * 100); setOverallProgress(Math.round(totalProgress / courses.length))
}
calculateAllProgress()
}
}, [courses, token])
// Цвета для прогресс-баров в оттенках cyan // Цвета для прогресс-баров в оттенках cyan
const progressColors = [ const progressColors = ["from-cyan-600 to-cyan-900"]
"from-cyan-600 to-cyan-900",
];
//item.exercise.title //item.exercise.title
return ( return (
<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="все назначенные" />
<AnalitcsCards /> <AnalitcsCards overallProgress={overallProgress} />
<div className="px-6 mb-8"> <div className="px-6 mb-8">
{error && ( {error && (
@ -157,83 +206,79 @@ export const Courses = () => {
</div> </div>
{/* Выводим список курсов */} {/* Выводим список курсов */}
<div className='space-y-4'> <div className="space-y-4">
{courses.length > 0 ? ( {courses.length > 0
courses.map((course, index) => { ? courses.map((course, index) => {
const progress = getRandomProgress(); const progress = courseProgress[course.ID] || 0
const colorClass = progressColors[index % progressColors.length]; const colorClass = progressColors[index % progressColors.length]
return ( return (
<div <div
key={course.ID} key={course.ID}
onClick={() => history.push(getRouteCourseExercises(course.ID.toString()), { course })} onClick={() => history.push(getRouteCourseExercises(course.ID.toString()), { course })}
className="bg-white/30 backdrop-blur-2xl rounded-3xl p-6 border border-white/20 shadow-xl cursor-pointer hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02]" className="bg-white/30 backdrop-blur-2xl rounded-3xl p-6 border border-white/20 shadow-xl cursor-pointer hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02]"
> >
<div className="flex flex-col sm:flex-row sm:content-center space-x-5 space-y-2 sm:space-y-0"> <div className="flex flex-col sm:flex-row sm:content-center space-x-5 space-y-2 sm:space-y-0">
{/* Изображение курса */} {/* Изображение курса */}
{course.url_file_img && ( {course.url_file_img && (
<div className="h-10 w-10 md:h-20 md:w-20 rounded-xl sm:rounded-2xl overflow-hidden flex-shrink-0"> <div className="h-10 w-10 md:h-20 md:w-20 rounded-xl sm:rounded-2xl overflow-hidden flex-shrink-0">
<img <img
src={course.url_file_img || "/placeholder.svg"} src={course.url_file_img || "/placeholder.svg"}
alt={course.title} alt={course.title}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => { onError={(e) => {
e.currentTarget.src = "/placeholder.svg?height=64&width=64&text=Course" e.currentTarget.src = "/placeholder.svg?height=64&width=64&text=Course"
}} }}
/> />
</div> </div>
)}
<div className="flex-1">
<h3 className="font-semibold text-[#5F5C5C] text-lg mb-2">{course.title}</h3>
{/* Описание курса */}
{course.desc && (
<p className="text-sm text-[#5F5C5C]/60 mb-3 line-clamp-2">{course.desc}</p>
)} )}
{/* Индикатор прогресса */} <div className="flex-1">
<div className="bg-white/50 rounded-full h-3 mb-2 overflow-hidden"> <h3 className="font-semibold text-[#5F5C5C] text-lg mb-2">{course.title}</h3>
<div
className={`bg-gradient-to-r ${colorClass} h-3 rounded-full transition-all duration-700 shadow-sm`} {/* Описание курса */}
style={{ width: `${progress}%` }} {course.desc && <p className="text-sm text-[#5F5C5C]/60 mb-3 line-clamp-2">{course.desc}</p>}
/>
{/* Индикатор прогресса */}
<div className="bg-white/50 rounded-full h-3 mb-2 overflow-hidden">
<div
className={`bg-gradient-to-r ${colorClass} h-3 rounded-full transition-all duration-700 shadow-sm`}
style={{ width: `${progress}%` }}
/>
</div>
{/* Информация о прогрессе */}
<div className="flex flex-col md:flex-row md:justify-between content-center">
<p className="text-sm text-[#5F5C5C]/70 font-semibold">{progress}% завершено</p>
<p className="text-xs text-[#5F5C5C]/50">{"надо/не надо?"} упражнений</p>
</div>
</div> </div>
{/* Информация о прогрессе */} {/* Иконка стрелки */}
<div className="flex flex-col md:flex-row md:justify-between content-center"> <div className="hidden sm:block text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1 my-auto">
<p className="text-sm text-[#5F5C5C]/70 font-semibold">{progress}% завершено</p> <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<p className="text-xs text-[#5F5C5C]/50">{"надо/не надо?"} упражнений</p> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div> </div>
</div> </div>
{/* Иконка стрелки */}
<div className="hidden sm:block text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1 my-auto">
<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>
)
})
: !error && (
<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.push("/courses")}
className="bg-[#2BACBE] text-white px-4 py-2 rounded-lg hover:bg-[#2A9FB8] transition-colors"
>
Просмотреть доступные курсы
</button>
</div> </div>
) )}
})
) : (
!error && (
<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.push("/courses")}
className="bg-[#2BACBE] text-white px-4 py-2 rounded-lg hover:bg-[#2A9FB8] transition-colors"
>
Просмотреть доступные курсы
</button>
</div>
)
)}
</div> </div>
</div> </div>
<BottomNavigation /> <BottomNavigation />
</div> </div>
); )
}; }

View File

@ -41,7 +41,6 @@ export const Exercise = () => {
const { courseId, exerciseId, exerciseIndex } = useParams<RouteParams>() const { courseId, exerciseId, exerciseIndex } = useParams<RouteParams>()
const location = useLocation() const location = useLocation()
const [course_exercises, setCourseExercises] = useState<CourseExercises[]>([]) const [course_exercises, setCourseExercises] = useState<CourseExercises[]>([])
const [actualExerciseId, setActualExerciseId] = useState<string>("") const [actualExerciseId, setActualExerciseId] = useState<string>("")
const [currentDay, setCurrentDay] = useState<number | null>(null) const [currentDay, setCurrentDay] = useState<number | null>(null)
@ -68,7 +67,7 @@ export const Exercise = () => {
// ========== СОСТОЯНИЯ ОТДЫХА МЕЖДУ ПОДХОДАМИ ========== // ========== СОСТОЯНИЯ ОТДЫХА МЕЖДУ ПОДХОДАМИ ==========
const [isResting, setIsResting] = useState(false) const [isResting, setIsResting] = useState(false)
const [restTime, setRestTime] = useState(0) const [restTime, setRestTime] = useState(0)
const [totalRestTime] = useState(60) const [totalRestTime] = useState(10) // Set default rest time to 60 seconds
const [isRestPaused, setIsRestPaused] = useState(false) const [isRestPaused, setIsRestPaused] = useState(false)
useEffect(() => { useEffect(() => {
@ -154,7 +153,14 @@ export const Exercise = () => {
}) })
if (exerciseData.time) { if (exerciseData.time) {
const timeInSeconds = Number.parseInt(exerciseData.time) * 60 let timeInSeconds: number
if (typeof exerciseData.time === "string" && exerciseData.time.includes(":")) {
// Time is in MM:SS format, convert to seconds
timeInSeconds = convertTimeToSeconds(exerciseData.time)
} else {
// Time is already in seconds
timeInSeconds = Number.parseInt(exerciseData.time)
}
setTotalTime(timeInSeconds) setTotalTime(timeInSeconds)
console.log("Установлено время упражнения:", timeInSeconds, "секунд") console.log("Установлено время упражнения:", timeInSeconds, "секунд")
} }
@ -198,7 +204,6 @@ export const Exercise = () => {
}, },
}) })
console.log("Ответ сервера с прогрессом:", response.data) console.log("Ответ сервера с прогрессом:", response.data)
if ( if (
@ -213,7 +218,7 @@ export const Exercise = () => {
const matchesCourse = record.course_id === Number.parseInt(courseId) const matchesCourse = record.course_id === Number.parseInt(courseId)
const matchesDay = currentDay ? record.day === currentDay : true const matchesDay = currentDay ? record.day === currentDay : true
console.log( console.log(
"[v0] Filtering progress - Course match:", "Filtering progress - Course match:",
matchesCourse, matchesCourse,
"Day match:", "Day match:",
matchesDay, matchesDay,
@ -249,7 +254,12 @@ export const Exercise = () => {
} }
if (lastIncompleteSet) { if (lastIncompleteSet) {
const timeInSeconds = convertTimeToSeconds(lastSavedTime) let timeInSeconds: number
if (typeof lastSavedTime === "string" && lastSavedTime.includes(":")) {
timeInSeconds = convertTimeToSeconds(lastSavedTime)
} else {
timeInSeconds = Number.parseInt(lastSavedTime) || 0
}
setCurrentSet(lastIncompleteSet.set) setCurrentSet(lastIncompleteSet.set)
setCurrentTime(timeInSeconds) setCurrentTime(timeInSeconds)
@ -303,9 +313,9 @@ export const Exercise = () => {
const saveSetProgress = async (setNumber: number, timeString: string, status: number) => { const saveSetProgress = async (setNumber: number, timeString: string, status: number) => {
try { try {
const dayToSave = currentDay || 1 const dayToSave = currentDay || 1
console.log(`[v0] SAVING PROGRESS: Set ${setNumber}, Status ${status}, Day ${dayToSave}`) console.log(`[SAVING PROGRESS: Set ${setNumber}, Status ${status}, Day ${dayToSave}`)
console.log(`[v0] Current state - currentDay: ${currentDay}, dayToSave: ${dayToSave}`) console.log(`[Current state - currentDay: ${currentDay}, dayToSave: ${dayToSave}`)
console.log(`[v0] URL search params:`, location.search) console.log(`[URL search params:`, location.search)
const progressData = { const progressData = {
time_users: timeString, time_users: timeString,
@ -327,21 +337,13 @@ export const Exercise = () => {
} }
} }
console.log("TEST DAY COMPLETE", course_exercises)
console.log("Всего упражнений в курсе", course_exercises.length)
console.log("TEST DAY COMPLETE", course_exercises)
console.log("Всего упражнений в курсе", course_exercises.length)
// ========== ФУНКЦИЯ перехода к след упражнению ========== // ========== ФУНКЦИЯ перехода к след упражнению ==========
const goToNextExercise = () => { const goToNextExercise = () => {
console.log("Переходим к следующему упражнению") console.log("Переходим к следующему упражнению")
if (exerciseIndex !== undefined) { if (exerciseIndex !== undefined) {
const currentIndex = Number.parseInt(exerciseIndex) const currentIndex = Number.parseInt(exerciseIndex)
const nextIndex = currentIndex + 1 const nextIndex = currentIndex + 1
@ -349,8 +351,6 @@ export const Exercise = () => {
const dayParam = selectedDay ? `?day=${selectedDay}` : "" const dayParam = selectedDay ? `?day=${selectedDay}` : ""
history.push(`/course/${courseId}/exercise/${nextIndex}${dayParam}`) history.push(`/course/${courseId}/exercise/${nextIndex}${dayParam}`)
} else { } else {
const currentExerciseNum = Number.parseInt(actualExerciseId) const currentExerciseNum = Number.parseInt(actualExerciseId)
const nextExerciseId = currentExerciseNum + 1 const nextExerciseId = currentExerciseNum + 1
@ -584,8 +584,9 @@ export const Exercise = () => {
// ========== ФУНКЦИЯ ФОРМАТИРОВАНИЯ ВРЕМЕНИ ========== // ========== ФУНКЦИЯ ФОРМАТИРОВАНИЯ ВРЕМЕНИ ==========
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60) const totalSeconds = Math.round(seconds)
const secs = seconds % 60 const mins = Math.floor(totalSeconds / 60)
const secs = totalSeconds % 60
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}` return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
} }
@ -873,7 +874,6 @@ export const Exercise = () => {
<span>Следующее упражнение</span> <span>Следующее упражнение</span>
</button> </button>
<div className="px-4 py-3 bg-cyan-500 text-white font-bold rounded-xl flex items-center justify-center space-x-2"> <div className="px-4 py-3 bg-cyan-500 text-white font-bold rounded-xl flex items-center justify-center space-x-2">
<CheckIcon /> <CheckIcon />
<span>Завершено</span> <span>Завершено</span>

View File

@ -1,49 +1,242 @@
"use client" "use client"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom"
// import { useParams } from "react-router-dom"
import { CalendarIcon } from "../components/icons/CalendarIcon"; import { CalendarIcon } from "../components/icons/CalendarIcon"
import { DumbbellIcon } from "../components/icons/DumbbellIcon"; import { DumbbellIcon } from "../components/icons/DumbbellIcon"
import HeaderNav from "../components/HeaderNav"
import BottomNavigation from "../components/BottomNavigation"
import CircularProgressDisplay from "../components/CircularProgressDisplay"
import HeaderNav from "../components/HeaderNav"; import { StatCardHome } from "../components/cards/StatCardHome"
import BottomNavigation from "../components/BottomNavigation"; import { WorkoutCardHome } from "../components/cards/WorkoutCardHome"
import CircularProgressDisplay from "../components/CircularProgressDisplay";
import { connect } from "../confconnect"
import { getRouteExerciseByIndex } from "../shared/consts/router"
import { getRouteCourses } from "../shared/consts/router"
import { getRouteCourseExercises } from "../shared/consts/router"
import { StatCardHome } from "../components/cards/StatCardHome"; import type { Course, User, CoursesApiResponse } from "../types/course"
import { WorkoutCardHome } from "../components/cards/WorkoutCardHome";
import { connect } from '../confconnect'; interface CourseExercises {
import { getRouteExerciseByIndex } from "../shared/consts/router"; id_exercise: number
import { getRouteCourses } from "../shared/consts/router"; day: number
import { getRouteCourseExercises } from "../shared/consts/router"; position: number
title: string
desc: string
time: string
count: number
repeats: number
}
import type { Course, User, CoursesApiResponse } from "../types/course"; interface CurrentExercise {
courseId: string
exerciseId: string
exerciseIndex: number
day: number
title: string
position: number
isCompleted: boolean
}
interface ProgressStats {
completedExercises: number
totalExercises: number
completedCourses: number
totalCourses: number
overallProgress: number
}
// interface RouteParams {
// courseId: string
// exerciseId?: string // Made optional since we might have exerciseIndex instead
// exerciseIndex?: string // Added exerciseIndex parameter
// }
//НАЧАЛО //
export default function Home() { export default function Home() {
const history = useHistory()
const [currentDate, setCurrentDate] = useState("")
const [error, setError] = useState<string>("")
const [courses, setCourses] = useState<Course[]>([])
const [loading, setLoading] = useState(true)
const history = useHistory(); const [currentExercise, setCurrentExercise] = useState<CurrentExercise | null>(null)
const [currentDate, setCurrentDate] = useState(""); const [loadingCurrentExercise, setLoadingCurrentExercise] = useState(false)
const [, setError] = useState<string>(''); // const { courseId, exerciseId, exerciseIndex } = useParams<RouteParams>()
const [courses, setCourses] = useState<Course[]>([]);
const [loading, setLoading] = useState(true);
const token = localStorage.getItem('authToken'); const [progressStats, setProgressStats] = useState<ProgressStats>({
completedExercises: 0,
totalExercises: 0,
completedCourses: 0,
totalCourses: 0,
overallProgress: 0,
})
const exerciseProgress = localStorage.getItem('exerciseProgress'); const token = localStorage.getItem("authToken")
const currentProgress = localStorage.getItem('currentProgress')
const calculateProgressStats = async (coursesList: Course[]): Promise<ProgressStats> => {
let totalExercises = 0
let completedExercises = 0
let completedCourses = 0
for (const course of coursesList) {
try {
// Получаем упражнения для каждого курса
const response = await connect.get(`/pacient/${course.id}`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
const exercises = response.data.course_exercises || []
totalExercises += exercises.length
console.log("упражнения для каждого курса", response.data)
let courseCompletedExercises = 0
// Проверяем каждое упражнение на завершенность
for (const exercise of exercises) {
const storageKey = `exerciseProgress_${course.id}_${exercise.id_exercise}_day_${exercise.day}`
const savedProgress = localStorage.getItem(storageKey)
if (savedProgress) {
const progress = JSON.parse(savedProgress)
const isCompleted =
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
if (isCompleted) {
completedExercises++
courseCompletedExercises++
}
}
}
// Курс считается завершенным если все упражнения выполнены
if (exercises.length > 0 && courseCompletedExercises === exercises.length) {
completedCourses++
}
} catch (error) {
console.error(`Ошибка при получении упражнений для курса ${course.id}:`, error)
// Если не удалось загрузить упражнения, считаем что в курсе 0 упражнений
}
}
const overallProgress = totalExercises > 0 ? Math.round((completedExercises / totalExercises) * 100) : 0
return {
completedExercises,
totalExercises,
completedCourses,
totalCourses: coursesList.length,
overallProgress,
}
}
const getCurrentExercise = async () => {
if (courses.length === 0) return
setLoadingCurrentExercise(true)
try {
// Берем первый доступный курс
const firstCourse = courses[0]
// Загружаем упражнения курса
const response = await connect.get(`/pacient/${firstCourse.id}`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
const exercises = response.data.course_exercises || []
if (exercises.length === 0) {
setCurrentExercise(null)
return
}
// Группируем упражнения по дням
const exercisesByDay = exercises.reduce((acc: any, exercise: CourseExercises) => {
if (!acc[exercise.day]) {
acc[exercise.day] = []
}
acc[exercise.day].push(exercise)
return acc
}, {})
// Ищем первое незавершенное упражнение
let foundExercise: CurrentExercise | null = null
for (const day of Object.keys(exercisesByDay).sort((a, b) => Number(a) - Number(b))) {
const dayExercises = exercisesByDay[day].sort(
(a: CourseExercises, b: CourseExercises) => a.position - b.position,
)
for (let i = 0; i < dayExercises.length; i++) {
const exercise = dayExercises[i]
// Проверяем прогресс упражнения в localStorage
const storageKey = `exerciseProgress_${firstCourse.id}_${exercise.id_exercise}_day_${day}`
const savedProgress = localStorage.getItem(storageKey)
let isCompleted = false
if (savedProgress) {
const progress = JSON.parse(savedProgress)
isCompleted =
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
}
if (!isCompleted) {
foundExercise = {
courseId: firstCourse.id.toString(),
exerciseId: exercise.id_exercise.toString(),
exerciseIndex: i,
day: Number(day),
title: exercise.title,
position: exercise.position,
isCompleted: false,
}
break
}
}
if (foundExercise) break
}
// Если все упражнения завершены, берем последнее
if (!foundExercise && exercises.length > 0) {
const lastExercise = exercises[exercises.length - 1]
foundExercise = {
courseId: firstCourse.id.toString(),
exerciseId: lastExercise.id_exercise.toString(),
exerciseIndex: exercises.length - 1,
day: lastExercise.day,
title: lastExercise.title,
position: lastExercise.position,
isCompleted: true,
}
}
setCurrentExercise(foundExercise)
} catch (error) {
console.error("Ошибка при получении текущего упражнения:", error)
} finally {
setLoadingCurrentExercise(false)
}
}
useEffect(() => { useEffect(() => {
console.log(token) console.log(token)
if (!token) { if (!token) {
setError('Токен не найден'); setError("Токен не найден")
setLoading(false); setLoading(false)
return; return
} }
setCurrentDate( setCurrentDate(
@ -54,9 +247,9 @@ export default function Home() {
}), }),
) )
setLoading(true) setLoading(true)
connect connect
.get<CoursesApiResponse>("/pacient/courses") .get<CoursesApiResponse>("/pacient/courses")
.then((response) => { .then((response) => {
console.log("Response data:", response.data) console.log("Response data:", response.data)
@ -67,7 +260,7 @@ export default function Home() {
if (user.Courses && Array.isArray(user.Courses)) { if (user.Courses && Array.isArray(user.Courses)) {
user.Courses.forEach((course) => { user.Courses.forEach((course) => {
allCourses.push({ allCourses.push({
ID: course.ID, id: course.id,
title: course.title, title: course.title,
desc: course.desc, desc: course.desc,
url_file_img: course.url_file_img, url_file_img: course.url_file_img,
@ -97,21 +290,26 @@ export default function Home() {
}) })
}, [token]) }, [token])
// Расчет статистики / выполненные где брать??? useEffect(() => {
const totalCourses = courses.length if (courses.length > 0 && !loading) {
const totalExercises = courses.reduce((sum, course) => { getCurrentExercise()
if (course.course_exercises && Array.isArray(course.course_exercises)) {
return sum + course.course_exercises.length calculateProgressStats(courses).then((stats) => {
setProgressStats(stats)
})
} }
return sum + Math.floor(Math.random() * 10) + 5 }, [courses, loading])
}, 0)
const overallProgress = courses.length > 0 ? Math.floor(Math.random() * 100) : 0
//пока передаю тестовые значения
const handleWorkoutClick = () => { const handleWorkoutClick = () => {
history.push(getRouteExerciseByIndex(1,1,1)) if (currentExercise) {
// Используем правильный роут с day параметром
history.push(
getRouteExerciseByIndex(currentExercise.courseId, currentExercise.exerciseIndex, currentExercise.day),
)
} else {
// Fallback если нет текущего упражнения
history.push(getRouteExerciseByIndex(1, 0, 1))
}
} }
const handleBackClick = () => { const handleBackClick = () => {
@ -124,7 +322,7 @@ export default function Home() {
const handleExercisesClick = () => { const handleExercisesClick = () => {
if (courses.length > 0) { if (courses.length > 0) {
history.push(getRouteCourseExercises(`${'1'}`)) history.push(getRouteCourseExercises(courses[0].id))
} else { } else {
history.push(getRouteCourses()) history.push(getRouteCourses())
} }
@ -144,88 +342,68 @@ export default function Home() {
) )
} }
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={currentDate} />
return ( <div className="bg-white rounded-3xl p-6 shadow-lg mx-4 sm:mx-6">
<div className="bg-gray-50 w-full h-full overflow-auto"> <div className="flex content-center items-center justify-between"></div>
<div className="my-36 min-h-screen max-w-4xl mx-auto"> <div className="flex justify-between items-center">
<HeaderNav item="Прогресс" text={currentDate} /> <div className="flex flex-col gap-6">
<div className="text-left">
<div className="bg-white rounded-3xl p-6 shadow-lg mx-4 sm:mx-6"> <div className="text-sm sm:text-base text-gray-800">Все курсы</div>
<div className="flex content-center items-center justify-between "> <div className="text-2xl font-bold text-orange-500">
{progressStats.completedCourses}/{progressStats.totalCourses}
</div>
<div className="flex justify-between items-center">
<div className="flex flex-col gap-6">
<div className="text-left">
<div className="text-sm sm:text-base text-gray-800">Все курсы</div>
<div className="text-2xl font-bold text-cyan-500">{totalCourses}/?</div>
</div>
<div className="text-left">
<div className="text-sm sm:text-base text-gray-800">Все упражнения</div>
<div className="text-2xl font-bold text-orange-400">{totalExercises}/?</div>
</div> </div>
</div> </div>
<div className="flex justify-center items-center gap-8"> <div className="text-left">
<CircularProgressDisplay <div className="text-sm sm:text-base text-gray-800">Все упражнения</div>
overallProgress={overallProgress} <div className="text-2xl font-bold text-cyan-400">
totalCourses={totalCourses} {progressStats.completedExercises}/{progressStats.totalExercises}
totalExercises={totalExercises} </div>
/>
</div> </div>
</div> </div>
</div> <div className="flex justify-center items-center gap-8">
<CircularProgressDisplay
<div className="px-4 sm:px-6 space-y-6"> overallProgress={progressStats.overallProgress}
{/* Текущее упражнение */} totalCourses={progressStats.totalCourses}
<WorkoutCardHome onBackClick={handleBackClick} onCardClick={handleWorkoutClick} totalExercises={progressStats.totalExercises}
/> completedCourses={progressStats.completedCourses}
completedExercises={progressStats.completedExercises}
{/* Quick Stats (Total Exercises & Total Courses) */}
<div className="grid grid-cols-2 gap-4 md:gap-5">
<StatCardHome
title="Курсы"
subtitle="назначенные"
icon={CalendarIcon}
fill="#2BACBE"
onClick={handleCoursesClick}
/>
<StatCardHome
title="Упражнения"
subtitle="текущего курса"
icon={DumbbellIcon}
fill="#FF8D28"
onClick={handleExercisesClick}
/> />
</div> </div>
<div className="bg-green-100">
<p className="font-bold ">Взяли из локального хранилища:</p>
надо считать прогресс исходя из выполненных/ вопрос: то есть, если выполнено, то как его исключить / как флаг проставлять?==
все курсы мы вяли из ручки курсов, назначенные по роутингу (пути в router.ts)
надо достать текущий курс. который не выполнен.
как узнать, что курс выполнен?==
мы берем данные для текущей тренировки, которые записываются с помощью post, --оттуда достаем данные для текущего упражнения, которое записано (1 строка в БД всегда),
а потом переходим на это упражнение.
и еще - мы считаем по индексу отображение, это же не повлияет на вывод итоговый на домашней странице, например?
<div className="bg-red-200">
<p>exerciseProgress:{exerciseProgress}</p>
<p>currentProgress{currentProgress}</p>
</div>
</div>
</div> </div>
<BottomNavigation />
</div> </div>
<div className="px-4 sm:px-6 space-y-6">
<WorkoutCardHome
onBackClick={handleBackClick}
onCardClick={handleWorkoutClick}
currentExercise={currentExercise}
loading={loadingCurrentExercise}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-5">
<StatCardHome
title="Курсы"
subtitle="назначенные"
icon={CalendarIcon}
fill="#2BACBE"
onClick={handleCoursesClick}
/>
<StatCardHome
title="Упражнения"
subtitle="текущего курса"
icon={DumbbellIcon}
fill="#FF8D28"
onClick={handleExercisesClick}
/>
</div>
</div>
<BottomNavigation />
</div> </div>
) </div>
} )
}

View File

@ -8,7 +8,7 @@ export interface CourseExercise {
} }
export interface Course { export interface Course {
ID: number id: number
title: string title: string
desc: string desc: string
url_file_img: string url_file_img: string