mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: enhance LanguageSwitch component with language toggle functionality and improve dropdown behavior; update UserProfileMenu styles (#248)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LanguageIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
@@ -13,7 +16,7 @@ export const LanguageIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) =>
|
||||
strokeWidth={2}
|
||||
{...props}
|
||||
>
|
||||
<title>Language</title>
|
||||
<title>{t('common.language')}</title>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2 12h20" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ThemeSwitch from '@/components/ui/ThemeSwitch';
|
||||
import LanguageSwitch from '@/components/ui/LanguageSwitch';
|
||||
import GitHubIcon from '@/components/icons/GitHubIcon';
|
||||
import SponsorDialog from '@/components/ui/SponsorDialog';
|
||||
import WeChatDialog from '@/components/ui/WeChatDialog';
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar: () => void;
|
||||
@@ -12,8 +10,6 @@ interface HeaderProps {
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
||||
const { t } = useTranslation();
|
||||
const [sponsorDialogOpen, setSponsorDialogOpen] = useState(false);
|
||||
const [wechatDialogOpen, setWechatDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="bg-white dark:bg-gray-800 shadow-sm z-10">
|
||||
@@ -51,38 +47,10 @@ const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
||||
>
|
||||
<GitHubIcon className="h-5 w-5" />
|
||||
</a>
|
||||
{/* {i18n.language === 'zh' ? (
|
||||
<button
|
||||
onClick={() => setWechatDialogOpen(true)}
|
||||
className="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
||||
aria-label={t('wechat.label')}
|
||||
>
|
||||
<WeChatIcon className="h-5 w-5" />
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href="https://discord.gg/qMKNsn5Q"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
aria-label={t('discord.label')}
|
||||
>
|
||||
<DiscordIcon className="h-5 w-5" />
|
||||
</a>
|
||||
)} */}
|
||||
{/* <button
|
||||
onClick={() => setSponsorDialogOpen(true)}
|
||||
className="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
||||
aria-label={t('sponsor.label')}
|
||||
>
|
||||
<SponsorIcon className="h-5 w-5" />
|
||||
</button> */}
|
||||
<ThemeSwitch />
|
||||
<LanguageSwitch />
|
||||
</div>
|
||||
</div>
|
||||
<SponsorDialog open={sponsorDialogOpen} onOpenChange={setSponsorDialogOpen} />
|
||||
<WeChatDialog open={wechatDialogOpen} onOpenChange={setWechatDialogOpen} />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,13 +7,21 @@ const LanguageSwitch: React.FC = () => {
|
||||
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false);
|
||||
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
|
||||
|
||||
// Available languages
|
||||
const availableLanguages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'zh', label: '中文' }
|
||||
];
|
||||
|
||||
// Update current language when it changes
|
||||
useEffect(() => {
|
||||
setCurrentLanguage(i18n.language);
|
||||
}, [i18n.language]);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
// Close dropdown when clicking outside (only needed when more than 2 languages)
|
||||
useEffect(() => {
|
||||
if (availableLanguages.length <= 2) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.language-dropdown')) {
|
||||
@@ -28,7 +36,7 @@ const LanguageSwitch: React.FC = () => {
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [languageDropdownOpen]);
|
||||
}, [languageDropdownOpen, availableLanguages.length]);
|
||||
|
||||
const handleLanguageChange = (lang: string) => {
|
||||
localStorage.setItem('i18nextLng', lang);
|
||||
@@ -36,37 +44,47 @@ const LanguageSwitch: React.FC = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// Toggle between languages when only two options are available
|
||||
const handleLanguageToggle = () => {
|
||||
if (availableLanguages.length === 2) {
|
||||
// Direct toggle between two languages
|
||||
const currentLangCode = currentLanguage.startsWith('zh') ? 'zh' : 'en';
|
||||
const nextLang = availableLanguages.find(lang => lang.code !== currentLangCode);
|
||||
if (nextLang) {
|
||||
handleLanguageChange(nextLang.code);
|
||||
}
|
||||
} else {
|
||||
// Show dropdown for multiple languages
|
||||
setLanguageDropdownOpen(!languageDropdownOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative language-dropdown">
|
||||
<button
|
||||
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
|
||||
onClick={handleLanguageToggle}
|
||||
className="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
||||
aria-label="Language Switcher"
|
||||
>
|
||||
<LanguageIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{languageDropdownOpen && (
|
||||
{/* Only show dropdown if there are more than 2 languages or if explicitly opened */}
|
||||
{languageDropdownOpen && availableLanguages.length > 2 && (
|
||||
<div className="absolute right-0 mt-2 w-24 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
||||
<div>
|
||||
<button
|
||||
onClick={() => handleLanguageChange('en')}
|
||||
className={`flex items-center w-full px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${currentLanguage.startsWith('en')
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
English
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleLanguageChange('zh')}
|
||||
className={`flex items-center w-full px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${currentLanguage.startsWith('zh')
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
中文
|
||||
</button>
|
||||
{availableLanguages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => handleLanguageChange(lang.code)}
|
||||
className={`flex items-center w-full px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${currentLanguage.startsWith(lang.code)
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
{lang.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,11 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { User, Settings, LogOut, Info } from 'lucide-react';
|
||||
import AboutDialog from './AboutDialog';
|
||||
import SponsorDialog from './SponsorDialog';
|
||||
import WeChatDialog from './WeChatDialog';
|
||||
import WeChatIcon from '@/components/icons/WeChatIcon';
|
||||
import DiscordIcon from '@/components/icons/DiscordIcon';
|
||||
import SponsorIcon from '@/components/icons/SponsorIcon';
|
||||
import { checkLatestVersion, compareVersions } from '@/utils/version';
|
||||
|
||||
interface UserProfileMenuProps {
|
||||
@@ -12,12 +17,14 @@ interface UserProfileMenuProps {
|
||||
}
|
||||
|
||||
const UserProfileMenu: React.FC<UserProfileMenuProps> = ({ collapsed, version }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { auth, logout } = useAuth();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showNewVersionInfo, setShowNewVersionInfo] = useState(false);
|
||||
const [showAboutDialog, setShowAboutDialog] = useState(false);
|
||||
const [sponsorDialogOpen, setSponsorDialogOpen] = useState(false);
|
||||
const [wechatDialogOpen, setWechatDialogOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Check for new version on login and component mount
|
||||
@@ -65,6 +72,16 @@ const UserProfileMenu: React.FC<UserProfileMenuProps> = ({ collapsed, version })
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleSponsorClick = () => {
|
||||
setSponsorDialogOpen(true);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleWeChatClick = () => {
|
||||
setWechatDialogOpen(true);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
@@ -90,7 +107,35 @@ const UserProfileMenu: React.FC<UserProfileMenuProps> = ({ collapsed, version })
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-0 transform -translate-y-full left-0 w-full min-w-max bg-white border border-gray-200 dark:bg-gray-800 py-1 z-50">
|
||||
<div className="absolute top-0 transform -translate-y-full left-0 w-full min-w-max bg-white border border-gray-200 dark:bg-gray-800 z-50">
|
||||
<button
|
||||
onClick={handleSponsorClick}
|
||||
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<SponsorIcon className="h-4 w-4 mr-2" />
|
||||
{t('sponsor.label')}
|
||||
</button>
|
||||
|
||||
{i18n.language === 'zh' ? (
|
||||
<button
|
||||
onClick={handleWeChatClick}
|
||||
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<WeChatIcon className="h-4 w-4 mr-2" />
|
||||
{t('wechat.label')}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href="https://discord.gg/qMKNsn5Q"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<DiscordIcon className="h-4 w-4 mr-2" />
|
||||
{t('discord.label')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleSettingsClick}
|
||||
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
@@ -108,6 +153,9 @@ const UserProfileMenu: React.FC<UserProfileMenuProps> = ({ collapsed, version })
|
||||
<span className="absolute top-2 right-4 block w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-600"></div>
|
||||
|
||||
<button
|
||||
onClick={handleLogoutClick}
|
||||
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
@@ -124,6 +172,12 @@ const UserProfileMenu: React.FC<UserProfileMenuProps> = ({ collapsed, version })
|
||||
onClose={() => setShowAboutDialog(false)}
|
||||
version={version}
|
||||
/>
|
||||
|
||||
{/* Sponsor dialog */}
|
||||
<SponsorDialog open={sponsorDialogOpen} onOpenChange={setSponsorDialogOpen} />
|
||||
|
||||
{/* WeChat dialog */}
|
||||
<WeChatDialog open={wechatDialogOpen} onOpenChange={setWechatDialogOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -186,7 +186,8 @@
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"copyFailed": "Copy failed",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm"
|
||||
"confirm": "Confirm",
|
||||
"language": "Language"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
@@ -187,7 +187,8 @@
|
||||
"copySuccess": "已复制到剪贴板",
|
||||
"copyFailed": "复制失败",
|
||||
"close": "关闭",
|
||||
"confirm": "确认"
|
||||
"confirm": "确认",
|
||||
"language": "语言"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "仪表盘",
|
||||
|
||||
Reference in New Issue
Block a user