aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/components/FavoriteButton.vue40
-rw-r--r--src/components/MenuIconItem.vue4
-rw-r--r--src/components/SearchBar.vue6
-rw-r--r--src/components/SearchBarItem.vue4
-rw-r--r--src/components/ToolCard.vue31
-rw-r--r--src/pages/Home.page.vue53
-rw-r--r--src/tools/index.ts10
-rw-r--r--src/tools/tool.ts21
-rw-r--r--src/tools/tools.store.ts44
-rw-r--r--src/tools/tools.types.ts19
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 };