feat(tools): added favorite tool handling
This commit is contained in:
		
							parent
							
								
									8d09086e78
								
							
						
					
					
						commit
						4cd809bd0c
					
				
							
								
								
									
										40
									
								
								src/components/FavoriteButton.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/components/FavoriteButton.vue
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||||
| @ -6,11 +6,11 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { ITool } from '@/tools/tool'; | import type { Tool } from '@/tools/tools.types'; | ||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import { toRefs } from 'vue'; | import { toRefs } from 'vue'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ tool: ITool }>(); | const props = defineProps<{ tool: Tool }>(); | ||||||
| const { tool } = toRefs(props); | const { tool } = toRefs(props); | ||||||
| 
 | 
 | ||||||
| const theme = useThemeVars(); | const theme = useThemeVars(); | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { useFuzzySearch } from '@/composable/fuzzySearch'; | import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||||
| import { tools } from '@/tools'; | import { tools } from '@/tools'; | ||||||
| import type { ITool } from '@/tools/tool'; | import type { Tool } from '@/tools/tools.types'; | ||||||
| import { SearchRound } from '@vicons/material'; | import { SearchRound } from '@vicons/material'; | ||||||
| import { useMagicKeys, whenever } from '@vueuse/core'; | import { useMagicKeys, whenever } from '@vueuse/core'; | ||||||
| import { computed, h, ref } from 'vue'; | import { computed, h, ref } from 'vue'; | ||||||
| @ -17,7 +17,7 @@ const { searchResult } = useFuzzySearch({ | |||||||
|   options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] }, |   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(() => { | const options = computed(() => { | ||||||
|   if (queryString.value === '') { |   if (queryString.value === '') { | ||||||
| @ -47,7 +47,7 @@ whenever(keys.ctrl_k, () => { | |||||||
|   focusTarget.value.focus(); |   focusTarget.value.focus(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| function renderOption({ tool }: { tool: ITool }) { | function renderOption({ tool }: { tool: Tool }) { | ||||||
|   return h(SearchBarItem, { tool }); |   return h(SearchBarItem, { tool }); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import type { ITool } from '@/tools/tool'; | import type { Tool } from '@/tools/tools.types'; | ||||||
| import { toRefs } from 'vue'; | import { toRefs } from 'vue'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ tool: ITool }>(); | const props = defineProps<{ tool: Tool }>(); | ||||||
| const { tool } = toRefs(props); | const { tool } = toRefs(props); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
|     <n-card class="tool-card"> |     <n-card class="tool-card"> | ||||||
|       <n-space justify="space-between" align="center"> |       <n-space justify="space-between" align="center"> | ||||||
|         <n-icon class="icon" size="40" :component="tool.icon" /> |         <n-icon class="icon" size="40" :component="tool.icon" /> | ||||||
|  |         <n-space align="center"> | ||||||
|           <n-tag |           <n-tag | ||||||
|             v-if="tool.isNew" |             v-if="tool.isNew" | ||||||
|             size="small" |             size="small" | ||||||
| @ -14,6 +15,9 @@ | |||||||
|           > |           > | ||||||
|             New |             New | ||||||
|           </n-tag> |           </n-tag> | ||||||
|  | 
 | ||||||
|  |           <favorite-button :tool="tool" /> | ||||||
|  |         </n-space> | ||||||
|       </n-space> |       </n-space> | ||||||
|       <n-h3 class="title"> |       <n-h3 class="title"> | ||||||
|         <n-ellipsis>{{ tool.name }}</n-ellipsis> |         <n-ellipsis>{{ tool.name }}</n-ellipsis> | ||||||
| @ -29,11 +33,12 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { ITool } from '@/tools/tool'; | import type { Tool } from '@/tools/tools.types'; | ||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import { toRefs } from 'vue'; | 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 { tool } = toRefs(props); | ||||||
| const theme = useThemeVars(); | const theme = useThemeVars(); | ||||||
| </script> | </script> | ||||||
|  | |||||||
| @ -1,10 +1,12 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { toolsWithCategory } from '@/tools'; | import { useToolStore } from '@/tools/tools.store'; | ||||||
| import { Heart } from '@vicons/tabler'; | import { Heart } from '@vicons/tabler'; | ||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
| import ColoredCard from '../components/ColoredCard.vue'; | import ColoredCard from '../components/ColoredCard.vue'; | ||||||
| import ToolCard from '../components/ToolCard.vue'; | import ToolCard from '../components/ToolCard.vue'; | ||||||
| 
 | 
 | ||||||
|  | const toolStore = useToolStore(); | ||||||
|  | 
 | ||||||
| useHead({ title: 'IT Tools - Handy online tools for developers' }); | useHead({ title: 'IT Tools - Handy online tools for developers' }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| @ -32,15 +34,60 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); | |||||||
|           <n-icon :component="Heart" /> |           <n-icon :component="Heart" /> | ||||||
|         </colored-card> |         </colored-card> | ||||||
|       </n-gi> |       </n-gi> | ||||||
|       <n-gi v-for="tool in toolsWithCategory" :key="tool.name"> |     </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" /> |             <tool-card :tool="tool" /> | ||||||
|           </n-gi> |           </n-gi> | ||||||
|         </n-grid> |         </n-grid> | ||||||
|       </div> |       </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> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped lang="less"> | <style scoped lang="less"> | ||||||
| .home-page { | .home-page { | ||||||
|   padding-top: 50px; |   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> | </style> | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| import { LockOpen } from '@vicons/tabler'; | import { LockOpen } from '@vicons/tabler'; | ||||||
| import type { ToolCategory } from './tool'; |  | ||||||
| 
 | 
 | ||||||
| import { tool as chmodCalculator } from './chmod-calculator'; | import { tool as chmodCalculator } from './chmod-calculator'; | ||||||
| import { tool as mimeTypes } from './mime-types'; | 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 urlEncoder } from './url-encoder'; | ||||||
| import { tool as urlParser } from './url-parser'; | import { tool as urlParser } from './url-parser'; | ||||||
| import { tool as uuidGenerator } from './uuid-generator'; | import { tool as uuidGenerator } from './uuid-generator'; | ||||||
|  | import type { ToolCategory } from './tools.types'; | ||||||
| 
 | 
 | ||||||
| export const toolsByCategory: ToolCategory[] = [ | export const toolsByCategory: ToolCategory[] = [ | ||||||
|   { |   { | ||||||
|     name: 'Crypto', |     name: 'Crypto', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator], |     components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Converter', |     name: 'Converter', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [ |     components: [ | ||||||
|       dateTimeConverter, |       dateTimeConverter, | ||||||
|       baseConverter, |       baseConverter, | ||||||
| @ -58,7 +56,6 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Web', |     name: 'Web', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [ |     components: [ | ||||||
|       urlEncoder, |       urlEncoder, | ||||||
|       htmlEntities, |       htmlEntities, | ||||||
| @ -72,27 +69,22 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Images', |     name: 'Images', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [qrCodeGenerator, svgPlaceholderGenerator], |     components: [qrCodeGenerator, svgPlaceholderGenerator], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Development', |     name: 'Development', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator], |     components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Math', |     name: 'Math', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [mathEvaluator, etaCalculator], |     components: [mathEvaluator, etaCalculator], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Measurement', |     name: 'Measurement', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [chronometer], |     components: [chronometer], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Text', |     name: 'Text', | ||||||
|     icon: LockOpen, |  | ||||||
|     components: [loremIpsumGenerator, textStatistics], |     components: [loremIpsumGenerator, textStatistics], | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  | |||||||
| @ -1,27 +1,10 @@ | |||||||
| import { config } from '@/config'; | import { config } from '@/config'; | ||||||
| import type { Component } from 'vue'; | import type { Tool } from './tools.types'; | ||||||
| 
 |  | ||||||
| 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[]; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; | type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; | ||||||
| 
 | 
 | ||||||
| export function defineTool( | export function defineTool( | ||||||
|   tool: WithOptional<ITool, 'isNew'>, |   tool: WithOptional<Tool, 'isNew'>, | ||||||
|   { newTools }: { newTools: string[] } = { newTools: config.tools.newTools }, |   { newTools }: { newTools: string[] } = { newTools: config.tools.newTools }, | ||||||
| ) { | ) { | ||||||
|   const isNew = newTools.includes(tool.name); |   const isNew = newTools.includes(tool.name); | ||||||
|  | |||||||
							
								
								
									
										44
									
								
								src/tools/tools.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/tools/tools.store.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										19
									
								
								src/tools/tools.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/tools/tools.types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 }; | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user