Skip to content

Instantly share code, notes, and snippets.

@shricodev
Last active January 18, 2026 15:38
Show Gist options
  • Select an option

  • Save shricodev/6a8eea20c34d31429b254c82079a1972 to your computer and use it in GitHub Desktop.

Select an option

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
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