убрала лишнее + дизайн

This commit is contained in:
Tatyana 2025-09-08 16:04:44 +03:00
parent df8cc1596c
commit d53cfb5156
8 changed files with 4000959 additions and 1074 deletions

1111374
CourseExercises Normal file

File diff suppressed because it is too large Load Diff

1333595
react Normal file

File diff suppressed because it is too large Load Diff

1555816
react-router-dom Normal file

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,7 @@ const AppRoutes = () => (
<Route exact path="/course/:courseId/exercise/:exerciseIndex" component={Exercise} /> <Route exact path="/course/:courseId/exercise/:exerciseIndex" component={Exercise} />
<Route exact path="/course/:courseId/:exerciseId" component={Exercise} /> <Route exact path="/course/:courseId/:exerciseId" component={Exercise} />
<Route path={getRouteSettings()} component={Settings} /> <Route path={getRouteSettings()} component={Settings} />
<Route path="/course-complete" component={CourseComplete} />
<Route path={getRouteCourseComplete()} component={CourseComplete} /> <Route path={getRouteCourseComplete()} component={CourseComplete} />
</Switch> </Switch>
) )

View File

@ -39,6 +39,7 @@ export const CourseExercises = () => {
const [course_exercises, setExercises] = useState<CourseExercises[]>([]) const [course_exercises, setExercises] = useState<CourseExercises[]>([])
const [selectedDay, setSelectedDay] = useState<number | null>(null) const [selectedDay, setSelectedDay] = useState<number | null>(null)
const [exerciseProgress, setExerciseProgress] = useState<{ [key: string]: boolean }>({})
const token = localStorage.getItem("authToken") const token = localStorage.getItem("authToken")
@ -77,8 +78,28 @@ export const CourseExercises = () => {
} }
}, [course_exercises]) }, [course_exercises])
const checkExerciseCompletion = (exerciseId: number, day: number): boolean => {
const storageKey = `exerciseProgress_${id}_${exerciseId}_day_${day}`
const savedProgress = localStorage.getItem(storageKey)
if (!savedProgress) return false
const progress = JSON.parse(savedProgress)
return progress.status === 1 && progress.completedSets && progress.completedSets.length >= progress.set
}
useEffect(() => {
if (course_exercises.length > 0) {
const progressMap: { [key: string]: boolean } = {}
course_exercises.forEach((exercise) => {
const key = `${exercise.id_exercise}_${exercise.day}`
progressMap[key] = checkExerciseCompletion(exercise.id_exercise, exercise.day)
})
setExerciseProgress(progressMap)
}
}, [course_exercises, id])
const uniqueDays = Array.from(new Set(course_exercises.map((ex) => ex.day))).sort((a, b) => a - b) const uniqueDays = Array.from(new Set(course_exercises.map((ex) => ex.day))).sort((a, b) => a - b)
const dayMap: { [key: number]: number } = {} const dayMap: { [key: number]: number } = {}
@ -87,7 +108,7 @@ export const CourseExercises = () => {
dayMap[day] = index + 1 dayMap[day] = index + 1
}) })
console.log('Уникальные дни', uniqueDays) console.log("Уникальные дни", uniqueDays)
const daysNav = uniqueDays.map((day) => dayMap[day]) const daysNav = uniqueDays.map((day) => dayMap[day])
@ -132,36 +153,50 @@ export const CourseExercises = () => {
<div className="exercise-list mb-20"> <div className="exercise-list mb-20">
{filteredExercises.length > 0 ? ( {filteredExercises.length > 0 ? (
filteredExercises.map((item, index) => ( filteredExercises.map((item, index) => {
const exerciseKey = `${item.id_exercise}_${item.day}`
const isCompleted = exerciseProgress[exerciseKey]
return (
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
history.push( history.push(getRouteExerciseByIndex(item.id_course.toString(), index, selectedDay || undefined))
getRouteExerciseByIndex(
item.id_course.toString(),
index, // Используем индекс из отфильтрованного массива
selectedDay || undefined, // Передаем выбранный день для контекста
),
)
}} }}
className="p-4 mb-4 cursor-pointer hover:scale-105 transition duration-300 glass-morphism rounded-3xl border border-white/50 shadow-2xl overflow-hidden backdrop-blur-2xl relative" className={`p-4 mb-4 cursor-pointer hover:scale-105 transition duration-300 glass-morphism rounded-3xl border border-white/50 shadow-2xl overflow-hidden backdrop-blur-2xl relative ${
isCompleted ? "opacity-60 bg-gray-100/50" : ""
}`}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h3 className="text-xs sm:text-base">Упражнение {index + 1}</h3> <h3 className="text-xs sm:text-base">Упражнение {index + 1}</h3>
<h3 className="text-base sm:text-xl font-semibold text-gray-600">{item.exercise.title}</h3> <h3
className={`text-base sm:text-xl font-semibold ${isCompleted ? "text-gray-500" : "text-gray-600"}`}
>
{item.exercise.title}
</h3>
</div>
<div className="flex items-center space-x-2">
{isCompleted && (
<div className="w-6 h-6 bg-cyan-500 rounded-full flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
</div>
)}
<ArrowIcon className={isCompleted ? "text-gray-400" : "text-cyan-600"} />
</div> </div>
<ArrowIcon className="text-cyan-600" />
</div> </div>
<div className="h-0.5 w-full bg-gray-200 my-3"></div> <div className="h-0.5 w-full bg-gray-200 my-3"></div>
<div className="flex gap-10 text-xs text-gray-500"> <div className={`flex gap-10 text-xs ${isCompleted ? "text-gray-400" : "text-gray-500"}`}>
<p>Повторений: {item.repeats}</p> <p>Повторений: {item.repeats}</p>
<p>Время выполнения: {item.time}</p> <p>Время выполнения: {item.time}</p>
</div> </div>
</div> </div>
)) )
})
) : ( ) : (
<p>Нет упражнений для отображения</p> <p>Нет упражнений для отображения</p>
)} )}

View File

@ -211,12 +211,15 @@ export const Courses = () => {
? courses.map((course, index) => { ? courses.map((course, index) => {
const progress = courseProgress[course.ID] || 0 const progress = courseProgress[course.ID] || 0
const colorClass = progressColors[index % progressColors.length] const colorClass = progressColors[index % progressColors.length]
const isCourseCompleted = progress === 100
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] ${
isCourseCompleted ? "opacity-70 bg-gray-100/30" : ""
}`}
> >
<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">
{/* Изображение курса */} {/* Изображение курса */}
@ -225,7 +228,7 @@ export const Courses = () => {
<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 ${isCourseCompleted ? "grayscale" : ""}`}
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"
}} }}
@ -234,28 +237,54 @@ export const Courses = () => {
)} )}
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold text-[#5F5C5C] text-lg mb-2">{course.title}</h3> <div className="flex items-center space-x-2 mb-2">
<h3
className={`font-semibold text-lg ${isCourseCompleted ? "text-gray-500" : "text-[#5F5C5C]"}`}
>
{course.title}
</h3>
{isCourseCompleted && (
<div className="w-6 h-6 bg-cyan-500 rounded-full flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
</div>
)}
</div>
{/* Описание курса */} {/* Описание курса */}
{course.desc && <p className="text-sm text-[#5F5C5C]/60 mb-3 line-clamp-2">{course.desc}</p>} {course.desc && (
<p
className={`text-sm mb-3 line-clamp-2 ${isCourseCompleted ? "text-gray-400" : "text-[#5F5C5C]/60"}`}
>
{course.desc}
</p>
)}
{/* Индикатор прогресса */} {/* Индикатор прогресса */}
<div className="bg-white/50 rounded-full h-3 mb-2 overflow-hidden"> <div className="bg-white/50 rounded-full h-3 mb-2 overflow-hidden">
<div <div
className={`bg-gradient-to-r ${colorClass} h-3 rounded-full transition-all duration-700 shadow-sm`} className={`bg-gradient-to-r ${isCourseCompleted ? "from-cyan-400 to-cyan-600" : colorClass} h-3 rounded-full transition-all duration-700 shadow-sm`}
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
/> />
</div> </div>
{/* Информация о прогрессе */} {/* Информация о прогрессе */}
<div className="flex flex-col md:flex-row md:justify-between content-center"> <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
<p className="text-xs text-[#5F5C5C]/50">{"надо/не надо?"} упражнений</p> className={`text-sm font-semibold ${isCourseCompleted ? "text-cyan-600" : "text-cyan-700"}`}
>
{isCourseCompleted ? "Завершен" : `${progress}%`}
</p>
</div> </div>
</div> </div>
{/* Иконка стрелки */} {/* Иконка стрелки */}
<div className="hidden sm:block text-[#2BACBE] transform transition-transform duration-300 hover:translate-x-1 my-auto"> <div
className={`hidden sm:block transform transition-transform duration-300 hover:translate-x-1 my-auto ${
isCourseCompleted ? "text-gray-400" : "text-[#2BACBE]"
}`}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>

View File

@ -350,6 +350,24 @@ export const Exercise = () => {
const selectedDay = new URLSearchParams(location.search).get("day") const selectedDay = new URLSearchParams(location.search).get("day")
const dayParam = selectedDay ? `?day=${selectedDay}` : "" const dayParam = selectedDay ? `?day=${selectedDay}` : ""
const dayExercises = course_exercises.filter((ex) => ex.day === (currentDay || 1))
if (nextIndex >= dayExercises.length) {
console.log("Последнее упражнение дня завершено")
// Проверяем, завершен ли весь курс
const allExercisesCompleted = checkIfCourseCompleted()
if (allExercisesCompleted) {
console.log("Весь курс завершен, переходим к CourseComplete")
history.push("/course-complete")
} else {
// Показываем сообщение о завершении дня
showDayCompletionMessage()
}
return
}
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)
@ -358,6 +376,60 @@ export const Exercise = () => {
} }
} }
const checkIfCourseCompleted = (): boolean => {
if (!course_exercises.length) return false
let allCompleted = true
for (const exercise of course_exercises) {
const storageKey = `exerciseProgress_${courseId}_${exercise.id_exercise}_day_${exercise.day}`
const savedProgress = localStorage.getItem(storageKey)
if (!savedProgress) {
allCompleted = false
break
}
const progress = JSON.parse(savedProgress)
const isCompleted =
progress.status === 1 && progress.completedSets && progress.completedSets.length >= exercise.count
if (!isCompleted) {
allCompleted = false
break
}
}
return allCompleted
}
const showDayCompletionMessage = () => {
const modal = document.createElement("div")
modal.className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50"
modal.innerHTML = `
<div class="bg-white rounded-2xl p-8 mx-4 max-w-md text-center">
<div class="w-16 h-16 bg-cyan-700 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-800 mb-2">Поздравляем!</h2>
<p class="text-gray-600 mb-6">Вы успешно закончили день</p>
<button id="homeButton" class="w-full bg-[#2BACBE] text-white py-3 px-6 rounded-xl font-semibold hover:bg-[#2A9FB8] transition-colors">
На главную страницу
</button>
</div>
`
document.body.appendChild(modal)
const homeButton = modal.querySelector("#homeButton")
homeButton?.addEventListener("click", () => {
document.body.removeChild(modal)
history.push("/home")
})
}
// ========== ФУНКЦИЯ ЗАВЕРШЕНИЯ ТЕКУЩЕГО ПОДХОДА ========== // ========== ФУНКЦИЯ ЗАВЕРШЕНИЯ ТЕКУЩЕГО ПОДХОДА ==========
const handleCompleteSet = async () => { const handleCompleteSet = async () => {
console.log("Пользователь завершает подход", currentSet, "из", totalSets) console.log("Пользователь завершает подход", currentSet, "из", totalSets)
@ -904,18 +976,6 @@ export const Exercise = () => {
</> </>
)} )}
</button> </button>
<button
onClick={() => {
console.log("Пропускаем отдых, переходим к следующему подходу")
setIsResting(false)
setIsRestPaused(false)
setCurrentSet(currentSet + 1)
setCurrentTime(0)
}}
className="px-6 py-3 bg-gray-500 hover:bg-gray-600 text-white font-bold rounded-xl transition-all duration-300 flex items-center justify-center"
>
Пропустить
</button>
</> </>
) : ( ) : (
<> <>
@ -948,12 +1008,12 @@ export const Exercise = () => {
)} )}
</button> </button>
<button {/* <button
onClick={handleCompleteSet} onClick={handleCompleteSet}
className="px-6 py-3 bg-cyan-500 hover:bg-cyan-600 hover:scale-110 text-white font-bold rounded-xl transition-all duration-300 flex items-center justify-center" className="px-6 py-3 bg-cyan-500 hover:bg-cyan-600 hover:scale-110 text-white font-bold rounded-xl transition-all duration-300 flex items-center justify-center"
> >
<CheckIcon /> <CheckIcon />
</button> </button> */}
</> </>
)} )}

File diff suppressed because it is too large Load Diff