Добавлены новые зависимости: "react-select" и "@floating-ui/core". Реализована локализация с использованием i18next, добавлены переводы для английского и русского языков. Обновлены компоненты для поддержки локализации, включая AppHeader, Attendance, Dashboard и другие. Улучшена логика отображения данных и взаимодействия с пользователем.
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
IconButton,
|
||||
useColorMode,
|
||||
Button,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import { MoonIcon, SunIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AppHeaderProps {
|
||||
serviceMenuContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
@@ -12,7 +16,13 @@ interface AppHeaderProps {
|
||||
|
||||
export const AppHeader = ({ serviceMenuContainerRef }: AppHeaderProps) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language === 'ru' ? 'en' : 'ru';
|
||||
i18n.changeLanguage(newLang);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
as="header"
|
||||
@@ -28,14 +38,33 @@ export const AppHeader = ({ serviceMenuContainerRef }: AppHeaderProps) => {
|
||||
boxShadow="sm"
|
||||
>
|
||||
{serviceMenuContainerRef && <div id="dots" ref={serviceMenuContainerRef}></div>}
|
||||
|
||||
<IconButton
|
||||
aria-label={colorMode === 'light' ? 'Включить темную тему' : 'Включить светлую тему'}
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size="md"
|
||||
/>
|
||||
<Box>
|
||||
|
||||
</Box>
|
||||
<HStack spacing={4}>
|
||||
<Button
|
||||
onClick={toggleLanguage}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={i18n.language === 'ru'
|
||||
? t('journal.pl.lang.switchToEn')
|
||||
: t('journal.pl.lang.switchToRu')
|
||||
}
|
||||
>
|
||||
{i18n.language === 'ru' ? 'EN' : 'RU'}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
aria-label={colorMode === 'light'
|
||||
? t('journal.pl.theme.switchDark')
|
||||
: t('journal.pl.theme.switchLight')
|
||||
}
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size="md"
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,18 @@
|
||||
import { Alert } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// Компонент-обертка для использования хука useTranslation внутри классового компонента
|
||||
const ErrorMessage = ({ error }: { error: string | null }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Alert status="error" title={t('journal.pl.common.error')}>
|
||||
{t('journal.pl.common.error.something')}<br />
|
||||
{error && <span>{error}</span>}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<
|
||||
React.PropsWithChildren,
|
||||
@@ -13,12 +26,7 @@ export class ErrorBoundary extends React.Component<
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Alert status="error" title="Ошибка">
|
||||
Что-то пошло не так<br />
|
||||
{this.state.error && <span>{this.state.error}</span>}
|
||||
</Alert>
|
||||
)
|
||||
return <ErrorMessage error={this.state.error} />
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
|
||||
165
src/components/user-select/index.tsx
Normal file
165
src/components/user-select/index.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Select, { components } from 'react-select';
|
||||
import { Avatar, HStack, Box, Text, useColorMode } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getGravatarURL } from '../../utils/gravatar';
|
||||
|
||||
// Кастомный компонент для отображения опций с аватарами
|
||||
const Option = ({ children, ...props }: any) => {
|
||||
const { email, picture, value } = props.data;
|
||||
const avatarUrl = picture || getGravatarURL(email);
|
||||
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<HStack spacing={2}>
|
||||
<Avatar size="xs" src={avatarUrl} name={value} />
|
||||
<span>{children}</span>
|
||||
</HStack>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
// Кастомный компонент для отображения выбранных значений с аватарами
|
||||
const SingleValue = ({ children, ...props }: any) => {
|
||||
const { email, picture, value } = props.data;
|
||||
const avatarUrl = picture || getGravatarURL(email);
|
||||
|
||||
return (
|
||||
<components.SingleValue {...props}>
|
||||
<HStack spacing={2}>
|
||||
<Avatar size="xs" src={avatarUrl} name={value} />
|
||||
<span>{children}</span>
|
||||
</HStack>
|
||||
</components.SingleValue>
|
||||
);
|
||||
};
|
||||
|
||||
// Кастомный компонент для отображения множественных выбранных значений с аватарами
|
||||
const MultiValue = ({ children, ...props }: any) => {
|
||||
const { email, picture, value } = props.data;
|
||||
const avatarUrl = picture || getGravatarURL(email);
|
||||
|
||||
return (
|
||||
<components.MultiValue {...props}>
|
||||
<HStack spacing={2}>
|
||||
<Avatar size="xs" src={avatarUrl} name={value} />
|
||||
<span>{children}</span>
|
||||
</HStack>
|
||||
</components.MultiValue>
|
||||
);
|
||||
};
|
||||
|
||||
interface UserSelectProps {
|
||||
isMulti?: boolean;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface MockUserData {
|
||||
value: string;
|
||||
label: string;
|
||||
email: string;
|
||||
sub: string;
|
||||
}
|
||||
|
||||
const UserSelect = ({ isMulti = false, value, onChange, placeholder }: UserSelectProps) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [options, setOptions] = useState<MockUserData[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// В реальном приложении здесь будет запрос к API для получения списка пользователей
|
||||
const mockUserData: MockUserData[] = [
|
||||
{ value: 'Иван Иванов', label: 'Иван Иванов', email: 'ivan@example.com', sub: '1' },
|
||||
{ value: 'Мария Петрова', label: 'Мария Петрова', email: 'maria@example.com', sub: '2' },
|
||||
{ value: 'Алексей Сидоров', label: 'Алексей Сидоров', email: 'alexey@example.com', sub: '3' },
|
||||
{ value: 'Екатерина Смирнова', label: 'Екатерина Смирнова', email: 'ekaterina@example.com', sub: '4' },
|
||||
{ value: 'Дмитрий Козлов', label: 'Дмитрий Козлов', email: 'dmitry@example.com', sub: '5' },
|
||||
{ value: 'Ольга Новикова', label: 'Ольга Новикова', email: 'olga@example.com', sub: '6' },
|
||||
{ value: 'Сергей Морозов', label: 'Сергей Морозов', email: 'sergey@example.com', sub: '7' },
|
||||
{ value: 'Анна Волкова', label: 'Анна Волкова', email: 'anna@example.com', sub: '8' },
|
||||
{ value: 'Павел Соловьев', label: 'Павел Соловьев', email: 'pavel@example.com', sub: '9' },
|
||||
{ value: 'Наталья Лебедева', label: 'Наталья Лебедева', email: 'natalia@example.com', sub: '10' },
|
||||
];
|
||||
|
||||
// Mock data на английском языке
|
||||
const mockEnUserData: MockUserData[] = [
|
||||
{ value: 'John Smith', label: 'John Smith', email: 'john@example.com', sub: '1' },
|
||||
{ value: 'Mary Johnson', label: 'Mary Johnson', email: 'mary@example.com', sub: '2' },
|
||||
{ value: 'Alex Brown', label: 'Alex Brown', email: 'alex@example.com', sub: '3' },
|
||||
{ value: 'Kate Williams', label: 'Kate Williams', email: 'kate@example.com', sub: '4' },
|
||||
{ value: 'David Miller', label: 'David Miller', email: 'david@example.com', sub: '5' },
|
||||
{ value: 'Olivia Jones', label: 'Olivia Jones', email: 'olivia@example.com', sub: '6' },
|
||||
{ value: 'Steven Davis', label: 'Steven Davis', email: 'steven@example.com', sub: '7' },
|
||||
{ value: 'Anna Wilson', label: 'Anna Wilson', email: 'anna_w@example.com', sub: '8' },
|
||||
{ value: 'Paul Taylor', label: 'Paul Taylor', email: 'paul@example.com', sub: '9' },
|
||||
{ value: 'Natalie Moore', label: 'Natalie Moore', email: 'natalie@example.com', sub: '10' },
|
||||
];
|
||||
|
||||
setOptions(i18n.language === 'ru' ? mockUserData : mockEnUserData);
|
||||
}, [i18n.language]);
|
||||
|
||||
const customStyles = {
|
||||
control: (provided: any) => ({
|
||||
...provided,
|
||||
backgroundColor: colorMode === 'dark' ? '#2D3748' : 'white',
|
||||
borderColor: colorMode === 'dark' ? '#4A5568' : '#E2E8F0',
|
||||
}),
|
||||
menu: (provided: any) => ({
|
||||
...provided,
|
||||
backgroundColor: colorMode === 'dark' ? '#2D3748' : 'white',
|
||||
}),
|
||||
option: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isFocused
|
||||
? colorMode === 'dark'
|
||||
? '#4A5568'
|
||||
: '#EDF2F7'
|
||||
: colorMode === 'dark'
|
||||
? '#2D3748'
|
||||
: 'white',
|
||||
color: colorMode === 'dark' ? 'white' : 'black',
|
||||
}),
|
||||
multiValue: (provided: any) => ({
|
||||
...provided,
|
||||
backgroundColor: colorMode === 'dark' ? '#4A5568' : '#EDF2F7',
|
||||
}),
|
||||
multiValueLabel: (provided: any) => ({
|
||||
...provided,
|
||||
color: colorMode === 'dark' ? 'white' : 'black',
|
||||
}),
|
||||
multiValueRemove: (provided: any) => ({
|
||||
...provided,
|
||||
color: colorMode === 'dark' ? 'white' : 'black',
|
||||
':hover': {
|
||||
backgroundColor: colorMode === 'dark' ? '#718096' : '#CBD5E0',
|
||||
color: colorMode === 'dark' ? 'white' : 'black',
|
||||
},
|
||||
}),
|
||||
singleValue: (provided: any) => ({
|
||||
...provided,
|
||||
color: colorMode === 'dark' ? 'white' : 'black',
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti={isMulti}
|
||||
components={{ Option, SingleValue, MultiValue }}
|
||||
styles={customStyles}
|
||||
placeholder={placeholder || t('journal.pl.userSelect.placeholder')}
|
||||
noOptionsMessage={() => t('journal.pl.userSelect.noOptions')}
|
||||
formatGroupLabel={(data) => (
|
||||
<Box>
|
||||
<Text fontWeight="bold">{data.label}</Text>
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSelect;
|
||||
Reference in New Issue
Block a user