UI improvements

This commit is contained in:
Estifo77
2026-08-14 15:03:02 +03:00
parent 9a7b93cfb6
commit 7733fe8efd
5 changed files with 322 additions and 109 deletions

View File

@@ -1,45 +1,118 @@
import { UnstyledButton, useMantineColorScheme, useComputedColorScheme, rem } from '@mantine/core';
import { IconSun, IconMoon } from '@tabler/icons-react';
import {
Menu,
UnstyledButton,
Text,
Group,
rem,
useMantineColorScheme,
useComputedColorScheme,
} from '@mantine/core';
import {
IconSun,
IconMoon,
IconDeviceDesktop,
IconCheck,
} from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
export function ColorSchemeToggle() {
const { t } = useTranslation();
const { setColorScheme } = useMantineColorScheme();
const { colorScheme, setColorScheme } = useMantineColorScheme();
const computed = useComputedColorScheme('light', { getInitialValueInEffect: true });
const isDark = computed === 'dark';
const themes = [
{
value: 'light' as const,
label: t('profile.appearance.light', 'Light'),
icon: IconSun,
color: '#e67e22',
},
{
value: 'dark' as const,
label: t('profile.appearance.dark', 'Dark'),
icon: IconMoon,
color: '#9b59b6',
},
{
value: 'auto' as const,
label: t('profile.appearance.system', 'System'),
icon: IconDeviceDesktop,
color: '#3498db',
},
];
const CurrentIcon = colorScheme === 'auto' ? IconDeviceDesktop : isDark ? IconMoon : IconSun;
return (
<UnstyledButton
aria-label={t('common.toggleTheme')}
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: rem(38),
height: rem(38),
borderRadius: rem(12),
background: 'var(--mantine-color-body)',
color: 'var(--mantine-color-gray-6)',
boxShadow: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transition: 'all 150ms ease',
cursor: 'pointer',
border: 'none',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--mantine-primary-color-light)';
e.currentTarget.style.color = 'var(--mantine-primary-color-6)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-primary-color-2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--mantine-color-body)';
e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)';
}}
>
{isDark ? <IconSun size={19} /> : <IconMoon size={19} />}
</UnstyledButton>
<Menu shadow="lg" width={170} position="bottom-end" withinPortal transitionProps={{ transition: 'pop-top-right', duration: 150 }}>
<Menu.Target>
<UnstyledButton
aria-label={t('common.toggleTheme', 'Toggle light / dark mode')}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: rem(38),
height: rem(38),
borderRadius: rem(12),
background: 'var(--mantine-color-body)',
color: isDark ? 'var(--mantine-color-yellow-4)' : 'var(--mantine-color-blue-6)',
boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)',
transition: 'all 200ms ease',
cursor: 'pointer',
border: 'none',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow =
'0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px var(--mantine-primary-color-3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)';
}}
>
<CurrentIcon size={19} stroke={1.8} />
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown
style={{
borderRadius: rem(14),
padding: rem(6),
boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
backdropFilter: 'blur(8px)',
}}
>
<Menu.Label style={{ fontWeight: 600, fontSize: rem(11), letterSpacing: '0.5px', textTransform: 'uppercase' }}>
{t('profile.appearance.title', 'Appearance')}
</Menu.Label>
{themes.map((theme) => {
const Icon = theme.icon;
const isSelected = colorScheme === theme.value;
return (
<Menu.Item
key={theme.value}
onClick={() => setColorScheme(theme.value)}
leftSection={<Icon size={16} stroke={1.8} style={{ color: isSelected ? 'var(--mantine-primary-color-filled)' : 'currentColor' }} />}
rightSection={
isSelected ? <IconCheck size={16} stroke={2.4} style={{ color: 'var(--mantine-primary-color-filled)' }} /> : undefined
}
style={{
borderRadius: rem(8),
fontWeight: isSelected ? 600 : 400,
backgroundColor: isSelected ? 'var(--mantine-primary-color-light)' : undefined,
color: isSelected ? 'var(--mantine-primary-color-filled)' : undefined,
}}
>
<Text size="sm">{theme.label}</Text>
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
);
}

View File

@@ -1,83 +1,179 @@
import { Menu, UnstyledButton, Text, rem } from '@mantine/core';
import { IconWorld, IconCheck, IconChevronDown } from '@tabler/icons-react';
import type { ComponentType, SVGProps } from 'react';
import { Menu, UnstyledButton, Text, Group, rem } from '@mantine/core';
import { IconCheck, IconChevronDown, IconWorld } from '@tabler/icons-react';
import { GB, ET } from 'country-flag-icons/react/3x2';
import { useTranslation } from 'react-i18next';
interface LanguageSwitcherProps {
supportedLanguages: readonly string[];
/** `icon` renders a compact globe button; `button` shows the language label. */
/** `icon` renders a compact flag button; `button` shows the language label. */
variant?: 'icon' | 'button';
}
export function LanguageSwitcher({ supportedLanguages, variant = 'icon' }: LanguageSwitcherProps) {
type FlagComponentType = ComponentType<SVGProps<SVGSVGElement>>;
const LANGUAGE_CONFIG: Record<
string,
{ Flag: FlagComponentType; nativeName: string; shortCode: string }
> = {
en: { Flag: GB, nativeName: 'English', shortCode: 'EN' },
am: { Flag: ET, nativeName: 'አማርኛ', shortCode: 'AM' },
};
export function LanguageSwitcher({
supportedLanguages,
variant = 'icon',
}: LanguageSwitcherProps) {
const { t, i18n } = useTranslation();
const current = i18n.language;
const current = i18n.language || 'en';
const change = (lng: string) => {
if (lng !== current) i18n.changeLanguage(lng);
};
const currentConfig = LANGUAGE_CONFIG[current] || {
Flag: null,
nativeName: current.toUpperCase(),
shortCode: current.toUpperCase(),
};
const CurrentFlag = currentConfig.Flag;
return (
<Menu shadow="md" width={160} position="bottom-end" withinPortal>
<Menu
shadow="lg"
width={190}
position="bottom-end"
withinPortal
transitionProps={{ transition: 'pop-top-right', duration: 150 }}
>
<Menu.Target>
<UnstyledButton
aria-label={t('language.label')}
aria-label={t('language.label', 'Language')}
style={{
display: 'flex',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: rem(4),
gap: rem(6),
width: variant === 'icon' ? rem(38) : 'auto',
height: rem(38),
padding: variant === 'icon' ? 0 : '0 12px',
borderRadius: rem(12),
background: 'var(--mantine-color-body)',
color: 'var(--mantine-color-gray-6)',
boxShadow: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transition: 'all 150ms ease',
color: 'var(--mantine-color-gray-7)',
boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)',
transition: 'all 200ms ease',
cursor: 'pointer',
border: 'none',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--mantine-primary-color-light)';
e.currentTarget.style.color = 'var(--mantine-primary-color-6)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-primary-color-2)';
'0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px var(--mantine-primary-color-3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--mantine-color-body)';
e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)';
'0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)';
}}
>
<IconWorld size={19} />
{CurrentFlag ? (
<CurrentFlag
style={{
width: rem(20),
height: rem(14),
borderRadius: rem(2),
display: 'block',
boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
}}
/>
) : (
<IconWorld size={19} />
)}
{variant !== 'icon' && (
<>
<Text size="sm" fw={500}>
{t(`language.${current}`)}
<Group gap={6} wrap="nowrap">
<Text size="sm" fw={600}>
{t(`language.${current}`, currentConfig.nativeName)}
</Text>
<IconChevronDown size={14} />
</>
<IconChevronDown size={14} stroke={2} style={{ opacity: 0.7 }} />
</Group>
)}
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown
style={{ borderRadius: rem(12), padding: rem(6) }}
style={{
borderRadius: rem(14),
padding: rem(6),
boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
backdropFilter: 'blur(8px)',
}}
>
<Menu.Label>{t('language.label')}</Menu.Label>
{supportedLanguages.map((lng) => (
<Menu.Item
key={lng}
onClick={() => change(lng)}
rightSection={
current === lng ? <IconCheck size={16} /> : undefined
}
style={{ borderRadius: rem(8) }}
>
{t(`language.${lng}`)}
</Menu.Item>
))}
<Menu.Label
style={{
fontWeight: 600,
fontSize: rem(11),
letterSpacing: '0.5px',
textTransform: 'uppercase',
}}
>
{t('language.label', 'Language')}
</Menu.Label>
{supportedLanguages.map((lng) => {
const config = LANGUAGE_CONFIG[lng] || {
Flag: null,
nativeName: lng.toUpperCase(),
shortCode: lng.toUpperCase(),
};
const Flag = config.Flag;
const isSelected = current === lng;
return (
<Menu.Item
key={lng}
onClick={() => change(lng)}
leftSection={
Flag ? (
<Flag
style={{
width: rem(20),
height: rem(14),
borderRadius: rem(2),
display: 'block',
boxShadow: '0 1px 2px rgba(0,0,0,0.15)',
}}
/>
) : (
<IconWorld size={16} />
)
}
rightSection={
isSelected ? (
<IconCheck
size={16}
stroke={2.4}
style={{ color: 'var(--mantine-primary-color-filled)' }}
/>
) : undefined
}
style={{
borderRadius: rem(8),
fontWeight: isSelected ? 600 : 400,
backgroundColor: isSelected
? 'var(--mantine-primary-color-light)'
: undefined,
color: isSelected ? 'var(--mantine-primary-color-filled)' : undefined,
}}
>
<Group justify="space-between" wrap="nowrap" style={{ width: '100%' }}>
<Text size="sm">{t(`language.${lng}`, config.nativeName)}</Text>
</Group>
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
);
}