init + api use

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-11-03 17:59:08 +03:00
commit e777b57991
52 changed files with 20725 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import React from 'react'
import {
DialogRoot,
DialogContent,
DialogHeader,
DialogTitle,
DialogBody,
DialogFooter,
DialogActionTrigger,
} from '@chakra-ui/react'
import { Button } from '@chakra-ui/react'
interface ConfirmDialogProps {
isOpen: boolean
onClose: () => void
onConfirm: () => void
title: string
message: string
confirmLabel?: string
cancelLabel?: string
isLoading?: boolean
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
onClose,
onConfirm,
title,
message,
confirmLabel = 'Подтвердить',
cancelLabel = 'Отмена',
isLoading = false,
}) => {
return (
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<DialogBody>
{message}
</DialogBody>
<DialogFooter>
<DialogActionTrigger asChild>
<Button variant="outline" onClick={onClose} disabled={isLoading}>
{cancelLabel}
</Button>
</DialogActionTrigger>
<Button
colorPalette="red"
onClick={onConfirm}
loading={isLoading}
>
{confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</DialogRoot>
)
}

View File

@@ -0,0 +1,46 @@
import React from 'react'
import { Box, Text, VStack, Button } from '@chakra-ui/react'
interface EmptyStateProps {
title: string
description?: string
actionLabel?: string
onAction?: () => void
}
export const EmptyState: React.FC<EmptyStateProps> = ({
title,
description,
actionLabel,
onAction,
}) => {
return (
<Box
bg="white"
borderRadius="lg"
borderWidth="2px"
borderColor="gray.200"
borderStyle="dashed"
p={12}
textAlign="center"
>
<VStack gap={4}>
<Text fontSize="4xl">📭</Text>
<Text fontSize="lg" fontWeight="semibold" color="gray.700">
{title}
</Text>
{description && (
<Text color="gray.600" fontSize="sm">
{description}
</Text>
)}
{actionLabel && onAction && (
<Button colorPalette="teal" onClick={onAction} mt={2}>
{actionLabel}
</Button>
)}
</VStack>
</Box>
)
}

View File

@@ -0,0 +1,33 @@
import React from 'react'
import { Box, Text, Button } from '@chakra-ui/react'
interface ErrorAlertProps {
message?: string
onRetry?: () => void
}
export const ErrorAlert: React.FC<ErrorAlertProps> = ({
message = 'Произошла ошибка при загрузке данных',
onRetry,
}) => {
return (
<Box
bg="red.50"
borderWidth="1px"
borderColor="red.200"
borderRadius="lg"
p={6}
textAlign="center"
>
<Text color="red.700" fontWeight="medium" mb={4}>
{message}
</Text>
{onRetry && (
<Button colorPalette="red" size="sm" onClick={onRetry}>
Попробовать снова
</Button>
)}
</Box>
)
}

103
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,103 @@
import React from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Box, Container, Flex, HStack, VStack, Button, Text } from '@chakra-ui/react'
import { useAppSelector } from '../__data__/store'
import { URLs } from '../__data__/urls'
import { keycloak } from '../__data__/kc'
interface LayoutProps {
children: React.ReactNode
}
export const Layout: React.FC<LayoutProps> = ({ children }) => {
const location = useLocation()
const navigate = useNavigate()
const user = useAppSelector((state) => state.user)
const handleLogout = () => {
keycloak.logout()
}
const handleNavigateToPlayer = () => {
navigate(URLs.challengePlayer)
}
const isActive = (path: string) => {
return location.pathname === path
}
const navItems = [
{ label: 'Dashboard', path: URLs.dashboard },
{ label: 'Задания', path: URLs.tasks },
{ label: 'Цепочки', path: URLs.chains },
{ label: 'Пользователи', path: URLs.users },
{ label: 'Попытки', path: URLs.submissions },
]
return (
<Box minH="100vh" bg="gray.50">
{/* Header */}
<Box bg="white" borderBottom="1px" borderColor="gray.200" position="sticky" top={0} zIndex={10}>
<Container maxW="container.xl">
<Flex h="16" alignItems="center" justifyContent="space-between">
<Text fontSize="xl" fontWeight="bold" color="teal.600">
Challenge Admin
</Text>
<HStack gap={4}>
<Button
size="sm"
variant="ghost"
onClick={handleNavigateToPlayer}
>
Открыть проигрыватель
</Button>
{user && (
<HStack gap={2}>
<Text fontSize="sm" color="gray.600">
{user.preferred_username || user.email}
</Text>
<Button
size="sm"
colorPalette="red"
variant="ghost"
onClick={handleLogout}
>
Выйти
</Button>
</HStack>
)}
</HStack>
</Flex>
</Container>
</Box>
{/* Navigation */}
<Box bg="white" borderBottom="1px" borderColor="gray.200">
<Container maxW="container.xl">
<HStack gap={1} py={2}>
{navItems.map((item) => (
<Button
key={item.path}
as={Link}
to={item.path}
size="sm"
variant={isActive(item.path) ? 'solid' : 'ghost'}
colorPalette={isActive(item.path) ? 'teal' : 'gray'}
>
{item.label}
</Button>
))}
</HStack>
</Container>
</Box>
{/* Main Content */}
<Container maxW="container.xl" py={8}>
{children}
</Container>
</Box>
)
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
import { Flex, Spinner, Text, VStack } from '@chakra-ui/react'
interface LoadingSpinnerProps {
message?: string
}
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ message = 'Загрузка...' }) => {
return (
<Flex justify="center" align="center" minH="400px">
<VStack gap={4}>
<Spinner size="xl" color="teal.500" />
<Text color="gray.600">{message}</Text>
</VStack>
</Flex>
)
}

View File

@@ -0,0 +1,37 @@
import React from 'react'
import { Box, Text, Flex, Icon } from '@chakra-ui/react'
interface StatCardProps {
label: string
value: string | number
icon?: React.ReactElement
colorScheme?: string
}
export const StatCard: React.FC<StatCardProps> = ({ label, value, icon, colorScheme = 'teal' }) => {
return (
<Box
bg="white"
p={6}
borderRadius="lg"
boxShadow="sm"
borderWidth="1px"
borderColor="gray.200"
>
<Flex align="center" justify="space-between" mb={2}>
<Text fontSize="sm" color="gray.600" fontWeight="medium">
{label}
</Text>
{icon && (
<Box color={`${colorScheme}.500`}>
{icon}
</Box>
)}
</Flex>
<Text fontSize="3xl" fontWeight="bold" color={`${colorScheme}.600`}>
{value}
</Text>
</Box>
)
}

View File

@@ -0,0 +1,46 @@
import React from 'react'
import { Badge } from '@chakra-ui/react'
import type { SubmissionStatus } from '../types/challenge'
interface StatusBadgeProps {
status: SubmissionStatus
}
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status }) => {
const getColorPalette = () => {
switch (status) {
case 'accepted':
return 'green'
case 'needs_revision':
return 'red'
case 'in_progress':
return 'blue'
case 'pending':
return 'orange'
default:
return 'gray'
}
}
const getLabel = () => {
switch (status) {
case 'accepted':
return 'Принято'
case 'needs_revision':
return 'Доработка'
case 'in_progress':
return 'Проверяется'
case 'pending':
return 'Ожидает'
default:
return status
}
}
return (
<Badge colorPalette={getColorPalette()} variant="subtle">
{getLabel()}
</Badge>
)
}

View File

@@ -0,0 +1,12 @@
import React from 'react'
import { createToaster, Toaster as ChakraToaster } from '@chakra-ui/react'
export const toaster = createToaster({
placement: 'top-end',
duration: 3000,
})
export const Toaster = () => {
return <ChakraToaster toaster={toaster} />
}