убрала лишнее + дизайн
This commit is contained in:
parent
df8cc1596c
commit
d53cfb5156
1111374
CourseExercises
Normal file
1111374
CourseExercises
Normal file
File diff suppressed because it is too large
Load Diff
1555816
react-router-dom
Normal file
1555816
react-router-dom
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user