Implement language and theme switchers in Header (#247)

This commit is contained in:
samanhappy
2025-07-26 21:46:14 +08:00
committed by GitHub
parent 9304653c34
commit df872823c1
6 changed files with 132 additions and 92 deletions

View File

@@ -0,0 +1,24 @@
import React from 'react';
export const LanguageIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeWidth={2}
{...props}
>
<title>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" />
</svg>
);
};
export default LanguageIcon;

View File

@@ -1,10 +1,8 @@
import React, { useState } 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 SponsorIcon from '@/components/icons/SponsorIcon';
import WeChatIcon from '@/components/icons/WeChatIcon';
import DiscordIcon from '@/components/icons/DiscordIcon';
import SponsorDialog from '@/components/ui/SponsorDialog';
import WeChatDialog from '@/components/ui/WeChatDialog';
@@ -13,7 +11,7 @@ interface HeaderProps {
}
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const [sponsorDialogOpen, setSponsorDialogOpen] = useState(false);
const [wechatDialogOpen, setWechatDialogOpen] = useState(false);
@@ -36,26 +34,27 @@ const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
<h1 className="ml-4 text-xl font-bold text-gray-900 dark:text-white">{t('app.title')}</h1>
</div>
{/* Theme Switch and Version */}
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-500 dark:text-gray-400">
{/* Theme Switch and Language Switcher and Version */}
<div className="flex items-center space-x-1">
<span className="text-sm text-gray-500 dark:text-gray-400 mr-2">
{import.meta.env.PACKAGE_VERSION === 'dev'
? import.meta.env.PACKAGE_VERSION
: `v${import.meta.env.PACKAGE_VERSION}`}
</span>
<a
href="https://github.com/samanhappy/mcphub"
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
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="GitHub Repository"
>
<GitHubIcon className="h-5 w-5" />
</a>
{i18n.language === 'zh' ? (
{/* {i18n.language === 'zh' ? (
<button
onClick={() => setWechatDialogOpen(true)}
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 focus:outline-none"
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" />
@@ -65,20 +64,21 @@ const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
href="https://discord.gg/qMKNsn5Q"
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
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
)} */}
{/* <button
onClick={() => setSponsorDialogOpen(true)}
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 focus:outline-none"
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>
</button> */}
<ThemeSwitch />
<LanguageSwitch />
</div>
</div>
<SponsorDialog open={sponsorDialogOpen} onOpenChange={setSponsorDialogOpen} />

View File

@@ -0,0 +1,77 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import LanguageIcon from '@/components/icons/LanguageIcon';
const LanguageSwitch: React.FC = () => {
const { i18n } = useTranslation();
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false);
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
// Update current language when it changes
useEffect(() => {
setCurrentLanguage(i18n.language);
}, [i18n.language]);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.language-dropdown')) {
setLanguageDropdownOpen(false);
}
};
if (languageDropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [languageDropdownOpen]);
const handleLanguageChange = (lang: string) => {
localStorage.setItem('i18nextLng', lang);
setLanguageDropdownOpen(false);
window.location.reload();
};
return (
<div className="relative language-dropdown">
<button
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
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 && (
<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>
</div>
</div>
)}
</div>
);
};
export default LanguageSwitch;

View File

@@ -7,44 +7,19 @@ const ThemeSwitch: React.FC = () => {
const { t } = useTranslation();
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<div className="flex items-center space-x-2">
<div className="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
<button
onClick={() => setTheme('light')}
className={`flex items-center justify-center rounded-md p-1.5 ${theme === 'light'
? 'bg-white text-yellow-600 shadow'
: 'text-black dark:text-gray-300 hover:text-yellow-600 dark:hover:text-yellow-500'
}`}
title={t('theme.light')}
aria-label={t('theme.light')}
>
<Sun size={18} />
</button>
<button
onClick={() => setTheme('dark')}
className={`flex items-center justify-center rounded-md p-1.5 ${theme === 'dark'
? 'bg-gray-800 text-blue-400 shadow'
: 'text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400'
}`}
title={t('theme.dark')}
aria-label={t('theme.dark')}
>
<Moon size={18} />
</button>
{/* <button
onClick={() => setTheme('system')}
className={`flex items-center justify-center rounded-md p-1.5 ${theme === 'system'
? 'bg-white dark:bg-gray-800 text-green-600 dark:text-green-400 shadow'
: 'text-black dark:text-gray-300 hover:text-green-600 dark:hover:text-green-400'
}`}
title={t('theme.system')}
aria-label={t('theme.system')}
>
<Monitor size={18} />
</button> */}
</div>
</div>
<button
onClick={toggleTheme}
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"
title={theme === 'light' ? t('theme.dark') : t('theme.light')}
aria-label={theme === 'light' ? t('theme.dark') : t('theme.light')}
>
{theme === 'light' ? <Moon className="h-5 w-5" /> : <Sun className="h-5 w-5" />}
</button>
);
};

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import ThemeSwitch from '@/components/ui/ThemeSwitch';
import LanguageSwitch from '@/components/ui/LanguageSwitch';
const LoginPage: React.FC = () => {
const { t } = useTranslation();
@@ -41,8 +42,9 @@ const LoginPage: React.FC = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8 login-container">
<div className="absolute top-4 right-4">
<div className="absolute top-4 right-4 w-full max-w-xs flex justify-end">
<ThemeSwitch />
<LanguageSwitch />
</div>
<div className="max-w-md w-full space-y-8 login-card p-8">
<div>

View File

@@ -10,15 +10,9 @@ import { PermissionChecker } from '@/components/PermissionChecker';
import { PERMISSIONS } from '@/constants/permissions';
const SettingsPage: React.FC = () => {
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const navigate = useNavigate();
const { showToast } = useToast();
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
// Update current language when it changes
useEffect(() => {
setCurrentLanguage(i18n.language);
}, [i18n.language]);
const [installConfig, setInstallConfig] = useState<{
pythonIndexUrl: string;
@@ -197,42 +191,10 @@ const SettingsPage: React.FC = () => {
}, 2000);
};
const handleLanguageChange = (lang: string) => {
localStorage.setItem('i18nextLng', lang);
window.location.reload();
};
return (
<div className="container mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-8">{t('pages.settings.title')}</h1>
{/* Language Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card">
<div className="flex items-center justify-between">
<h2 className="font-semibold text-gray-800">{t('pages.settings.language')}</h2>
<div className="flex space-x-3">
<button
className={`px-3 py-1.5 rounded-md transition-all duration-200 text-sm ${currentLanguage.startsWith('en')
? 'bg-blue-500 text-white btn-primary'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200 btn-secondary'
}`}
onClick={() => handleLanguageChange('en')}
>
English
</button>
<button
className={`px-3 py-1.5 rounded-md transition-all duration-200 text-sm ${currentLanguage.startsWith('zh')
? 'bg-blue-500 text-white btn-primary'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200 btn-secondary'
}`}
onClick={() => handleLanguageChange('zh')}
>
</button>
</div>
</div>
</div>
{/* Smart Routing Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_SMART_ROUTING}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card">