Enhance task navigation and progress tracking across components. Update TaskWorkspace to improve task completion handling and button interactions. Refactor ChainsPage to select the furthest accessible task based on user progress. Implement storage functions for managing furthest task indices, ensuring users can only navigate to available tasks. Update TaskPage to check task accessibility and redirect users appropriately, improving overall user experience.
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit

This commit is contained in:
2025-12-14 14:06:18 +03:00
parent c9bbe83bbb
commit 1a52901b90
4 changed files with 135 additions and 28 deletions

View File

@@ -21,7 +21,7 @@ interface TaskWorkspaceProps {
export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
const { refreshStats } = useChallenge()
const { result, setResult, submit, reset, queueStatus, finalSubmission, isSubmitting } = useSubmission({
const { result, setResult, submit, queueStatus, finalSubmission, isSubmitting } = useSubmission({
taskId: task.id,
})
@@ -364,12 +364,18 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
/>
</Box>
<HStack justify="flex-end" gap={2}>
<HStack justify="space-between" gap={2}>
{!isAccepted && (
<>
{/* @ts-expect-error Chakra UI v2 uses isDisabled */}
<Button onClick={reset} variant="ghost" size="sm" isDisabled={isChecking}>
Сбросить
<Button
onClick={onTaskComplete}
variant="outline"
size="sm"
colorScheme="gray"
// @ts-expect-error Chakra UI v2 uses isDisabled
isDisabled={isChecking}
>
Пропустить
</Button>
{/* @ts-expect-error Chakra UI v2 uses isLoading/isDisabled */}
<Button onClick={submit} colorScheme="teal" size="sm" isLoading={isChecking} isDisabled={!result.trim() || isChecking}>

View File

@@ -26,10 +26,20 @@ export const ChainsPage = () => {
const handleSelectChain = (chain: ChallengeChain) => {
storage.setSelectedChainId(chain.id)
// Переходим к первому заданию цепочки
if (chain.tasks.length > 0) {
storage.setSelectedTaskId(chain.tasks[0].id)
navigate(URLs.task(chain.id, chain.tasks[0].id))
// Получаем самый дальний достигнутый индекс
const furthestIndex = storage.getFurthestTaskIndex(chain.id)
// Если нет прогресса, инициализируем с первого задания
const targetIndex = furthestIndex >= 0 ? furthestIndex : 0
const targetTask = chain.tasks[targetIndex] || chain.tasks[0]
storage.setSelectedTaskId(targetTask.id)
// Убеждаемся, что прогресс установлен
storage.setFurthestTaskIndex(chain.id, targetIndex)
navigate(URLs.task(chain.id, targetTask.id))
}
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
Box,
@@ -7,7 +7,6 @@ import {
HStack,
Text,
} from '@chakra-ui/react'
import { Alert } from '@chakra-ui/react/alert'
import { URLs } from '../../__data__/urls'
import { useChallenge } from '../../context/ChallengeContext'
@@ -46,19 +45,48 @@ export const TaskPage = () => {
return chain.tasks.findIndex(t => t.id === taskId)
}, [chain, taskId])
// Сохраняем текущее состояние в storage
// Получаем самый дальний достигнутый индекс задания (используем state для реактивности)
const [furthestTaskIndex, setFurthestTaskIndex] = useState(() => {
if (!chainId) return 0
return storage.getFurthestTaskIndex(chainId)
})
// Обновляем furthestTaskIndex при изменении chainId или currentTaskIndex
useEffect(() => {
if (chainId && taskId) {
if (!chainId) return
const currentFurthest = storage.getFurthestTaskIndex(chainId)
setFurthestTaskIndex(currentFurthest)
}, [chainId, currentTaskIndex])
// Сохраняем текущее состояние в storage и обновляем прогресс
useEffect(() => {
if (chainId && taskId && currentTaskIndex >= 0) {
storage.setSelectedChainId(chainId)
storage.setSelectedTaskId(taskId)
// Обновляем прогресс, если текущее задание дальше предыдущего
const newFurthest = Math.max(furthestTaskIndex, currentTaskIndex)
if (newFurthest > furthestTaskIndex) {
storage.setFurthestTaskIndex(chainId, newFurthest)
setFurthestTaskIndex(newFurthest)
}
}
}, [chainId, taskId, currentTaskIndex, furthestTaskIndex])
// Проверка доступности задания
const isTaskAccessible = (taskIndex: number): boolean => {
return taskIndex <= furthestTaskIndex
}
}, [chainId, taskId])
const handleTaskComplete = () => {
if (!chain || currentTaskIndex === -1) return
const nextTask = chain.tasks[currentTaskIndex + 1]
const nextTaskIndex = currentTaskIndex + 1
const nextTask = chain.tasks[nextTaskIndex]
if (nextTask) {
// Обновляем прогресс перед переходом
storage.setFurthestTaskIndex(chain.id, nextTaskIndex)
setFurthestTaskIndex(nextTaskIndex) // Обновляем state сразу
navigate(URLs.task(chain.id, nextTask.id))
} else {
// Цепочка завершена
@@ -68,10 +96,38 @@ export const TaskPage = () => {
}
const handleNavigateToTask = (newTaskId: string) => {
if (chain) {
if (!chain) return
const newTaskIndex = chain.tasks.findIndex(t => t.id === newTaskId)
if (newTaskIndex === -1) return
// Проверяем доступность
if (!isTaskAccessible(newTaskIndex)) {
return // Не переходим к заблокированному заданию
}
// Обновляем прогресс при переходе
const newFurthest = Math.max(furthestTaskIndex, newTaskIndex)
if (newFurthest > furthestTaskIndex) {
storage.setFurthestTaskIndex(chain.id, newFurthest)
setFurthestTaskIndex(newFurthest)
}
navigate(URLs.task(chain.id, newTaskId))
}
// Проверяем доступность текущего задания при загрузке
useEffect(() => {
if (chain && currentTaskIndex >= 0 && !isTaskAccessible(currentTaskIndex)) {
// Если пытаемся открыть недоступное задание, перенаправляем на последнее доступное
const lastAccessibleIndex = furthestTaskIndex
if (lastAccessibleIndex >= 0 && chain.tasks[lastAccessibleIndex]) {
navigate(URLs.task(chain.id, chain.tasks[lastAccessibleIndex].id), { replace: true })
} else if (chain.tasks[0]) {
// Если нет прогресса, идём к первому заданию
navigate(URLs.task(chain.id, chain.tasks[0].id), { replace: true })
}
}
}, [chain, currentTaskIndex, furthestTaskIndex, navigate])
const handleBackToChains = () => {
storage.clearSessionData()
@@ -104,18 +160,27 @@ export const TaskPage = () => {
<Text fontSize="sm" fontWeight="medium" color="gray.600" mr={2}>
Задания:
</Text>
{chain.tasks.map((t, index) => (
{chain.tasks.map((t, index) => {
const isAccessible = isTaskAccessible(index)
const isCurrent = t.id === taskId
return (
<Button
key={t.id}
size="sm"
variant={t.id === taskId ? 'solid' : 'outline'}
colorScheme={t.id === taskId ? 'teal' : 'gray'}
onClick={() => handleNavigateToTask(t.id)}
variant={isCurrent ? 'solid' : isAccessible ? 'outline' : 'ghost'}
colorScheme={isCurrent ? 'teal' : isAccessible ? 'gray' : 'gray'}
// @ts-expect-error Chakra UI v2 uses isDisabled
isDisabled={!isAccessible}
onClick={() => isAccessible && handleNavigateToTask(t.id)}
minW="40px"
opacity={isAccessible ? 1 : 0.5}
cursor={isAccessible ? 'pointer' : 'not-allowed'}
>
{index + 1}
{isAccessible ? index + 1 : `🔒${index + 1}`}
</Button>
))}
)
})}
</HStack>
<Button

View File

@@ -14,6 +14,9 @@ export const STORAGE_KEYS = {
SELECTED_TASK_ID: 'challengeSelectedTaskId',
} as const
// Вспомогательная функция для ключа прогресса цепочки
const getFurthestTaskKey = (chainId: string) => `challengeFurthestTask_${chainId}`
// Получение значений
export const storage = {
getUserId: (): string | null => {
@@ -109,5 +112,28 @@ export const storage = {
localStorage.removeItem(STORAGE_KEYS.SELECTED_CHAIN_ID)
localStorage.removeItem(STORAGE_KEYS.SELECTED_TASK_ID)
},
// Получение самого дальнего достигнутого индекса задания в цепочке
getFurthestTaskIndex: (chainId: string): number => {
if (!isBrowser()) return 0
const value = localStorage.getItem(getFurthestTaskKey(chainId))
return value ? parseInt(value, 10) : 0
},
// Установка самого дальнего достигнутого индекса задания
setFurthestTaskIndex: (chainId: string, index: number): void => {
if (!isBrowser()) return
const current = storage.getFurthestTaskIndex(chainId)
// Обновляем только если новый индекс больше текущего
if (index > current) {
localStorage.setItem(getFurthestTaskKey(chainId), index.toString())
}
},
// Очистка прогресса цепочки
clearChainProgress: (chainId: string): void => {
if (!isBrowser()) return
localStorage.removeItem(getFurthestTaskKey(chainId))
},
}