diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 03cab77..4cae00e 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -15,11 +15,16 @@ import { useState } from 'react'; import { DefinedTool } from '@tools/defineTool'; import { filterTools, tools } from '@tools/index'; import { useNavigate } from 'react-router-dom'; -import _ from 'lodash'; import { Icon } from '@iconify/react'; import { getToolCategoryTitle } from '@utils/string'; import { useTranslation } from 'react-i18next'; import { validNamespaces } from '../i18n'; +import { + getBookmarkedToolPaths, + isBookmarked, + toggleBookmarked +} from '@utils/bookmark'; +import IconButton from '@mui/material/IconButton'; const GroupHeader = styled('div')(({ theme }) => ({ position: 'sticky', @@ -36,61 +41,59 @@ const GroupItems = styled('ul')({ padding: 0 }); +type ToolInfo = { + label: string; + url: string; +}; + export default function Hero() { const { t } = useTranslation(validNamespaces); const [inputValue, setInputValue] = useState(''); const theme = useTheme(); const [filteredTools, setFilteredTools] = useState(tools); + const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState( + getBookmarkedToolPaths() + ); const navigate = useNavigate(); - const exampleTools: { label: string; url: string; translationKey: string }[] = - [ - { - label: t('translation:hero.examples.createTransparentImage'), - url: '/image-generic/create-transparent', - translationKey: 'translation:hero.examples.createTransparentImage' - }, - { - label: t('translation:hero.examples.prettifyJson'), - url: '/json/prettify', - translationKey: 'translation:hero.examples.prettifyJson' - }, - { - label: t('translation:hero.examples.changeGifSpeed'), - url: '/gif/change-speed', - translationKey: 'translation:hero.examples.changeGifSpeed' - }, - { - label: t('translation:hero.examples.sortList'), - url: '/list/sort', - translationKey: 'translation:hero.examples.sortList' - }, - { - label: t('translation:hero.examples.compressPng'), - url: '/png/compress-png', - translationKey: 'translation:hero.examples.compressPng' - }, - { - label: t('translation:hero.examples.splitText'), - url: '/string/split', - translationKey: 'translation:hero.examples.splitText' - }, - { - label: t('translation:hero.examples.splitPdf'), - url: '/pdf/split-pdf', - translationKey: 'translation:hero.examples.splitPdf' - }, - { - label: t('translation:hero.examples.trimVideo'), - url: '/video/trim', - translationKey: 'translation:hero.examples.trimVideo' - }, - { - label: t('translation:hero.examples.calculateNumberSum'), - url: '/number/sum', - translationKey: 'translation:hero.examples.calculateNumberSum' - } - ]; + const exampleTools: ToolInfo[] = [ + { + label: t('translation:hero.examples.createTransparentImage'), + url: '/image-generic/create-transparent' + }, + { + label: t('translation:hero.examples.prettifyJson'), + url: '/json/prettify' + }, + { + label: t('translation:hero.examples.changeGifSpeed'), + url: '/gif/change-speed' + }, + { + label: t('translation:hero.examples.sortList'), + url: '/list/sort' + }, + { + label: t('translation:hero.examples.compressPng'), + url: '/png/compress-png' + }, + { + label: t('translation:hero.examples.splitText'), + url: '/string/split' + }, + { + label: t('translation:hero.examples.splitPdf'), + url: '/pdf/split-pdf' + }, + { + label: t('translation:hero.examples.trimVideo'), + url: '/video/trim' + }, + { + label: t('translation:hero.examples.calculateNumberSum'), + url: '/number/sum' + } + ]; const handleInputChange = ( event: React.ChangeEvent<{}>, @@ -99,6 +102,24 @@ export default function Hero() { setInputValue(newInputValue); setFilteredTools(filterTools(tools, newInputValue, t)); }; + const toolsMap = new Map(); + for (const tool of filteredTools) { + toolsMap.set(tool.path, { + label: tool.name, + url: '/' + tool.path + }); + } + + const displayedTools = + bookmarkedToolPaths.length > 0 + ? bookmarkedToolPaths.flatMap((path) => { + const tool = toolsMap.get(path); + if (tool === undefined) { + return []; + } + return [{ ...tool, label: t(tool.label) }]; + }) + : exampleTools; return ( @@ -159,14 +180,42 @@ export default function Hero() { {...props} onClick={() => navigate('/' + option.path)} > - - - - {t(option.name)} - - {t(option.shortDescription)} - - + + + + + {t(option.name)} + + {t(option.shortDescription)} + + + + { + e.stopPropagation(); + toggleBookmarked(option.path); + setBookmarkedToolPaths(getBookmarkedToolPaths()); + }} + > + + )} @@ -177,7 +226,7 @@ export default function Hero() { }} /> - {exampleTools.map((tool) => ( + {displayedTools.map((tool) => ( navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`) @@ -186,7 +235,7 @@ export default function Hero() { xs={12} md={6} lg={4} - key={tool.translationKey} + key={tool.label} > - {tool.label} + + {tool.label} + {bookmarkedToolPaths.length > 0 && ( + { + e.stopPropagation(); + const path = tool.url.substring(1); + toggleBookmarked(path); + setBookmarkedToolPaths(getBookmarkedToolPaths()); + }} + size={'small'} + > + + + )} + ))} diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx index dfe42f6..47292f8 100644 --- a/src/components/ToolHeader.tsx +++ b/src/components/ToolHeader.tsx @@ -1,4 +1,4 @@ -import { Box, Button, styled, useTheme } from '@mui/material'; +import { Box, Button, Stack, styled, useTheme } from '@mui/material'; import Typography from '@mui/material/Typography'; import ToolBreadcrumb from './ToolBreadcrumb'; import { capitalizeFirstLetter } from '../utils/string'; @@ -7,6 +7,8 @@ import { Icon, IconifyIcon } from '@iconify/react'; import { categoriesColors } from '../config/uiConfig'; import { getToolsByCategory } from '@tools/index'; import { useEffect, useState } from 'react'; +import { isBookmarked, toggleBookmarked } from '@utils/bookmark'; +import IconButton from '@mui/material/IconButton'; import { useTranslation } from 'react-i18next'; const StyledButton = styled(Button)(({ theme }) => ({ @@ -22,6 +24,7 @@ interface ToolHeaderProps { description: string; icon?: IconifyIcon | string; type: string; + path: string; } function ToolLinks() { @@ -82,8 +85,11 @@ export default function ToolHeader({ icon, title, description, - type + type, + path }: ToolHeaderProps) { + const theme = useTheme(); + const [bookmarked, setBookmarked] = useState(isBookmarked(path)); return ( - - {title} - + + + {title} + + { + toggleBookmarked(path); + setBookmarked(!bookmarked); + }} + > + + + {description} diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx index f5a1e97..7f127c8 100644 --- a/src/components/ToolLayout.tsx +++ b/src/components/ToolLayout.tsx @@ -17,11 +17,13 @@ import { FullI18nKey } from '../i18n'; export default function ToolLayout({ children, icon, + i18n, type, - i18n + fullPath }: { icon?: IconifyIcon | string; type: ToolCategory; + fullPath: string; children: ReactNode; i18n?: { name: FullI18nKey; @@ -68,6 +70,7 @@ export default function ToolLayout({ description={toolDescription} icon={icon} type={type} + path={fullPath} /> {children} diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx index 48692e1..0ec3113 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -65,7 +65,12 @@ export const defineTool = ( component: function ToolComponent() { const { t } = useTranslation(); return ( - + path) ?? [] + ); +} + +export function isBookmarked(toolPath: string): boolean { + return getBookmarkedToolPaths().some((path) => path === toolPath); +} + +export function toggleBookmarked(toolPath: string) { + if (isBookmarked(toolPath)) { + unbookmark(toolPath); + } else { + bookmark(toolPath); + } +} + +function bookmark(toolPath: string) { + localStorage.setItem( + bookmarkedToolsKey, + [toolPath, ...getBookmarkedToolPaths()].join(',') + ); +} + +function unbookmark(toolPath: string) { + localStorage.setItem( + bookmarkedToolsKey, + getBookmarkedToolPaths() + .filter((path) => path !== toolPath) + .join(',') + ); +}