Enhance task management by adding skip functionality in TaskWorkspace and TaskPage. Implement storage methods for tracking skipped tasks, allowing users to navigate to the next task or the first skipped task seamlessly. Update polling manager configuration for improved performance.
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
This commit is contained in:
@@ -18,9 +18,10 @@ import { LearningMaterialViewer } from './LearningMaterialViewer'
|
|||||||
interface TaskWorkspaceProps {
|
interface TaskWorkspaceProps {
|
||||||
task: ChallengeTask
|
task: ChallengeTask
|
||||||
onTaskComplete?: () => void
|
onTaskComplete?: () => void
|
||||||
|
onTaskSkip?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
export const TaskWorkspace = ({ task, onTaskComplete, onTaskSkip }: TaskWorkspaceProps) => {
|
||||||
const { refreshStats } = useChallenge()
|
const { refreshStats } = useChallenge()
|
||||||
const { result, setResult, submit, queueStatus, finalSubmission, isSubmitting } = useSubmission({
|
const { result, setResult, submit, queueStatus, finalSubmission, isSubmitting } = useSubmission({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
@@ -397,7 +398,7 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
|||||||
{!isAccepted && (
|
{!isAccepted && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={onTaskComplete}
|
onClick={onTaskSkip}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const ChallengeProvider = ({ children }: PropsWithChildren) => {
|
|||||||
const metricsCollector = useMemo(() => new MetricsCollector(), [])
|
const metricsCollector = useMemo(() => new MetricsCollector(), [])
|
||||||
const behaviorTracker = useMemo(() => new BehaviorTracker(), [])
|
const behaviorTracker = useMemo(() => new BehaviorTracker(), [])
|
||||||
const eventEmitter = useMemo(() => new ChallengeEventEmitter(), [])
|
const eventEmitter = useMemo(() => new ChallengeEventEmitter(), [])
|
||||||
const pollingManager = useMemo(() => new PollingManager({ initialDelay: 800, maxDelay: 5000, multiplier: 1.15 }), [])
|
const pollingManager = useMemo(() => new PollingManager({ initialDelay: 800, maxDelay: 5000, multiplier: 1 }), [])
|
||||||
|
|
||||||
const [userId, setUserId] = useState<string | null>(() => storage.getUserId())
|
const [userId, setUserId] = useState<string | null>(() => storage.getUserId())
|
||||||
const [nickname, setNickname] = useState<string | null>(() => storage.getNickname())
|
const [nickname, setNickname] = useState<string | null>(() => storage.getNickname())
|
||||||
|
|||||||
@@ -51,11 +51,20 @@ export const TaskPage = () => {
|
|||||||
return storage.getFurthestTaskIndex(chainId)
|
return storage.getFurthestTaskIndex(chainId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Отслеживаем пропущенные задания
|
||||||
|
const [skippedTasks, setSkippedTasks] = useState<string[]>(() => {
|
||||||
|
if (!chainId) return []
|
||||||
|
return storage.getSkippedTasks(chainId)
|
||||||
|
})
|
||||||
|
|
||||||
// Обновляем furthestTaskIndex при изменении chainId или currentTaskIndex
|
// Обновляем furthestTaskIndex при изменении chainId или currentTaskIndex
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chainId) return
|
if (!chainId) return
|
||||||
const currentFurthest = storage.getFurthestTaskIndex(chainId)
|
const currentFurthest = storage.getFurthestTaskIndex(chainId)
|
||||||
setFurthestTaskIndex(currentFurthest)
|
setFurthestTaskIndex(currentFurthest)
|
||||||
|
// Также обновляем список пропущенных заданий
|
||||||
|
const currentSkipped = storage.getSkippedTasks(chainId)
|
||||||
|
setSkippedTasks(currentSkipped)
|
||||||
}, [chainId, currentTaskIndex])
|
}, [chainId, currentTaskIndex])
|
||||||
|
|
||||||
// Сохраняем текущее состояние в storage и обновляем прогресс
|
// Сохраняем текущее состояние в storage и обновляем прогресс
|
||||||
@@ -77,8 +86,12 @@ export const TaskPage = () => {
|
|||||||
return taskIndex <= furthestTaskIndex
|
return taskIndex <= furthestTaskIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTaskComplete = () => {
|
const handleTaskSkip = () => {
|
||||||
if (!chain || currentTaskIndex === -1) return
|
if (!chain || currentTaskIndex === -1 || !taskId) return
|
||||||
|
|
||||||
|
// Добавляем задание в список пропущенных
|
||||||
|
storage.addSkippedTask(chain.id, taskId)
|
||||||
|
setSkippedTasks(storage.getSkippedTasks(chain.id))
|
||||||
|
|
||||||
const nextTaskIndex = currentTaskIndex + 1
|
const nextTaskIndex = currentTaskIndex + 1
|
||||||
const nextTask = chain.tasks[nextTaskIndex]
|
const nextTask = chain.tasks[nextTaskIndex]
|
||||||
@@ -89,9 +102,49 @@ export const TaskPage = () => {
|
|||||||
setFurthestTaskIndex(nextTaskIndex) // Обновляем state сразу
|
setFurthestTaskIndex(nextTaskIndex) // Обновляем state сразу
|
||||||
navigate(URLs.task(chain.id, nextTask.id))
|
navigate(URLs.task(chain.id, nextTask.id))
|
||||||
} else {
|
} else {
|
||||||
// Цепочка завершена
|
// Достигнут конец списка заданий - проверяем пропущенные
|
||||||
storage.clearSessionData()
|
const currentSkipped = storage.getSkippedTasks(chain.id)
|
||||||
navigate(URLs.completed(chain.id))
|
if (currentSkipped.length > 0) {
|
||||||
|
// Есть пропущенные задания - переходим к первому пропущенному
|
||||||
|
const firstSkippedId = currentSkipped[0]
|
||||||
|
navigate(URLs.task(chain.id, firstSkippedId))
|
||||||
|
} else {
|
||||||
|
// Нет пропущенных заданий - переходим на страницу завершения
|
||||||
|
storage.clearSessionData()
|
||||||
|
navigate(URLs.completed(chain.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskComplete = () => {
|
||||||
|
if (!chain || currentTaskIndex === -1) return
|
||||||
|
|
||||||
|
// При успешном выполнении удаляем задание из пропущенных (если оно там было)
|
||||||
|
if (taskId) {
|
||||||
|
storage.removeSkippedTask(chain.id, taskId)
|
||||||
|
setSkippedTasks(storage.getSkippedTasks(chain.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Достигнут конец списка заданий - проверяем пропущенные
|
||||||
|
const currentSkipped = storage.getSkippedTasks(chain.id)
|
||||||
|
if (currentSkipped.length > 0) {
|
||||||
|
// Есть пропущенные задания - переходим к первому пропущенному
|
||||||
|
const firstSkippedId = currentSkipped[0]
|
||||||
|
navigate(URLs.task(chain.id, firstSkippedId))
|
||||||
|
} else {
|
||||||
|
// Нет пропущенных заданий - переходим на страницу завершения
|
||||||
|
storage.clearSessionData()
|
||||||
|
navigate(URLs.completed(chain.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,19 +224,23 @@ export const TaskPage = () => {
|
|||||||
const taskIndex = chain.tasks.indexOf(t)
|
const taskIndex = chain.tasks.indexOf(t)
|
||||||
const isAccessible = isTaskAccessible(taskIndex)
|
const isAccessible = isTaskAccessible(taskIndex)
|
||||||
const isCurrent = t.id === taskId
|
const isCurrent = t.id === taskId
|
||||||
|
const isSkipped = skippedTasks.includes(t.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={t.id}
|
key={t.id}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={isCurrent ? 'solid' : isAccessible ? 'outline' : 'ghost'}
|
variant={isCurrent ? 'solid' : isAccessible ? 'outline' : 'ghost'}
|
||||||
colorScheme={isCurrent ? 'teal' : 'gray'}
|
colorScheme={isCurrent ? 'teal' : isSkipped ? 'gray' : 'gray'}
|
||||||
// @ts-expect-error Chakra UI v2 uses isDisabled
|
// @ts-expect-error Chakra UI v2 uses isDisabled
|
||||||
isDisabled={!isAccessible}
|
isDisabled={!isAccessible}
|
||||||
onClick={() => isAccessible && handleNavigateToTask(t.id)}
|
onClick={() => isAccessible && handleNavigateToTask(t.id)}
|
||||||
minW="40px"
|
minW="40px"
|
||||||
opacity={isAccessible ? 1 : 0.5}
|
opacity={isAccessible ? (isSkipped ? 0.6 : 1) : 0.5}
|
||||||
cursor={isAccessible ? 'pointer' : 'not-allowed'}
|
cursor={isAccessible ? 'pointer' : 'not-allowed'}
|
||||||
|
bg={isSkipped && !isCurrent ? 'gray.200' : undefined}
|
||||||
|
color={isSkipped && !isCurrent ? 'gray.500' : undefined}
|
||||||
|
_hover={isAccessible ? (isSkipped ? { bg: 'gray.300' } : undefined) : undefined}
|
||||||
>
|
>
|
||||||
{isAccessible ? taskIndex + 1 : `🔒${taskIndex + 1}`}
|
{isAccessible ? taskIndex + 1 : `🔒${taskIndex + 1}`}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -202,7 +259,7 @@ export const TaskPage = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TaskWorkspace task={task} onTaskComplete={handleTaskComplete} />
|
<TaskWorkspace task={task} onTaskComplete={handleTaskComplete} onTaskSkip={handleTaskSkip} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class PollingManager {
|
|||||||
constructor(options: PollingOptions = {}) {
|
constructor(options: PollingOptions = {}) {
|
||||||
this.currentDelay = options.initialDelay ?? 2000
|
this.currentDelay = options.initialDelay ?? 2000
|
||||||
this.maxDelay = options.maxDelay ?? 10000
|
this.maxDelay = options.maxDelay ?? 10000
|
||||||
this.multiplier = options.multiplier ?? 1.5
|
this.multiplier = options.multiplier ?? 1.01
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(callback: PollCallback) {
|
async start(callback: PollCallback) {
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ export const STORAGE_KEYS = {
|
|||||||
SELECTED_TASK_ID: 'challengeSelectedTaskId',
|
SELECTED_TASK_ID: 'challengeSelectedTaskId',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// Вспомогательная функция для ключа прогресса цепочки
|
// Вспомогательные функции для ключей
|
||||||
const getFurthestTaskKey = (chainId: string) => `challengeFurthestTask_${chainId}`
|
const getFurthestTaskKey = (chainId: string) => `challengeFurthestTask_${chainId}`
|
||||||
|
const getSkippedTasksKey = (chainId: string) => `challengeSkippedTasks_${chainId}`
|
||||||
|
|
||||||
// Получение значений
|
// Получение значений
|
||||||
export const storage = {
|
export const storage = {
|
||||||
@@ -121,11 +122,11 @@ export const storage = {
|
|||||||
// Очистка всех прогрессов по цепочкам
|
// Очистка всех прогрессов по цепочкам
|
||||||
clearAllChainProgress: (): void => {
|
clearAllChainProgress: (): void => {
|
||||||
if (!isBrowser()) return
|
if (!isBrowser()) return
|
||||||
// Перебираем все ключи localStorage и удаляем те, что начинаются с challengeFurthestTask_
|
// Перебираем все ключи localStorage и удаляем те, что начинаются с challengeFurthestTask_ или challengeSkippedTasks_
|
||||||
const keysToRemove: string[] = []
|
const keysToRemove: string[] = []
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const key = localStorage.key(i)
|
const key = localStorage.key(i)
|
||||||
if (key && key.startsWith('challengeFurthestTask_')) {
|
if (key && (key.startsWith('challengeFurthestTask_') || key.startsWith('challengeSkippedTasks_'))) {
|
||||||
keysToRemove.push(key)
|
keysToRemove.push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,5 +162,43 @@ export const storage = {
|
|||||||
if (!isBrowser()) return
|
if (!isBrowser()) return
|
||||||
localStorage.removeItem(getFurthestTaskKey(chainId))
|
localStorage.removeItem(getFurthestTaskKey(chainId))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Получение пропущенных заданий для цепочки
|
||||||
|
getSkippedTasks: (chainId: string): string[] => {
|
||||||
|
if (!isBrowser()) return []
|
||||||
|
const value = localStorage.getItem(getSkippedTasksKey(chainId))
|
||||||
|
return value ? JSON.parse(value) : []
|
||||||
|
},
|
||||||
|
|
||||||
|
// Добавление задания в список пропущенных
|
||||||
|
addSkippedTask: (chainId: string, taskId: string): void => {
|
||||||
|
if (!isBrowser()) return
|
||||||
|
const skipped = storage.getSkippedTasks(chainId)
|
||||||
|
if (!skipped.includes(taskId)) {
|
||||||
|
skipped.push(taskId)
|
||||||
|
localStorage.setItem(getSkippedTasksKey(chainId), JSON.stringify(skipped))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Удаление задания из списка пропущенных (когда оно выполнено)
|
||||||
|
removeSkippedTask: (chainId: string, taskId: string): void => {
|
||||||
|
if (!isBrowser()) return
|
||||||
|
const skipped = storage.getSkippedTasks(chainId)
|
||||||
|
const filtered = skipped.filter(id => id !== taskId)
|
||||||
|
localStorage.setItem(getSkippedTasksKey(chainId), JSON.stringify(filtered))
|
||||||
|
},
|
||||||
|
|
||||||
|
// Проверка, пропущено ли задание
|
||||||
|
isTaskSkipped: (chainId: string, taskId: string): boolean => {
|
||||||
|
if (!isBrowser()) return false
|
||||||
|
const skipped = storage.getSkippedTasks(chainId)
|
||||||
|
return skipped.includes(taskId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Очистка всех пропущенных заданий цепочки
|
||||||
|
clearSkippedTasks: (chainId: string): void => {
|
||||||
|
if (!isBrowser()) return
|
||||||
|
localStorage.removeItem(getSkippedTasksKey(chainId))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user