Stirling-PDF/frontend/src/components/shared/LanguageSelector.tsx

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

219 lines
7.7 KiB
TypeScript
Raw Normal View History

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;