mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-22 19:46:39 +00:00
Disable language selection for everything other than English GB (#4467)
# Description of Changes For the first release of V2, we'll not have any reasonable translations for anything other than English GB, so with that in mind, this PR disables language selection for anything other than English GB, with a tooltip saying the other languages are coming soon. I also split the JSX up a little bit while I was at it to make it easier to manage. --------- Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
parent
9cbd1f7f0c
commit
64beb4d076
@ -104,6 +104,7 @@
|
||||
"green": "Green",
|
||||
"blue": "Blue",
|
||||
"custom": "Custom...",
|
||||
"comingSoon": "Coming soon",
|
||||
"WorkInProgess": "Work in progress, May not work or be buggy, Please report any problems!",
|
||||
"poweredBy": "Powered by",
|
||||
"yes": "Yes",
|
||||
|
@ -1,24 +1,161 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Menu, Button, ScrollArea, ActionIcon } from '@mantine/core';
|
||||
import { Menu, Button, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../../i18n';
|
||||
import LocalIcon from './LocalIcon';
|
||||
import styles from './LanguageSelector.module.css';
|
||||
|
||||
// Types
|
||||
interface LanguageSelectorProps {
|
||||
position?: React.ComponentProps<typeof Menu>['position'];
|
||||
offset?: number;
|
||||
compact?: boolean; // icon-only trigger
|
||||
}
|
||||
|
||||
const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = false }: LanguageSelectorProps) => {
|
||||
interface LanguageOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RippleEffect {
|
||||
x: number;
|
||||
y: number;
|
||||
key: number;
|
||||
}
|
||||
|
||||
// Sub-components
|
||||
interface LanguageItemProps {
|
||||
option: LanguageOption;
|
||||
index: number;
|
||||
animationTriggered: boolean;
|
||||
isSelected: boolean;
|
||||
onClick: (event: React.MouseEvent) => void;
|
||||
rippleEffect?: RippleEffect | null;
|
||||
pendingLanguage: string | null;
|
||||
compact: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LanguageItem: React.FC<LanguageItemProps> = ({
|
||||
option,
|
||||
index,
|
||||
animationTriggered,
|
||||
isSelected,
|
||||
onClick,
|
||||
rippleEffect,
|
||||
pendingLanguage,
|
||||
compact,
|
||||
disabled = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const label = disabled ? (
|
||||
<Tooltip label={t('comingSoon', 'Coming soon')} position="left" withArrow>
|
||||
<p>{option.label}</p>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<p>{option.label}</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.languageItem}
|
||||
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`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
fullWidth
|
||||
onClick={disabled ? undefined : onClick}
|
||||
data-selected={isSelected}
|
||||
disabled={disabled}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: '4px',
|
||||
minHeight: '32px',
|
||||
padding: '4px 8px',
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: isSelected
|
||||
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
|
||||
: 'transparent',
|
||||
color: disabled
|
||||
? 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))'
|
||||
: isSelected
|
||||
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
|
||||
: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
|
||||
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
'&:hover': !disabled ? {
|
||||
backgroundColor: isSelected
|
||||
? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
|
||||
: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
} : {}
|
||||
},
|
||||
label: {
|
||||
fontSize: '13px',
|
||||
fontWeight: isSelected ? 600 : 400,
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{!compact && rippleEffect && pendingLanguage === option.value && (
|
||||
<div
|
||||
key={rippleEffect.key}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: rippleEffect.x,
|
||||
top: rippleEffect.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--mantine-color-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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RippleStyles: React.FC = () => (
|
||||
<style>
|
||||
{`
|
||||
@keyframes ripple-expand {
|
||||
0% { width: 0; height: 0; opacity: 0.6; }
|
||||
50% { opacity: 0.3; }
|
||||
100% { width: 100px; height: 100px; opacity: 0; }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
);
|
||||
|
||||
// Main component
|
||||
const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-start', offset = 8, compact = false }) => {
|
||||
const { i18n } = useTranslation();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [animationTriggered, setAnimationTriggered] = useState(false);
|
||||
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
||||
const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null);
|
||||
const [rippleEffect, setRippleEffect] = useState<RippleEffect | null>(null);
|
||||
|
||||
const languageOptions = Object.entries(supportedLanguages)
|
||||
const languageOptions: LanguageOption[] = Object.entries(supportedLanguages)
|
||||
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
||||
.map(([code, name]) => ({
|
||||
value: code,
|
||||
@ -65,15 +202,7 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
@keyframes ripple-expand {
|
||||
0% { width: 0; height: 0; opacity: 0.6; }
|
||||
50% { opacity: 0.3; }
|
||||
100% { width: 100px; height: 100px; opacity: 0; }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<RippleStyles />
|
||||
<Menu
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
@ -86,138 +215,85 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
||||
timingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||
}}
|
||||
>
|
||||
<Menu.Target>
|
||||
{compact ? (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
title={currentLanguage}
|
||||
className="right-rail-icon"
|
||||
styles={{
|
||||
root: {
|
||||
color: 'var(--right-rail-icon)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||
<Menu.Target>
|
||||
{compact ? (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
title={currentLanguage}
|
||||
className="right-rail-icon"
|
||||
styles={{
|
||||
root: {
|
||||
color: 'var(--right-rail-icon)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LocalIcon icon="language" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leftSection={<LocalIcon icon="language" width="1.5rem" height="1.5rem" />}
|
||||
styles={{
|
||||
root: {
|
||||
border: 'none',
|
||||
color: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))',
|
||||
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||
}
|
||||
},
|
||||
label: { fontSize: '12px', fontWeight: 500 }
|
||||
}}
|
||||
>
|
||||
<span className={styles.languageText}>
|
||||
{currentLanguage}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</Menu.Target>
|
||||
}}
|
||||
>
|
||||
<LocalIcon icon="language" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leftSection={<LocalIcon icon="language" width="1.5rem" height="1.5rem" />}
|
||||
styles={{
|
||||
root: {
|
||||
border: 'none',
|
||||
color: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))',
|
||||
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||
}
|
||||
},
|
||||
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)',
|
||||
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||
border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
|
||||
}}
|
||||
>
|
||||
<ScrollArea h={190} type="scroll">
|
||||
<div className={styles.languageGrid}>
|
||||
{languageOptions.map((option, index) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={styles.languageItem}
|
||||
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`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
fullWidth
|
||||
onClick={(event) => handleLanguageChange(option.value, event)}
|
||||
data-selected={option.value === i18n.language}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: '4px',
|
||||
minHeight: '32px',
|
||||
padding: '4px 8px',
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: option.value === i18n.language
|
||||
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
|
||||
: 'transparent',
|
||||
color: option.value === i18n.language
|
||||
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
|
||||
: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
|
||||
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
'&:hover': {
|
||||
backgroundColor: option.value === i18n.language
|
||||
? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
|
||||
: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
}
|
||||
},
|
||||
label: {
|
||||
fontSize: '13px',
|
||||
fontWeight: option.value === i18n.language ? 600 : 400,
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
{!compact && rippleEffect && pendingLanguage === option.value && (
|
||||
<div
|
||||
key={rippleEffect.key}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: rippleEffect.x,
|
||||
top: rippleEffect.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--mantine-color-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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Menu.Dropdown>
|
||||
<Menu.Dropdown
|
||||
style={{
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||
border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
|
||||
}}
|
||||
>
|
||||
<ScrollArea h={190} type="scroll">
|
||||
<div className={styles.languageGrid}>
|
||||
{languageOptions.map((option, index) => {
|
||||
const isEnglishGB = option.value === 'en-GB'; // Currently only English GB has enough translations to use
|
||||
const isDisabled = !isEnglishGB;
|
||||
|
||||
return (
|
||||
<LanguageItem
|
||||
key={option.value}
|
||||
option={option}
|
||||
index={index}
|
||||
animationTriggered={animationTriggered}
|
||||
isSelected={option.value === i18n.language}
|
||||
onClick={(event) => handleLanguageChange(option.value, event)}
|
||||
rippleEffect={rippleEffect}
|
||||
pendingLanguage={pendingLanguage}
|
||||
compact={compact}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
||||
export type { LanguageSelectorProps, LanguageOption, RippleEffect };
|
||||
|
Loading…
x
Reference in New Issue
Block a user