Compare commits

...

11 Commits

Author SHA1 Message Date
Corentin Thomasset
ebb7301a98 chore(release): 2.16.0 2022-12-21 21:15:16 +01:00
Corentin Thomasset
def60e7248 refactor(tracker): better tracker injection 2022-12-21 21:02:57 +01:00
Corentin Thomasset
bf88836dbe feat(search-bar): use cmd + k to focus on mac 2022-12-21 00:21:12 +01:00
Corentin Thomasset
bfc2e24bbf feat(tracker): added actions monitoring 2022-12-21 00:03:31 +01:00
Corentin Thomasset
40872859a5 refactor(clean): removed unused import 2022-12-20 21:15:00 +01:00
Corentin Thomasset
cf723f144e refactor(clean): removed empty style tag 2022-12-20 21:14:40 +01:00
Corentin Thomasset
7f964941d3 chore(docs): updated readme 2022-12-20 20:57:24 +01:00
Corentin Thomasset
af075dcccc feat(tool): improved favorite tool management 2022-12-17 15:33:52 +01:00
Corentin Thomasset
274ff02b54 chore(git): added .env to gitignore 2022-12-17 11:42:47 +01:00
Corentin Thomasset
679dd1c1f6 refactor(menu): improve support button 2022-12-17 01:30:27 +01:00
Corentin Thomasset
4cd809bd0c feat(tools): added favorite tool handling 2022-12-17 01:30:02 +01:00
24 changed files with 392 additions and 160 deletions

2
.gitignore vendored
View File

@@ -26,3 +26,5 @@ coverage
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.env

View File

@@ -2,6 +2,24 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.16.0](https://github.com/CorentinTh/it-tools/compare/v2.15.0...v2.16.0) (2022-12-21)
### Features
* **search-bar:** use cmd + k to focus on mac ([bf88836](https://github.com/CorentinTh/it-tools/commit/bf88836dbe4037019e9545deaae1db06e5768cfb))
* **tool:** improved favorite tool management ([af075dc](https://github.com/CorentinTh/it-tools/commit/af075dccccec959a0863e6d11516206860bed91f))
* **tools:** added favorite tool handling ([4cd809b](https://github.com/CorentinTh/it-tools/commit/4cd809bd0c94836532f58a2ec6aa131694cce10d))
* **tracker:** added actions monitoring ([bfc2e24](https://github.com/CorentinTh/it-tools/commit/bfc2e24bbfc08f67ed9c9b1d93474029bc01dc8b))
### Refactors
* **clean:** removed empty style tag ([cf723f1](https://github.com/CorentinTh/it-tools/commit/cf723f144ee865b6de7323d3be58eb7a9586fa56))
* **clean:** removed unused import ([4087285](https://github.com/CorentinTh/it-tools/commit/40872859a580a20bb838b79db2b3c88c00995e37))
* **menu:** improve support button ([679dd1c](https://github.com/CorentinTh/it-tools/commit/679dd1c1f6265227cc9db60c55d83f8eaf8f72b4))
* **tracker:** better tracker injection ([def60e7](https://github.com/CorentinTh/it-tools/commit/def60e7248003e74ed67e9ff116b438bab410a92))
## [2.15.0](https://github.com/CorentinTh/it-tools/compare/v2.14.1...v2.15.0) (2022-12-16) ## [2.15.0](https://github.com/CorentinTh/it-tools/compare/v2.14.1...v2.15.0) (2022-12-16)

View File

@@ -10,46 +10,53 @@ You have an idea of a tool? Submit a [feature request](https://github.com/Corent
## Contribute ## Contribute
### Recommended IDE Setup ## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin). [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
### Node version ## Type Support for `.vue` Imports in TS
Ensure you have the correct node/npm version TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh ```sh
nvm use pnpm install
``` ```
### Project Setup ### Compile and Hot-Reload for Development
```sh ```sh
npm install pnpm dev
``` ```
#### Compile and Hot-Reload for Development ### Type-Check, Compile and Minify for Production
```sh ```sh
npm run dev pnpm build
``` ```
#### Type-Check, Compile and Minify for Production ### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh ```sh
npm run build pnpm test
``` ```
#### Run Unit Tests with [Vitest](https://vitest.dev/) ### Lint with [ESLint](https://eslint.org/)
```sh ```sh
npm run test pnpm lint
```
#### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
``` ```
### Create a new tool ### Create a new tool
@@ -68,12 +75,9 @@ Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
This project is continuously deployed using [vercel.com](https://vercel.com). This project is continuously deployed using [vercel.com](https://vercel.com).
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## License ## License
This project is under the [GNU GPLv3](LICENSE). This project is under the [GNU GPLv3](LICENSE).

View File

@@ -1,6 +1,6 @@
{ {
"name": "it-tools", "name": "it-tools",
"version": "2.15.0", "version": "2.16.0",
"description": "Collection of handy online tools for developers, with great UX. ", "description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [ "keywords": [
"productivity", "productivity",

View File

@@ -0,0 +1,38 @@
<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>

View File

@@ -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();

View File

@@ -1,23 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFuzzySearch } from '@/composable/fuzzySearch'; import { useFuzzySearch } from '@/composable/fuzzySearch';
import { useTracker } from '@/modules/tracker/tracker.services';
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 type { NInput } from 'naive-ui';
import { computed, h, ref } from 'vue'; import { computed, h, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import SearchBarItem from './SearchBarItem.vue'; import SearchBarItem from './SearchBarItem.vue';
const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool });
const router = useRouter(); const router = useRouter();
const { tracker } = useTracker();
const queryString = ref(''); const queryString = ref('');
const inputEl = ref<HTMLElement>();
const { searchResult } = useFuzzySearch({ const displayDropDown = ref(true);
search: queryString, const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
data: tools,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const toolToOption = (tool: ITool) => ({ label: tool.name, value: tool.path, tool });
const options = computed(() => { const options = computed(() => {
if (queryString.value === '') { if (queryString.value === '') {
@@ -27,12 +28,11 @@ const options = computed(() => {
return searchResult.value.map(toolToOption); return searchResult.value.map(toolToOption);
}); });
function onSelect(path: string) { const { searchResult } = useFuzzySearch({
router.push(path); search: queryString,
queryString.value = ''; data: tools,
} options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const focusTarget = ref();
const keys = useMagicKeys({ const keys = useMagicKeys({
passive: false, passive: false,
@@ -40,16 +40,40 @@ const keys = useMagicKeys({
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') { if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault(); e.preventDefault();
} }
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault();
}
}, },
}); });
whenever(keys.ctrl_k, () => { whenever(keys.ctrl_k, claimFocus);
focusTarget.value.focus(); whenever(keys.meta_k, claimFocus);
}); whenever(keys.escape, releaseFocus);
function renderOption({ tool }: { tool: ITool }) { function renderOption({ tool }: { tool: Tool }) {
return h(SearchBarItem, { tool }); return h(SearchBarItem, { tool });
} }
function onSelect(path: string) {
router.push(path);
queryString.value = '';
}
function claimFocus() {
displayDropDown.value = true;
inputEl.value?.focus();
}
function releaseFocus() {
displayDropDown.value = false;
}
function onFocus() {
tracker.trackEvent({ eventName: 'Search-bar focused' });
displayDropDown.value = true;
}
</script> </script>
<template> <template>
@@ -60,14 +84,16 @@ function renderOption({ tool }: { tool: ITool }) {
:on-select="(value) => onSelect(String(value))" :on-select="(value) => onSelect(String(value))"
:render-label="renderOption" :render-label="renderOption"
:default-value="'aa'" :default-value="'aa'"
:get-show="() => true" :get-show="() => displayDropDown"
:on-focus="onFocus"
@update:value="() => (displayDropDown = true)"
> >
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
<n-input <n-input
ref="focusTarget" ref="inputEl"
round round
clearable clearable
placeholder="Search a tool... [Ctrl + K]" :placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`"
:value="slotValue" :value="slotValue"
:input-props="{ autocomplete: 'disabled' }" :input-props="{ autocomplete: 'disabled' }"
@input="handleInput" @input="handleInput"
@@ -82,5 +108,3 @@ function renderOption({ tool }: { tool: ITool }) {
</n-auto-complete> </n-auto-complete>
</div> </div>
</template> </template>
<style lang="less" scoped></style>

View File

@@ -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>

View File

@@ -3,17 +3,21 @@
<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-tag <n-space align="center">
v-if="tool.isNew" <n-tag
size="small" v-if="tool.isNew"
class="badge-new" size="small"
round class="badge-new"
type="success" round
:bordered="false" type="success"
:color="{ color: theme.primaryColor, textColor: theme.tagColor }" :bordered="false"
> :color="{ color: theme.primaryColor, textColor: theme.tagColor }"
New >
</n-tag> New
</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>

View File

@@ -1,13 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { NIcon, useThemeVars, type MenuGroupOption } from 'naive-ui'; import { NIcon, useThemeVars, type MenuGroupOption } from 'naive-ui';
import { h } from 'vue'; import { computed, h } from 'vue';
import { RouterLink, useRoute } from 'vue-router'; import { RouterLink, useRoute } from 'vue-router';
import { Heart, Menu2, Home2 } from '@vicons/tabler'; import { Heart, Menu2, Home2 } from '@vicons/tabler';
import { toolsByCategory } from '@/tools'; import { toolsByCategory } from '@/tools';
import { useStyleStore } from '@/stores/style.store'; import { useStyleStore } from '@/stores/style.store';
import { config } from '@/config'; import { config } from '@/config';
import MenuIconItem from '@/components/MenuIconItem.vue'; import MenuIconItem from '@/components/MenuIconItem.vue';
import type { ITool } from '@/tools/tool'; import type { Tool } from '@/tools/tools.types';
import { useToolStore } from '@/tools/tools.store';
import { useTracker } from '@/modules/tracker/tracker.services';
import SearchBar from '../components/SearchBar.vue'; import SearchBar from '../components/SearchBar.vue';
import HeroGradient from '../assets/hero-gradient.svg?component'; import HeroGradient from '../assets/hero-gradient.svg?component';
import MenuLayout from '../components/MenuLayout.vue'; import MenuLayout from '../components/MenuLayout.vue';
@@ -19,19 +21,30 @@ const styleStore = useStyleStore();
const version = config.app.version; const version = config.app.version;
const commitSha = config.app.lastCommitSha.slice(0, 7); const commitSha = config.app.lastCommitSha.slice(0, 7);
const makeLabel = (tool: ITool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name }); const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
const makeIcon = (tool: ITool) => () => h(MenuIconItem, { tool }); const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool });
const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({ const { tracker } = useTracker();
label: category.name,
key: category.name, const toolStore = useToolStore();
type: 'group',
children: category.components.map((tool) => ({ const menuOptions = computed<MenuGroupOption[]>(() =>
label: makeLabel(tool), [
icon: makeIcon(tool), ...(toolStore.favoriteTools.length > 0
key: tool.name, ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }]
: []),
...toolsByCategory,
].map((category) => ({
label: category.name,
key: category.name,
type: 'group',
children: category.components.map((tool) => ({
label: makeLabel(tool),
icon: makeIcon(tool),
key: tool.name,
})),
})), })),
})); );
</script> </script>
<template> <template>
@@ -145,6 +158,9 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
href="https://github.com/sponsors/CorentinTh" href="https://github.com/sponsors/CorentinTh"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
class="support-button"
:bordered="false"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
> >
Buy me a coffee Buy me a coffee
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-left: 5px" /> <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-left: 5px" />
@@ -170,6 +186,19 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
// background-size: @size @size; // background-size: @size @size;
// } // }
.support-button {
background: rgb(37, 99, 108);
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
color: #fff;
transition: all ease 0.2s;
&:hover {
color: #fff;
padding-left: 30px;
padding-right: 30px;
}
}
.footer { .footer {
text-align: center; text-align: center;
color: #838587; color: #838587;

View File

@@ -3,22 +3,22 @@ import { useRoute } from 'vue-router';
import { useHead } from '@vueuse/head'; import { useHead } from '@vueuse/head';
import type { HeadObject } from '@vueuse/head'; import type { HeadObject } from '@vueuse/head';
import { computed } from 'vue'; import { computed } from 'vue';
import { useThemeVars } from 'naive-ui'; import FavoriteButton from '@/components/FavoriteButton.vue';
import type { Tool } from '@/tools/tools.types';
import BaseLayout from './base.layout.vue'; import BaseLayout from './base.layout.vue';
const route = useRoute(); const route = useRoute();
const theme = useThemeVars();
const head = computed<HeadObject>(() => ({ const head = computed<HeadObject>(() => ({
title: `${route.meta.name} - IT Tools`, title: `${route.meta.name} - IT Tools`,
meta: [ meta: [
{ {
name: 'description', name: 'description',
content: route.meta.description, content: route.meta?.description as string,
}, },
{ {
name: 'keywords', name: 'keywords',
content: route.meta.keywords, content: ((route.meta.keywords ?? []) as string[]).join(','),
}, },
], ],
})); }));
@@ -29,22 +29,18 @@ useHead(head);
<base-layout> <base-layout>
<div class="tool-layout"> <div class="tool-layout">
<div class="tool-header"> <div class="tool-header">
<n-h1> <n-space align="center" justify="space-between" :wrap="false">
{{ route.meta.name }} <n-h1>
{{ route.meta.name }}
</n-h1>
<n-tag <div>
v-if="route.meta.isNew" <favorite-button :tool="{name: route.meta.name} as Tool" />
round </div>
type="success" </n-space>
:bordered="false"
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
>
New tool
</n-tag>
<!-- <span class="new-tool-badge">New !</span> -->
</n-h1>
<div class="separator" /> <div class="separator" />
<div class="description"> <div class="description">
{{ route.meta.description }} {{ route.meta.description }}
</div> </div>
@@ -92,6 +88,7 @@ useHead(head);
width: 200px; width: 200px;
height: 2px; height: 2px;
background: rgb(161, 161, 161); background: rgb(161, 161, 161);
opacity: 0.2;
margin: 10px 0; margin: 10px 0;
} }

View File

@@ -0,0 +1,27 @@
import _ from 'lodash';
import type Plausible from 'plausible-tracker';
import { inject } from 'vue';
export { createTrackerService, useTracker };
function createTrackerService({ plausible }: { plausible: ReturnType<typeof Plausible> }) {
return {
trackEvent({ eventName }: { eventName: string }) {
plausible.trackEvent(eventName);
},
};
}
function useTracker() {
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
if (_.isNil(plausible)) {
throw new Error('Plausible must be instantiated');
}
const tracker = createTrackerService({ plausible });
return {
tracker,
};
}

View File

@@ -0,0 +1,3 @@
import type { createTrackerService } from './tracker.services';
export type TrackerService = ReturnType<typeof createTrackerService>;

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTracker } from '@/modules/tracker/tracker.services';
import { useHead } from '@vueuse/head'; import { useHead } from '@vueuse/head';
useHead({ title: 'About - IT Tools' }); useHead({ title: 'About - IT Tools' });
const { tracker } = useTracker();
</script> </script>
<template> <template>
@@ -25,6 +27,7 @@ useHead({ title: 'About - IT Tools' });
href="https://github.com/sponsors/CorentinTh" href="https://github.com/sponsors/CorentinTh"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
> >
sponsoring me </n-button sponsoring me </n-button
>. >.

View File

@@ -1,41 +1,71 @@
<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>
<template> <template>
<div class="home-page"> <div class="home-page">
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> <div class="grid-wrapper">
<n-gi> <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<colored-card title="You like it-tools?" :icon="Heart"> <n-gi>
Give us a star on <colored-card title="You like it-tools?" :icon="Heart">
<a Give us a star on
href="https://github.com/CorentinTh/it-tools" <a
rel="noopener" href="https://github.com/CorentinTh/it-tools"
target="_blank" rel="noopener"
aria-label="IT-Tools' github repository" target="_blank"
>github</a aria-label="IT-Tools' github repository"
> >github</a
or follow us on >
<a or follow us on
href="https://twitter.com/ittoolsdottech" <a
rel="noopener" href="https://twitter.com/ittoolsdottech"
target="_blank" rel="noopener"
aria-label="IT-Tools' twitter account" target="_blank"
>twitter</a aria-label="IT-Tools' twitter account"
>! Thank you >twitter</a
<n-icon :component="Heart" /> >! Thank you
</colored-card> <n-icon :component="Heart" />
</n-gi> </colored-card>
<n-gi v-for="tool in toolsWithCategory" :key="tool.name"> </n-gi>
<tool-card :tool="tool" /> </n-grid>
</n-gi>
</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>
</div> </div>
</template> </template>
@@ -43,4 +73,27 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
.home-page { .home-page {
padding-top: 50px; padding-top: 50px;
} }
.n-h3 {
margin-bottom: 10px;
}
::v-deep(.n-grid) {
margin-bottom: 30px;
}
.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>

View File

@@ -1,4 +1,5 @@
import { config } from '@/config'; import { config } from '@/config';
import Plausible from 'plausible-tracker'; import Plausible from 'plausible-tracker';
import type { App } from 'vue'; import type { App } from 'vue';
@@ -7,6 +8,6 @@ export const plausible = {
const plausible = Plausible(config.plausible); const plausible = Plausible(config.plausible);
plausible.enableAutoPageviews(); plausible.enableAutoPageviews();
app.config.globalProperties.$plausible = plausible; app.provide('plausible', plausible);
}, },
}; };

View File

@@ -53,5 +53,3 @@ const b64Validation = useValidation({
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }], rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
}); });
</script> </script>
<style lang="less" scoped></style>

View File

@@ -94,5 +94,3 @@ const hmac = computed(() =>
); );
const { copy } = useCopy({ source: hmac }); const { copy } = useCopy({ source: hmac });
</script> </script>
<style lang="less" scoped></style>

View File

@@ -1,6 +1,3 @@
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';
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
@@ -36,16 +33,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 +54,6 @@ export const toolsByCategory: ToolCategory[] = [
}, },
{ {
name: 'Web', name: 'Web',
icon: LockOpen,
components: [ components: [
urlEncoder, urlEncoder,
htmlEntities, htmlEntities,
@@ -72,27 +67,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],
}, },
]; ];

View File

@@ -29,5 +29,3 @@ const expression = ref('');
const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', '')); const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
</script> </script>
<style lang="less" scoped></style>

View File

@@ -95,5 +95,3 @@ const selectedExtension = ref(undefined);
const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : [])); const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : []));
</script> </script>
<style lang="less" scoped></style>

View File

@@ -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
View 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
View 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 };