Добавлены новые зависимости: "react-select" и "@floating-ui/core". Реализована локализация с использованием i18next, добавлены переводы для английского и русского языков. Обновлены компоненты для поддержки локализации, включая AppHeader, Attendance, Dashboard и другие. Улучшена логика отображения данных и взаимодействия с пользователем.

This commit is contained in:
2025-03-23 11:41:29 +03:00
parent d5b5838e51
commit d3a7f70d12
27 changed files with 995 additions and 191 deletions

View File

@@ -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>
);
};

View File

@@ -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

View 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;