init + api use
This commit is contained in:
61
src/components/ConfirmDialog.tsx
Normal file
61
src/components/ConfirmDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
46
src/components/EmptyState.tsx
Normal file
46
src/components/EmptyState.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
33
src/components/ErrorAlert.tsx
Normal file
33
src/components/ErrorAlert.tsx
Normal 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
103
src/components/Layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
18
src/components/LoadingSpinner.tsx
Normal file
18
src/components/LoadingSpinner.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
37
src/components/StatCard.tsx
Normal file
37
src/components/StatCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
46
src/components/StatusBadge.tsx
Normal file
46
src/components/StatusBadge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
12
src/components/ui/toaster.tsx
Normal file
12
src/components/ui/toaster.tsx
Normal 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} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user