feat(app): tools management base

This commit is contained in:
Corentin Thomasset 2024-10-26 10:54:32 +02:00
parent 202896fa95
commit b22173681c
No known key found for this signature in database
GPG key ID: DBD997E935996158
29 changed files with 1372 additions and 45 deletions

View file

@ -5,7 +5,7 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 98%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
@ -14,7 +14,7 @@
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%; --primary: 150 76% 38%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%; --secondary: 240 4.8% 95.9%;
@ -36,10 +36,10 @@
} }
.dark { .dark {
--background:0 0% 9%; --background:240 5% 6%;
--foreground:0 0% 98%; --foreground:0 0% 98%;
--card: 0 0% 7%; --card: 240 5% 8%;
--card-foreground:0 0% 98%; --card-foreground:0 0% 98%;
--popover:240 10% 3.9%; --popover:240 10% 3.9%;
@ -74,4 +74,4 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View file

@ -1,12 +1,5 @@
export default defineI18nConfig(() => ({ export default defineI18nConfig(() => ({
legacy: false, legacy: false,
locale: 'en', locale: 'en',
messages: { fallbackLocale: 'en',
en: {
welcome: 'Welcome',
},
fr: {
welcome: 'Bienvenue',
},
},
})); }));

View file

@ -1,3 +1,5 @@
import toolsModule from './src/modules/tools/modules/tools.modules';
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2024-04-03', compatibilityDate: '2024-04-03',
@ -14,9 +16,18 @@ export default defineNuxtConfig({
'@nuxt/icon', '@nuxt/icon',
'@vueuse/nuxt', '@vueuse/nuxt',
'@nuxtjs/color-mode', '@nuxtjs/color-mode',
toolsModule, // Must be imported before i18n
'@nuxtjs/i18n', '@nuxtjs/i18n',
'@nuxtjs/seo',
'@pinia/nuxt',
], ],
site: {
url: 'https://it-tools.tech',
name: 'IT Tools',
description: 'The open-source collection of handy online tools to help developers in their daily life.',
},
fonts: { fonts: {
provider: 'bunny', provider: 'bunny',
defaults: { defaults: {
@ -35,7 +46,11 @@ export default defineNuxtConfig({
i18n: { i18n: {
strategy: 'prefix', strategy: 'prefix',
vueI18n: './i18n.config.ts', vueI18n: './i18n.config.ts',
locales: ['en', 'fr'],
defaultLocale: 'en', defaultLocale: 'en',
langDir: './src/locales',
locales: [
{ code: 'en', file: 'en.yaml', name: 'English' },
{ code: 'fr', file: 'fr.yaml', name: 'Français' },
],
}, },
}); });

View file

@ -3,7 +3,6 @@
"type": "module", "type": "module",
"private": true, "private": true,
"packageManager": "pnpm@9.12.2", "packageManager": "pnpm@9.12.2",
"scripts": { "scripts": {
"dev": "nuxt dev", "dev": "nuxt dev",
"build": "nuxt build", "build": "nuxt build",
@ -19,12 +18,16 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@corentinth/chisels": "^1.1.0",
"@nuxt/fonts": "^0.10.2", "@nuxt/fonts": "^0.10.2",
"@nuxt/icon": "^1.5.6", "@nuxt/icon": "^1.5.6",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^8.5.5", "@nuxtjs/i18n": "^8.5.5",
"@nuxtjs/seo": "2.0.0-rc.23",
"@pinia/nuxt": "^0.5.5",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.453.0", "lucide-vue-next": "^0.453.0",
"nuxt": "^3.13.2", "nuxt": "^3.13.2",
"radix-vue": "^1.9.7", "radix-vue": "^1.9.7",
@ -37,6 +40,7 @@
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^3.8.0", "@antfu/eslint-config": "^3.8.0",
"@nuxtjs/tailwindcss": "^6.12.2", "@nuxtjs/tailwindcss": "^6.12.2",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "^11.1.0", "@vueuse/core": "^11.1.0",
"@vueuse/nuxt": "^11.1.0", "@vueuse/nuxt": "^11.1.0",
"eslint": "^9.13.0", "eslint": "^9.13.0",

View file

@ -0,0 +1,4 @@
/* TEAM */
Developer: Corentin Thomasset
Site: https://corentin.tech
Twitter: @cthmsst

View file

@ -1 +0,0 @@

View file

@ -0,0 +1,31 @@
app:
title: IT-Tools
description: The open-source collection of handy online tools to help developers in their daily life.
home:
all-the-tools: All the tools
search-tools: Search for a tool
open-source: Open Source
free: Free
self-hostable: Self-hostable
open-tool: Open tool
footer:
resources:
title: Resources
all-tools: All the tools
github: GitHub repository
support: Support IT-Tools
license: License
support:
title: Support
report-bug: Report a bug
request-feature: Request a feature
contribute: Contribute to the project
contact: Contact me
friends:
title: Friends
tools:
token-generator:
title: Token Generator
description: >-
Generate random string with the characters you want, uppercase, lowercase
letters, numbers and/or symbols.

View file

@ -0,0 +1,29 @@
app:
title: IT-Tools
description: La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne.
home:
all-the-tools: Tous les outils
search-tools: Rechercher un outil
open-source: Open Source
free: Gratuit
self-hostable: Self-hostable
open-tool: Ouvrir l'outil
footer:
resources:
title: Ressources
all-tools: Tous les outils
github: Dépôt GitHub
support: Soutenir IT-Tools
license: Licence
support:
title: Support
report-bug: Signaler un bug
request-feature: Demander une fonctionnalité
contribute: Contribuer au projet
contact: Me contacter
friends:
title: Ami·e·s
tools:
token-generator:
title: Générateur de token
description: Générer des chaines de caractères aléatoires, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles.

View file

@ -1,29 +1,32 @@
<script setup> <script setup>
const localePath = useLocalePath(); const localePath = useLocalePath();
const { t } = useI18n();
const sections = computed(() => [ const sections = computed(() => [
{ {
title: 'Lorem', title: t('footer.resources.title'),
items: [ items: [
{ label: 'Foo', to: '/foo' }, { label: t('footer.resources.all-tools'), to: localePath('/tools') },
{ label: 'Bar', to: '/bar' }, { label: t('footer.resources.github'), href: 'https://github.com/CorentinTh/it-tools' },
{ label: 'Baz', to: '/baz' }, { label: t('footer.resources.support'), href: 'https://buymeacoffee.com/cthmsst' },
{ label: 'Humans.txt', href: '/humans.txt' },
{ label: t('footer.resources.license'), href: 'https://github.com/CorentinTh/it-tools/blob/main/LICENSE' },
], ],
}, },
{ {
title: 'Ipsum', title: t('footer.support.title'),
items: [ items: [
{ label: 'Foo', to: '/foo' }, { label: t('footer.support.report-bug'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
{ label: 'Bar', to: '/bar' }, { label: t('footer.support.request-feature'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
{ label: 'Baz', to: '/baz' }, { label: t('footer.support.contribute'), href: 'https://github.com/CorentinTh/it-tools/blob/main/CONTRIBUTING.md' },
{ label: t('footer.support.contact'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
], ],
}, },
{ {
title: 'Dolor', title: t('footer.friends.title'),
items: [ items: [
{ label: 'Foo', to: '/foo' }, { label: 'Jugly.io', href: 'https://jugly.io' },
{ label: 'Bar', to: '/bar' }, { label: 'Enclosed.cc', href: 'https://enclosed.cc' },
{ label: 'Baz', to: '/baz' },
], ],
}, },
@ -49,7 +52,7 @@ const socialLinks = [
</script> </script>
<template> <template>
<footer class="bg-card border-t border-border"> <footer class="light:bg-muted/50 dark:bg-black/20 mt-12">
<div class="py-12 px-6 max-w-screen-xl mx-auto "> <div class="py-12 px-6 max-w-screen-xl mx-auto ">
<div class="flex items-start justify-between flex-col md:flex-row gap-12"> <div class="flex items-start justify-between flex-col md:flex-row gap-12">
<div> <div>

View file

@ -6,7 +6,7 @@ const colorMode = useColorMode();
</script> </script>
<template> <template>
<div class="w-full border-b"> <div class="w-full border-b bg-card">
<div class="max-w-screen-xl mx-auto flex items-center justify-between py-2 px-6"> <div class="max-w-screen-xl mx-auto flex items-center justify-between py-2 px-6">
<NuxtLink variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto px-1 rounded-none !transition-border-color-250" :as="Button" :to="localePath('/')" aria-label="Home"> <NuxtLink variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto px-1 rounded-none !transition-border-color-250" :as="Button" :to="localePath('/')" aria-label="Home">
<span class="font-bold text-foreground">IT</span> <span class="font-bold text-foreground">IT</span>

View file

@ -5,7 +5,7 @@ const localePath = useLocalePath();
</script> </script>
<template> <template>
<div class="flex justify-center text-center"> <div class="flex justify-center text-center mt-24">
<div> <div>
<h1 class="text-3xl font-light text-muted-foreground"> <h1 class="text-3xl font-light text-muted-foreground">
404 404

View file

@ -1,7 +1,12 @@
<script setup> <script setup>
import { Badge } from '@/src/modules/ui/components/badge'; import { Badge } from '@/src/modules/ui/components/badge';
import { Button } from '@/src/modules/ui/components/button'; import { Button, buttonVariants } from '@/src/modules/ui/components/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/src/modules/ui/components/dropdown-menu'; import { cn } from '../../shared/style/cn';
import { useToolsStore } from '../../tools/tools.store';
import { CardContent } from '../../ui/components/card';
import Card from '../../ui/components/card/Card.vue';
const { tools } = useToolsStore();
</script> </script>
<template> <template>
@ -10,16 +15,13 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
<div class="max-w-xl"> <div class="max-w-xl">
<div class="flex gap-2"> <div class="flex gap-2">
<Badge class="text-primary bg-primary/10 hover:bg-primary/10"> <Badge class="text-primary bg-primary/10 hover:bg-primary/10">
<!-- {{ $t('landing.hero.badges.open-source') }} --> {{ $t('home.open-source') }}
Open Source
</Badge> </Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10"> <Badge class="text-primary bg-primary/10 hover:bg-primary/10">
<!-- {{ $t('landing.hero.badges.free') }} --> {{ $t('home.free') }}
Free
</Badge> </Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10"> <Badge class="text-primary bg-primary/10 hover:bg-primary/10">
<!-- {{ $t('landing.hero.badges.self-hostable') }} --> {{ $t('home.self-hostable') }}
Self-hostable
</Badge> </Badge>
</div> </div>
@ -29,21 +31,18 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
</h1> </h1>
<p class="text-xl text-gray-400 mb-4"> <p class="text-xl text-gray-400 mb-4">
<!-- {{ $t('app.description') }} --> {{ $t('app.description') }}
The open-source collection of handy online tools to help developers in their daily life.
</p> </p>
<div class="flex gap-4"> <div class="flex gap-4">
<Button> <Button>
<!-- {{ $t('landing.hero.all-the-tools') }} --> {{ $t('home.all-the-tools') }}
All the tools
<Icon name="i-tabler-arrow-right" class="ml-2 size-4" /> <Icon name="i-tabler-arrow-right" class="ml-2 size-4" />
</Button> </Button>
<Button variant="outline"> <Button variant="outline">
<!-- {{ $t('landing.hero.search-tools') }} -->
<Icon name="i-tabler-search" class="mr-2 size-4" /> <Icon name="i-tabler-search" class="mr-2 size-4" />
Search tools {{ $t('home.search-tools') }}
</Button> </Button>
</div> </div>
</div> </div>
@ -54,4 +53,22 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
</div> </div>
</div> </div>
</grid-background> </grid-background>
<div class="max-w-screen-xl mx-auto px-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-12">
<NuxtLink v-for="tool in tools" :key="tool.key" :to="tool.path">
<Card class="p-6 h-full cursor-pointer hover:shadow-lg transition hover:translate-y-[-2px]">
<Icon :name="tool.icon" class="size-12 text-muted-foreground/60" />
<div class="font-semibold text-base">
{{ tool.title }}
</div>
<p class="text-muted-foreground mt-2">
{{ tool.description }}
</p>
</Card>
</NuxtLink>
</div>
</div>
</template> </template>

View file

@ -0,0 +1,21 @@
// @vitest-environment nuxt
import { describe, expect, test } from 'vitest';
import { useRefreshableState } from './useRefreshableState';
describe('useRefreshableState composables', () => {
describe('useRefreshableState', () => {
test('the tuple provided by useRefreshableState contain the state that is the result of the provided function and a refresh function', () => {
let index = 0;
const [state, refresh] = useRefreshableState('key', () => ++index);
expect(state.value).to.equal(1);
expect(index).to.equal(1);
refresh();
expect(state.value).to.equal(2);
expect(index).to.equal(2);
});
});
});

View file

@ -0,0 +1,12 @@
import { get } from '@vueuse/core';
export function useRefreshableState<T>(key: string, getState: () => T | Ref<T>) {
const state = useState(key, getState);
const refresh = () => {
const value = getState();
state.value = get(value);
};
return [state, refresh] as const;
}

View file

@ -0,0 +1,28 @@
import { sample as sampleImpl, times } from 'lodash-es';
export function createToken({
withUppercase = true,
withLowercase = true,
withNumbers = true,
withSymbols = false,
length = 64,
alphabet,
sample = sampleImpl,
}: {
withUppercase?: boolean;
withLowercase?: boolean;
withNumbers?: boolean;
withSymbols?: boolean;
length?: number;
alphabet?: string;
sample?: (str: string) => string;
}) {
const allAlphabet = alphabet ?? [
withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '',
withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
withNumbers ? '0123456789' : '',
withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
].join('');
return times(length, () => sample(allAlphabet)).join('');
}

View file

@ -0,0 +1,9 @@
import { defineTool } from '../../tools.models';
export const tokenGeneratorTool = defineTool({
slug: 'token-generator',
entryFile: './token-generator.vue',
icon: 'i-tabler-key',
createdAt: new Date('2024-02-13'),
currentDirUrl: import.meta.url,
});

View file

@ -0,0 +1,36 @@
<script setup lang="ts">
import { useRefreshableState } from '~/src/modules/shared/composables/useRefreshableState';
import { Button } from '~/src/modules/ui/components/button';
import { createToken } from './token-generator.models';
const withUppercase = ref(true);
const withLowercase = ref(true);
const withNumbers = ref(true);
const withSymbols = ref(false);
const length = ref(64);
const [token, refreshToken] = useRefreshableState(
'token-generator:token',
() => createToken({
withUppercase: withUppercase.value,
withLowercase: withLowercase.value,
withNumbers: withNumbers.value,
withSymbols: withSymbols.value,
length: length.value,
}),
);
watch([withUppercase, withLowercase, withNumbers, withSymbols, length], refreshToken);
// const { copy: copyToken } = useCopy({ source: token, notificationText: 'Token copied to clipboard' });
</script>
<template>
<div class="max-w-screen-md mx-auto p-6">
<div>{{ token }}</div>
<Button class="mt-4" @click="refreshToken">
Generate new token
</Button>
</div>
</template>

View file

@ -0,0 +1,21 @@
import { defineNuxtModule, extendPages } from '@nuxt/kit';
import { toolDefinitions } from '../tools.registry';
export default defineNuxtModule({
meta: {
name: 'tools',
},
setup() {
extendPages((pages) => {
pages.push(...toolDefinitions.map((tool) => {
return {
path: `/${tool.slug}`,
file: tool.entryFile,
meta: {
toolKey: tool.key,
},
};
}));
});
},
});

View file

@ -0,0 +1,18 @@
export function defineTool(toolDefinition: {
slug: string;
entryFile: string;
currentDirUrl: string;
icon: string;
createdAt: Date;
}) {
const entryFile = new URL(toolDefinition.entryFile, toolDefinition.currentDirUrl).pathname;
const baseGithubUrlPath = entryFile.match(/(\/tools\/.*)$/)?.[1];
const entryFileGithubUrl = `https://github.com/CorentinTh/crucials-tools/blob/main${baseGithubUrlPath}`;
return {
...toolDefinition,
key: toolDefinition.slug,
entryFile,
entryFileGithubUrl,
};
}

View file

@ -0,0 +1,5 @@
import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool';
export const toolDefinitions = [
tokenGeneratorTool,
];

View file

@ -0,0 +1,36 @@
import { joinUrlPaths } from '@corentinth/chisels';
import { toolDefinitions } from './tools.registry';
export const useToolsStore = defineStore('tools', () => {
const { t, locale } = useI18n();
const localizedTools = computed(() => {
return toolDefinitions.map((tool) => {
const { key, slug } = tool;
return {
...tool,
title: t(`tools.${key}.title`),
description: t(`tools.${key}.description`),
path: `/${joinUrlPaths(locale.value, slug)}`,
};
});
});
return {
tools: localizedTools,
getToolByKey({ key }: { key: unknown }) {
if (typeof key !== 'string') {
throw new TypeError('Invalid key');
}
const tool = localizedTools.value.find(tool => tool.key === key);
if (!tool) {
throw new Error('Tool not found');
}
return tool;
},
};
});

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
:class="
cn(
'rounded-xl border bg-card text-card-foreground shadow',
props.class,
)
"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<h3
:class="
cn('font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</h3>
</template>

View file

@ -0,0 +1,6 @@
export { default as Card } from './Card.vue'
export { default as CardContent } from './CardContent.vue'
export { default as CardDescription } from './CardDescription.vue'
export { default as CardFooter } from './CardFooter.vue'
export { default as CardHeader } from './CardHeader.vue'
export { default as CardTitle } from './CardTitle.vue'

945
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff