-
-
Save shricodev/6a8eea20c34d31429b254c82079a1972 to your computer and use it in GitHub Desktop.
Test 1: Add a global Action Palette (Ctrl + K) - gpt-5.2-high
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/gpt-5.2-high.patch b/gpt-5.2-high.patch | |
| new file mode 100644 | |
| index 0000000..ac22113 | |
| --- /dev/null | |
| +++ b/gpt-5.2-high.patch | |
| @@ -0,0 +1,445 @@ | |
| +diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json | |
| +index e907627..8c88bb2 100644 | |
| +--- a/public/locales/de/translation.json | |
| ++++ b/public/locales/de/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "Alle Tools durchsuchen", | |
| + "title": "Erledigen Sie Dinge schnell mit" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "Tools oder Aktionen durchsuchen", | |
| ++ "empty": "Keine Treffer gefunden", | |
| ++ "groups": { | |
| ++ "actions": "Aktionen", | |
| ++ "languages": "Sprachen", | |
| ++ "recent": "Zuletzt verwendet", | |
| ++ "tools": "Tools" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "Dunkelmodus umschalten", | |
| ++ "switchLanguageTo": "Sprache auf {{language}} wechseln", | |
| ++ "toggleUserType": "{{userType}}-Tools umschalten", | |
| ++ "goHome": "Zur Startseite", | |
| ++ "goBookmarks": "Zu Lesezeichen", | |
| ++ "clearRecent": "Zuletzt verwendete Tools löschen" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "Lesezeichen", | |
| ++ "subtitle": "Deine gespeicherten Tools an einem Ort", | |
| ++ "empty": "Noch keine Tools gespeichert." | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "Klar", | |
| + "copyToClipboard": "In die Zwischenablage kopieren", | |
| +diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json | |
| +index 98b0d5a..3a3224b 100644 | |
| +--- a/public/locales/en/translation.json | |
| ++++ b/public/locales/en/translation.json | |
| +@@ -109,6 +109,29 @@ | |
| + "searchPlaceholder": "Search all tools", | |
| + "title": "Get Things Done Quickly with" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "Search tools or actions", | |
| ++ "empty": "No matches found", | |
| ++ "groups": { | |
| ++ "actions": "Actions", | |
| ++ "languages": "Languages", | |
| ++ "recent": "Recent", | |
| ++ "tools": "Tools" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "Toggle dark mode", | |
| ++ "switchLanguageTo": "Switch language to {{language}}", | |
| ++ "toggleUserType": "Toggle {{userType}} tools", | |
| ++ "goHome": "Go to Home", | |
| ++ "goBookmarks": "Go to Bookmarks", | |
| ++ "clearRecent": "Clear recently used tools" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "Bookmarks", | |
| ++ "subtitle": "Your saved tools in one place", | |
| ++ "empty": "No bookmarked tools yet." | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "Clear", | |
| + "copyToClipboard": "Copy to clipboard", | |
| +diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json | |
| +index 6ddab10..1d4c5bc 100644 | |
| +--- a/public/locales/es/translation.json | |
| ++++ b/public/locales/es/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "Buscar en todas las herramientas", | |
| + "title": "Haga las cosas rápidamente con" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "Buscar herramientas o acciones", | |
| ++ "empty": "No se encontraron resultados", | |
| ++ "groups": { | |
| ++ "actions": "Acciones", | |
| ++ "languages": "Idiomas", | |
| ++ "recent": "Recientes", | |
| ++ "tools": "Herramientas" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "Alternar modo oscuro", | |
| ++ "switchLanguageTo": "Cambiar idioma a {{language}}", | |
| ++ "toggleUserType": "Alternar herramientas de {{userType}}", | |
| ++ "goHome": "Ir a inicio", | |
| ++ "goBookmarks": "Ir a marcadores", | |
| ++ "clearRecent": "Borrar herramientas usadas recientemente" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "Marcadores", | |
| ++ "subtitle": "Tus herramientas guardadas en un solo lugar", | |
| ++ "empty": "Aún no hay herramientas marcadas." | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "Claro", | |
| + "copyToClipboard": "Copiar al portapapeles", | |
| +diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json | |
| +index 2e27a0c..a29b07a 100644 | |
| +--- a/public/locales/fr/translation.json | |
| ++++ b/public/locales/fr/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "Rechercher tous les outils", | |
| + "title": "Faites avancer les choses rapidement avec" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "Rechercher des outils ou des actions", | |
| ++ "empty": "Aucun résultat", | |
| ++ "groups": { | |
| ++ "actions": "Actions", | |
| ++ "languages": "Langues", | |
| ++ "recent": "Récents", | |
| ++ "tools": "Outils" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "Basculer le mode sombre", | |
| ++ "switchLanguageTo": "Changer la langue en {{language}}", | |
| ++ "toggleUserType": "Basculer les outils {{userType}}", | |
| ++ "goHome": "Aller à l'accueil", | |
| ++ "goBookmarks": "Aller aux favoris", | |
| ++ "clearRecent": "Effacer les outils récents" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "Favoris", | |
| ++ "subtitle": "Vos outils enregistrés au même endroit", | |
| ++ "empty": "Aucun outil enregistré pour l'instant." | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "Clair", | |
| + "copyToClipboard": "Copier dans le presse-papiers", | |
| +diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json | |
| +index f30c7ee..ca249e2 100644 | |
| +--- a/public/locales/hi/translation.json | |
| ++++ b/public/locales/hi/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "सभी टूल्स खोजें", | |
| + "title": "के साथ जल्दी काम करें" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "टूल्स या कार्रवाइयाँ खोजें", | |
| ++ "empty": "कोई परिणाम नहीं मिला", | |
| ++ "groups": { | |
| ++ "actions": "कार्रवाइयाँ", | |
| ++ "languages": "भाषाएँ", | |
| ++ "recent": "हाल के", | |
| ++ "tools": "टूल्स" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "डार्क मोड टॉगल करें", | |
| ++ "switchLanguageTo": "{{language}} में भाषा बदलें", | |
| ++ "toggleUserType": "{{userType}} टूल्स टॉगल करें", | |
| ++ "goHome": "होम पर जाएँ", | |
| ++ "goBookmarks": "बुकमार्क पर जाएँ", | |
| ++ "clearRecent": "हाल ही में उपयोग किए गए टूल्स साफ़ करें" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "बुकमार्क", | |
| ++ "subtitle": "आपके सेव किए गए टूल्स एक जगह", | |
| ++ "empty": "अभी कोई बुकमार्क नहीं है।" | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "साफ़ करें", | |
| + "copyToClipboard": "क्लिपबोर्ड पर कॉपी करें", | |
| +diff --git a/public/locales/ja/translation.json b/public/locales/ja/translation.json | |
| +index 4acaebc..d4e1e64 100644 | |
| +--- a/public/locales/ja/translation.json | |
| ++++ b/public/locales/ja/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "すべてのツールを検索", | |
| + "title": "物事を素早く終わらせる" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "ツールやアクションを検索", | |
| ++ "empty": "一致する結果がありません", | |
| ++ "groups": { | |
| ++ "actions": "アクション", | |
| ++ "languages": "言語", | |
| ++ "recent": "最近", | |
| ++ "tools": "ツール" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "ダークモードを切り替える", | |
| ++ "switchLanguageTo": "言語を{{language}}に切り替える", | |
| ++ "toggleUserType": "{{userType}}向けツールを切り替える", | |
| ++ "goHome": "ホームへ移動", | |
| ++ "goBookmarks": "ブックマークへ移動", | |
| ++ "clearRecent": "最近使ったツールをクリア" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "ブックマーク", | |
| ++ "subtitle": "保存したツールをまとめて表示", | |
| ++ "empty": "ブックマークはまだありません。" | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "クリア", | |
| + "copyToClipboard": "クリップボードにコピー", | |
| +diff --git a/public/locales/nl/translation.json b/public/locales/nl/translation.json | |
| +index b95d610..7724d0b 100644 | |
| +--- a/public/locales/nl/translation.json | |
| ++++ b/public/locales/nl/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "Zoek in alle tools", | |
| + "title": "Krijg dingen snel gedaan met" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "Zoek tools of acties", | |
| ++ "empty": "Geen resultaten gevonden", | |
| ++ "groups": { | |
| ++ "actions": "Acties", | |
| ++ "languages": "Talen", | |
| ++ "recent": "Recent", | |
| ++ "tools": "Tools" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "Donkere modus wisselen", | |
| ++ "switchLanguageTo": "Taal wijzigen naar {{language}}", | |
| ++ "toggleUserType": "Tools voor {{userType}} wisselen", | |
| ++ "goHome": "Ga naar Home", | |
| ++ "goBookmarks": "Ga naar bladwijzers", | |
| ++ "clearRecent": "Recent gebruikte tools wissen" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "Bladwijzers", | |
| ++ "subtitle": "Je opgeslagen tools op één plek", | |
| ++ "empty": "Nog geen tools in bladwijzers." | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "Duidelijk", | |
| + "copyToClipboard": "Kopiëren naar klembord", | |
| +diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json | |
| +index 0dbf4be..0ccc8b1 100644 | |
| +--- a/public/locales/pt/translation.json | |
| ++++ b/public/locales/pt/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "Pesquisar todas as ferramentas", | |
| + "title": "Faça as coisas rapidamente com" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "Pesquisar ferramentas ou ações", | |
| ++ "empty": "Nenhum resultado encontrado", | |
| ++ "groups": { | |
| ++ "actions": "Ações", | |
| ++ "languages": "Idiomas", | |
| ++ "recent": "Recentes", | |
| ++ "tools": "Ferramentas" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "Alternar modo escuro", | |
| ++ "switchLanguageTo": "Mudar idioma para {{language}}", | |
| ++ "toggleUserType": "Alternar ferramentas de {{userType}}", | |
| ++ "goHome": "Ir para a página inicial", | |
| ++ "goBookmarks": "Ir para favoritos", | |
| ++ "clearRecent": "Limpar ferramentas usadas recentemente" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "Favoritos", | |
| ++ "subtitle": "Suas ferramentas salvas em um só lugar", | |
| ++ "empty": "Nenhuma ferramenta marcada ainda." | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "Claro", | |
| + "copyToClipboard": "Copiar para a área de transferência", | |
| +diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json | |
| +index 7a79aa8..3c53e53 100644 | |
| +--- a/public/locales/ru/translation.json | |
| ++++ b/public/locales/ru/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "Найти инструмент…", | |
| + "title": "Выполняйте задачи быстро с помощью" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "Искать инструменты или действия", | |
| ++ "empty": "Ничего не найдено", | |
| ++ "groups": { | |
| ++ "actions": "Действия", | |
| ++ "languages": "Языки", | |
| ++ "recent": "Недавние", | |
| ++ "tools": "Инструменты" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "Переключить темную тему", | |
| ++ "switchLanguageTo": "Сменить язык на {{language}}", | |
| ++ "toggleUserType": "Переключить инструменты для {{userType}}", | |
| ++ "goHome": "Перейти на главную", | |
| ++ "goBookmarks": "Перейти к закладкам", | |
| ++ "clearRecent": "Очистить недавние инструменты" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "Закладки", | |
| ++ "subtitle": "Ваши сохраненные инструменты в одном месте", | |
| ++ "empty": "Закладок пока нет." | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "Прозрачный", | |
| + "copyToClipboard": "Копировать в буфер обмена", | |
| +diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json | |
| +index ce34027..995a8b8 100644 | |
| +--- a/public/locales/zh/translation.json | |
| ++++ b/public/locales/zh/translation.json | |
| +@@ -97,6 +97,29 @@ | |
| + "searchPlaceholder": "搜索所有工具", | |
| + "title": "快速完成工作" | |
| + }, | |
| ++ "actionPalette": { | |
| ++ "placeholder": "搜索工具或操作", | |
| ++ "empty": "未找到结果", | |
| ++ "groups": { | |
| ++ "actions": "操作", | |
| ++ "languages": "语言", | |
| ++ "recent": "最近", | |
| ++ "tools": "工具" | |
| ++ }, | |
| ++ "actions": { | |
| ++ "toggleDarkMode": "切换深色模式", | |
| ++ "switchLanguageTo": "切换语言为 {{language}}", | |
| ++ "toggleUserType": "切换 {{userType}} 工具", | |
| ++ "goHome": "前往首页", | |
| ++ "goBookmarks": "前往书签", | |
| ++ "clearRecent": "清除最近使用的工具" | |
| ++ } | |
| ++ }, | |
| ++ "bookmarks": { | |
| ++ "title": "书签", | |
| ++ "subtitle": "将保存的工具集中在一起", | |
| ++ "empty": "暂无书签工具。" | |
| ++ }, | |
| + "inputFooter": { | |
| + "clear": "清除", | |
| + "copyToClipboard": "复制到剪贴板", | |
| +diff --git a/src/components/App.tsx b/src/components/App.tsx | |
| +index a578cc8..1322e54 100644 | |
| +--- a/src/components/App.tsx | |
| ++++ b/src/components/App.tsx | |
| +@@ -13,6 +13,7 @@ import ScrollToTopButton from './ScrollToTopButton'; | |
| + import { I18nextProvider } from 'react-i18next'; | |
| + import i18n from '../i18n'; | |
| + import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider'; | |
| ++import ActionPalette from './ActionPalette'; | |
| + | |
| + export type Mode = 'dark' | 'light' | 'system'; | |
| + | |
| +@@ -46,6 +47,22 @@ function App() { | |
| + }; | |
| + }, []); | |
| + | |
| ++ const handleModeCycle = () => { | |
| ++ setMode((prev) => { | |
| ++ const next = nextMode(prev); | |
| ++ localStorage.setItem('theme', next); | |
| ++ return next; | |
| ++ }); | |
| ++ }; | |
| ++ | |
| ++ const handleToggleDarkMode = () => { | |
| ++ setMode((prev) => { | |
| ++ const next = prev === 'dark' ? 'light' : 'dark'; | |
| ++ localStorage.setItem('theme', next); | |
| ++ return next; | |
| ++ }); | |
| ++ }; | |
| ++ | |
| + return ( | |
| + <I18nextProvider i18n={i18n}> | |
| + <ThemeProvider theme={theme}> | |
| +@@ -60,12 +77,10 @@ function App() { | |
| + <CustomSnackBarProvider> | |
| + <UserTypeFilterProvider> | |
| + <BrowserRouter> | |
| +- <Navbar | |
| ++ <Navbar mode={mode} onChangeMode={handleModeCycle} /> | |
| ++ <ActionPalette | |
| + mode={mode} | |
| +- onChangeMode={() => { | |
| +- setMode((prev) => nextMode(prev)); | |
| +- localStorage.setItem('theme', nextMode(mode)); | |
| +- }} | |
| ++ onToggleDarkMode={handleToggleDarkMode} | |
| + /> | |
| + <Suspense fallback={<Loading />}> | |
| + <AppRoutes /> | |
| +diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx | |
| +index dbacb3a..1f26350 100644 | |
| +--- a/src/components/Navbar/index.tsx | |
| ++++ b/src/components/Navbar/index.tsx | |
| +@@ -22,25 +22,13 @@ import useMediaQuery from '@mui/material/useMediaQuery'; | |
| + import { useTheme } from '@mui/material/styles'; | |
| + import { Icon } from '@iconify/react'; | |
| + import { Mode } from 'components/App'; | |
| ++import { languages } from 'config/languages'; | |
| + import { useTranslation } from 'react-i18next'; | |
| + | |
| + interface NavbarProps { | |
| + mode: Mode; | |
| + onChangeMode: () => void; | |
| + } | |
| +-const languages = [ | |
| +- { code: 'en', label: 'English' }, | |
| +- { code: 'de', label: 'Deutsch' }, | |
| +- { code: 'es', label: 'Español' }, | |
| +- { code: 'fr', label: 'Français' }, | |
| +- { code: 'pt', label: 'Português' }, | |
| +- { code: 'ja', label: '日本語' }, | |
| +- { code: 'hi', label: 'हिंदी' }, | |
| +- { code: 'nl', label: 'Nederlands' }, | |
| +- { code: 'ru', label: 'Русский' }, | |
| +- { code: 'zh', label: '中文' } | |
| +-]; | |
| +- | |
| + const Navbar: React.FC<NavbarProps> = ({ | |
| + mode, | |
| + onChangeMode: onChangeMode | |
| +diff --git a/src/config/routesConfig.tsx b/src/config/routesConfig.tsx | |
| +index 78f47e6..f38c934 100644 | |
| +--- a/src/config/routesConfig.tsx | |
| ++++ b/src/config/routesConfig.tsx | |
| +@@ -3,6 +3,7 @@ import { lazy } from 'react'; | |
| + | |
| + const Home = lazy(() => import('../pages/home')); | |
| + const ToolsByCategory = lazy(() => import('../pages/tools-by-category')); | |
| ++const Bookmarks = lazy(() => import('../pages/bookmarks')); | |
| + | |
| + const routes: RouteObject[] = [ | |
| + { | |
| +@@ -13,6 +14,10 @@ const routes: RouteObject[] = [ | |
| + path: '/categories/:categoryName', | |
| + element: <ToolsByCategory /> | |
| + }, | |
| ++ { | |
| ++ path: '/bookmarks', | |
| ++ element: <Bookmarks /> | |
| ++ }, | |
| + { | |
| + path: '*', | |
| + element: <Navigate to="404" /> | |
| diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json | |
| index e907627..8c88bb2 100644 | |
| --- a/public/locales/de/translation.json | |
| +++ b/public/locales/de/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "Alle Tools durchsuchen", | |
| "title": "Erledigen Sie Dinge schnell mit" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "Tools oder Aktionen durchsuchen", | |
| + "empty": "Keine Treffer gefunden", | |
| + "groups": { | |
| + "actions": "Aktionen", | |
| + "languages": "Sprachen", | |
| + "recent": "Zuletzt verwendet", | |
| + "tools": "Tools" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "Dunkelmodus umschalten", | |
| + "switchLanguageTo": "Sprache auf {{language}} wechseln", | |
| + "toggleUserType": "{{userType}}-Tools umschalten", | |
| + "goHome": "Zur Startseite", | |
| + "goBookmarks": "Zu Lesezeichen", | |
| + "clearRecent": "Zuletzt verwendete Tools löschen" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "Lesezeichen", | |
| + "subtitle": "Deine gespeicherten Tools an einem Ort", | |
| + "empty": "Noch keine Tools gespeichert." | |
| + }, | |
| "inputFooter": { | |
| "clear": "Klar", | |
| "copyToClipboard": "In die Zwischenablage kopieren", | |
| diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json | |
| index 98b0d5a..3a3224b 100644 | |
| --- a/public/locales/en/translation.json | |
| +++ b/public/locales/en/translation.json | |
| @@ -109,6 +109,29 @@ | |
| "searchPlaceholder": "Search all tools", | |
| "title": "Get Things Done Quickly with" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "Search tools or actions", | |
| + "empty": "No matches found", | |
| + "groups": { | |
| + "actions": "Actions", | |
| + "languages": "Languages", | |
| + "recent": "Recent", | |
| + "tools": "Tools" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "Toggle dark mode", | |
| + "switchLanguageTo": "Switch language to {{language}}", | |
| + "toggleUserType": "Toggle {{userType}} tools", | |
| + "goHome": "Go to Home", | |
| + "goBookmarks": "Go to Bookmarks", | |
| + "clearRecent": "Clear recently used tools" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "Bookmarks", | |
| + "subtitle": "Your saved tools in one place", | |
| + "empty": "No bookmarked tools yet." | |
| + }, | |
| "inputFooter": { | |
| "clear": "Clear", | |
| "copyToClipboard": "Copy to clipboard", | |
| diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json | |
| index 6ddab10..1d4c5bc 100644 | |
| --- a/public/locales/es/translation.json | |
| +++ b/public/locales/es/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "Buscar en todas las herramientas", | |
| "title": "Haga las cosas rápidamente con" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "Buscar herramientas o acciones", | |
| + "empty": "No se encontraron resultados", | |
| + "groups": { | |
| + "actions": "Acciones", | |
| + "languages": "Idiomas", | |
| + "recent": "Recientes", | |
| + "tools": "Herramientas" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "Alternar modo oscuro", | |
| + "switchLanguageTo": "Cambiar idioma a {{language}}", | |
| + "toggleUserType": "Alternar herramientas de {{userType}}", | |
| + "goHome": "Ir a inicio", | |
| + "goBookmarks": "Ir a marcadores", | |
| + "clearRecent": "Borrar herramientas usadas recientemente" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "Marcadores", | |
| + "subtitle": "Tus herramientas guardadas en un solo lugar", | |
| + "empty": "Aún no hay herramientas marcadas." | |
| + }, | |
| "inputFooter": { | |
| "clear": "Claro", | |
| "copyToClipboard": "Copiar al portapapeles", | |
| diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json | |
| index 2e27a0c..a29b07a 100644 | |
| --- a/public/locales/fr/translation.json | |
| +++ b/public/locales/fr/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "Rechercher tous les outils", | |
| "title": "Faites avancer les choses rapidement avec" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "Rechercher des outils ou des actions", | |
| + "empty": "Aucun résultat", | |
| + "groups": { | |
| + "actions": "Actions", | |
| + "languages": "Langues", | |
| + "recent": "Récents", | |
| + "tools": "Outils" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "Basculer le mode sombre", | |
| + "switchLanguageTo": "Changer la langue en {{language}}", | |
| + "toggleUserType": "Basculer les outils {{userType}}", | |
| + "goHome": "Aller à l'accueil", | |
| + "goBookmarks": "Aller aux favoris", | |
| + "clearRecent": "Effacer les outils récents" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "Favoris", | |
| + "subtitle": "Vos outils enregistrés au même endroit", | |
| + "empty": "Aucun outil enregistré pour l'instant." | |
| + }, | |
| "inputFooter": { | |
| "clear": "Clair", | |
| "copyToClipboard": "Copier dans le presse-papiers", | |
| diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json | |
| index f30c7ee..ca249e2 100644 | |
| --- a/public/locales/hi/translation.json | |
| +++ b/public/locales/hi/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "सभी टूल्स खोजें", | |
| "title": "के साथ जल्दी काम करें" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "टूल्स या कार्रवाइयाँ खोजें", | |
| + "empty": "कोई परिणाम नहीं मिला", | |
| + "groups": { | |
| + "actions": "कार्रवाइयाँ", | |
| + "languages": "भाषाएँ", | |
| + "recent": "हाल के", | |
| + "tools": "टूल्स" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "डार्क मोड टॉगल करें", | |
| + "switchLanguageTo": "{{language}} में भाषा बदलें", | |
| + "toggleUserType": "{{userType}} टूल्स टॉगल करें", | |
| + "goHome": "होम पर जाएँ", | |
| + "goBookmarks": "बुकमार्क पर जाएँ", | |
| + "clearRecent": "हाल ही में उपयोग किए गए टूल्स साफ़ करें" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "बुकमार्क", | |
| + "subtitle": "आपके सेव किए गए टूल्स एक जगह", | |
| + "empty": "अभी कोई बुकमार्क नहीं है।" | |
| + }, | |
| "inputFooter": { | |
| "clear": "साफ़ करें", | |
| "copyToClipboard": "क्लिपबोर्ड पर कॉपी करें", | |
| diff --git a/public/locales/ja/translation.json b/public/locales/ja/translation.json | |
| index 4acaebc..d4e1e64 100644 | |
| --- a/public/locales/ja/translation.json | |
| +++ b/public/locales/ja/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "すべてのツールを検索", | |
| "title": "物事を素早く終わらせる" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "ツールやアクションを検索", | |
| + "empty": "一致する結果がありません", | |
| + "groups": { | |
| + "actions": "アクション", | |
| + "languages": "言語", | |
| + "recent": "最近", | |
| + "tools": "ツール" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "ダークモードを切り替える", | |
| + "switchLanguageTo": "言語を{{language}}に切り替える", | |
| + "toggleUserType": "{{userType}}向けツールを切り替える", | |
| + "goHome": "ホームへ移動", | |
| + "goBookmarks": "ブックマークへ移動", | |
| + "clearRecent": "最近使ったツールをクリア" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "ブックマーク", | |
| + "subtitle": "保存したツールをまとめて表示", | |
| + "empty": "ブックマークはまだありません。" | |
| + }, | |
| "inputFooter": { | |
| "clear": "クリア", | |
| "copyToClipboard": "クリップボードにコピー", | |
| diff --git a/public/locales/nl/translation.json b/public/locales/nl/translation.json | |
| index b95d610..7724d0b 100644 | |
| --- a/public/locales/nl/translation.json | |
| +++ b/public/locales/nl/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "Zoek in alle tools", | |
| "title": "Krijg dingen snel gedaan met" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "Zoek tools of acties", | |
| + "empty": "Geen resultaten gevonden", | |
| + "groups": { | |
| + "actions": "Acties", | |
| + "languages": "Talen", | |
| + "recent": "Recent", | |
| + "tools": "Tools" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "Donkere modus wisselen", | |
| + "switchLanguageTo": "Taal wijzigen naar {{language}}", | |
| + "toggleUserType": "Tools voor {{userType}} wisselen", | |
| + "goHome": "Ga naar Home", | |
| + "goBookmarks": "Ga naar bladwijzers", | |
| + "clearRecent": "Recent gebruikte tools wissen" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "Bladwijzers", | |
| + "subtitle": "Je opgeslagen tools op één plek", | |
| + "empty": "Nog geen tools in bladwijzers." | |
| + }, | |
| "inputFooter": { | |
| "clear": "Duidelijk", | |
| "copyToClipboard": "Kopiëren naar klembord", | |
| diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json | |
| index 0dbf4be..0ccc8b1 100644 | |
| --- a/public/locales/pt/translation.json | |
| +++ b/public/locales/pt/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "Pesquisar todas as ferramentas", | |
| "title": "Faça as coisas rapidamente com" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "Pesquisar ferramentas ou ações", | |
| + "empty": "Nenhum resultado encontrado", | |
| + "groups": { | |
| + "actions": "Ações", | |
| + "languages": "Idiomas", | |
| + "recent": "Recentes", | |
| + "tools": "Ferramentas" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "Alternar modo escuro", | |
| + "switchLanguageTo": "Mudar idioma para {{language}}", | |
| + "toggleUserType": "Alternar ferramentas de {{userType}}", | |
| + "goHome": "Ir para a página inicial", | |
| + "goBookmarks": "Ir para favoritos", | |
| + "clearRecent": "Limpar ferramentas usadas recentemente" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "Favoritos", | |
| + "subtitle": "Suas ferramentas salvas em um só lugar", | |
| + "empty": "Nenhuma ferramenta marcada ainda." | |
| + }, | |
| "inputFooter": { | |
| "clear": "Claro", | |
| "copyToClipboard": "Copiar para a área de transferência", | |
| diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json | |
| index 7a79aa8..3c53e53 100644 | |
| --- a/public/locales/ru/translation.json | |
| +++ b/public/locales/ru/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "Найти инструмент…", | |
| "title": "Выполняйте задачи быстро с помощью" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "Искать инструменты или действия", | |
| + "empty": "Ничего не найдено", | |
| + "groups": { | |
| + "actions": "Действия", | |
| + "languages": "Языки", | |
| + "recent": "Недавние", | |
| + "tools": "Инструменты" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "Переключить темную тему", | |
| + "switchLanguageTo": "Сменить язык на {{language}}", | |
| + "toggleUserType": "Переключить инструменты для {{userType}}", | |
| + "goHome": "Перейти на главную", | |
| + "goBookmarks": "Перейти к закладкам", | |
| + "clearRecent": "Очистить недавние инструменты" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "Закладки", | |
| + "subtitle": "Ваши сохраненные инструменты в одном месте", | |
| + "empty": "Закладок пока нет." | |
| + }, | |
| "inputFooter": { | |
| "clear": "Прозрачный", | |
| "copyToClipboard": "Копировать в буфер обмена", | |
| diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json | |
| index ce34027..995a8b8 100644 | |
| --- a/public/locales/zh/translation.json | |
| +++ b/public/locales/zh/translation.json | |
| @@ -97,6 +97,29 @@ | |
| "searchPlaceholder": "搜索所有工具", | |
| "title": "快速完成工作" | |
| }, | |
| + "actionPalette": { | |
| + "placeholder": "搜索工具或操作", | |
| + "empty": "未找到结果", | |
| + "groups": { | |
| + "actions": "操作", | |
| + "languages": "语言", | |
| + "recent": "最近", | |
| + "tools": "工具" | |
| + }, | |
| + "actions": { | |
| + "toggleDarkMode": "切换深色模式", | |
| + "switchLanguageTo": "切换语言为 {{language}}", | |
| + "toggleUserType": "切换 {{userType}} 工具", | |
| + "goHome": "前往首页", | |
| + "goBookmarks": "前往书签", | |
| + "clearRecent": "清除最近使用的工具" | |
| + } | |
| + }, | |
| + "bookmarks": { | |
| + "title": "书签", | |
| + "subtitle": "将保存的工具集中在一起", | |
| + "empty": "暂无书签工具。" | |
| + }, | |
| "inputFooter": { | |
| "clear": "清除", | |
| "copyToClipboard": "复制到剪贴板", | |
| diff --git a/src/components/ActionPalette.tsx b/src/components/ActionPalette.tsx | |
| new file mode 100644 | |
| index 0000000..28871f9 | |
| --- /dev/null | |
| +++ b/src/components/ActionPalette.tsx | |
| @@ -0,0 +1,437 @@ | |
| +import React, { useEffect, useMemo, useRef, useState } from 'react'; | |
| +import { | |
| + Box, | |
| + Dialog, | |
| + List, | |
| + ListItemButton, | |
| + ListItemIcon, | |
| + ListItemText, | |
| + TextField, | |
| + Typography | |
| +} from '@mui/material'; | |
| +import SearchIcon from '@mui/icons-material/Search'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { useTranslation } from 'react-i18next'; | |
| +import { useLocation, useNavigate } from 'react-router-dom'; | |
| +import { tools, filterTools } from '@tools/index'; | |
| +import { DefinedTool, UserType } from '@tools/defineTool'; | |
| +import { useUserTypeFilter } from 'providers/UserTypeFilterProvider'; | |
| +import { languages } from 'config/languages'; | |
| +import { | |
| + addRecentToolPath, | |
| + clearRecentTools, | |
| + getRecentToolPaths | |
| +} from '@utils/recentTools'; | |
| +import { validNamespaces } from '../i18n'; | |
| +import type { Mode } from './App'; | |
| + | |
| +type PaletteItem = { | |
| + id: string; | |
| + label: string; | |
| + description?: string; | |
| + icon?: string; | |
| + keywords?: string[]; | |
| + onSelect: () => void; | |
| +}; | |
| + | |
| +type PaletteSection = { | |
| + id: string; | |
| + title: string; | |
| + items: PaletteItem[]; | |
| +}; | |
| + | |
| +interface ActionPaletteProps { | |
| + mode: Mode; | |
| + onToggleDarkMode: () => void; | |
| +} | |
| + | |
| +const userTypeOptions: UserType[] = ['generalUsers', 'developers']; | |
| + | |
| +export default function ActionPalette({ | |
| + mode, | |
| + onToggleDarkMode | |
| +}: ActionPaletteProps) { | |
| + const { t, i18n } = useTranslation(validNamespaces); | |
| + const navigate = useNavigate(); | |
| + const location = useLocation(); | |
| + const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter(); | |
| + const [open, setOpen] = useState(false); | |
| + const [query, setQuery] = useState(''); | |
| + const [selectedIndex, setSelectedIndex] = useState(0); | |
| + const [recentToolPaths, setRecentToolPaths] = useState<string[]>(() => | |
| + getRecentToolPaths() | |
| + ); | |
| + const inputRef = useRef<HTMLInputElement>(null); | |
| + const selectedItemRef = useRef<HTMLDivElement | null>(null); | |
| + | |
| + const toolMap = useMemo( | |
| + () => new Map<string, DefinedTool>(tools.map((tool) => [tool.path, tool])), | |
| + [] | |
| + ); | |
| + const toolPathSet = useMemo( | |
| + () => new Set<string>(tools.map((tool) => tool.path)), | |
| + [] | |
| + ); | |
| + | |
| + useEffect(() => { | |
| + const handleKeyDown = (event: KeyboardEvent) => { | |
| + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { | |
| + event.preventDefault(); | |
| + setOpen((prev) => !prev); | |
| + } | |
| + }; | |
| + window.addEventListener('keydown', handleKeyDown); | |
| + return () => window.removeEventListener('keydown', handleKeyDown); | |
| + }, []); | |
| + | |
| + useEffect(() => { | |
| + if (!open) { | |
| + return; | |
| + } | |
| + setQuery(''); | |
| + setSelectedIndex(0); | |
| + setRecentToolPaths(getRecentToolPaths()); | |
| + requestAnimationFrame(() => inputRef.current?.focus()); | |
| + }, [open]); | |
| + | |
| + useEffect(() => { | |
| + if (!open) { | |
| + return; | |
| + } | |
| + setSelectedIndex(0); | |
| + }, [query, open]); | |
| + | |
| + useEffect(() => { | |
| + const toolPath = location.pathname.replace(/^\/+/, ''); | |
| + if (!toolPath || !toolPathSet.has(toolPath)) { | |
| + return; | |
| + } | |
| + const updated = addRecentToolPath(toolPath); | |
| + setRecentToolPaths(updated); | |
| + }, [location.pathname, toolPathSet]); | |
| + | |
| + useEffect(() => { | |
| + if (selectedItemRef.current) { | |
| + selectedItemRef.current.scrollIntoView({ block: 'nearest' }); | |
| + } | |
| + }, [selectedIndex]); | |
| + | |
| + const lowerQuery = query.trim().toLowerCase(); | |
| + | |
| + const handleToggleUserType = (userType: UserType) => { | |
| + const isSelected = selectedUserTypes.includes(userType); | |
| + const updated = isSelected | |
| + ? selectedUserTypes.filter((type) => type !== userType) | |
| + : [...selectedUserTypes, userType]; | |
| + setSelectedUserTypes(updated); | |
| + }; | |
| + | |
| + const handleSwitchLanguage = (code: string) => { | |
| + i18n.changeLanguage(code); | |
| + localStorage.setItem('lang', code); | |
| + }; | |
| + | |
| + const handleClearRecents = () => { | |
| + clearRecentTools(); | |
| + setRecentToolPaths([]); | |
| + }; | |
| + | |
| + const userTypeLabels = { | |
| + generalUsers: t('translation:userTypes.generalUsers'), | |
| + developers: t('translation:userTypes.developers') | |
| + }; | |
| + | |
| + const actionItems: PaletteItem[] = [ | |
| + { | |
| + id: 'toggle-theme', | |
| + label: t('translation:actionPalette.actions.toggleDarkMode'), | |
| + icon: mode === 'dark' ? 'ic:round-dark-mode' : 'ic:round-light-mode', | |
| + keywords: ['theme', 'dark', 'light', 'mode'], | |
| + onSelect: onToggleDarkMode | |
| + }, | |
| + { | |
| + id: 'go-home', | |
| + label: t('translation:actionPalette.actions.goHome'), | |
| + icon: 'mdi:home', | |
| + keywords: ['home', 'navigate'], | |
| + onSelect: () => navigate('/') | |
| + }, | |
| + { | |
| + id: 'go-bookmarks', | |
| + label: t('translation:actionPalette.actions.goBookmarks'), | |
| + icon: 'mdi:bookmark', | |
| + keywords: ['bookmarks', 'favorites', 'saved'], | |
| + onSelect: () => navigate('/bookmarks') | |
| + }, | |
| + { | |
| + id: 'clear-recents', | |
| + label: t('translation:actionPalette.actions.clearRecent'), | |
| + icon: 'mdi:history', | |
| + keywords: ['recent', 'history', 'clear'], | |
| + onSelect: handleClearRecents | |
| + }, | |
| + ...userTypeOptions.map((userType) => ({ | |
| + id: `toggle-user-type-${userType}`, | |
| + label: t('translation:actionPalette.actions.toggleUserType', { | |
| + userType: userTypeLabels[userType] | |
| + }), | |
| + icon: userType === 'developers' ? 'mdi:code-tags' : 'mdi:account', | |
| + keywords: ['filter', 'user', userType], | |
| + onSelect: () => handleToggleUserType(userType) | |
| + })) | |
| + ]; | |
| + | |
| + const languageItems: PaletteItem[] = languages.map((language) => ({ | |
| + id: `language-${language.code}`, | |
| + label: t('translation:actionPalette.actions.switchLanguageTo', { | |
| + language: language.label | |
| + }), | |
| + icon: 'mdi:translate', | |
| + keywords: [language.label.toLowerCase(), language.code], | |
| + onSelect: () => handleSwitchLanguage(language.code) | |
| + })); | |
| + | |
| + const filteredToolMatches = query | |
| + ? filterTools(tools, query, selectedUserTypes, t) | |
| + : []; | |
| + const availableToolMatches = filterTools(tools, query, selectedUserTypes, t); | |
| + const availableToolPaths = new Set( | |
| + availableToolMatches.map((tool) => tool.path) | |
| + ); | |
| + | |
| + const recentTools = recentToolPaths | |
| + .map((path) => toolMap.get(path)) | |
| + .filter((tool): tool is DefinedTool => Boolean(tool)) | |
| + .filter((tool) => availableToolPaths.has(tool.path)); | |
| + | |
| + const recentItems: PaletteItem[] = recentTools.map((tool) => ({ | |
| + id: `recent-${tool.path}`, | |
| + label: t(tool.name), | |
| + description: t(tool.shortDescription), | |
| + icon: tool.icon, | |
| + keywords: tool.keywords, | |
| + onSelect: () => navigate('/' + tool.path) | |
| + })); | |
| + | |
| + const toolItems: PaletteItem[] = filteredToolMatches | |
| + .slice(0, 20) | |
| + .map((tool) => ({ | |
| + id: `tool-${tool.path}`, | |
| + label: t(tool.name), | |
| + description: t(tool.shortDescription), | |
| + icon: tool.icon, | |
| + keywords: tool.keywords, | |
| + onSelect: () => navigate('/' + tool.path) | |
| + })); | |
| + | |
| + const matchesQuery = (item: PaletteItem) => { | |
| + if (!lowerQuery) return true; | |
| + const haystack = [ | |
| + item.label, | |
| + item.description ?? '', | |
| + ...(item.keywords ?? []) | |
| + ] | |
| + .join(' ') | |
| + .toLowerCase(); | |
| + return haystack.includes(lowerQuery); | |
| + }; | |
| + | |
| + const filteredActionItems = actionItems.filter(matchesQuery); | |
| + const filteredLanguageItems = languageItems.filter(matchesQuery); | |
| + | |
| + const sections = useMemo<PaletteSection[]>(() => { | |
| + const entries: PaletteSection[] = []; | |
| + if (recentItems.length > 0) { | |
| + entries.push({ | |
| + id: 'recent', | |
| + title: t('translation:actionPalette.groups.recent'), | |
| + items: recentItems | |
| + }); | |
| + } | |
| + if (filteredActionItems.length > 0) { | |
| + entries.push({ | |
| + id: 'actions', | |
| + title: t('translation:actionPalette.groups.actions'), | |
| + items: filteredActionItems | |
| + }); | |
| + } | |
| + if (filteredLanguageItems.length > 0) { | |
| + entries.push({ | |
| + id: 'languages', | |
| + title: t('translation:actionPalette.groups.languages'), | |
| + items: filteredLanguageItems | |
| + }); | |
| + } | |
| + if (toolItems.length > 0) { | |
| + entries.push({ | |
| + id: 'tools', | |
| + title: t('translation:actionPalette.groups.tools'), | |
| + items: toolItems | |
| + }); | |
| + } | |
| + return entries; | |
| + }, [filteredActionItems, filteredLanguageItems, recentItems, t, toolItems]); | |
| + | |
| + const sectionedItems = useMemo(() => { | |
| + let index = 0; | |
| + return sections.map((section) => ({ | |
| + ...section, | |
| + items: section.items.map((item) => ({ | |
| + ...item, | |
| + index: index++ | |
| + })) | |
| + })); | |
| + }, [sections]); | |
| + | |
| + const flatItems = useMemo( | |
| + () => sectionedItems.flatMap((section) => section.items), | |
| + [sectionedItems] | |
| + ); | |
| + | |
| + useEffect(() => { | |
| + if (flatItems.length === 0) { | |
| + setSelectedIndex(-1); | |
| + return; | |
| + } | |
| + setSelectedIndex((prev) => | |
| + prev >= 0 && prev < flatItems.length ? prev : 0 | |
| + ); | |
| + }, [flatItems.length]); | |
| + | |
| + const handleExecute = (item?: PaletteItem) => { | |
| + if (!item) return; | |
| + item.onSelect(); | |
| + setOpen(false); | |
| + }; | |
| + | |
| + const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { | |
| + if (event.key === 'ArrowDown') { | |
| + if (!flatItems.length) { | |
| + return; | |
| + } | |
| + event.preventDefault(); | |
| + setSelectedIndex((prev) => | |
| + prev < 0 ? 0 : (prev + 1) % flatItems.length | |
| + ); | |
| + return; | |
| + } | |
| + | |
| + if (event.key === 'ArrowUp') { | |
| + if (!flatItems.length) { | |
| + return; | |
| + } | |
| + event.preventDefault(); | |
| + setSelectedIndex((prev) => (prev <= 0 ? flatItems.length - 1 : prev - 1)); | |
| + return; | |
| + } | |
| + | |
| + if (event.key === 'Enter') { | |
| + if (!flatItems.length) { | |
| + return; | |
| + } | |
| + event.preventDefault(); | |
| + handleExecute(flatItems[selectedIndex]); | |
| + return; | |
| + } | |
| + | |
| + if (event.key === 'Escape') { | |
| + event.preventDefault(); | |
| + setOpen(false); | |
| + } | |
| + }; | |
| + | |
| + return ( | |
| + <Dialog | |
| + open={open} | |
| + onClose={() => setOpen(false)} | |
| + fullWidth | |
| + maxWidth="sm" | |
| + PaperProps={{ | |
| + sx: { | |
| + borderRadius: 3, | |
| + overflow: 'hidden', | |
| + backgroundColor: 'background.paper' | |
| + } | |
| + }} | |
| + > | |
| + <Box px={2} pt={2}> | |
| + <TextField | |
| + fullWidth | |
| + inputRef={inputRef} | |
| + placeholder={t('translation:actionPalette.placeholder')} | |
| + value={query} | |
| + onChange={(event) => setQuery(event.target.value)} | |
| + onKeyDown={handleKeyDown} | |
| + InputProps={{ | |
| + startAdornment: ( | |
| + <Box mr={1} display="flex" alignItems="center"> | |
| + <SearchIcon /> | |
| + </Box> | |
| + ), | |
| + sx: { | |
| + borderRadius: 2, | |
| + backgroundColor: 'background.default' | |
| + } | |
| + }} | |
| + /> | |
| + </Box> | |
| + <Box px={1} pb={2}> | |
| + {sectionedItems.length === 0 ? ( | |
| + <Box px={2} py={3}> | |
| + <Typography color="text.secondary"> | |
| + {t('translation:actionPalette.empty')} | |
| + </Typography> | |
| + </Box> | |
| + ) : ( | |
| + <List sx={{ maxHeight: '50vh', overflowY: 'auto' }}> | |
| + {sectionedItems.map((section) => ( | |
| + <Box key={section.id} component="li"> | |
| + <Typography | |
| + variant="caption" | |
| + sx={{ | |
| + px: 2, | |
| + py: 1, | |
| + display: 'block', | |
| + color: 'text.secondary', | |
| + textTransform: 'uppercase', | |
| + letterSpacing: 1 | |
| + }} | |
| + > | |
| + {section.title} | |
| + </Typography> | |
| + {section.items.map((item) => { | |
| + const isSelected = item.index === selectedIndex; | |
| + return ( | |
| + <ListItemButton | |
| + key={item.id} | |
| + selected={isSelected} | |
| + onClick={() => handleExecute(item)} | |
| + onMouseEnter={() => setSelectedIndex(item.index)} | |
| + ref={isSelected ? selectedItemRef : null} | |
| + sx={{ | |
| + borderRadius: 2, | |
| + mx: 1, | |
| + mb: 0.5 | |
| + }} | |
| + > | |
| + {item.icon && ( | |
| + <ListItemIcon sx={{ minWidth: 36 }}> | |
| + <Icon icon={item.icon} fontSize={20} /> | |
| + </ListItemIcon> | |
| + )} | |
| + <ListItemText | |
| + primary={item.label} | |
| + secondary={item.description} | |
| + primaryTypographyProps={{ fontWeight: 600 }} | |
| + /> | |
| + </ListItemButton> | |
| + ); | |
| + })} | |
| + </Box> | |
| + ))} | |
| + </List> | |
| + )} | |
| + </Box> | |
| + </Dialog> | |
| + ); | |
| +} | |
| diff --git a/src/components/App.tsx b/src/components/App.tsx | |
| index a578cc8..1322e54 100644 | |
| --- a/src/components/App.tsx | |
| +++ b/src/components/App.tsx | |
| @@ -13,6 +13,7 @@ import ScrollToTopButton from './ScrollToTopButton'; | |
| import { I18nextProvider } from 'react-i18next'; | |
| import i18n from '../i18n'; | |
| import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider'; | |
| +import ActionPalette from './ActionPalette'; | |
| export type Mode = 'dark' | 'light' | 'system'; | |
| @@ -46,6 +47,22 @@ function App() { | |
| }; | |
| }, []); | |
| + const handleModeCycle = () => { | |
| + setMode((prev) => { | |
| + const next = nextMode(prev); | |
| + localStorage.setItem('theme', next); | |
| + return next; | |
| + }); | |
| + }; | |
| + | |
| + const handleToggleDarkMode = () => { | |
| + setMode((prev) => { | |
| + const next = prev === 'dark' ? 'light' : 'dark'; | |
| + localStorage.setItem('theme', next); | |
| + return next; | |
| + }); | |
| + }; | |
| + | |
| return ( | |
| <I18nextProvider i18n={i18n}> | |
| <ThemeProvider theme={theme}> | |
| @@ -60,12 +77,10 @@ function App() { | |
| <CustomSnackBarProvider> | |
| <UserTypeFilterProvider> | |
| <BrowserRouter> | |
| - <Navbar | |
| + <Navbar mode={mode} onChangeMode={handleModeCycle} /> | |
| + <ActionPalette | |
| mode={mode} | |
| - onChangeMode={() => { | |
| - setMode((prev) => nextMode(prev)); | |
| - localStorage.setItem('theme', nextMode(mode)); | |
| - }} | |
| + onToggleDarkMode={handleToggleDarkMode} | |
| /> | |
| <Suspense fallback={<Loading />}> | |
| <AppRoutes /> | |
| diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx | |
| index dbacb3a..1f26350 100644 | |
| --- a/src/components/Navbar/index.tsx | |
| +++ b/src/components/Navbar/index.tsx | |
| @@ -22,25 +22,13 @@ import useMediaQuery from '@mui/material/useMediaQuery'; | |
| import { useTheme } from '@mui/material/styles'; | |
| import { Icon } from '@iconify/react'; | |
| import { Mode } from 'components/App'; | |
| +import { languages } from 'config/languages'; | |
| import { useTranslation } from 'react-i18next'; | |
| interface NavbarProps { | |
| mode: Mode; | |
| onChangeMode: () => void; | |
| } | |
| -const languages = [ | |
| - { code: 'en', label: 'English' }, | |
| - { code: 'de', label: 'Deutsch' }, | |
| - { code: 'es', label: 'Español' }, | |
| - { code: 'fr', label: 'Français' }, | |
| - { code: 'pt', label: 'Português' }, | |
| - { code: 'ja', label: '日本語' }, | |
| - { code: 'hi', label: 'हिंदी' }, | |
| - { code: 'nl', label: 'Nederlands' }, | |
| - { code: 'ru', label: 'Русский' }, | |
| - { code: 'zh', label: '中文' } | |
| -]; | |
| - | |
| const Navbar: React.FC<NavbarProps> = ({ | |
| mode, | |
| onChangeMode: onChangeMode | |
| diff --git a/src/config/languages.ts b/src/config/languages.ts | |
| new file mode 100644 | |
| index 0000000..84e1612 | |
| --- /dev/null | |
| +++ b/src/config/languages.ts | |
| @@ -0,0 +1,14 @@ | |
| +export const languages = [ | |
| + { code: 'en', label: 'English' }, | |
| + { code: 'de', label: 'Deutsch' }, | |
| + { code: 'es', label: 'Español' }, | |
| + { code: 'fr', label: 'Français' }, | |
| + { code: 'pt', label: 'Português' }, | |
| + { code: 'ja', label: '日本語' }, | |
| + { code: 'hi', label: 'हिंदी' }, | |
| + { code: 'nl', label: 'Nederlands' }, | |
| + { code: 'ru', label: 'Русский' }, | |
| + { code: 'zh', label: '中文' } | |
| +] as const; | |
| + | |
| +export type LanguageOption = (typeof languages)[number]; | |
| diff --git a/src/config/routesConfig.tsx b/src/config/routesConfig.tsx | |
| index 78f47e6..f38c934 100644 | |
| --- a/src/config/routesConfig.tsx | |
| +++ b/src/config/routesConfig.tsx | |
| @@ -3,6 +3,7 @@ import { lazy } from 'react'; | |
| const Home = lazy(() => import('../pages/home')); | |
| const ToolsByCategory = lazy(() => import('../pages/tools-by-category')); | |
| +const Bookmarks = lazy(() => import('../pages/bookmarks')); | |
| const routes: RouteObject[] = [ | |
| { | |
| @@ -13,6 +14,10 @@ const routes: RouteObject[] = [ | |
| path: '/categories/:categoryName', | |
| element: <ToolsByCategory /> | |
| }, | |
| + { | |
| + path: '/bookmarks', | |
| + element: <Bookmarks /> | |
| + }, | |
| { | |
| path: '*', | |
| element: <Navigate to="404" /> | |
| diff --git a/src/pages/bookmarks/index.tsx b/src/pages/bookmarks/index.tsx | |
| new file mode 100644 | |
| index 0000000..f3708fb | |
| --- /dev/null | |
| +++ b/src/pages/bookmarks/index.tsx | |
| @@ -0,0 +1,108 @@ | |
| +import { Box, Grid, Stack, Typography, useTheme } from '@mui/material'; | |
| +import { Helmet } from 'react-helmet'; | |
| +import { useTranslation } from 'react-i18next'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { useNavigate } from 'react-router-dom'; | |
| +import UserTypeFilter from '@components/UserTypeFilter'; | |
| +import { useUserTypeFilter } from 'providers/UserTypeFilterProvider'; | |
| +import { getBookmarkedToolPaths } from '@utils/bookmark'; | |
| +import { filterTools, tools } from '@tools/index'; | |
| +import { DefinedTool } from '@tools/defineTool'; | |
| +import { categoriesColors } from 'config/uiConfig'; | |
| +import { validNamespaces } from '../../i18n'; | |
| + | |
| +export default function Bookmarks() { | |
| + const theme = useTheme(); | |
| + const navigate = useNavigate(); | |
| + const { t } = useTranslation(validNamespaces); | |
| + const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter(); | |
| + | |
| + const bookmarkedToolPaths = getBookmarkedToolPaths(); | |
| + const toolMap = new Map<string, DefinedTool>( | |
| + tools.map((tool) => [tool.path, tool]) | |
| + ); | |
| + const bookmarkedTools = bookmarkedToolPaths | |
| + .map((path) => toolMap.get(path)) | |
| + .filter((tool): tool is DefinedTool => Boolean(tool)); | |
| + | |
| + const filteredTools = filterTools(bookmarkedTools, '', selectedUserTypes, t); | |
| + | |
| + return ( | |
| + <Box sx={{ backgroundColor: 'background.default', minHeight: '100vh' }}> | |
| + <Helmet> | |
| + <title>{`${t('translation:bookmarks.title')} - OmniTools`}</title> | |
| + </Helmet> | |
| + <Box | |
| + padding={{ xs: 1, md: 3, lg: 5 }} | |
| + display={'flex'} | |
| + flexDirection={'column'} | |
| + alignItems={'center'} | |
| + justifyContent={'center'} | |
| + width={'100%'} | |
| + > | |
| + <Stack spacing={1} alignItems="center" mb={2}> | |
| + <Typography fontSize={{ xs: 22, md: 28 }} color="primary"> | |
| + {t('translation:bookmarks.title')} | |
| + </Typography> | |
| + <Typography color="text.secondary"> | |
| + {t('translation:bookmarks.subtitle')} | |
| + </Typography> | |
| + </Stack> | |
| + <Box my={3}> | |
| + <UserTypeFilter | |
| + selectedUserTypes={selectedUserTypes} | |
| + onUserTypesChange={setSelectedUserTypes} | |
| + /> | |
| + </Box> | |
| + {filteredTools.length === 0 ? ( | |
| + <Typography color="text.secondary"> | |
| + {t('translation:bookmarks.empty')} | |
| + </Typography> | |
| + ) : ( | |
| + <Grid container spacing={2}> | |
| + {filteredTools.map((tool, index) => ( | |
| + <Grid item xs={12} md={6} lg={4} key={tool.path}> | |
| + <Stack | |
| + sx={{ | |
| + backgroundColor: 'background.paper', | |
| + boxShadow: `5px 4px 2px ${ | |
| + theme.palette.mode === 'dark' ? 'black' : '#E9E9ED' | |
| + }`, | |
| + cursor: 'pointer', | |
| + height: '100%', | |
| + '&:hover': { | |
| + backgroundColor: theme.palette.background.hover | |
| + } | |
| + }} | |
| + onClick={() => navigate('/' + tool.path)} | |
| + direction={'row'} | |
| + alignItems={'center'} | |
| + spacing={2} | |
| + padding={2} | |
| + border={`1px solid ${theme.palette.background.default}`} | |
| + borderRadius={2} | |
| + > | |
| + <Icon | |
| + icon={tool.icon ?? 'ph:compass-tool-thin'} | |
| + fontSize={'60px'} | |
| + color={categoriesColors[index % categoriesColors.length]} | |
| + /> | |
| + <Box> | |
| + <Typography fontSize={20} fontWeight={600}> | |
| + {/*@ts-ignore*/} | |
| + {t(tool.name)} | |
| + </Typography> | |
| + <Typography sx={{ mt: 2 }}> | |
| + {/*@ts-ignore*/} | |
| + {t(tool.shortDescription)} | |
| + </Typography> | |
| + </Box> | |
| + </Stack> | |
| + </Grid> | |
| + ))} | |
| + </Grid> | |
| + )} | |
| + </Box> | |
| + </Box> | |
| + ); | |
| +} | |
| diff --git a/src/utils/recentTools.ts b/src/utils/recentTools.ts | |
| new file mode 100644 | |
| index 0000000..5d1c6ea | |
| --- /dev/null | |
| +++ b/src/utils/recentTools.ts | |
| @@ -0,0 +1,24 @@ | |
| +const recentToolsKey = 'recentTools'; | |
| +const recentToolsLimit = 8; | |
| + | |
| +export function getRecentToolPaths(): string[] { | |
| + return ( | |
| + localStorage | |
| + .getItem(recentToolsKey) | |
| + ?.split(',') | |
| + ?.filter((path) => path) ?? [] | |
| + ); | |
| +} | |
| + | |
| +export function addRecentToolPath(toolPath: string): string[] { | |
| + const updated = [ | |
| + toolPath, | |
| + ...getRecentToolPaths().filter((path) => path !== toolPath) | |
| + ].slice(0, recentToolsLimit); | |
| + localStorage.setItem(recentToolsKey, updated.join(',')); | |
| + return updated; | |
| +} | |
| + | |
| +export function clearRecentTools(): void { | |
| + localStorage.removeItem(recentToolsKey); | |
| +} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment