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> | ||||
| 
 | ||||
| <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(); | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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> | ||||
| 
 | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
|     <n-card class="tool-card"> | ||||
|       <n-space justify="space-between" align="center"> | ||||
|         <n-icon class="icon" size="40" :component="tool.icon" /> | ||||
|         <n-space align="center"> | ||||
|           <n-tag | ||||
|             v-if="tool.isNew" | ||||
|             size="small" | ||||
| @ -14,6 +15,9 @@ | ||||
|           > | ||||
|             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> | ||||
|  | ||||
| @ -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,15 +34,60 @@ 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"> | ||||
|     </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> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped lang="less"> | ||||
| .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> | ||||
|  | ||||
| @ -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], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
							
								
								
									
										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