mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-24 00:36:14 -04:00
feat(tools): added favorite tool handling
This commit is contained in:
parent
8d09086e78
commit
4cd809bd0c
10 changed files with 181 additions and 51 deletions
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,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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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…
Add table
Add a link
Reference in a new issue