Compare commits

..

4 Commits
v3 ... next

Author SHA1 Message Date
Corentin Thomasset
78e185a281 wip 2025-08-14 23:51:29 +02:00
Corentin Thomasset
00fd51a8e3 feat(ui): tool header 2024-10-06 11:32:23 +02:00
Corentin Thomasset
161b9e6bca chore(cd): added deploy on cloudflare pages 2024-10-05 21:13:40 +02:00
Corentin Thomasset
f8b5cbfd87 feat: it-tools v3 base 2024-10-02 22:15:38 +02:00
174 changed files with 5694 additions and 13642 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -3,7 +3,7 @@ name: CD - Production
on:
push:
branches:
- v3
- next
jobs:
publish-app-prod:
@@ -25,7 +25,7 @@ jobs:
run: pnpm i
- name: Build the app
run: pnpm -F @it-tools/app build:cloudflare-pages
run: pnpm -F @it-tools/app build
- name: Publish to Cloudflare Pages
uses: AdrianGonz97/refined-cf-pages-action@v1
@@ -37,7 +37,7 @@ jobs:
workingDirectory: packages/app
directory: dist
deploymentName: Production App
branch: v3
branch: next
wranglerVersion: '3'

View File

@@ -24,8 +24,8 @@ jobs:
- name: Run linters
run: pnpm lint
# - name: Run unit test
# run: pnpm test:unit
- name: Run unit test
run: pnpm test
- name: Type check
run: pnpm typecheck

2
.npmrc
View File

@@ -1,2 +0,0 @@
shamefully-hoist=true
strict-peer-dependencies=false

View File

@@ -1,3 +0,0 @@
# IT-Tools
It-Tools v3 nuxt exploration branch

24
apps/it-tools/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

47
apps/it-tools/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```sh
pnpm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `pnpm install` | Installs dependencies |
| `pnpm dev` | Starts local dev server at `localhost:4321` |
| `pnpm build` | Build your production site to `./dist/` |
| `pnpm preview` | Preview your build locally, before deploying |
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
| `pnpm astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

@@ -0,0 +1,32 @@
// @ts-check
import { defineConfig } from 'astro/config';
import solidJs from '@astrojs/solid-js';
import UnoCSS from 'unocss/astro'
import { locales, defaultLocale } from './src/i18n/languages';
import pagefind from "astro-pagefind";
// https://astro.build/config
export default defineConfig({
integrations: [
solidJs(),
UnoCSS({ injectReset: true }),
pagefind(),
],
i18n: {
locales,
defaultLocale,
},
markdown: {
shikiConfig: {
themes: {
light: 'vitesse-light',
dark: 'vitesse-dark',
},
},
},
build: {
},
});

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-solid.com/schema.json",
"uno": {
"config": "uno.config.ts",
"css": {
"path": "src/assets/app.css",
"variable": true
},
"color": "neutral",
"prefix": ""
},
"alias": {
"component": "@/components",
"cn": "@/libs/cn"
}
}

814
apps/it-tools/package-lock.json generated Normal file
View File

@@ -0,0 +1,814 @@
{
"name": "@it-tools/app",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@it-tools/app",
"version": "0.0.1",
"dependencies": {
"@astrojs/solid-js": "^5.1.0",
"@kobalte/core": "^0.13.10",
"astro": "^5.12.0",
"astro-pagefind": "^1.8.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"solid-js": "^1.9.6",
"tailwind-merge": "^3.2.0",
"unocss-preset-animations": "^1.2.1"
},
"devDependencies": {
"@iconify-json/solar": "^1.2.2",
"@iconify-json/tabler": "^1.2.19",
"@unocss/reset": "^66.3.3",
"unocss": "66.1.0-beta.13",
"vitest": "^3.2.4"
}
},
"../../node_modules/.pnpm/@astrojs+solid-js@5.1.0_@types+node@24.0.14_jiti@2.4.2_solid-js@1.9.6/node_modules/@astrojs/solid-js": {
"version": "5.1.0",
"license": "MIT",
"dependencies": {
"vite": "^6.3.5",
"vite-plugin-solid": "^2.11.6"
},
"devDependencies": {
"astro": "5.8.0",
"astro-scripts": "0.0.14",
"solid-js": "^1.9.7"
},
"engines": {
"node": "18.20.8 || ^20.3.0 || >=22.0.0"
},
"peerDependencies": {
"solid-devtools": "^0.30.1",
"solid-js": "^1.8.5"
},
"peerDependenciesMeta": {
"solid-devtools": {
"optional": true
}
}
},
"../../node_modules/.pnpm/@iconify-json+solar@1.2.2/node_modules/@iconify-json/solar": {
"version": "1.2.2",
"dev": true,
"license": "CC-BY-4.0",
"dependencies": {
"@iconify/types": "*"
}
},
"../../node_modules/.pnpm/@iconify-json+tabler@1.2.19/node_modules/@iconify-json/tabler": {
"version": "1.2.19",
"dev": true,
"license": "MIT",
"dependencies": {
"@iconify/types": "*"
}
},
"../../node_modules/.pnpm/@unocss+reset@66.3.3/node_modules/@unocss/reset": {
"version": "66.3.3",
"dev": true,
"license": "MIT",
"devDependencies": {
"@csstools/normalize.css": "^12.1.1",
"sanitize.css": "^13.0.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"../../node_modules/.pnpm/astro-pagefind@1.8.3_astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3_/node_modules/astro-pagefind": {
"version": "1.8.3",
"license": "MIT",
"dependencies": {
"@pagefind/default-ui": "^1.2.0",
"pagefind": "^1.2.0",
"sirv": "^3.0.0"
},
"devDependencies": {
"@astrojs/check": "0.9.4",
"@astrojs/markdown-remark": "6.3.0",
"@semantic-release/changelog": "6.0.3",
"@semantic-release/git": "10.0.1",
"@types/semantic-release": "20.0.6",
"astro": "5.5.2",
"semantic-release": "24.2.3",
"typescript": "5.8.2"
},
"peerDependencies": {
"astro": "^2.0.4 || ^3 || ^4 || ^5"
}
},
"../../node_modules/.pnpm/astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3/node_modules/astro": {
"version": "5.12.0",
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.12.2",
"@astrojs/internal-helpers": "0.6.1",
"@astrojs/markdown-remark": "6.3.3",
"@astrojs/telemetry": "3.3.0",
"@capsizecss/unpack": "^2.4.0",
"@oslojs/encoding": "^1.1.0",
"@rollup/pluginutils": "^5.1.4",
"acorn": "^8.14.1",
"aria-query": "^5.3.2",
"axobject-query": "^4.1.0",
"boxen": "8.0.1",
"ci-info": "^4.2.0",
"clsx": "^2.1.1",
"common-ancestor-path": "^1.0.1",
"cookie": "^1.0.2",
"cssesc": "^3.0.0",
"debug": "^4.4.0",
"deterministic-object-hash": "^2.0.2",
"devalue": "^5.1.1",
"diff": "^5.2.0",
"dlv": "^1.1.3",
"dset": "^3.1.4",
"es-module-lexer": "^1.6.0",
"esbuild": "^0.25.0",
"estree-walker": "^3.0.3",
"flattie": "^1.1.1",
"fontace": "~0.3.0",
"github-slugger": "^2.0.0",
"html-escaper": "3.0.3",
"http-cache-semantics": "^4.1.1",
"import-meta-resolve": "^4.1.0",
"js-yaml": "^4.1.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.17",
"magicast": "^0.3.5",
"mrmime": "^2.0.1",
"neotraverse": "^0.6.18",
"p-limit": "^6.2.0",
"p-queue": "^8.1.0",
"package-manager-detector": "^1.1.0",
"picomatch": "^4.0.2",
"prompts": "^2.4.2",
"rehype": "^13.0.2",
"semver": "^7.7.1",
"shiki": "^3.2.1",
"smol-toml": "^1.3.4",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.12",
"tsconfck": "^3.1.5",
"ultrahtml": "^1.6.0",
"unifont": "~0.5.0",
"unist-util-visit": "^5.0.0",
"unstorage": "^1.15.0",
"vfile": "^6.0.3",
"vite": "^6.3.4",
"vitefu": "^1.0.6",
"xxhash-wasm": "^1.1.0",
"yargs-parser": "^21.1.1",
"yocto-spinner": "^0.2.1",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5",
"zod-to-ts": "^1.2.0"
},
"bin": {
"astro": "astro.js"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@playwright/test": "^1.51.1",
"@types/aria-query": "^5.0.4",
"@types/common-ancestor-path": "^1.0.2",
"@types/cssesc": "^3.0.2",
"@types/debug": "^4.1.12",
"@types/diff": "^5.2.3",
"@types/dlv": "^1.1.5",
"@types/hast": "^3.0.4",
"@types/html-escaper": "3.0.4",
"@types/http-cache-semantics": "^4.0.4",
"@types/js-yaml": "^4.0.9",
"@types/picomatch": "^3.0.2",
"@types/prompts": "^2.4.9",
"@types/semver": "^7.7.0",
"@types/yargs-parser": "^21.0.3",
"astro-scripts": "0.0.14",
"cheerio": "1.0.0",
"eol": "^0.10.0",
"execa": "^8.0.1",
"expect-type": "^1.2.0",
"fs-fixture": "^2.7.1",
"mdast-util-mdx": "^3.0.0",
"mdast-util-mdx-jsx": "^3.2.0",
"node-mocks-http": "^1.16.2",
"parse-srcset": "^1.0.2",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"rehype-toc": "^3.0.2",
"remark-code-titles": "^0.1.2",
"rollup": "^4.37.0",
"sass": "^1.86.0",
"typescript": "^5.8.3",
"undici": "^7.5.0",
"unified": "^11.0.5",
"vitest": "^3.0.9"
},
"engines": {
"node": "18.20.8 || ^20.3.0 || >=22.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/astrodotbuild"
},
"optionalDependencies": {
"sharp": "^0.33.3"
}
},
"../../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority": {
"version": "0.7.1",
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"
},
"devDependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.4.16",
"@types/node": "20.12.7",
"@types/react": "18.2.79",
"@types/react-dom": "18.2.25",
"bundlesize": "0.18.2",
"npm-run-all": "4.1.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"ts-node": "10.9.2",
"typescript": "5.4.5"
},
"funding": {
"url": "https://polar.sh/cva"
}
},
"../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx": {
"version": "2.1.1",
"license": "MIT",
"devDependencies": {
"esm": "3.2.25",
"terser": "4.8.0",
"uvu": "0.5.4"
},
"engines": {
"node": ">=6"
}
},
"../../node_modules/.pnpm/solid-js@1.9.6/node_modules/solid-js": {
"version": "1.9.6",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.0",
"seroval": "^1.1.0",
"seroval-plugins": "^1.1.0"
}
},
"../../node_modules/.pnpm/tailwind-merge@3.2.0/node_modules/tailwind-merge": {
"version": "3.2.0",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@codspeed/vitest-plugin": "^4.0.1",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/eslint-plugin": "^1.1.38",
"babel-plugin-annotate-pure-calls": "^0.5.0",
"babel-plugin-polyfill-regenerator": "^0.6.4",
"eslint": "^9.23.0",
"eslint-plugin-import": "^2.31.0",
"globby": "^11.1.0",
"prettier": "^3.5.3",
"rollup": "^4.38.0",
"rollup-plugin-delete": "^3.0.1",
"rollup-plugin-dts": "^6.2.1",
"tslib": "^2.8.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.0",
"vitest": "^3.1.1",
"zx": "^8.4.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"../../node_modules/.pnpm/unocss-preset-animations@1.2.1_unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24._pi5ihopp7lbxzhd777cycsuybi/node_modules/unocss-preset-animations": {
"version": "1.2.1",
"license": "MIT",
"devDependencies": {
"@aelita-dev/eslint-config": "3.19.0",
"@iconify/json": "^2.2.330",
"@types/dom-view-transitions": "^1.0.6",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.17.30",
"@unocss/core": "^66.0.0",
"@unocss/eslint-plugin": "^66.0.0",
"@unocss/preset-mini": "^66.0.0",
"@vitest/coverage-v8": "^3.1.2",
"@vitest/eslint-plugin": "^1.1.43",
"@vue/language-server": "^2.2.10",
"bumpp": "^10.1.0",
"bundle-require": "^5.1.0",
"changelogithub": "^13.13.0",
"eslint": "^9.25.1",
"eslint-import-resolver-typescript": "^4.3.4",
"eslint-plugin-import-x": "^4.10.6",
"eslint-plugin-vue": "^10.0.0",
"eslint-plugin-vuejs-accessibility": "^2.4.1",
"eslint-processor-vue-blocks": "^2.0.0",
"lint-staged": "^15.5.1",
"markdown-it": "^14.1.0",
"sass-embedded": "^1.87.0",
"simple-git-hooks": "^2.12.1",
"typescript": "~5.8.3",
"unbuild": "3.5.0",
"unocss": "^66.0.0",
"vite-tsconfig-paths": "^5.1.4",
"vitepress": "1.6.3",
"vitest": "^3.1.2",
"vue": "^3.5.13",
"vue-eslint-parser": "^10.1.3",
"vue-tsc": "^2.2.10"
},
"peerDependencies": {
"@unocss/preset-wind3": ">=0.56.0 < 101",
"unocss": ">=0.56.0 < 101"
},
"peerDependenciesMeta": {
"@unocss/preset-wind3": {
"optional": true
}
}
},
"../../node_modules/.pnpm/unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24.0.14_jiti@2.4.2__vue@3.5.13_typescript@5.8.3_/node_modules/unocss": {
"version": "66.1.0-beta.13",
"dev": true,
"license": "MIT",
"dependencies": {
"@unocss/astro": "66.1.0-beta.13",
"@unocss/cli": "66.1.0-beta.13",
"@unocss/core": "66.1.0-beta.13",
"@unocss/postcss": "66.1.0-beta.13",
"@unocss/preset-attributify": "66.1.0-beta.13",
"@unocss/preset-icons": "66.1.0-beta.13",
"@unocss/preset-mini": "66.1.0-beta.13",
"@unocss/preset-tagify": "66.1.0-beta.13",
"@unocss/preset-typography": "66.1.0-beta.13",
"@unocss/preset-uno": "66.1.0-beta.13",
"@unocss/preset-web-fonts": "66.1.0-beta.13",
"@unocss/preset-wind": "66.1.0-beta.13",
"@unocss/preset-wind3": "66.1.0-beta.13",
"@unocss/preset-wind4": "66.1.0-beta.13",
"@unocss/transformer-attributify-jsx": "66.1.0-beta.13",
"@unocss/transformer-compile-class": "66.1.0-beta.13",
"@unocss/transformer-directives": "66.1.0-beta.13",
"@unocss/transformer-variant-group": "66.1.0-beta.13",
"@unocss/vite": "66.1.0-beta.13"
},
"devDependencies": {
"@unocss/webpack": "66.1.0-beta.13",
"vite": "^6.2.6"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@unocss/webpack": "66.1.0-beta.13",
"vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0"
},
"peerDependenciesMeta": {
"@unocss/webpack": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"../../node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.0.14_jiti@2.4.2/node_modules/vitest": {
"version": "3.2.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
"@vitest/mocker": "3.2.4",
"@vitest/pretty-format": "^3.2.4",
"@vitest/runner": "3.2.4",
"@vitest/snapshot": "3.2.4",
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"std-env": "^3.9.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.14",
"tinypool": "^1.1.1",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.2.4",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"devDependencies": {
"@ampproject/remapping": "^2.3.0",
"@antfu/install-pkg": "^1.1.0",
"@edge-runtime/vm": "^5.0.0",
"@sinonjs/fake-timers": "14.0.0",
"@types/debug": "^4.1.12",
"@types/estree": "^1.0.8",
"@types/istanbul-lib-coverage": "^2.0.6",
"@types/istanbul-reports": "^3.0.4",
"@types/jsdom": "^21.1.7",
"@types/mime": "^4.0.0",
"@types/node": "^22.15.32",
"@types/picomatch": "^4.0.0",
"@types/prompts": "^2.4.9",
"@types/sinonjs__fake-timers": "^8.1.5",
"acorn-walk": "^8.3.4",
"birpc": "2.4.0",
"cac": "^6.7.14",
"chai-subset": "^1.6.0",
"find-up": "^6.3.0",
"flatted": "^3.3.3",
"happy-dom": "^17.6.3",
"jsdom": "^26.1.0",
"local-pkg": "^1.1.1",
"mime": "^4.0.7",
"pretty-format": "^29.7.0",
"prompts": "^2.4.2",
"strip-literal": "^3.0.0",
"ws": "^8.18.2"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.2.4",
"@vitest/ui": "3.2.4",
"happy-dom": "*",
"jsdom": "*"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@types/debug": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
}
}
},
"node_modules/@astrojs/solid-js": {
"resolved": "../../node_modules/.pnpm/@astrojs+solid-js@5.1.0_@types+node@24.0.14_jiti@2.4.2_solid-js@1.9.6/node_modules/@astrojs/solid-js",
"link": true
},
"node_modules/@corvu/utils": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@corvu/utils/-/utils-0.4.2.tgz",
"integrity": "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.11"
},
"peerDependencies": {
"solid-js": "^1.8"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.2",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@iconify-json/solar": {
"resolved": "../../node_modules/.pnpm/@iconify-json+solar@1.2.2/node_modules/@iconify-json/solar",
"link": true
},
"node_modules/@iconify-json/tabler": {
"resolved": "../../node_modules/.pnpm/@iconify-json+tabler@1.2.19/node_modules/@iconify-json/tabler",
"link": true
},
"node_modules/@internationalized/date": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz",
"integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@internationalized/number": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz",
"integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@kobalte/core": {
"version": "0.13.10",
"resolved": "https://registry.npmjs.org/@kobalte/core/-/core-0.13.10.tgz",
"integrity": "sha512-lzP64ThxZqZB6O6MnMq6w7DxK38o2ClbW3Ob6afUI6p86cUMz5Hb4rdysvYI6m1TKYlOAlFODKkoRznqybQohw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.5.1",
"@internationalized/date": "^3.4.0",
"@internationalized/number": "^3.2.1",
"@kobalte/utils": "^0.9.1",
"@solid-primitives/props": "^3.1.8",
"@solid-primitives/resize-observer": "^2.0.26",
"solid-presence": "^0.1.8",
"solid-prevent-scroll": "^0.1.4"
},
"peerDependencies": {
"solid-js": "^1.8.15"
}
},
"node_modules/@kobalte/utils": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@kobalte/utils/-/utils-0.9.1.tgz",
"integrity": "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w==",
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.2.14",
"@solid-primitives/keyed": "^1.2.0",
"@solid-primitives/map": "^0.4.7",
"@solid-primitives/media": "^2.2.4",
"@solid-primitives/props": "^3.1.8",
"@solid-primitives/refs": "^1.0.5",
"@solid-primitives/utils": "^6.2.1"
},
"peerDependencies": {
"solid-js": "^1.8.8"
}
},
"node_modules/@solid-primitives/event-listener": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.3.tgz",
"integrity": "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==",
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/keyed": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.5.2.tgz",
"integrity": "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg==",
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/map": {
"version": "0.4.13",
"resolved": "https://registry.npmjs.org/@solid-primitives/map/-/map-0.4.13.tgz",
"integrity": "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew==",
"license": "MIT",
"dependencies": {
"@solid-primitives/trigger": "^1.1.0"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/media": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/media/-/media-2.3.3.tgz",
"integrity": "sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA==",
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/static-store": "^0.1.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/props": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/props/-/props-3.2.2.tgz",
"integrity": "sha512-lZOTwFJajBrshSyg14nBMEP0h8MXzPowGO0s3OeiR3z6nXHTfj0FhzDtJMv+VYoRJKQHG2QRnJTgCzK6erARAw==",
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/refs/-/refs-1.1.2.tgz",
"integrity": "sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg==",
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/resize-observer": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/resize-observer/-/resize-observer-2.1.3.tgz",
"integrity": "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ==",
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/static-store": "^0.1.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/rootless": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.5.2.tgz",
"integrity": "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==",
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/static-store": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.2.tgz",
"integrity": "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==",
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/trigger": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/trigger/-/trigger-1.2.2.tgz",
"integrity": "sha512-IWoptVc0SWYgmpBPpCMehS5b07+tpFcvw15tOQ3QbXedSYn6KP8zCjPkHNzMxcOvOicTneleeZDP7lqmz+PQ6g==",
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/utils": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.3.2.tgz",
"integrity": "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==",
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@unocss/reset": {
"resolved": "../../node_modules/.pnpm/@unocss+reset@66.3.3/node_modules/@unocss/reset",
"link": true
},
"node_modules/astro": {
"resolved": "../../node_modules/.pnpm/astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3/node_modules/astro",
"link": true
},
"node_modules/astro-pagefind": {
"resolved": "../../node_modules/.pnpm/astro-pagefind@1.8.3_astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3_/node_modules/astro-pagefind",
"link": true
},
"node_modules/class-variance-authority": {
"resolved": "../../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority",
"link": true
},
"node_modules/clsx": {
"resolved": "../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx",
"link": true
},
"node_modules/solid-js": {
"resolved": "../../node_modules/.pnpm/solid-js@1.9.6/node_modules/solid-js",
"link": true
},
"node_modules/solid-presence": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/solid-presence/-/solid-presence-0.1.8.tgz",
"integrity": "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==",
"license": "MIT",
"dependencies": {
"@corvu/utils": "~0.4.0"
},
"peerDependencies": {
"solid-js": "^1.8"
}
},
"node_modules/solid-prevent-scroll": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/solid-prevent-scroll/-/solid-prevent-scroll-0.1.10.tgz",
"integrity": "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw==",
"license": "MIT",
"dependencies": {
"@corvu/utils": "~0.4.1"
},
"peerDependencies": {
"solid-js": "^1.8"
}
},
"node_modules/tailwind-merge": {
"resolved": "../../node_modules/.pnpm/tailwind-merge@3.2.0/node_modules/tailwind-merge",
"link": true
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/unocss": {
"resolved": "../../node_modules/.pnpm/unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24.0.14_jiti@2.4.2__vue@3.5.13_typescript@5.8.3_/node_modules/unocss",
"link": true
},
"node_modules/unocss-preset-animations": {
"resolved": "../../node_modules/.pnpm/unocss-preset-animations@1.2.1_unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24._pi5ihopp7lbxzhd777cycsuybi/node_modules/unocss-preset-animations",
"link": true
},
"node_modules/vitest": {
"resolved": "../../node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.0.14_jiti@2.4.2/node_modules/vitest",
"link": true
}
}
}

View File

@@ -0,0 +1,31 @@
{
"name": "@it-tools/app",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"test": "vitest run",
"test:watch": "vitest watch"
},
"dependencies": {
"@astrojs/solid-js": "^5.1.0",
"@kobalte/core": "^0.13.10",
"astro": "^5.12.0",
"astro-pagefind": "^1.8.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"solid-js": "^1.9.6",
"tailwind-merge": "^3.2.0",
"unocss-preset-animations": "^1.2.1"
},
"devDependencies": {
"@iconify-json/solar": "^1.2.2",
"@iconify-json/tabler": "^1.2.19",
"@unocss/reset": "^66.3.3",
"unocss": "66.1.0-beta.13",
"vitest": "^3.2.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,75 @@
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
[data-kb-theme="dark"] {
--background: 0 0% 6%;
--foreground: 0 0% 96%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 96%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
[data-kb-theme="dark"] .astro-code,
[data-kb-theme="dark"] .astro-code span {
background-color: var(--shiki-dark-bg) !important;
}

View File

@@ -0,0 +1,103 @@
---
import { cn } from '@/libs/cn';
const socials = [
{
label: 'Bluesky',
url: 'https://bsky.app/profile/it-tools.tech',
icon: 'i-tabler-brand-bluesky',
},
{
label: 'GitHub',
url: 'https://github.com/CorentinTh/it-tools',
icon: 'i-tabler-brand-github',
},
{
label: 'X (Twitter)',
url: 'https://x.com/ittoolsdottech',
icon: 'i-tabler-brand-x',
},
];
const sections: {
title: string;
links: { label: string; url: string; target?: string; rel?: string }[];
}[] = [
{
title: 'Community',
links: socials,
},
{
title: 'Open Source',
links: [
{
label: 'Repository',
url: 'https://github.com/CorentinTh/it-tools',
},
{
label: 'Contributing',
url: 'https://github.com/CorentinTh/it-tools/blob/main/CONTRIBUTING.md',
},
{
label: 'Code of Conduct',
url: 'https://github.com/CorentinTh/it-tools/blob/main/CODE_OF_CONDUCT.md',
},
{
label: 'License',
url: 'https://github.com/CorentinTh/it-tools/blob/main/LICENSE',
},
],
},
];
---
<footer class="bg-card border-t border-border py-8 text-muted-foreground light:bg-muted">
<div class="max-w-screen-lg mx-auto p-6">
<div class="flex justify-between flex-col md:flex-row gap-10">
<div>
<a href="/" class="text-xl inline-flex items-center group mb-2">
<div class="i-solar-programming-line-duotone size-7 text-primary group-hover:(rotate-12deg) transition transform"></div>
<span class="ml-2 text-foreground group-hover:text-foreground/80 transition">IT-Tools</span>
</a>
<div class="flex gap-2">
{
socials.map(social => (
<a href={social.url} class="hover:text-primary transition" target="_blank" rel="noopener noreferrer" aria-label={social.label}>
<div class={`${social.icon} text-2xl`} aria-hidden="true" />
</a>
))
}
</div>
<p class="mt-4 text-sm max-w-420px">
IT-Tools is made in Europe with <span class="i-tabler-heart-filled size-3.5 mb--0.3 text-primary inline-block"></span>
by <a href="https://corentin.tech" class="text-primary border-b hover:border-b-primary transition">Corentin Thomasset</a>.
</p>
</div>
<div class={cn('grid gap-10 grid-cols-1', `sm:grid-cols-${sections.length}`)}>
{
sections.map(section => (
<div>
<div class="text-foreground font-semibold">{section.title}</div>
<div class="mt-2">
{section.links.map(link => (
<a href={link.url} class="block hover:text-primary transition py-0.75 font-medium" target={link.target} rel={link.rel}>
{link.label}
</a>
))}
</div>
</div>
))
}
</div>
</div>
<div class="mt-8 border-t border-border pt-4">
© {new Date().getFullYear()} IT-Tools. All rights reserved.
</div>
</div>
</footer>

View File

@@ -0,0 +1,60 @@
---
import { LanguagePicker } from "./language-picker";
import { Button } from "./ui/button";
const { lang } = Astro.params;
---
<header class="fixed top-0 w-full z-50 bg-background/50 backdrop-blur-sm">
<div class="max-w-screen-lg mx-auto px-6 py-3 flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<a href={lang ? `/${lang}` : "/"} class="flex items-center gap-2 group">
<span class="i-solar-programming-line-duotone flex-shrink-0 size-6 group-hover:rotate-12 transition-transform"></span>
<span class="font-semibold">IT-Tools</span>
</a>
<span class="text-sm text-muted-foreground font-medium hidden sm:block">
Handy online tools.
</span>
</div>
<div class="flex items-center gap-1">
<Button variant="ghost" size="icon" class="size-8">
<span class="i-tabler-search flex-shrink-0 size-4"></span>
</Button>
<Button variant="ghost" size="icon" class="size-8 toggle-theme">
<span class="i-tabler-moon flex-shrink-0 hidden dark:block size-4"></span>
<span class="i-tabler-sun flex-shrink-0 block dark:hidden size-4"></span>
</Button>
<LanguagePicker client:load />
<Button variant="ghost" size="icon" class="size-8" as="a" href="https://github.com/CorentinTh/it-tools" target="_blank" rel="noopener noreferrer">
<span class="i-tabler-brand-github flex-shrink-0 size-4"></span>
</Button>
<Button variant="outline" size="sm" as="a" href="https://www.buymeacoffee.com/cthmsst" target="_blank" rel="noopener noreferrer">
Buy me a coffee
</Button>
</div>
</div>
</header>
<script>
const themeButtons = document.querySelectorAll("button.toggle-theme");
themeButtons.forEach(button => {
button.addEventListener("click", () => {
const localStorageTheme = localStorage.getItem("it-tools-theme");
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
const theme = localStorageTheme ?? systemTheme;
const newTheme = theme === "dark" ? "light" : "dark";
localStorage.setItem("it-tools-theme", newTheme);
document.documentElement.setAttribute("data-kb-theme", newTheme);
});
});
</script>

View File

@@ -0,0 +1,30 @@
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
import type { DropdownMenuSubTriggerProps } from "@kobalte/core/dropdown-menu";
import { languages } from "@/i18n/languages";
import { navigate } from "astro:transitions/client";
export const LanguagePicker = () => {
return (
<DropdownMenu placement="bottom">
<DropdownMenuTrigger
as={(props: DropdownMenuSubTriggerProps) => (
<Button variant="ghost" size="icon" {...props}>
<span class="i-tabler-language flex-shrink-0 size-4"></span>
</Button>
)}
/>
<DropdownMenuContent class="w-42">
{Object.entries(languages).map(([locale, name]) => (
<DropdownMenuItem as="a" href={`/${locale}`}>
{name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,24 @@
---
import { buildLocalizedUrl } from "@/i18n/i18n.models";
import { cn } from "@/libs/cn";
export type Props = {
name: string;
description: string;
icon: string;
slug: string;
};
const { slug, name, description, icon }: Props = Astro.props;
const {lang} = Astro.params;
---
<a href={buildLocalizedUrl({lang, path: slug})} class="border rounded-lg p-5 hover:bg-muted transition-colors flex flex-col gap-2">
<div class="flex items-center gap-4">
<span class="size-10 flex-shrink-0 bg-muted rounded-lg flex items-center justify-center">
<span class={cn(icon, "size-6 text-muted-foreground")}></span>
</span>
<h3 class="text-lg font-semibold truncate">{name}</h3>
</div>
<p class="text-sm text-muted-foreground">{description}</p>
</a>

View File

@@ -0,0 +1,66 @@
import { cn } from "@/libs/cn";
import type { ButtonRootProps } from "@kobalte/core/button";
import { Button as ButtonPrimitive } from "@kobalte/core/button";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) bg-inherit",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:(bg-accent text-accent-foreground)",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type buttonProps<T extends ValidComponent = "button"> = ButtonRootProps<T> &
VariantProps<typeof buttonVariants> & {
class?: string;
};
export const Button = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, buttonProps<T>>,
) => {
const [local, rest] = splitProps(props as buttonProps, [
"class",
"variant",
"size",
]);
return (
<ButtonPrimitive
class={cn(
buttonVariants({
size: local.size,
variant: local.variant,
}),
local.class,
)}
{...rest}
/>
);
};

View File

@@ -0,0 +1,320 @@
import { cn } from "@/libs/cn";
import type {
DropdownMenuCheckboxItemProps,
DropdownMenuContentProps,
DropdownMenuGroupLabelProps,
DropdownMenuItemLabelProps,
DropdownMenuItemProps,
DropdownMenuRadioItemProps,
DropdownMenuRootProps,
DropdownMenuSeparatorProps,
DropdownMenuSubTriggerProps,
} from "@kobalte/core/dropdown-menu";
import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core/dropdown-menu";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
import { mergeProps, splitProps } from "solid-js";
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
export const DropdownMenu = (props: DropdownMenuRootProps) => {
const merge = mergeProps<DropdownMenuRootProps[]>(
{
gutter: 4,
flip: false,
},
props,
);
return <DropdownMenuPrimitive {...merge} />;
};
type dropdownMenuContentProps<T extends ValidComponent = "div"> =
DropdownMenuContentProps<T> & {
class?: string;
};
export const DropdownMenuContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuContentProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuContentProps, [
"class",
]);
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
class={cn(
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95) focus-visible:(outline-none ring-1.5 ring-ring) transition-shadow",
local.class,
)}
{...rest}
/>
</DropdownMenuPrimitive.Portal>
);
};
type dropdownMenuItemProps<T extends ValidComponent = "div"> =
DropdownMenuItemProps<T> & {
class?: string;
inset?: boolean;
};
export const DropdownMenuItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuItemProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuItemProps, [
"class",
"inset",
]);
return (
<DropdownMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)",
local.inset && "pl-8",
local.class,
)}
{...rest}
/>
);
};
type dropdownMenuGroupLabelProps<T extends ValidComponent = "span"> =
DropdownMenuGroupLabelProps<T> & {
class?: string;
};
export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [
"class",
]);
return (
<DropdownMenuPrimitive.GroupLabel
as="div"
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
{...rest}
/>
);
};
type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> =
DropdownMenuItemLabelProps<T> & {
class?: string;
};
export const DropdownMenuItemLabel = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [
"class",
]);
return (
<DropdownMenuPrimitive.ItemLabel
as="div"
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
{...rest}
/>
);
};
type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> =
DropdownMenuSeparatorProps<T> & {
class?: string;
};
export const DropdownMenuSeparator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [
"class",
]);
return (
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", local.class)}
{...rest}
/>
);
};
export const DropdownMenuShortcut = (props: ComponentProps<"span">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<span
class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)}
{...rest}
/>
);
};
type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> =
ParentProps<
DropdownMenuSubTriggerProps<T> & {
class?: string;
}
>;
export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [
"class",
"children",
]);
return (
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent",
local.class,
)}
{...rest}
>
{local.children}
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
class="ml-auto h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
<title>Arrow</title>
</svg>
</DropdownMenuPrimitive.SubTrigger>
);
};
type dropdownMenuSubContentProps<T extends ValidComponent = "div"> =
DropdownMenuSubTriggerProps<T> & {
class?: string;
};
export const DropdownMenuSubContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [
"class",
]);
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.SubContent
class={cn(
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95)",
local.class,
)}
{...rest}
/>
</DropdownMenuPrimitive.Portal>
);
};
type dropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> =
ParentProps<
DropdownMenuCheckboxItemProps<T> & {
class?: string;
}
>;
export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [
"class",
"children",
]);
return (
<DropdownMenuPrimitive.CheckboxItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)",
local.class,
)}
{...rest}
>
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m5 12l5 5L20 7"
/>
<title>Checkbox</title>
</svg>
</DropdownMenuPrimitive.ItemIndicator>
{props.children}
</DropdownMenuPrimitive.CheckboxItem>
);
};
type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
DropdownMenuRadioItemProps<T> & {
class?: string;
}
>;
export const DropdownMenuRadioItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [
"class",
"children",
]);
return (
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)",
local.class,
)}
{...rest}
>
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-2 w-2"
>
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
/>
</g>
<title>Radio</title>
</svg>
</DropdownMenuPrimitive.ItemIndicator>
{props.children}
</DropdownMenuPrimitive.RadioItem>
);
};

View File

@@ -0,0 +1,19 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import { cn } from "@/libs/cn"
const Label: Component<ComponentProps<"label">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
local.class
)}
{...others}
/>
)
}
export { Label }

View File

@@ -0,0 +1,214 @@
import { cn } from "@/libs/cn";
import type {
NumberFieldDecrementTriggerProps,
NumberFieldDescriptionProps,
NumberFieldErrorMessageProps,
NumberFieldIncrementTriggerProps,
NumberFieldInputProps,
NumberFieldLabelProps,
NumberFieldRootProps,
} from "@kobalte/core/number-field";
import { NumberField as NumberFieldPrimitive } from "@kobalte/core/number-field";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ComponentProps, ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
import { textfieldLabel } from "./textfield";
export const NumberFieldHiddenInput = NumberFieldPrimitive.HiddenInput;
type numberFieldLabelProps<T extends ValidComponent = "div"> =
NumberFieldLabelProps<T> & {
class?: string;
};
export const NumberFieldLabel = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldLabelProps, ["class"]);
return (
<NumberFieldPrimitive.Label
class={cn(textfieldLabel({ label: true }), local.class)}
{...rest}
/>
);
};
type numberFieldDescriptionProps<T extends ValidComponent = "div"> =
NumberFieldDescriptionProps<T> & {
class?: string;
};
export const NumberFieldDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldDescriptionProps, [
"class",
]);
return (
<NumberFieldPrimitive.Description
class={cn(
textfieldLabel({ description: true, label: false }),
local.class,
)}
{...rest}
/>
);
};
type numberFieldErrorMessageProps<T extends ValidComponent = "div"> =
NumberFieldErrorMessageProps<T> & {
class?: string;
};
export const NumberFieldErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldErrorMessageProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldErrorMessageProps, [
"class",
]);
return (
<NumberFieldPrimitive.ErrorMessage
class={cn(textfieldLabel({ error: true }), local.class)}
{...rest}
/>
);
};
type numberFieldProps<T extends ValidComponent = "div"> =
NumberFieldRootProps<T> & {
class?: string;
};
export const NumberField = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldProps, ["class"]);
return (
<NumberFieldPrimitive class={cn("grid gap-1.5", local.class)} {...rest} />
);
};
export const NumberFieldGroup = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"relative focus-within:(outline-none ring-1.5 ring-ring) transition-shadow rounded-md",
local.class,
)}
{...rest}
/>
);
};
type numberFieldInputProps<T extends ValidComponent = "input"> =
NumberFieldInputProps<T> & {
class?: string;
};
export const NumberFieldInput = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, VoidProps<numberFieldInputProps<T>>>,
) => {
const [local, rest] = splitProps(props as numberFieldInputProps, ["class"]);
return (
<NumberFieldPrimitive.Input
class={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-10 py-1 text-sm text-center shadow-sm placeholder:text-muted-foreground disabled:(cursor-not-allowed opacity-50) focus-visible:outline-none",
local.class,
)}
{...rest}
/>
);
};
type numberFieldDecrementTriggerProps<T extends ValidComponent = "button"> =
VoidProps<
NumberFieldDecrementTriggerProps<T> & {
class?: string;
}
>;
export const NumberFieldDecrementTrigger = <
T extends ValidComponent = "button",
>(
props: PolymorphicProps<T, VoidProps<numberFieldDecrementTriggerProps<T>>>,
) => {
const [local, rest] = splitProps(props as numberFieldDecrementTriggerProps, [
"class",
]);
return (
<NumberFieldPrimitive.DecrementTrigger
class={cn(
"absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:(cursor-not-allowed opacity-20)",
local.class,
)}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 12h14"
/>
<title>Decrease number</title>
</svg>
</NumberFieldPrimitive.DecrementTrigger>
);
};
type numberFieldIncrementTriggerProps<T extends ValidComponent = "button"> =
VoidProps<
NumberFieldIncrementTriggerProps<T> & {
class?: string;
}
>;
export const NumberFieldIncrementTrigger = <
T extends ValidComponent = "button",
>(
props: PolymorphicProps<T, numberFieldIncrementTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldIncrementTriggerProps, [
"class",
]);
return (
<NumberFieldPrimitive.IncrementTrigger
class={cn(
"absolute top-1/2 -translate-y-1/2 right-0 disabled:(cursor-not-allowed opacity-20) p-3",
local.class,
)}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m-7-7h14"
/>
<title>Increase number</title>
</svg>
</NumberFieldPrimitive.IncrementTrigger>
);
};

View File

@@ -0,0 +1,92 @@
import type { JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as SliderPrimitive from "@kobalte/core/slider"
import { cn } from "@/libs/cn";
import { Label } from "@/components/ui/label"
type SliderRootProps<T extends ValidComponent = "div"> = SliderPrimitive.SliderRootProps<T> & {
class?: string | undefined
}
const Slider = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SliderRootProps<T>>
) => {
const [local, others] = splitProps(props as SliderRootProps, ["class"])
return (
<SliderPrimitive.Root
class={cn("relative flex w-full touch-none select-none flex-col items-center", local.class)}
{...others}
/>
)
}
type SliderTrackProps<T extends ValidComponent = "div"> = SliderPrimitive.SliderTrackProps<T> & {
class?: string | undefined
}
const SliderTrack = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SliderTrackProps<T>>
) => {
const [local, others] = splitProps(props as SliderTrackProps, ["class"])
return (
<SliderPrimitive.Track
class={cn("relative h-2 w-full grow rounded-full bg-secondary", local.class)}
{...others}
/>
)
}
type SliderFillProps<T extends ValidComponent = "div"> = SliderPrimitive.SliderFillProps<T> & {
class?: string | undefined
}
const SliderFill = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SliderFillProps<T>>
) => {
const [local, others] = splitProps(props as SliderFillProps, ["class"])
return (
<SliderPrimitive.Fill
class={cn("absolute h-full rounded-full bg-primary", local.class)}
{...others}
/>
)
}
type SliderThumbProps<T extends ValidComponent = "span"> = SliderPrimitive.SliderThumbProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const SliderThumb = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, SliderThumbProps<T>>
) => {
const [local, others] = splitProps(props as SliderThumbProps, ["class", "children"])
return (
<SliderPrimitive.Thumb
class={cn(
"top-[-6px] block size-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
local.class
)}
{...others}
>
<SliderPrimitive.Input />
</SliderPrimitive.Thumb>
)
}
const SliderLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, SliderPrimitive.SliderLabelProps<T>>
) => {
return <SliderPrimitive.Label as={Label} {...props} />
}
const SliderValueLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, SliderPrimitive.SliderValueLabelProps<T>>
) => {
return <SliderPrimitive.ValueLabel as={Label} {...props} />
}
export { Slider, SliderTrack, SliderFill, SliderThumb, SliderLabel, SliderValueLabel }

View File

@@ -0,0 +1,84 @@
import { cn } from "@/libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
SwitchControlProps,
SwitchThumbProps,
} from "@kobalte/core/switch";
import { Switch as SwitchPrimitive } from "@kobalte/core/switch";
import type { ParentProps, ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
import { Label } from "./label";
export const SwitchLabel = SwitchPrimitive.Label;
export const Switch = SwitchPrimitive;
export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage;
export const SwitchDescription = SwitchPrimitive.Description;
type switchControlProps<T extends ValidComponent = "input"> = ParentProps<
SwitchControlProps<T> & { class?: string }
>;
export const SwitchControl = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, switchControlProps<T>>,
) => {
const [local, rest] = splitProps(props as switchControlProps, [
"class",
"children",
]);
return (
<>
<SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" />
<SwitchPrimitive.Control
class={cn(
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]",
local.class,
)}
{...rest}
>
{local.children}
</SwitchPrimitive.Control>
</>
);
};
type switchThumbProps<T extends ValidComponent = "div"> = VoidProps<
SwitchThumbProps<T> & { class?: string }
>;
export const SwitchThumb = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, switchThumbProps<T>>,
) => {
const [local, rest] = splitProps(props as switchThumbProps, ["class"]);
return (
<SwitchPrimitive.Thumb
class={cn(
"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4",
local.class,
)}
{...rest}
/>
);
};
type SwitchCardProps = {
label: string;
description: string;
checked: boolean;
onChange: (checked: boolean) => void;
}
export const SwitchCard = (props: SwitchCardProps) => {
return <Switch checked={props.checked} onChange={props.onChange} class="flex items-center justify-between gap-2 border rounded-md py-2 px-4">
<div >
<SwitchLabel class="text-sm font-medium">{props.label}</SwitchLabel>
<SwitchDescription class="text-sm text-muted-foreground">{props.description}</SwitchDescription>
</div>
<SwitchControl >
<SwitchThumb />
</SwitchControl>
</Switch>
}

View File

@@ -0,0 +1,28 @@
import { cn } from "@/libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { TextFieldTextAreaProps } from "@kobalte/core/text-field";
import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps<
TextFieldTextAreaProps<T> & {
class?: string;
}
>;
export const TextArea = <T extends ValidComponent = "textarea">(
props: PolymorphicProps<T, textAreaProps<T>>,
) => {
const [local, rest] = splitProps(props as textAreaProps, ["class"]);
return (
<TextFieldPrimitive
class={cn(
"flex min-h-[30px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow",
local.class,
)}
{...rest}
/>
);
};

View File

@@ -0,0 +1,126 @@
import { cn } from "@/libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
TextFieldDescriptionProps,
TextFieldErrorMessageProps,
TextFieldInputProps,
TextFieldLabelProps,
TextFieldRootProps,
} from "@kobalte/core/text-field";
import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field";
import { cva } from "class-variance-authority";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
type textFieldProps<T extends ValidComponent = "div"> =
TextFieldRootProps<T> & {
class?: string;
};
export const TextFieldRoot = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldProps, ["class"]);
return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />;
};
export const textfieldLabel = cva(
"text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium",
{
variants: {
label: {
true: "data-[invalid]:text-destructive",
},
error: {
true: "text-destructive text-xs",
},
description: {
true: "font-normal text-muted-foreground",
},
},
defaultVariants: {
label: true,
},
},
);
type textFieldLabelProps<T extends ValidComponent = "label"> =
TextFieldLabelProps<T> & {
class?: string;
};
export const TextFieldLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, textFieldLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]);
return (
<TextFieldPrimitive.Label
class={cn(textfieldLabel(), local.class)}
{...rest}
/>
);
};
type textFieldErrorMessageProps<T extends ValidComponent = "div"> =
TextFieldErrorMessageProps<T> & {
class?: string;
};
export const TextFieldErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldErrorMessageProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldErrorMessageProps, [
"class",
]);
return (
<TextFieldPrimitive.ErrorMessage
class={cn(textfieldLabel({ error: true }), local.class)}
{...rest}
/>
);
};
type textFieldDescriptionProps<T extends ValidComponent = "div"> =
TextFieldDescriptionProps<T> & {
class?: string;
};
export const TextFieldDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldDescriptionProps, [
"class",
]);
return (
<TextFieldPrimitive.Description
class={cn(textfieldLabel({ description: true }), local.class)}
{...rest}
/>
);
};
type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps<
TextFieldInputProps<T> & {
class?: string;
}
>;
export const TextField = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, textFieldInputProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldInputProps, ["class"]);
return (
<TextFieldPrimitive.Input
class={cn(
"flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow",
local.class,
)}
{...rest}
/>
);
};

View File

@@ -0,0 +1,19 @@
import { buildLocalizedUrl } from './i18n.models';
import { describe, test, expect } from 'vitest';
describe('i18n models', () => {
describe('buildLocalizedUrl', () => {
test('build an url prefixed with the language', () => {
expect(buildLocalizedUrl({lang: 'fr', path: '/tools/token-generator'})).toBe('/fr/tools/token-generator');
expect(buildLocalizedUrl({lang: 'en', path: '/token-generator'})).toBe('/en/token-generator');
expect(buildLocalizedUrl({path: '/token-generator'})).toBe('/token-generator');
});
test('the path may not start with a slash', () => {
expect(buildLocalizedUrl({lang: 'fr', path: 'tools/token-generator'})).toBe('/fr/tools/token-generator');
expect(buildLocalizedUrl({lang: 'en', path: 'token-generator'})).toBe('/en/token-generator');
expect(buildLocalizedUrl({path: 'token-generator'})).toBe('/token-generator');
});
});
});

View File

@@ -0,0 +1,9 @@
export function buildLocalizedUrl({lang, path}: {lang?: string, path: string}) {
const slashlessPath = path.replace(/^\//, "");
if (lang) {
return `/${lang}/${slashlessPath}`;
}
return `/${slashlessPath}`;
}

View File

@@ -0,0 +1,8 @@
import { locales } from "./languages";
export function getStaticPaths() {
return [
{ params: { lang: undefined }, },
...locales.map((lang) => ({params: { lang },})),
];
}

View File

@@ -0,0 +1,9 @@
export const languages = {
en: "English",
fr: "Français",
} as const;
export type LocaleKey = keyof typeof languages;
export const locales = Object.keys(languages) as LocaleKey[];
export const defaultLocale: LocaleKey = "en";

View File

@@ -0,0 +1,39 @@
---
import { ColorModeScript } from "@kobalte/core";
import Header from "@/components/header.astro";
import '@/assets/app.css'
import Footer from "@/components/footer.astro";
import { ClientRouter } from "astro:transitions";
import { defaultLocale } from "@/i18n/languages";
const info = {
title: 'IT-Tools',
description: 'Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT.',
};
const { lang } = Astro.params;
const locale = lang ?? defaultLocale;
---
<html lang={locale}>
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{info.title}</title>
<meta name="description" content={info.description} />
<ClientRouter />
<script is:inline>function e(){document.documentElement.setAttribute("data-kb-theme",localStorage.getItem("it-tools-theme")??(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"))}e(),document.addEventListener("astro:page-load",e)</script>
</head>
<body class="bg-background text-foreground font-sans min-h-screen text-sm antialiased flex flex-col">
<Header />
<main class="flex-1">
<slot />
</main>
<Footer />
</body>
</html>

View File

@@ -0,0 +1,5 @@
import type { ClassValue } from "clsx";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));

View File

@@ -0,0 +1,21 @@
---
import { getStaticPaths } from "@/i18n/i18n.routing";
import { defaultLocale } from "@/i18n/languages";
import BaseLayout from "@/layouts/base.layout.astro";
import { Button } from "@/components/ui/button";
import { toolDefinitions } from "@/tools/definitions/tools.registry";
export { getStaticPaths };
const { lang= defaultLocale } = Astro.params;
---
<BaseLayout>
<div class="max-w-screen-md mx-auto px-6 py-12">
<h1 class="text-2xl font-bold text-center">404</h1>
<p class="text-sm text-muted-foreground text-center">
Page not found
</p>
</div>
</BaseLayout>

View File

@@ -0,0 +1,22 @@
---
import BaseLayout from "@/layouts/base.layout.astro";
import { toolDefinitions } from "@/tools/definitions/tools.registry";
import { defaultLocale, locales } from "@/i18n/languages";
export function getStaticPaths() {
return [...locales, undefined].flatMap((lang) => toolDefinitions.map((tool) => ({
params: { lang: lang , toolSlug: tool.getLocalizedInfo({locale: lang ?? defaultLocale}).slug },
})));
}
const { toolSlug, lang = defaultLocale } = Astro.params;
const toolDefinition = toolDefinitions.find((tool) => tool.getLocalizedInfo({locale: lang ?? defaultLocale}).slug === toolSlug)!;
const { default: Tool } = await toolDefinition.entrypoint();
---
<Tool />

View File

@@ -0,0 +1,72 @@
---
import { getStaticPaths } from "@/i18n/i18n.routing";
import { defaultLocale } from "@/i18n/languages";
import BaseLayout from "@/layouts/base.layout.astro";
import { Button } from "@/components/ui/button";
import { toolDefinitions } from "@/tools/definitions/tools.registry";
import ToolCard from "@/components/tool-card.astro";
import { cn } from "@/libs/cn";
export { getStaticPaths };
const { lang } = Astro.params;
const locale = lang ?? defaultLocale;
const stats = [
{
label: "Tools",
value: toolDefinitions.length,
},
{
label: "Contributors",
value: Intl.NumberFormat('en-US').format(10),
},
{
label: "Self hosted instances",
value: Intl.NumberFormat('en-US').format(1000),
},
{
label: "GitHub Stars",
value: Intl.NumberFormat('en-US').format(1000),
},
]
---
<BaseLayout>
<div class="max-w-screen-md mx-auto px-6 mt-32 flex items-center gap-12">
<div class="max-w-md">
<h1 class="text-4xl font-semibold">IT-Tools</h1>
<p class="text-lg text-muted-foreground mt-4">
The open-source and self-hostable collection of handy online tools for developers and people working in IT.
</p>
</div>
<div class={cn('i-solar-programming-line-duotone size-42 text-muted-foreground flex-shrink-0')} />
</div>
<!-- Stats section -->
<div class="px-6 mt-24 bg-muted/20 light:border-y">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-screen-lg mx-auto">
{stats.map((stat) => {
return <div class="flex flex-col gap-2 items-center justify-center py-12">
<span class="text-2xl font-semibold">{stat.value}</span>
<span class="text-sm text-muted-foreground">{stat.label}</span>
</div>;
})}
</div>
</div>
<div class="max-w-screen-lg mx-auto px-6 py-12">
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4">
{toolDefinitions.map((tool) => {
const { icon, getLocalizedInfo } = tool;
const { slug, title, description } = getLocalizedInfo({locale});
return <ToolCard slug={slug} name={title} description={description} icon={icon} />;
})}
</div>
</div>
</BaseLayout>

View File

@@ -0,0 +1,54 @@
## What is a Token Generator?
A token generator creates random strings of characters used as unique identifiers for authentication, API keys, session tokens, and other security purposes.
## Features
### Token Generation
- Adjustable length from 1 to 512 characters
- Customizable character sets (uppercase, lowercase, numbers, symbols)
- Generate multiple tokens at once
- Real-time generation as settings change
### Security
- Uses secure random number generation
- Client-side generation for privacy
- Configurable complexity levels
### Interface
- Visual controls for easy configuration
- Copy functionality for generated tokens
- Refresh option for new tokens
- Responsive design
## Use Cases
- API keys for web services and applications
- Password reset verification tokens
- Session identifiers for authentication
- File upload and sharing tokens
- Database record identifiers
- Testing and development tokens
## How to Use
1. Set token length using the slider or number input (1-512 characters)
2. Choose character sets by toggling the switches
3. Tokens are generated automatically based on your settings
4. Copy tokens to clipboard or refresh to generate new ones
## Technical Details
- Character sets: uppercase letters (A-Z), lowercase letters (a-z), numbers (0-9), symbols
- Length range: 1 to 512 characters (or more)
- Generation method: secure random sampling
- Output format: plain text
## Benefits
- Client-side generation for privacy
- Fast and responsive interface
- Highly customizable settings
- Simple integration with development workflows
> **Security Notice**: For high-value production systems, always rotate tokens regularly and store them using secure hashing algorithms (bcrypt/scrypt).

View File

@@ -0,0 +1,5 @@
export default {
title: "Token Generator",
description: "A token is a random string of characters that is often used for unique identifiers, such as API keys or verification URLs.",
slug: "token-generator",
};

View File

@@ -0,0 +1,5 @@
export default {
title: "Générateur de tokens aléatoires",
description: "Générez des tokens aléatoires pour vos API, vos clés d'accès, ou tout autre cas où vous avez besoin d'un identifiant unique.",
slug: "generateur-de-tokens",
};

View File

@@ -0,0 +1,14 @@
import type { LocaleKey } from "@/i18n/languages";
import type { ToolDefinition } from "../tools.types";
import { locales } from "./token-generator.locales";
export default {
id: "token-generator",
icon: "i-tabler-key",
entrypoint: () => import("./token-generator.entry.astro"),
getLocalizedInfo: ({locale}: {locale: LocaleKey}) => ({
slug: locales[locale].slug,
title: locales[locale].title,
description: locales[locale].description,
})
} satisfies ToolDefinition;

View File

@@ -0,0 +1,49 @@
---
import { defaultLocale, locales, type LocaleKey } from "@/i18n/languages";
import BaseLayout from "@/layouts/base.layout.astro";
import { TokenGenerator as TokenGeneratorComponent } from "@/tools/definitions/token-generator/token-generator";
export function getStaticPaths() {
return [
{ params: { lang: undefined } },
...locales.map((lang) => ({params: { lang },}))
];
}
const { lang = defaultLocale } = Astro.params;
const { default: toolTranslations } = await import(`./i18n/${lang}.ts`);
async function getContent(lang: LocaleKey) {
try {
return await import(`./i18n/${lang}.md`).then(module => module.default);
} catch (error) {
return await import(`./i18n/en.md`).then(module => module.default);
}
}
const Content = await getContent(lang);
---
<BaseLayout>
<div class="max-w-screen-md mx-auto px-6 py-12 mt-12">
<div class="flex flex-col sm:items-center gap-4 mb-2">
<span class="flex-shrink-0 size-10 flex items-center justify-center bg-muted rounded-md p-2">
<span class="i-tabler-key flex-shrink-0 size-6"></span>
</span>
<h1 class="text-3xl font-bold text-center">{toolTranslations.title}</h1>
</div>
<!-- <p class="text-sm text-muted-foreground mb-6 text-center text-balance">{toolTranslations.description}</p> -->
<TokenGeneratorComponent client:load data={toolTranslations} />
</div>
<div class="border-b w-full my-12"></div>
<div class="prose max-w-screen-md mx-auto pb-24 dark:prose-invert px-6">
<Content />
</div>
</BaseLayout>

View File

@@ -0,0 +1,7 @@
import en from "./i18n/en.ts";
import fr from "./i18n/fr.ts";
export const locales = {
en,
fr,
} as const;

View File

@@ -0,0 +1,33 @@
export function generateToken({
withUppercase = true,
withLowercase = true,
withNumbers = true,
withSymbols = true,
length = 64,
sample = (corpus) => corpus[Math.floor(Math.random() * corpus.length)],
}: {
withUppercase?: boolean;
withLowercase?: boolean;
withNumbers?: boolean;
withSymbols?: boolean;
length?: number;
sample?: (corpus: string[]) => string;
}) {
const corpus = [
...(withUppercase ? "ABCDEFGHIJKLMNOPQRSTUVWXYZ" : []),
...(withLowercase ? "abcdefghijklmnopqrstuvwxyz" : []),
...(withNumbers ? "0123456789" : []),
...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : []),
];
if(corpus.length === 0) {
return "";
}
let token = "";
// imperative for loop for performance
for (let i = 0; i < length; i++) {
token += sample(corpus);
}
return token;
}

View File

@@ -0,0 +1,111 @@
import { Button } from "@/components/ui/button";
import { TextArea } from "@/components/ui/textarea";
import { TextField, TextFieldRoot } from "@/components/ui/textfield";
import type { Component } from "solid-js";
import { createEffect, createSignal, on, onMount } from "solid-js";
import { generateToken } from "./token-generator.models";
import { SwitchCard } from "@/components/ui/switch";
import { NumberField, NumberFieldDecrementTrigger, NumberFieldIncrementTrigger, NumberFieldGroup, NumberFieldLabel, NumberFieldInput } from "@/components/ui/number-field";
import { Label } from "@/components/ui/label";
import { SliderLabel, SliderTrack, SliderValueLabel, SliderThumb, SliderFill } from "@/components/ui/slider";
import { Slider } from "@/components/ui/slider";
export const TokenGenerator: Component<{ data: { title: string, description: string } }> = (props) => {
const [getLength, setLength] = createSignal<number>(64);
const [getTokenCount, setTokenCount] = createSignal<number>(1);
const [getWithUppercase, setWithUppercase] = createSignal<boolean>(true);
const [getWithLowercase, setWithLowercase] = createSignal<boolean>(true);
const [getWithNumbers, setWithNumbers] = createSignal<boolean>(true);
const [getWithSymbols, setWithSymbols] = createSignal<boolean>(false);
const [getTokens, setTokens] = createSignal<string[]>(buildTokens());
function buildTokens() {
return Array.from(
{ length: getTokenCount() },
() => generateToken({
length: getLength(),
withUppercase: getWithUppercase(),
withLowercase: getWithLowercase(),
withNumbers: getWithNumbers(),
withSymbols: getWithSymbols(),
}));
}
const refreshTokens = () => {
setTokens(buildTokens());
}
const getRowCount = () => {
const tokenCount = getTokenCount();
if(tokenCount < 10) {
return tokenCount;
}
return 10;
}
createEffect(
on([getWithUppercase, getWithLowercase, getWithNumbers, getWithSymbols, getLength, getTokenCount], () => refreshTokens())
);
const copyTokens = () => {
navigator.clipboard.writeText(getTokens().join("\n"));
}
return <div>
<TextFieldRoot class="w-full mb-4 mt-12">
<TextArea value={getTokens().join("\n")} placeholder="Tokens will appear here" rows={getRowCount()} class="text-center font-mono" autoResize />
</TextFieldRoot>
<div class="flex justify-center gap-2 mb-8">
<Button variant="outline" onClick={refreshTokens} class="gap-2">
<div class="i-tabler-refresh size-4 text-muted-foreground" />
Refresh
</Button>
<Button onClick={copyTokens} class="gap-2" >
<span class="i-tabler-copy size-4 text-muted" />
Copy
</Button>
</div>
<Label>Token length</Label>
<div class="flex flex-row gap-8 items-center mb-4">
<TextFieldRoot class="max-w-160px my-2">
<NumberField value={getLength()} onRawValueChange={(value) => setLength(value)} >
<NumberFieldGroup>
<NumberFieldDecrementTrigger aria-label="Decrement" />
<NumberFieldInput />
<NumberFieldIncrementTrigger aria-label="Increment" />
</NumberFieldGroup>
</NumberField>
</TextFieldRoot>
<Slider
minValue={1}
maxValue={512}
class="flex-1"
value={[Math.min(512, Math.max(1, getLength()))]}
onChange={(value) => setLength(value[0])}
>
<SliderTrack>
<SliderFill />
<SliderThumb />
<SliderThumb />
</SliderTrack>
</Slider>
</div>
<Label>Character set</Label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 my-2">
<SwitchCard label="Uppercase" description="Include uppercase letters" checked={getWithUppercase()} onChange={setWithUppercase} />
<SwitchCard label="Lowercase" description="Include lowercase letters" checked={getWithLowercase()} onChange={setWithLowercase} />
<SwitchCard label="Numbers" description="Include numbers" checked={getWithNumbers()} onChange={setWithNumbers} />
<SwitchCard label="Symbols" description="Include symbols" checked={getWithSymbols()} onChange={setWithSymbols} />
</div>
</div>;
};

View File

@@ -0,0 +1,6 @@
import tokenGeneratorDefinition from "./token-generator/token-generator.definition";
import type { ToolDefinition } from "./tools.types";
export const toolDefinitions: ToolDefinition[] = [
tokenGeneratorDefinition,
];

View File

@@ -0,0 +1,12 @@
import type { LocaleKey } from "@/i18n/languages";
export type ToolDefinition = {
id: string;
entrypoint: () => Promise<{ default: any }>;
icon: string;
getLocalizedInfo: ({locale}: {locale: LocaleKey}) => {
slug: string;
title: string;
description: string;
}
}

View File

@@ -0,0 +1,18 @@
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,99 @@
import { defineConfig, presetUno, transformerDirectives, transformerVariantGroup, presetIcons, presetTypography, presetWebFonts } from "unocss";
import presetAnimations from "unocss-preset-animations";
import { toolDefinitions } from "./src/tools/definitions/tools.registry";
export default defineConfig({
presets: [
presetUno({
dark: {
dark: '[data-kb-theme="dark"]',
light: '[data-kb-theme="light"]'
}
}),
presetAnimations(),
presetIcons(),
presetTypography(),
presetWebFonts({
provider: 'bunny',
fonts: {
sans: 'Inter:300,400,500,600,700,800',
},
}),
],
transformers: [transformerVariantGroup(), transformerDirectives()],
theme: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))"
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))"
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))"
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))"
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))"
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))"
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))"
}
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: "calc(var(--radius) - 4px)"
},
animation: {
keyframes: {
"accordion-down":
"{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }",
"accordion-up": "{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }",
"collapsible-down":
"{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }",
"collapsible-up":
"{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }"
},
timingFns: {
"accordion-down": "ease-out",
"accordion-up": "ease-out",
"collapsible-down": "ease-out",
"collapsible-up": "ease-out"
},
durations: {
"accordion-down": "0.2s",
"accordion-up": "0.2s",
"collapsible-down": "0.2s",
"collapsible-up": "0.2s"
}
}
},
safelist: [
'sm:grid-cols-2',
'sm:grid-cols-3',
'sm:grid-cols-4',
'sm:grid-cols-5',
'sm:grid-cols-6',
'sm:grid-cols-7',
'sm:grid-cols-8',
...toolDefinitions.map((tool) => tool.icon),
]
});

View File

@@ -1,393 +0,0 @@
home:
categories:
newestTools: Nyeste verktøy
favoriteTools: 'Dine favoritt verktøy'
allTools: 'Alle verktøyene'
subtitle: 'Nyttige verktøy for utviklere'
toggleMenu: 'Vekslemenmy'
home: Hjem
uiLib: 'UI Bib'
support: 'Støtt utviklingen av IT-Tools'
buyMeACoffee: 'Kjøp en kaffe til meg'
follow:
title: 'Liker du it-tools?'
p1: 'Gi oss en stjerne på'
githubRepository: 'IT-Tools GitHub-depotet'
p2: 'eller følg oss på'
twitterAccount: 'IT-Tools sin twitter konto'
thankYou: 'Tusen takk!'
nav:
github: 'GitHub-depot'
githubRepository: 'IT-Tools GitHub-depot'
twitter: 'Twitter konto'
twitterAccount: 'IT Tools Twitter konto'
about: 'Om IT-Tools'
aboutLabel: 'Om'
darkMode: 'Mørk modus'
lightMode: 'Lys modus'
mode: 'Veksle mørk/lys modus'
about:
content: >
# Om IT-Tools
Denne vidunderlige nettsiden, laget med ❤ av [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about) , sammenstiller nyttige verktøy for utviklere og folk som jobber innen IT. Hvis du finner dette nyttig, Del det gjerne med andre som du tror kan få nytte av dette, og ikke glem å lage et bokmerke!
IT Tools er åpen kildekode (under MIT lisensen) og gratis, og det vil det alltid være, men det koster å drifte og å fornye domenet. Hvis du ønsker å støtte arbeidet mitt, og motivere meg til å legge til flere verktøy, gjerne støtt meg ved å [sponse meg](https://www.buymeacoffee.com/cthmsst).
## Teknologier
IT Tools er laget i Vue.js (Vue 3) med Naive UI komponent bibliotektet og er hosted og kontinuerlig deployet av Vercel. Tredjeparts åpen-kildekode biblioteker er brukt i noen verktøy, du kan finne den komplette listen i [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) filen i depoet.
## Funnet en feil? Et verktøy som mangler?
Hvis du trenger et verktøy som foreløpig ikke er tilgjengelig her, og du tenker det kan være nyttig for andre, så er du velkommen til å legge til en funksjonsforespørsel i [problem seksjonen](https://github.com/CorentinTh/it-tools/issues/new/choose) i github-depotet.
Og hvis du har funnet en feil, eller noe ikke oppfører seg som forventet, vennligst send inn en feilrapport i [problem seksjonen](https://github.com/CorentinTh/it-tools/issues/new/choose) i github-depotet.
404:
notFound: '404 ikke funnet'
sorry: 'Beklager, denne siden ser ikke ut til å eksistere'
maybe: 'Kanskje informasjonskapslene oppfører seg rart, prøvd en tvungen oppfriskning?'
backHome: 'Tilbake til start'
favoriteButton:
remove: 'Fjern fra favoritter'
add: 'Legg til favoritter'
toolCard:
new: Ny
search:
label: Søk
tools:
categories:
favorite-tools: 'Dine favoritt verktøy'
crypto: Krypto
converter: Konvertering
web: Web
images and videos: 'Bilder & Videoer'
development: Utvikling
network: Nettverk
math: Matte
measurement: Måling
text: Tekst
data: Data
password-strength-analyser:
title: Analyseverktøy for passordstyrke
description: Oppdag styrken av passordet ditt med dette kun-klient-maskin passordstyrke analyse verktøyet og se den estimerte knekketiden.
chronometer:
title: Kronometer
description: Overvåk varigheten av noe. I bunn og grunn et kronometer med enkle funksjoner.
token-generator:
title: Token generator
description: Generer en tilfeldig streng med store og/eller små bokstaver, siffer og/eller symboler.
uppercase: Store bokstaver (ABC...)
lowercase: Små bokstaver (abc...)
numbers: Siffer (123...)
symbols: Symboler (!-;...)
length: Lengde
tokenPlaceholder: 'Tokenet...'
copied: Tokenet er kopiert til utklippstavlen.
button:
copy: Kopier
refresh: Oppfrisk
percentage-calculator:
title: Prosent kalkulator
description: Beregn enkelt prosenter fra en verdi til en annen, eller fra en prosent til en verdi.
svg-placeholder-generator:
title: SVG plassholder generator
description: Generer svg bilder til å bruke som plassholder i applikasjonen din.
json-to-csv:
title: JSON til CSV
description: Konverter JSON til CSV med automatisk oppdagelse av headeren.
camera-recorder:
title: Kameraopptak
description: Ta et bilde eller spill inn en video med webkamera eller kameraet ditt.
keycode-info:
title: Tastekode info
description: Finn javascript tastekode, kode, plassering og modifikatorer av hvilken som helst tast.
emoji-picker:
title: Emoji velger
description: Klipp og lim emojis og få unicode og kode verdien av hver emoji.
color-converter:
title: Farge konverter
description: Konverter farger mellom de forskjellige formatene (hex, rgb, hsl og css navn).
bcrypt:
title: Bcrypt
description: Hash og sammenlign tekst ved hjelp av bcrypt. Bcrypt er en passord-hashings funksjon basert på Blowfish cipher.
crontab-generator:
title: Crontab generator
description: Verifiser og generer crontab og få den mennesklig leselige beskrivelsen av cron timeplanen.
http-status-codes:
title: HTTP status koder
description: Liste over alle HTTP status koder, navnet dems, og betydningen.
sql-prettify:
title: SQL forskjønning and format
description: Formater og forskjønn SQL spørringene dine (den støtter forskjellige SQL dialekter).
benchmark-builder:
title: Bygg en referansemåler
description: Sammenlign enkelt kjøretiden av oppgaver med denne enkle referansemåls byggeren.
git-memo:
title: Git jukselapp
description: Git er en desentralisert versjons håndterings programvare. Med denne jukselappen vil du få kjapp tilgang til de vanligste kommandoene.
slugify-string:
title: Slugify streng
description: Lag en trygg url, filbane eller id.
encryption:
title: Krypter / decrypter tekst
description: Krypter klartekst og dekrypter ciphertekst ved bruk av krypteringsalgoritmer som AES, TripleDES, Rabbit eller RC4.
random-port-generator:
title: Tilfeldig port generator
description: Generer tilfeldige portnumre utenfor scopet av "kjente" porter (0-1023).
yaml-prettify:
title: YAML forskjønning og formatering
description: Forskjønn YAML strengene dine til et lettlest format.
eta-calculator:
title: ETA kalkulator
description: En ETA (Estimert Tid for Ankomst) kalkulator for å anslå den sannsynelige slutt tiden for en oppgave, for eksempel, slutttiden og varigheten av en filnedlastning.
roman-numeral-converter:
title: Romertall konverter
description: Konverter romertall til tall eller konverter tall til romertall.
hmac-generator:
title: Hmac generator
description: Beregn en hash-basert meldings authentiserings kode (HMAC) ved bruk av en hemmelig nøkkel og din foretrukne hashings funksjon.
bip39-generator:
title: BIP39 nøkkelords generator
description: Generer et BIP39 nøkkelord fra en eksisterende eller tilfeldig huskesetning, eller få ut en huskesetning fra nøkkelordet.
base64-file-converter:
title: Base64 fil konverter
description: Konverter en base64 streng til fil eller en fil, bilde til en base64 representasjon.
list-converter:
title: Liste konverterer
description: Dette verktøyet kan prosessere kolonnebasert data og foreta forskjellige endringer (transposering, legge til prefix og suffix, reversere lister, sortere lister, gjøre om til små bokstaver, trunkere verdier) på hver rad.
base64-string-converter:
title: Base64 string kode/dekoder
description: Enkelt kode eller dekode en tekststreng til base64 representasjonen av strengen.
toml-to-yaml:
title: TOML til YAML
description: Parser og konverter TOML til YAML.
math-evaluator:
title: Matematikkevaluator
description: En Kalkulator for å evaluere matematiske uttrykk. Du kan bruke funksjoner som sqrt, cos, sin, abs, etc.
json-to-yaml-converter:
title: JSON til YAML konverterer
description: Enkelt konverter JSON til YAML med dette verktøyet.
url-parser:
title: URL analyse
description: Parsere en URL ned til bestanddelene (protokoll, opprinnelse, parametre, port, brukernavn-passord, ...).
iban-validator-and-parser:
title: IBAN validering og analysering
description: Valider og parser IBAN numre. Sjekk om et IBAN er gyldig og få landet, BBAN, om det er en QR-IBAN og IBAN i et vennlig format.
user-agent-parser:
title: User-agent analysering
description: Detekter og parser nettleser, motor, OS, CPU, og enhet type/modell fra en user-agent tekst streng.
numeronym-generator:
title: Numeronym generator
description: Et numeronym er et ord hvor et nummer er brukt til å lage en forkortelse. For eksempel, "i18n" er et numeronym for "internasjonalisering" hvor 18 står for antall bokstaver mellom første bokstaven i og den siste bokstaven n i ordet.
case-converter:
title: Bokstavkonvertering
description: Formater bokstavene med store eller små bokstaver, samt andre format.
html-entities:
title: HTML streng rensing
description: Rens bort eller omsvøp HTML entiteter (erstatt tegn som <,>, &, " and \' med deres HTML versjon).
json-prettify:
title: JSON forskjønning og formatering
description: Forskjønn JSON strenger til et lettlest format.
docker-run-to-docker-compose-converter:
title: Docker run til Docker compose konverter
description: Konverter "docker run" kommandoer til docker-compose filer!
mac-address-lookup:
title: MAC address oppslagsverk
description: Finn forhandler og produsent basert på MAC adressen.
mime-types:
title: MIME typer
description: Konverter MIME typer til fil utvidelser og visa-versa.
toml-to-json:
title: TOML til JSON
description: Parser og konverter TOML til JSON.
lorem-ipsum-generator:
title: Lorem ipsum generator
description: Lorem ipsum er brukt som plassholder tekst, vanligvis brukt til å demonstrere den visuelle formen av et dokument eller font-type uten å måtte ha meningsfult innhold.
qrcode-generator:
title: QR Kode generator
description: Generer og last ned en QR kode til en URL (eller ren tekst), og tilpass bakgrunns og forgrunns farger.
wifi-qrcode-generator:
title: WiFi QR Kode generator
description: Generer og last ned QR koder for rask tilkobling til wifi nettverket.
xml-formatter:
title: XML formaterer
description: Forskjønn en XML streng til et lettlest format.
temperature-converter:
title: Temperatur konverter
description: Temperatur konversjoner mellom Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur, og Rømer.
chmod-calculator:
title: Chmod kalkulator
description: Beregn chmod tillatelser og kommandoer med denne chmod kalkulatoren.
rsa-key-pair-generator:
title: RSA nøkkelpar generator
description: Generer et nytt tilfeldig RSA privat og offentlig pem sertifikat nøkkel par.
html-wysiwyg-editor:
title: HTML WYSIWYG editor
description: Online, funksjonsrik WYSIWYG HTML editor som genererer kildekoden for innholdet øyeblikkelig.
yaml-to-toml:
title: YAML til TOML
description: Parser og konverter YAML til TOML.
mac-address-generator:
title: MAC adresse generator
description: Sett inn antall og prefix. MAC addressene blir generert i ønsket format
json-diff:
title: JSON diff
description: Sammenlign to JSON objekter og finn forskjellene mellom dem.
jwt-parser:
title: JWT parser
description: Parse og dekode et JSON Web Token (jwt) og vis innholdet.
date-converter:
title: Dato-tid konverter
description: Konverter dato og tid til forskjellige formater.
phone-parser-and-formatter:
title: Telefon format og parserer
description: Parser, valider og formater telefon numre. få innformasjonen om telefon nummeret, slik som landskoden, type etc.
ipv4-subnet-calculator:
title: IPv4 subnet kalkulator
description: Parser IPv4 CIDR blokker of åf all info du trenger om subnettet.
og-meta-generator:
title: Open graph meta generator
description: Generer open-graph og SoMe HTML meta tagger til nettsiden din.
ipv6-ula-generator:
title: IPv6 ULA generator
description: Generer din egen lokale, ikke-rutbare IP adresse til nettverket ditt i henhold til RFC4193.
hash-text:
title: Hash tekst
description: 'Hash en tekst streng med en av algoritmene : MD5, SHA1, SHA256, SHA224, SHA512, SHA384, SHA3 eller RIPEMD160'
json-to-toml:
title: JSON til TOML
description: Parser og konverter JSON til TOML.
device-information:
title: Enhets informasjon
description: Få informasjon om din nåværende enhet (skjermstørrelse, piksel-forhold, user agent, etc.)
pdf-signature-checker:
title: PDF signatur sjekker
description: Bekreft signaturen til en PDF fil. En signert PDF fil inneholder en eller flere signaturer som kan bli brukt til å bestemme om en fil har blitt endret etter at den var signert.
json-minify:
title: JSON minifiser
description: Minifiser og komprimer JSON ved å fjerne unødvendige mellomrom.
ulid-generator:
title: ULID generator
description: Generer tilfeldig Universell Unik Leksikografisk Sorterbar Identifikator (ULID).
string-obfuscator:
title: Streng obfuskator
description: Obfusker en streng (som en hemmelighet, en IBAN, eller et token) og gjør den delbar og identifiserbar uten å vise innholdet.
base-converter:
title: Heltalls konverter
description: Konverter et heltall mellom forskjellige baser (desimal, hexadesimal, binær, oktal, base64, etc.)
yaml-to-json-converter:
title: YAML til JSON konverter
description: Konverterl YAML til JSON.
uuid-generator:
title: UUIDs generator
description: En universell Unik Identifikator (UUID) er et 128-bit nummer, brukt til å identifisere informasjon i datasystemer.
ipv4-address-converter:
title: IPv4 adresse konverter
description: Konverter en IPv4 adresse til desimal, binær, hexadesimal, eller en IPv6 representasjon.
text-statistics:
title: Tekst statistikk
description: Få informasjonen om en tekst, antall karakterer, antall ord, størrelsen i bytes, etc.
text-to-nato-alphabet:
title: Tekst til NATO alfabetet
description: Transformer teksten til det NATO fonetiske alfabetet for muntlig gjengivelse.
basic-auth-generator:
title: Basic auth generator
description: Generer en base64 basic auth header fra et brukernavn og passord.
text-to-unicode:
title: Tekst til Unicode
description: Parser og konverter tekst til unicode og visa-versa
ipv4-range-expander:
title: IPv4 range utvider
description: Gitt en start og en slutt IPv4 adresse, kalkulerer dette verktøyet et gyldig IPv4 subnet sammen med sin CIDR notasjon.
text-diff:
title: Tekst diff
description: Sammenlign to tekster og vis forskjellen mellom dem.
otp-generator:
title: OTP kode generator
description: Generer og valider tidsbasert OTP (one time password) for multi-faktor autentisering.
url-encoder:
title: Kode/dekode URL-formaterte strenger
description: Kode tekst til URL-kodet format (også kjent som "prosent-kodet"), eller dekode fra det.
text-to-binary:
title: Tekst til ASCII binært
description: Konverter tekst til sin ASCII binære representasjon og visa-versa.

View File

@@ -2,8 +2,6 @@
"name": "@it-tools/root",
"version": "0.0.0",
"description": "IT Tools monorepo root",
"packageManager": "pnpm@9.12.2",
"scripts": {
"dev": "pnpm -F @it-tools/app dev"
},
@@ -12,5 +10,6 @@
"repository": {
"type": "git",
"url": "https://github.com/CorentinTh/it-tools"
}
},
"packageManager": "pnpm@9.11.0"
}

View File

@@ -1,24 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

View File

@@ -1,75 +0,0 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@@ -1,10 +0,0 @@
<script setup lang="ts">
import CommandPalette from './src/modules/command-palette/components/command-palette.vue';
</script>
<template>
<NuxtLayout>
<CommandPalette />
<NuxtPage />
</NuxtLayout>
</template>

View File

@@ -1,77 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 95 71% 68%;
--primary-foreground: 240 10% 3.9%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border:240 5.9% 90%;
--input:240 5.9% 90%;
--ring:240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background:240 4% 10%;
--foreground:0 0% 98%;
--card: 240 5% 8%;
--card-foreground:0 0% 98%;
--popover:240 10% 3.9%;
--popover-foreground:0 0% 98%;
--primary: 83 79% 55%;
--primary-foreground:240 5.9% 10%;
--secondary:240 3.7% 15.9%;
--secondary-foreground:0 0% 98%;
--muted:240 3.7% 15.9%;
--muted-foreground:240 5% 64.9%;
--accent:240 3.7% 15.9%;
--accent-foreground:0 0% 98%;
--destructive:0 62.8% 30.6%;
--destructive-foreground:0 0% 98%;
--border:240 3.7% 15.9%;
--input:240 3.7% 15.9%;
--ring:240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,19 +0,0 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/css/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"framework": "nuxt",
"aliases": {
"components": "@/src/modules/ui/components",
"ui": "@/src/modules/ui/components",
"utils": "@/src/modules/shared/style/cn"
}
}

View File

@@ -1,23 +0,0 @@
import antfu from '@antfu/eslint-config';
export default antfu({
stylistic: {
semi: true,
},
vue: true,
rules: {
// To allow export on top of files
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'curly': ['error', 'all'],
'vitest/consistent-test-it': ['error', { fn: 'test' }],
'ts/consistent-type-definitions': ['error', 'type'],
'style/brace-style': ['error', '1tbs', { allowSingleLine: false }],
'unused-imports/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
});

View File

@@ -1,5 +0,0 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
}));

View File

@@ -1,60 +0,0 @@
import toolsModule from './src/modules/tools/modules/tools.modules';
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
extends: [
'src/modules/app',
],
modules: [
'@nuxtjs/tailwindcss',
'shadcn-nuxt',
'@nuxt/fonts',
'@nuxt/icon',
'@vueuse/nuxt',
'@nuxtjs/color-mode',
toolsModule, // Must be imported before 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: {
provider: 'bunny',
defaults: {
weights: [400, 500, 600, 700, 800],
},
},
colorMode: {
preference: 'system',
fallback: 'dark',
classSuffix: '',
storage: 'cookie',
storageKey: 'itts-color-mode',
},
i18n: {
strategy: 'prefix',
vueI18n: './i18n.config.ts',
defaultLocale: 'en',
langDir: './src/locales',
locales: [
{ code: 'en', file: 'en.yaml', name: 'English' },
{ code: 'fr', file: 'fr.yaml', name: 'Français' },
],
},
experimental: {
scanPageMeta: false, // Causes some issues with layouts and hook-registered pages
},
});

View File

@@ -1,51 +0,0 @@
{
"name": "@it-tools/app",
"type": "module",
"private": true,
"packageManager": "pnpm@9.15.1",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"build:cloudflare-pages": "nuxt build --preset cloudflare_pages",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "pnpm run test:unit",
"test:unit": "vitest run",
"test:unit:watch": "vitest watch",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@corentinth/chisels": "^1.1.0",
"@nuxt/fonts": "^0.10.3",
"@nuxt/icon": "^1.10.3",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^8.5.6",
"@nuxtjs/seo": "2.0.0-rc.23",
"@pinia/nuxt": "^0.5.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.453.0",
"nuxt": "^3.15.0",
"radix-vue": "^1.9.11",
"shadcn-nuxt": "^0.10.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"@antfu/eslint-config": "^3.12.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tailwindcss/typography": "^0.5.15",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "^11.3.0",
"@vueuse/nuxt": "^11.3.0",
"eslint": "^9.17.0",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

@@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@@ -1,41 +0,0 @@
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.
placeholder: Generated token will appear here, please select at least one option.
use-uppercase: Include uppercase letters
use-lowercase: Include lowercase letters
use-numbers: Include numbers
use-symbols: Include symbols
exclude-similar: Exclude similar characters
length: Length
refresh: Refresh token
quantity: Quantity
format: Format

View File

@@ -1,29 +0,0 @@
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,116 +0,0 @@
<script setup>
const localePath = useLocalePath();
const { t } = useI18n();
const sections = computed(() => [
{
title: t('footer.resources.title'),
items: [
{ label: t('footer.resources.all-tools'), to: localePath('/tools') },
{ label: t('footer.resources.github'), href: 'https://github.com/CorentinTh/it-tools' },
{ 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: t('footer.support.title'),
items: [
{ label: t('footer.support.report-bug'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
{ label: t('footer.support.request-feature'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
{ 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: t('footer.friends.title'),
items: [
{ label: 'Jugly.io', href: 'https://jugly.io' },
{ label: 'Enclosed.cc', href: 'https://enclosed.cc' },
],
},
]);
const socialLinks = [
{
icon: 'i-tabler-brand-github',
href: 'https://github.com/CorentinTh/it-tools',
label: 'GitHub',
},
{
icon: 'i-tabler-brand-x',
href: 'https://x.com/ittoolsdottech',
label: 'X',
},
{
icon: 'i-tabler-coffee',
href: 'https://buymeacoffee.com/cthmsst',
label: 'Support the project',
},
];
</script>
<template>
<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="flex items-start justify-between flex-col md:flex-row gap-12">
<div>
<div class="flex items-center gap-2">
<NuxtLink :to="localePath('/')" class="text-2xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250 group text-muted-foreground flex items-center gap-1">
<span class="font-bold group-hover:text-foreground transition">IT</span>
<span class="text-[80%] font-extrabold border-[2px] leading-none border-current rounded-md px-1 pt-0.5 ml-1 group-hover:text-primary transition">TOOLS</span>
</NuxtLink>
</div>
<div class="flex items-center gap-2 mt-4">
<!-- {socialLinks.map(({ icon, href, label }) => (
<a href="{href}" target="_blank" rel="noopener noreferrer" class="text-2xl text-muted-foreground hover:text-primary transition" aria-label="{label}">
<div class="{icon}" />
</a>
))} -->
<a
v-for="socialLink in socialLinks" :key="socialLink.label" :href="socialLink.href" target="_blank" rel="noopener noreferrer" class="text-2xl text-muted-foreground hover:text-primary transition" :aria-label="socialLink.label"
>
<Icon :name="socialLink.icon" />
</a>
</div>
<div class="text-muted-foreground mt-2">
Crafted on Earth by
<a href="https://corentin.tech" target="_blank" rel="noopener" class="hover:text-primary transition">
Corentin Thomasset
</a>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-12">
<div v-for="section in sections" :key="section.title">
<h4 class="font-semibold text-foreground">
{{ section.title }}
</h4>
<ul class="mt-4">
<li v-for="item in section.items" :key="item.label" class="mt-1">
<NuxtLink v-if="item.to" :to="localePath(item.to)" class="text-muted-foreground hover:text-primary transition">
{{ item.label }}
</NuxtLink>
<a v-else :href="item.href" target="_blank" rel="noopener" class="text-muted-foreground hover:text-primary transition">
{{ item.label }}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="text-xs text-muted-foreground border-t border-border pt-4 mt-12">
<span>
&copy;
{{ new Date().getFullYear() }}
Corentin Thomasset
</span>
</div>
<div class="text-xs text-foreground opacity-80%" />
</div>
</footer>
</template>

View File

@@ -1,52 +0,0 @@
<script setup>
import { Button } from '@/src/modules/ui/components/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/src/modules/ui/components/dropdown-menu';
import { useCommandPaletteStore } from '../../command-palette/command-palette.store';
const { openCommandPalette } = useCommandPaletteStore();
const colorMode = useColorMode();
</script>
<template>
<div class="w-full flex items-center justify-between">
<div>
<Button variant="outline" class="sm:pr-12 md:pr-24 text-muted-foreground hidden sm:flex" @click="openCommandPalette">
<Icon name="i-tabler-search" class="mr-2 size-4" />
{{ $t('home.search-tools') }}
</Button>
</div>
<div class="flex items-center gap-0.5">
<Button variant="ghost" size="icon" class="sm:hidden" @click="openCommandPalette">
<Icon name="i-tabler-search" class="size-5" />
</Button>
<LocalePicker />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost">
<Icon v-if="colorMode.value === 'dark'" name="i-tabler-moon" class="size-5" />
<Icon v-else name="i-tabler-sun" class="size-5" />
<Icon name="i-tabler-chevron-down" class="ml-1 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'light' }" @click="colorMode.preference = 'light'">
<Icon name="i-tabler-sun" class="mr-2 size-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'dark' }" @click="colorMode.preference = 'dark'">
<Icon name="i-tabler-moon" class="mr-2 size-4" />
Dark
</DropdownMenuItem>
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'system' }" @click="colorMode.preference = 'system'">
<Icon name="i-tabler-device-laptop" class="mr-2 size-4" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{ fadeBottom?: boolean; faderClass?: string }>(), { fadeBottom: true });
</script>
<template>
<div class="w-full bg-[linear-gradient(to_right,#80808010_1px,transparent_1px),linear-gradient(to_bottom,#80808010_1px,transparent_1px)] bg-[size:48px_48px] pt-20">
<slot />
<div v-if="props.fadeBottom" class="bg-gradient-to-t from-background to-transparent h-24 mt-24" :class="props.faderClass" />
</div>
</template>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
import { Button } from '../../ui/components/button';
import { DropdownMenu } from '../../ui/components/dropdown-menu';
import DropdownMenuContent from '../../ui/components/dropdown-menu/DropdownMenuContent.vue';
import DropdownMenuItem from '../../ui/components/dropdown-menu/DropdownMenuItem.vue';
import DropdownMenuTrigger from '../../ui/components/dropdown-menu/DropdownMenuTrigger.vue';
const { locale, locales, setLocale } = useI18n();
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<slot>
<Button variant="ghost" aria-label="Change language">
<Icon name="i-tabler-language" class="size-5 mr-1" />
<Icon name="i-tabler-chevron-down" class="text-muted-foreground" />
</Button>
</slot>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
v-for="({ name, code }) in locales"
:key="code"
class="cursor-pointer"
:class="{ 'font-bold': locale === code }"
@click="setLocale(code)"
>
{{ name }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -1,28 +0,0 @@
<script setup>
import { Button } from '@/src/modules/ui/components/button';
import { useToolsStore } from '../../tools/tools.store';
const toolStore = useToolsStore();
const localePath = useLocalePath();
</script>
<template>
<div class="border-b h-[60px] flex items-center justify-between 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">
<span class="font-bold text-foreground">IT</span>
<span class="text-[80%] font-extrabold border-[2px] leading-none border-current rounded-md px-1 py-0.5 ml-1.5 text-primary">TOOLS</span>
</NuxtLink>
</div>
<div class="pt-4 px-3 flex flex-col gap-0.5">
<NuxtLink to="/" class="py-1.5 px-3 flex items-center text-muted-foreground hover:text-foreground transition hover:bg-muted rounded-lg">
<Icon name="i-tabler-home" class="mr-2 size-4" />
Home
</NuxtLink>
<NuxtLink v-for="tool in toolStore.tools" :key="tool.key" class="py-1.5 px-3 flex items-center text-muted-foreground hover:text-foreground transition hover:bg-muted rounded-lg" :to="tool.path" exact-active-class="bg-secondary !text-foreground">
<Icon :name="tool.icon" class="mr-2 size-4" />
{{ tool.title }}
</NuxtLink>
</div>
</template>

View File

@@ -1,27 +0,0 @@
<script setup>
import { Button } from '@/src/modules/ui/components/button';
import { useToolsStore } from '../../tools/tools.store';
const localePath = useLocalePath();
</script>
<template>
<div class="w-full min-h-screen text-sm relative font-sans flex flex-col">
<div class="h-[60px] border-b">
<div class="max-w-screen-xl mx-auto py-2 px-6 w-full flex items-center gap-4 h-full">
<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="text-[80%] font-extrabold border-[2px] leading-none border-current rounded-md px-1 py-0.5 ml-1.5 text-primary">TOOLS</span>
</NuxtLink>
<app-header />
</div>
</div>
<div class="flex-1 pb-6">
<slot />
</div>
<app-footer />
</div>
</template>

View File

@@ -1,36 +0,0 @@
<script setup>
import { Button } from '../../ui/components/button';
import { Sheet, SheetContent, SheetTrigger } from '../../ui/components/sheet';
</script>
<template>
<div class="w-full min-h-screen text-sm relative font-sans flex flex-row">
<div class="w-64 border-r bg-white dark:bg-background shrink-0 hidden sm:block">
<sidenav-menu />
</div>
<div class="flex-1 flex flex-col">
<div class="border-b h-[60px] flex items-center justify-between px-6 bg-white dark:bg-background">
<div class="flex items-center gap-4">
<div class="sm:hidden">
<Sheet>
<SheetTrigger>
<Button variant="ghost" size="icon">
<Icon name="i-tabler-menu-2" class="size-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" class="p-0 text-sm">
<sidenav-menu />
</SheetContent>
</Sheet>
</div>
</div>
<app-header />
</div>
<div class="flex-1">
<slot />
</div>
</div>
</div>
</template>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
import { Button } from '@/src/modules/ui/components/button';
const localePath = useLocalePath();
</script>
<template>
<div class="flex justify-center text-center mt-24">
<div>
<h1 class="text-3xl font-light text-muted-foreground">
404
</h1>
<h2 class="font-semibold text-lg my-2">
Page not found
</h2>
<p class="text-muted-foreground">
The page you are looking for does not seem to exist.
</p>
<p class="text-muted-foreground">
Please check the URL and try again.
</p>
<Button as-child variant="secondary" class="mt-4">
<NuxtLink :to="localePath('/')">
<Icon name="i-tabler-arrow-left" class="mr-2 size-4" />
Go back home
</NuxtLink>
</Button>
</div>
</div>
</template>

View File

@@ -1,3 +0,0 @@
<template>
TODO
</template>

View File

@@ -1,74 +0,0 @@
<script setup>
import { Badge } from '@/src/modules/ui/components/badge';
import { Button } from '@/src/modules/ui/components/button';
import { useCommandPaletteStore } from '../../command-palette/command-palette.store';
import { useToolsStore } from '../../tools/tools.store';
import Card from '../../ui/components/card/Card.vue';
const { tools } = useToolsStore();
const { openCommandPalette } = useCommandPaletteStore();
</script>
<template>
<grid-background>
<div class="flex gap-24 mx-auto justify-center pb-8 mt-8 items-center px-6">
<div class="max-w-xl">
<div class="flex gap-2">
<Badge class="text-primary bg-primary/10 hover:bg-primary/10 shadow-none">
{{ $t('home.open-source') }}
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10 shadow-none">
{{ $t('home.free') }}
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10 shadow-none">
{{ $t('home.self-hostable') }}
</Badge>
</div>
<h1 class="text-5xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250 my-6">
<span class="font-bold ">IT</span>
<span class="text-[90%] text-primary font-extrabold border-[5px] leading-none border-current rounded-xl px-2 py-0.5 ml-3">TOOLS</span>
</h1>
<p class="text-xl text-gray-400 mb-4">
{{ $t('app.description') }}
</p>
<div class="flex gap-4">
<Button>
{{ $t('home.all-the-tools') }}
<Icon name="i-tabler-arrow-right" class="ml-2 size-4" />
</Button>
<Button variant="outline" @click="openCommandPalette">
<Icon name="i-tabler-search" class="mr-2 size-4" />
{{ $t('home.search-tools') }}
</Button>
</div>
</div>
<div class="relative hidden sm:block">
<div class="absolute top-4 left-0 w-full h-full flex items-center justify-center blur-2xl rounded-full opacity-20 bg-gradient-to-br from-primary to-transparent" />
<Icon name="i-tabler-terminal" class="text-9xl text-primary m-8" />
</div>
</div>
</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>

View File

@@ -1,16 +0,0 @@
export const useCommandPaletteStore = defineStore('command-palette', () => {
const isCommandPaletteOpen = ref(false);
return {
isCommandPaletteOpen,
toggleCommandPalette() {
isCommandPaletteOpen.value = !isCommandPaletteOpen.value;
},
closeCommandPalette() {
isCommandPaletteOpen.value = false;
},
openCommandPalette() {
isCommandPaletteOpen.value = true;
},
};
});

View File

@@ -1,77 +0,0 @@
<script setup lang="ts">
import { useToolsStore } from '../../tools/tools.store';
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../../ui/components/command';
import { useCommandPaletteStore } from '../command-palette.store';
const commandPaletteStore = useCommandPaletteStore();
const { tools } = useToolsStore();
onKeyStroke('k', (e) => {
e.preventDefault();
if (!e.ctrlKey && !e.metaKey) {
return;
}
commandPaletteStore.toggleCommandPalette();
});
const commandSections = computed(() => [
{
title: 'Tools',
items: [
...tools.map(tool => ({
label: tool.title,
icon: tool.icon,
action: () => navigateTo(tool.path),
})),
],
},
]);
function handleSelectCommand({ item }: { item: { label: string; action: () => void; keepOpen?: boolean } }) {
item.action();
if (!item.keepOpen) {
commandPaletteStore.closeCommandPalette();
}
}
</script>
<template>
<CommandDialog v-model:open="commandPaletteStore.isCommandPaletteOpen">
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>{{ $t('command-palette.no-result') }}</CommandEmpty>
<!-- <CommandGroup heading="Suggestions">
<CommandItem value="calendar">
Calendar
</CommandItem>
<CommandItem value="search-emoji">
Search Emoji
</CommandItem>
<CommandItem value="calculator">
Calculator
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem value="profile">
Profile
</CommandItem>
<CommandItem value="billing">
Billing
</CommandItem>
<CommandItem value="settings">
Settings
</CommandItem>
</CommandGroup> -->
<CommandGroup v-for="section in commandSections" :key="section.title" :heading="section.title">
<CommandItem v-for="item in section.items" :key="item.label" :value="item.label" @select="handleSelectCommand({ item })">
<Icon :name="item.icon" class="mr-2 size-4" />
{{ item.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</template>

View File

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

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

@@ -1,4 +0,0 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));

View File

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

View File

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

@@ -1,362 +0,0 @@
<script setup lang="ts">
import { times } from 'lodash-es';
import { useRefreshableState } from '~/src/modules/shared/composables/useRefreshableState';
import { cn } from '~/src/modules/shared/style/cn';
import { Button } from '~/src/modules/ui/components/button';
import { Checkbox } from '~/src/modules/ui/components/checkbox';
import NumberField from '~/src/modules/ui/components/number-field/NumberField.vue';
import NumberFieldContent from '~/src/modules/ui/components/number-field/NumberFieldContent.vue';
import NumberFieldDecrement from '~/src/modules/ui/components/number-field/NumberFieldDecrement.vue';
import NumberFieldIncrement from '~/src/modules/ui/components/number-field/NumberFieldIncrement.vue';
import NumberFieldInput from '~/src/modules/ui/components/number-field/NumberFieldInput.vue';
import Slider from '~/src/modules/ui/components/slider/Slider.vue';
import { Textarea } from '~/src/modules/ui/components/textarea';
import ToggleGroup from '~/src/modules/ui/components/toggle-group/ToggleGroup.vue';
import ToggleGroupItem from '~/src/modules/ui/components/toggle-group/ToggleGroupItem.vue';
import { createToken } from './token-generator.models';
definePageMeta({
layout: 'sidenav',
});
const formats = {
raw: {
label: 'Raw',
format: ({ tokens }: { tokens: string[] }) => tokens.join('\n'),
},
JSON: {
label: 'JSON',
format: ({ tokens }: { tokens: string[] }) => JSON.stringify(tokens),
},
};
const similarChars = ['I', 'l', '1', 'O', '0'];
const withUppercase = ref(true);
const withLowercase = ref(true);
const withNumbers = ref(true);
const withSymbols = ref(false);
const excludeSimilar = ref(false);
const length = ref(64);
const format = ref<keyof typeof formats>('raw');
const quantity = ref(1);
const tab = ref<'generator' | 'about'>('generator');
function reset() {
withUppercase.value = true;
withLowercase.value = true;
withNumbers.value = true;
withSymbols.value = false;
excludeSimilar.value = false;
length.value = 64;
format.value = 'raw';
quantity.value = 1;
}
function generateToken() {
return createToken({
withUppercase: withUppercase.value,
withLowercase: withLowercase.value,
withNumbers: withNumbers.value,
withSymbols: withSymbols.value,
length: length.value,
exclude: excludeSimilar.value ? similarChars : [],
});
}
const [token, refreshToken] = useRefreshableState(
'token-generator:token',
() => {
const tokens = times(
quantity.value,
generateToken,
);
return formats[format.value].format({ tokens });
},
);
watch([
withUppercase,
withLowercase,
withNumbers,
withSymbols,
length,
format,
quantity,
excludeSimilar,
], refreshToken);
// const { copy: copyToken } = useCopy({ source: token, notificationText: 'Token copied to clipboard' });
</script>
<template>
<div class="flex flex-col h-full">
<div class="p-6 pb-0 bg-white dark:bg-background border-b">
<h1 class="text-2xl">
{{ $t('tools.token-generator.title') }}
</h1>
<p class="text-muted-foreground">
{{ $t('tools.token-generator.description') }}
</p>
<div class="mt-2 flex gap-4">
<Button variant="link" :class="cn('text-muted-foreground font-sm pb-0 px-0 rounded-none hover:no-underline', { 'border-b border-b-foreground text-foreground': tab === 'generator' })" @click="tab = 'generator'">
Generator
</Button>
<Button variant="link" :class="cn('border-b border-b-transparent text-muted-foreground font-sm pb-0 px-0 rounded-none hover:no-underline transition', { 'border-b-foreground text-foreground': tab === 'about' })" @click="tab = 'about'">
About
</Button>
</div>
</div>
<div v-if="tab === 'about'" class="p-6 bg-white dark:bg-background">
<div class="prose dark:prose-invert mx-auto mb-20">
<h2 id="about-the-random-token-generator">
About The Random Token Generator
</h2>
<p>
The Random Token Generator on Crucials.tools is designed to meet the
modern developers need for quick, secure, and customizable tokens.
Whether you&#39;re working on web applications, software development, system
administration, or creating secure passwords, our tool provides a
straightforward solution to create tokens that fit your specifications
perfectly.
</p>
<h2 id="why-are-tokens-important-">
Why Are Tokens Important?
</h2>
<p>
In the realm of software development and IT, tokens play a crucial role in
maintaining security and user authentication. They are essential for:
</p>
<ul>
<li>
<p>
<strong>Session Management:</strong> Tokens are used to manage user sessions,
allowing systems to recognize and verify users over multiple requests.
</p>
</li>
<li>
<p>
<strong>API Security:</strong> Securely accessing APIs often requires tokens to
ensure that the requestor has the necessary permissions.
</p>
</li>
<li>
<p>
<strong>Randomized Data Generation:</strong> Tokens can be used in testing
environments to generate randomized data inputs, helping developers
identify potential issues.
</p>
</li>
</ul>
<h2 id="ideal-for-generating-secure-passwords">
Ideal for Generating Secure Passwords
</h2>
<p>
In addition to creating tokens, our Random Token Generator is an excellent
tool for generating strong, secure passwords. With options to include a
mix of uppercase letters, lowercase letters, numbers, and special symbols,
you can create robust passwords that enhance security for any application
or service.
</p>
<h2 id="how-to-use-the-token-generator">
How to Use the Token Generator
</h2>
<p>Using our Token Generator is simple:</p>
<ol>
<li>
<p>
<strong>Select the characters you want:</strong> Choose from uppercase letters,
lowercase letters, numbers, and special symbols based on your token or
password requirements.
</p>
</li>
<li>
<p>
<strong>Choose the length:</strong> Determine how long you want your token or
password to be, depending on the level of complexity and security needed.
</p>
</li>
<li>
<p>
<strong>Generate with a click:</strong> Once your options are set, click &#39;Generate&#39;
to receive your token or password instantly.
</p>
</li>
</ol>
<h2 id="benefits-of-using-crucials-tools-token-generator">
Benefits of Using Crucials.tools Token Generator
</h2>
<ul>
<li>
<p>
<strong>Customization:</strong> Our tool allows for extensive customization, ensuring
you get exactly what you need for your project or personal security.
</p>
</li>
<li>
<p>
<strong>Speed:</strong> Generate tokens or passwords in seconds, speeding up your
development process and enhancing your security posture.
</p>
</li>
<li>
<p>
<strong>No Installation Required:</strong> As a web-based tool, there is no need to
install any software, making it accessible from anywhere at any time.
</p>
</li>
</ul>
<h2 id="perfect-tool-for-developers-and-people-in-it">
Perfect Tool For Developers and people in IT
</h2>
<p>
Whether you are developing a new application, testing existing
functionality, needing to ensure secure access, or generating a strong
password, our Random Token Generator is the perfect tool. It is built to
be reliable and secure, providing peace of mind in various development and
personal security scenarios.
</p>
</div>
</div>
<div v-if="tab === 'generator'" class="h-full flex-1 flex flex-col md:flex-row">
<div class="md:max-w-[360px] bg-white dark:bg-background h-full border-b md:border-b-none md:border-r">
<div class="px-6 pt-4 pb-6 ">
<div class="text-muted-foreground mb-3">
Token configuration
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-uppercase" v-model:checked="withUppercase" />
<label for="use-uppercase">
{{ $t('tools.token-generator.use-uppercase') }}
<span class="text-muted-foreground">
(A-Z)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-lowercase" v-model:checked="withLowercase" />
<label for="use-lowercase">
{{ $t('tools.token-generator.use-lowercase') }}
<span class="text-muted-foreground">
(a-z)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-numbers" v-model:checked="withNumbers" />
<label for="use-numbers">
{{ $t('tools.token-generator.use-numbers') }}
<span class="text-muted-foreground">
(0-9)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-symbols" v-model:checked="withSymbols" />
<label for="use-symbols">
{{ $t('tools.token-generator.use-symbols') }}
<span class="text-muted-foreground">
(!@,]*...)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="exclude-similar" v-model:checked="excludeSimilar" />
<label for="exclude-similar">
{{ $t('tools.token-generator.exclude-similar') }}
<span class="text-muted-foreground">
({{ similarChars.join('') }})
</span>
</label>
</div>
<div class="flex gap-4 items-center mb-3 mt-4">
<label for="length" class="w-20 shrink-0">{{ $t('tools.token-generator.length') }}</label>
<NumberField id="length" v-model="length" :min="1" :max="1024">
<NumberFieldContent class="flex-1">
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
<Slider
:model-value="[length]"
:max="512"
:min="1"
:step="1"
@update:model-value="(v) => length = v?.[0] ?? 1"
/>
</div>
<hr>
<div class="px-6 pt-4 pb-6 border-b">
<div class="text-muted-foreground mb-3">
Output configuration
</div>
<div class="flex items-center mb-2 gap-4">
<div class="w-20 shrink-0">
{{ $t('tools.token-generator.format') }}
</div>
<ToggleGroup v-model="format" type="single" variant="outline">
<ToggleGroupItem v-for="({ label }, key) in formats" :key="key" :value="key">
{{ label }}
</ToggleGroupItem>
</ToggleGroup>
</div>
<div class="flex items-center gap-4">
<div class="w-20 shrink-0">
{{ $t('tools.token-generator.quantity') }}
</div>
<NumberField v-model="quantity" :min="1" :max="100">
<NumberFieldContent class="flex-1">
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
</div>
<div class="p-6">
<Button class="w-full" variant="secondary" @click="reset">
Reset configuration
</Button>
</div>
</div>
<div class="flex-1 p-6 pt-4 bg-white dark:bg-background pb-20">
<div class="text-muted-foreground mb-3">
Generated token
</div>
<Textarea v-model="token" rows="12" class="font-mono" readonly :placeholder="$t('tools.token-generator.placeholder')" />
<div class="flex md:items-center mt-2 gap-2 flex-col md:flex-row">
<Button variant="secondary" @click="refreshToken">
<Icon name="i-tabler-refresh" class="mr-2" />
{{ $t('tools.token-generator.refresh') }}
</Button>
<Button variant="secondary">
<Icon name="i-tabler-copy" class="mr-2" />
Copy to clipboard
</Button>
</div>
</div>
</div>
</div>
</template>

View File

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

View File

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

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

View File

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

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

View File

@@ -1,25 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as Badge } from './Badge.vue';
export const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export type BadgeVariants = VariantProps<typeof badgeVariants>;

View File

@@ -1,26 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import { type ButtonVariants, buttonVariants } from '.';
type Props = {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
} & PrimitiveProps;
const props = withDefaults(defineProps<Props>(), {
as: 'button',
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -1,32 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as Button } from './Button.vue';
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -1,21 +0,0 @@
<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',
props.class,
)
"
>
<slot />
</div>
</template>

View File

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

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

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

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

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

@@ -1,6 +0,0 @@
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';

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)"
>
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>
<Icon name="i-tabler-check" class="h-4 w-4" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>

View File

@@ -1 +0,0 @@
export { default as Checkbox } from './Checkbox.vue';

View File

@@ -1,30 +0,0 @@
<script setup lang="ts">
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
open: true,
modelValue: '',
});
const emits = defineEmits<ComboboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxRoot
v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', props.class)"
>
<slot />
</ComboboxRoot>
</template>

Some files were not shown because too many files have changed in this diff Show More