diff options
author | 2022-12-17 01:30:02 +0100 | |
---|---|---|
committer | 2022-12-17 01:30:02 +0100 | |
commit | 4cd809bd0c94836532f58a2ec6aa131694cce10d (patch) | |
tree | 7a2f5f61c3101a3c0761cc32b67a7f9ad67222e5 /src | |
parent | 8d09086e78b6de1eb7108b8d3ba08fcca2c5d577 (diff) | |
download | it-tools-4cd809bd0c94836532f58a2ec6aa131694cce10d.tar.gz it-tools-4cd809bd0c94836532f58a2ec6aa131694cce10d.tar.zst it-tools-4cd809bd0c94836532f58a2ec6aa131694cce10d.zip |
feat(tools): added favorite tool handling
Diffstat (limited to 'src')
-rw-r--r-- | src/components/FavoriteButton.vue | 40 | ||||
-rw-r--r-- | src/components/MenuIconItem.vue | 4 | ||||
-rw-r--r-- | src/components/SearchBar.vue | 6 | ||||
-rw-r--r-- | src/components/SearchBarItem.vue | 4 | ||||
-rw-r--r-- | src/components/ToolCard.vue | 31 | ||||
-rw-r--r-- | src/pages/Home.page.vue | 53 | ||||
-rw-r--r-- | src/tools/index.ts | 10 | ||||
-rw-r--r-- | src/tools/tool.ts | 21 | ||||
-rw-r--r-- | src/tools/tools.store.ts | 44 | ||||
-rw-r--r-- | src/tools/tools.types.ts | 19 |
10 files changed, 181 insertions, 51 deletions
diff --git a/src/components/FavoriteButton.vue b/src/components/FavoriteButton.vue new file mode 100644 index 0000000..26791f3 --- /dev/null +++ b/src/components/FavoriteButton.vue @@ -0,0 +1,40 @@ +<template> + <n-tooltip trigger="hover"> + <template #trigger> + <n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite"> + <template #icon> + <n-icon :component="FavoriteFilled" /> + </template> + </n-button> + </template> + {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} + </n-tooltip> +</template> + +<script setup lang="ts"> +import { FavoriteFilled } from '@vicons/material'; +import { useToolStore } from '@/tools/tools.store'; +import type { Tool } from '@/tools/tools.types'; +import { computed, toRefs } from 'vue'; + +const toolStore = useToolStore(); + +const props = defineProps<{ tool: Tool }>(); +const { tool } = toRefs(props); + +const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); +const buttonType = computed(() => (isFavorite.value ? 'primary' : 'default')); + +function toggleFavorite(event: MouseEvent) { + event.preventDefault(); + + if (toolStore.isToolFavorite({ tool })) { + toolStore.removeToolFromFavorites({ tool }); + return; + } + + toolStore.addToolToFavorites({ tool }); +} +</script> + +<style scoped></style> diff --git a/src/components/MenuIconItem.vue b/src/components/MenuIconItem.vue index 0909e56..a08fe24 100644 --- a/src/components/MenuIconItem.vue +++ b/src/components/MenuIconItem.vue @@ -6,11 +6,11 @@ </template> <script setup lang="ts"> -import type { ITool } from '@/tools/tool'; +import type { Tool } from '@/tools/tools.types'; import { useThemeVars } from 'naive-ui'; import { toRefs } from 'vue'; -const props = defineProps<{ tool: ITool }>(); +const props = defineProps<{ tool: Tool }>(); const { tool } = toRefs(props); const theme = useThemeVars(); diff --git a/src/components/SearchBar.vue b/src/components/SearchBar.vue index bdb8eb2..95919d5 100644 --- a/src/components/SearchBar.vue +++ b/src/components/SearchBar.vue @@ -1,7 +1,7 @@ <script lang="ts" setup> import { useFuzzySearch } from '@/composable/fuzzySearch'; import { tools } from '@/tools'; -import type { ITool } from '@/tools/tool'; +import type { Tool } from '@/tools/tools.types'; import { SearchRound } from '@vicons/material'; import { useMagicKeys, whenever } from '@vueuse/core'; import { computed, h, ref } from 'vue'; @@ -17,7 +17,7 @@ const { searchResult } = useFuzzySearch({ options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] }, }); -const toolToOption = (tool: ITool) => ({ label: tool.name, value: tool.path, tool }); +const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool }); const options = computed(() => { if (queryString.value === '') { @@ -47,7 +47,7 @@ whenever(keys.ctrl_k, () => { focusTarget.value.focus(); }); -function renderOption({ tool }: { tool: ITool }) { +function renderOption({ tool }: { tool: Tool }) { return h(SearchBarItem, { tool }); } </script> diff --git a/src/components/SearchBarItem.vue b/src/components/SearchBarItem.vue index d81606a..ca79268 100644 --- a/src/components/SearchBarItem.vue +++ b/src/components/SearchBarItem.vue @@ -1,8 +1,8 @@ <script lang="ts" setup> -import type { ITool } from '@/tools/tool'; +import type { Tool } from '@/tools/tools.types'; import { toRefs } from 'vue'; -const props = defineProps<{ tool: ITool }>(); +const props = defineProps<{ tool: Tool }>(); const { tool } = toRefs(props); </script> diff --git a/src/components/ToolCard.vue b/src/components/ToolCard.vue index 926c5e9..79586f8 100644 --- a/src/components/ToolCard.vue +++ b/src/components/ToolCard.vue @@ -3,17 +3,21 @@ <n-card class="tool-card"> <n-space justify="space-between" align="center"> <n-icon class="icon" size="40" :component="tool.icon" /> - <n-tag - v-if="tool.isNew" - size="small" - class="badge-new" - round - type="success" - :bordered="false" - :color="{ color: theme.primaryColor, textColor: theme.tagColor }" - > - New - </n-tag> + <n-space align="center"> + <n-tag + v-if="tool.isNew" + size="small" + class="badge-new" + round + type="success" + :bordered="false" + :color="{ color: theme.primaryColor, textColor: theme.tagColor }" + > + New + </n-tag> + + <favorite-button :tool="tool" /> + </n-space> </n-space> <n-h3 class="title"> <n-ellipsis>{{ tool.name }}</n-ellipsis> @@ -29,11 +33,12 @@ </template> <script setup lang="ts"> -import type { ITool } from '@/tools/tool'; +import type { Tool } from '@/tools/tools.types'; import { useThemeVars } from 'naive-ui'; import { toRefs } from 'vue'; +import FavoriteButton from './FavoriteButton.vue'; -const props = defineProps<{ tool: ITool & { category: string } }>(); +const props = defineProps<{ tool: Tool & { category: string } }>(); const { tool } = toRefs(props); const theme = useThemeVars(); </script> diff --git a/src/pages/Home.page.vue b/src/pages/Home.page.vue index 88086ee..4c80494 100644 --- a/src/pages/Home.page.vue +++ b/src/pages/Home.page.vue @@ -1,10 +1,12 @@ <script setup lang="ts"> -import { toolsWithCategory } from '@/tools'; +import { useToolStore } from '@/tools/tools.store'; import { Heart } from '@vicons/tabler'; import { useHead } from '@vueuse/head'; import ColoredCard from '../components/ColoredCard.vue'; import ToolCard from '../components/ToolCard.vue'; +const toolStore = useToolStore(); + useHead({ title: 'IT Tools - Handy online tools for developers' }); </script> @@ -32,8 +34,34 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); <n-icon :component="Heart" /> </colored-card> </n-gi> - <n-gi v-for="tool in toolsWithCategory" :key="tool.name"> - <tool-card :tool="tool" /> + </n-grid> + + <transition name="height"> + <div v-if="toolStore.favoriteTools.length > 0"> + <n-h3>Your favorite tools</n-h3> + <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> + <n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name"> + <tool-card :tool="tool" /> + </n-gi> + </n-grid> + </div> + </transition> + + <div v-if="toolStore.newTools.length > 0"> + <n-h3>Newest tools</n-h3> + <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> + <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> + <tool-card :tool="tool" /> + </n-gi> + </n-grid> + </div> + + <n-h3>All the tools</n-h3> + <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> + <n-gi v-for="tool in toolStore.tools" :key="tool.name"> + <transition> + <tool-card :tool="tool" /> + </transition> </n-gi> </n-grid> </div> @@ -43,4 +71,23 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); .home-page { padding-top: 50px; } + +::v-deep(.n-grid) { + margin-bottom: 12px; +} + +.height-enter-active, +.height-leave-active { + transition: all 0.5s ease-in-out; + overflow: hidden; + max-height: 500px; +} + +.height-enter-from, +.height-leave-to { + max-height: 42px; + overflow: hidden; + opacity: 0; + margin-bottom: 0; +} </style> diff --git a/src/tools/index.ts b/src/tools/index.ts index 60ad21d..38975f6 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,5 +1,4 @@ import { LockOpen } from '@vicons/tabler'; -import type { ToolCategory } from './tool'; import { tool as chmodCalculator } from './chmod-calculator'; import { tool as mimeTypes } from './mime-types'; @@ -36,16 +35,15 @@ import { tool as tokenGenerator } from './token-generator'; import { tool as urlEncoder } from './url-encoder'; import { tool as urlParser } from './url-parser'; import { tool as uuidGenerator } from './uuid-generator'; +import type { ToolCategory } from './tools.types'; export const toolsByCategory: ToolCategory[] = [ { name: 'Crypto', - icon: LockOpen, components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator], }, { name: 'Converter', - icon: LockOpen, components: [ dateTimeConverter, baseConverter, @@ -58,7 +56,6 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Web', - icon: LockOpen, components: [ urlEncoder, htmlEntities, @@ -72,27 +69,22 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Images', - icon: LockOpen, components: [qrCodeGenerator, svgPlaceholderGenerator], }, { name: 'Development', - icon: LockOpen, components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator], }, { name: 'Math', - icon: LockOpen, components: [mathEvaluator, etaCalculator], }, { name: 'Measurement', - icon: LockOpen, components: [chronometer], }, { name: 'Text', - icon: LockOpen, components: [loremIpsumGenerator, textStatistics], }, ]; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index b2ebf49..8289aa3 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,27 +1,10 @@ import { config } from '@/config'; -import type { Component } from 'vue'; - -export interface ITool { - name: string; - path: string; - description: string; - keywords: string[]; - component: () => Promise<Component>; - icon: Component; - redirectFrom?: string[]; - isNew: boolean; -} - -export interface ToolCategory { - name: string; - icon: Component; - components: ITool[]; -} +import type { Tool } from './tools.types'; type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; export function defineTool( - tool: WithOptional<ITool, 'isNew'>, + tool: WithOptional<Tool, 'isNew'>, { newTools }: { newTools: string[] } = { newTools: config.tools.newTools }, ) { const isNew = newTools.includes(tool.name); diff --git a/src/tools/tools.store.ts b/src/tools/tools.store.ts new file mode 100644 index 0000000..2b0826c --- /dev/null +++ b/src/tools/tools.store.ts @@ -0,0 +1,44 @@ +import { get, useStorage, type MaybeRef } from '@vueuse/core'; +import { defineStore } from 'pinia'; +import type { Ref } from 'vue'; +import { toolsWithCategory } from './index'; +import type { Tool, ToolWithCategory } from './tools.types'; + +export const useToolStore = defineStore('tools', { + state: () => ({ + favoriteToolsName: useStorage('favoriteToolsName', []) as Ref<string[]>, + }), + getters: { + favoriteTools(state) { + return state.favoriteToolsName + .map((favoriteName) => toolsWithCategory.find(({ name }) => name === favoriteName)) + .filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type + }, + + notFavoriteTools(state): ToolWithCategory[] { + return toolsWithCategory.filter((tool) => !state.favoriteToolsName.includes(tool.name)); + }, + + tools(): ToolWithCategory[] { + return toolsWithCategory; + }, + + newTools(): ToolWithCategory[] { + return this.tools.filter(({ isNew }) => isNew); + }, + }, + + actions: { + addToolToFavorites({ tool }: { tool: MaybeRef<Tool> }) { + this.favoriteToolsName.push(get(tool).name); + }, + + removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) { + this.favoriteToolsName = this.favoriteToolsName.filter((name) => get(tool).name !== name); + }, + + isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) { + return this.favoriteToolsName.includes(get(tool).name); + }, + }, +}); diff --git a/src/tools/tools.types.ts b/src/tools/tools.types.ts new file mode 100644 index 0000000..5630a12 --- /dev/null +++ b/src/tools/tools.types.ts @@ -0,0 +1,19 @@ +import type { Component } from 'vue'; + +export type Tool = { + name: string; + path: string; + description: string; + keywords: string[]; + component: () => Promise<Component>; + icon: Component; + redirectFrom?: string[]; + isNew: boolean; +}; + +export type ToolCategory = { + name: string; + components: Tool[]; +}; + +export type ToolWithCategory = Tool & { category: string }; |