Refactor project structure and integrate Redux for state management. Update configuration files for new project name and add Webpack plugins for environment variables. Implement user authentication with Keycloak and create a context for challenge management. Add various components for user interaction, including dashboards and task workspaces. Enhance API integration and add error handling utilities. Introduce analytics and polling mechanisms for improved user experience.
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
This commit is contained in:
+153
-23
@@ -1,28 +1,158 @@
|
||||
import React from 'react'
|
||||
import { Grid, GridItem } from '@chakra-ui/react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
AlertIndicator,
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import type { ChallengeChain, ChallengeTask } from '../../__data__/types'
|
||||
import { useChallenge } from '../../context/ChallengeContext'
|
||||
import { MobileDashboard, PersonalDashboard, TaskWorkspace } from '../../components/personal'
|
||||
|
||||
export const MainPage = () => {
|
||||
const { nickname, personalDashboard, chains, eventEmitter } = useChallenge()
|
||||
const [selectedChain, setSelectedChain] = useState<ChallengeChain | null>(null)
|
||||
const [selectedTask, setSelectedTask] = useState<ChallengeTask | null>(null)
|
||||
const [isOffline, setIsOffline] = useState(() =>
|
||||
typeof navigator !== 'undefined' ? !navigator.onLine : false,
|
||||
)
|
||||
const [notification, setNotification] = useState<{ status: 'success' | 'warning'; title: string; description?: string } | null>(null)
|
||||
const notificationTimeoutRef = useRef<number | null>(null)
|
||||
|
||||
const isTaskSelected = Boolean(selectedChain && selectedTask)
|
||||
|
||||
const pageTitle = useMemo(() => {
|
||||
if (nickname) {
|
||||
return `Привет, ${nickname}!`
|
||||
}
|
||||
return 'Challenge Platform'
|
||||
}, [nickname])
|
||||
|
||||
const handleSelectTask = (task: ChallengeTask, chain: ChallengeChain) => {
|
||||
setSelectedChain(chain)
|
||||
setSelectedTask(task)
|
||||
}
|
||||
|
||||
const handleTaskComplete = () => {
|
||||
if (!selectedChain) return
|
||||
const currentIndex = selectedChain.tasks.findIndex((item) => item.id === selectedTask?.id)
|
||||
const nextTask = currentIndex >= 0 ? selectedChain.tasks[currentIndex + 1] : null
|
||||
|
||||
if (nextTask) {
|
||||
setSelectedTask(nextTask)
|
||||
} else {
|
||||
setSelectedChain(null)
|
||||
setSelectedTask(null)
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackTask = useMemo(() => {
|
||||
if (selectedTask) return selectedTask
|
||||
if (selectedChain) return selectedChain.tasks[0]
|
||||
if (chains.length) return chains[0].tasks[0]
|
||||
return null
|
||||
}, [chains, selectedChain, selectedTask])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = eventEmitter.on('submission_completed', (event) => {
|
||||
const submission = (event.data as any)?.submission
|
||||
const accepted = submission?.status === 'accepted'
|
||||
const title = accepted ? 'Задание принято' : 'Задание требует доработки'
|
||||
const description = submission ? `Попытка №${submission.attemptNumber}` : undefined
|
||||
|
||||
if (notificationTimeoutRef.current) {
|
||||
window.clearTimeout(notificationTimeoutRef.current)
|
||||
}
|
||||
|
||||
setNotification({ status: accepted ? 'success' : 'warning', title, description })
|
||||
notificationTimeoutRef.current = window.setTimeout(() => setNotification(null), 4000)
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
if (notificationTimeoutRef.current) {
|
||||
window.clearTimeout(notificationTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [eventEmitter])
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOffline(false)
|
||||
const handleOffline = () => setIsOffline(true)
|
||||
|
||||
window.addEventListener('online', handleOnline)
|
||||
window.addEventListener('offline', handleOffline)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Grid
|
||||
h="100%"
|
||||
bgColor="gray.300"
|
||||
templateAreas={{
|
||||
md: `"header header"
|
||||
"aside main"
|
||||
"footer footer"`,
|
||||
sm: `"header"
|
||||
"main"
|
||||
"aside"
|
||||
"footer"`,
|
||||
}}
|
||||
gridTemplateRows={{ sm: '1fr', md: '50px 1fr 30px' }}
|
||||
gridTemplateColumns={{ sm: '1fr', md: '150px 1fr' }}
|
||||
gap={4}
|
||||
>
|
||||
<GridItem bgColor="green.100" gridArea="header">header</GridItem>
|
||||
<GridItem bgColor="green.300" gridArea="aside">aside</GridItem>
|
||||
<GridItem bgColor="green.600" gridArea="main" h="100vh">main</GridItem>
|
||||
<GridItem bgColor="green.300" gridArea="footer">footer</GridItem>
|
||||
</Grid>
|
||||
<Box bg="gray.50" minH="100vh" py={8} px={{ base: 4, md: 8 }}>
|
||||
<VStack align="stretch" spacing={8} maxW="1200px" mx="auto">
|
||||
{notification && (
|
||||
<Alert status={notification.status} borderRadius="md">
|
||||
<AlertIndicator />
|
||||
<Box ml={3}>
|
||||
<Text fontWeight="semibold">{notification.title}</Text>
|
||||
{notification.description && <Text fontSize="sm">{notification.description}</Text>}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isOffline && (
|
||||
<Alert status="warning" borderRadius="md">
|
||||
<AlertIndicator />
|
||||
Вы находитесь офлайн. Черновики сохраняются локально и будут отправлены после восстановления связи.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Heading size="lg" mb={1}>
|
||||
{pageTitle}
|
||||
</Heading>
|
||||
<Text color="gray.600">Следите за прогрессом и отправляйте решения в одном месте.</Text>
|
||||
</Box>
|
||||
|
||||
<MobileDashboard />
|
||||
|
||||
<SimpleGrid columns={{ base: 1, xl: 2 }} spacing={6} alignItems="start">
|
||||
<PersonalDashboard onSelectTask={handleSelectTask} />
|
||||
|
||||
<Box position="sticky" top={8} height="fit-content">
|
||||
<Heading size="md" mb={4}>
|
||||
Рабочее пространство
|
||||
</Heading>
|
||||
|
||||
{personalDashboard && isTaskSelected && selectedTask && selectedChain ? (
|
||||
<TaskWorkspace task={selectedTask} onTaskComplete={handleTaskComplete} />
|
||||
) : fallbackTask ? (
|
||||
<TaskWorkspace task={fallbackTask} onTaskComplete={handleTaskComplete} />
|
||||
) : (
|
||||
<Flex
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
borderColor="gray.200"
|
||||
bg="white"
|
||||
height="260px"
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<Text color="gray.500" textAlign="center" px={6}>
|
||||
Нет доступных заданий. Попросите преподавателя открыть цепочку или попробуйте позже.
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user