2025-06-18 18:12:15 +01:00
|
|
|
import React, { useState, useEffect } from 'react';
|
2025-05-29 17:26:32 +01:00
|
|
|
import { Menu, Button, ScrollArea, useMantineTheme, useMantineColorScheme } from '@mantine/core';
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2025-06-19 22:41:05 +01:00
|
|
|
import { supportedLanguages } from '../../i18n';
|
2025-05-29 17:26:32 +01:00
|
|
|
import LanguageIcon from '@mui/icons-material/Language';
|
|
|
|
import styles from './LanguageSelector.module.css';
|
|
|
|
|
2025-06-19 19:47:44 +01:00
|
|
|
const LanguageSelector = () => {
|
2025-05-29 17:26:32 +01:00
|
|
|
const { i18n } = useTranslation();
|
|
|
|
const theme = useMantineTheme();
|
|
|
|
const { colorScheme } = useMantineColorScheme();
|
|
|
|
const [opened, setOpened] = useState(false);
|
2025-06-18 18:12:15 +01:00
|
|
|
const [animationTriggered, setAnimationTriggered] = useState(false);
|
|
|
|
const [isChanging, setIsChanging] = useState(false);
|
|
|
|
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
|
|
|
const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null);
|
2025-05-29 17:26:32 +01:00
|
|
|
|
|
|
|
const languageOptions = Object.entries(supportedLanguages)
|
|
|
|
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
|
|
|
.map(([code, name]) => ({
|
|
|
|
value: code,
|
|
|
|
label: name,
|
|
|
|
}));
|
|
|
|
|
2025-06-18 18:12:15 +01:00
|
|
|
const handleLanguageChange = (value: string, event: React.MouseEvent) => {
|
|
|
|
// Create ripple effect at click position
|
|
|
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
|
|
const x = event.clientX - rect.left;
|
|
|
|
const y = event.clientY - rect.top;
|
|
|
|
|
|
|
|
setRippleEffect({ x, y, key: Date.now() });
|
|
|
|
|
|
|
|
// Start transition animation
|
|
|
|
setIsChanging(true);
|
|
|
|
setPendingLanguage(value);
|
|
|
|
|
|
|
|
// Simulate processing time for smooth transition
|
|
|
|
setTimeout(() => {
|
|
|
|
i18n.changeLanguage(value);
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
setIsChanging(false);
|
|
|
|
setPendingLanguage(null);
|
|
|
|
setOpened(false);
|
|
|
|
|
|
|
|
// Clear ripple effect
|
|
|
|
setTimeout(() => setRippleEffect(null), 100);
|
|
|
|
}, 300);
|
|
|
|
}, 200);
|
2025-05-29 17:26:32 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] ||
|
|
|
|
supportedLanguages['en-GB'];
|
|
|
|
|
2025-06-18 18:12:15 +01:00
|
|
|
// Trigger animation when dropdown opens
|
|
|
|
useEffect(() => {
|
|
|
|
if (opened) {
|
|
|
|
setAnimationTriggered(false);
|
|
|
|
// Small delay to ensure DOM is ready
|
|
|
|
setTimeout(() => setAnimationTriggered(true), 50);
|
|
|
|
}
|
|
|
|
}, [opened]);
|
|
|
|
|
2025-05-29 17:26:32 +01:00
|
|
|
return (
|
2025-06-18 18:12:15 +01:00
|
|
|
<>
|
|
|
|
<style>
|
|
|
|
{`
|
|
|
|
@keyframes ripple-expand {
|
|
|
|
0% {
|
|
|
|
width: 0;
|
|
|
|
height: 0;
|
|
|
|
opacity: 0.6;
|
|
|
|
}
|
|
|
|
50% {
|
|
|
|
opacity: 0.3;
|
|
|
|
}
|
|
|
|
100% {
|
|
|
|
width: 100px;
|
|
|
|
height: 100px;
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`}
|
|
|
|
</style>
|
|
|
|
<Menu
|
|
|
|
opened={opened}
|
|
|
|
onChange={setOpened}
|
|
|
|
width={600}
|
|
|
|
position="bottom-start"
|
|
|
|
offset={8}
|
|
|
|
transitionProps={{
|
|
|
|
transition: 'scale-y',
|
|
|
|
duration: 200,
|
|
|
|
timingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
|
|
|
}}
|
|
|
|
>
|
2025-05-29 17:26:32 +01:00
|
|
|
<Menu.Target>
|
|
|
|
<Button
|
|
|
|
variant="subtle"
|
|
|
|
size="sm"
|
|
|
|
leftSection={<LanguageIcon style={{ fontSize: 18 }} />}
|
|
|
|
styles={{
|
|
|
|
root: {
|
|
|
|
border: 'none',
|
|
|
|
color: colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7],
|
2025-06-18 18:12:15 +01:00
|
|
|
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
2025-05-29 17:26:32 +01:00
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
|
|
|
}
|
|
|
|
},
|
|
|
|
label: {
|
|
|
|
fontSize: '12px',
|
|
|
|
fontWeight: 500,
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<span className={styles.languageText}>
|
|
|
|
{currentLanguage}
|
|
|
|
</span>
|
|
|
|
</Button>
|
|
|
|
</Menu.Target>
|
|
|
|
|
|
|
|
<Menu.Dropdown
|
|
|
|
style={{
|
|
|
|
padding: '12px',
|
|
|
|
borderRadius: '8px',
|
|
|
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
|
|
|
border: colorScheme === 'dark' ? `1px solid ${theme.colors.dark[4]}` : `1px solid ${theme.colors.gray[3]}`,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<ScrollArea h={190} type="scroll">
|
|
|
|
<div className={styles.languageGrid}>
|
2025-06-18 18:12:15 +01:00
|
|
|
{languageOptions.map((option, index) => (
|
2025-05-29 17:26:32 +01:00
|
|
|
<div
|
|
|
|
key={option.value}
|
|
|
|
className={styles.languageItem}
|
2025-06-18 18:12:15 +01:00
|
|
|
style={{
|
|
|
|
opacity: animationTriggered ? 1 : 0,
|
|
|
|
transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)',
|
|
|
|
transition: `opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s, transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s`,
|
|
|
|
}}
|
2025-05-29 17:26:32 +01:00
|
|
|
>
|
|
|
|
<Button
|
|
|
|
variant="subtle"
|
|
|
|
size="sm"
|
|
|
|
fullWidth
|
2025-06-18 18:12:15 +01:00
|
|
|
onClick={(event) => handleLanguageChange(option.value, event)}
|
2025-05-29 17:26:32 +01:00
|
|
|
styles={{
|
|
|
|
root: {
|
|
|
|
borderRadius: '4px',
|
|
|
|
minHeight: '32px',
|
|
|
|
padding: '4px 8px',
|
|
|
|
justifyContent: 'flex-start',
|
2025-06-18 18:12:15 +01:00
|
|
|
position: 'relative',
|
|
|
|
overflow: 'hidden',
|
2025-05-29 17:26:32 +01:00
|
|
|
backgroundColor: option.value === i18n.language ? (
|
|
|
|
colorScheme === 'dark' ? theme.colors.blue[8] : theme.colors.blue[1]
|
|
|
|
) : 'transparent',
|
|
|
|
color: option.value === i18n.language ? (
|
|
|
|
colorScheme === 'dark' ? theme.colors.blue[2] : theme.colors.blue[7]
|
|
|
|
) : (
|
|
|
|
colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7]
|
|
|
|
),
|
2025-06-18 18:12:15 +01:00
|
|
|
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
2025-05-29 17:26:32 +01:00
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: option.value === i18n.language ? (
|
|
|
|
colorScheme === 'dark' ? theme.colors.blue[7] : theme.colors.blue[2]
|
|
|
|
) : (
|
|
|
|
colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1]
|
|
|
|
),
|
2025-06-18 18:12:15 +01:00
|
|
|
transform: 'translateY(-1px)',
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
2025-05-29 17:26:32 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
label: {
|
|
|
|
fontSize: '13px',
|
|
|
|
fontWeight: option.value === i18n.language ? 600 : 400,
|
|
|
|
textAlign: 'left',
|
|
|
|
overflow: 'hidden',
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
whiteSpace: 'nowrap',
|
2025-06-18 18:12:15 +01:00
|
|
|
position: 'relative',
|
|
|
|
zIndex: 2,
|
2025-05-29 17:26:32 +01:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{option.label}
|
2025-06-18 18:12:15 +01:00
|
|
|
|
|
|
|
{/* Ripple effect */}
|
|
|
|
{rippleEffect && pendingLanguage === option.value && (
|
|
|
|
<div
|
|
|
|
key={rippleEffect.key}
|
|
|
|
style={{
|
|
|
|
position: 'absolute',
|
|
|
|
left: rippleEffect.x,
|
|
|
|
top: rippleEffect.y,
|
|
|
|
width: 0,
|
|
|
|
height: 0,
|
|
|
|
borderRadius: '50%',
|
|
|
|
backgroundColor: theme.colors.blue[4],
|
|
|
|
opacity: 0.6,
|
|
|
|
transform: 'translate(-50%, -50%)',
|
|
|
|
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
|
|
zIndex: 1,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
2025-05-29 17:26:32 +01:00
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</ScrollArea>
|
|
|
|
</Menu.Dropdown>
|
2025-06-18 18:12:15 +01:00
|
|
|
</Menu>
|
|
|
|
</>
|
2025-05-29 17:26:32 +01:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default LanguageSelector;
|