mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-11-02 13:03:16 +00:00
Compare commits
59 Commits
v2023.8.16
...
v2023.11.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5a67d96d | ||
|
|
8174db9cf3 | ||
|
|
e164afb664 | ||
|
|
d0136962b9 | ||
|
|
015c673e09 | ||
|
|
99b1eb944d | ||
|
|
cc3425dc77 | ||
|
|
681f7bf644 | ||
|
|
f5eb7a8c49 | ||
|
|
abb8335041 | ||
|
|
020e9cbe41 | ||
|
|
02e68d3f56 | ||
|
|
00562ed5e8 | ||
|
|
4365226d01 | ||
|
|
57ecda1623 | ||
|
|
d8d7a3b9ab | ||
|
|
d36b18f193 | ||
|
|
eea9f91276 | ||
|
|
ebb4ec4165 | ||
|
|
84a4a646f6 | ||
|
|
a2b53c2e38 | ||
|
|
35563b8457 | ||
|
|
720201aa7b | ||
|
|
b2ad4f7a27 | ||
|
|
b408df82c1 | ||
|
|
88b881880c | ||
|
|
ee4c853b9f | ||
|
|
cbf58fdd28 | ||
|
|
a757a5155a | ||
|
|
025f556023 | ||
|
|
2d2dffb14a | ||
|
|
5c4d775e2d | ||
|
|
557b30426f | ||
|
|
4972159aa7 | ||
|
|
e371ef702e | ||
|
|
0eedce69a6 | ||
|
|
8a30b6bdb3 | ||
|
|
233d5565f6 | ||
|
|
7ab9204e96 | ||
|
|
c7d4562d3b | ||
|
|
18dd1400bd | ||
|
|
f035f485c0 | ||
|
|
e18bae1fca | ||
|
|
d1dff428d8 | ||
|
|
3a63837d3d | ||
|
|
81bfe57cb8 | ||
|
|
a9cd91ca9c | ||
|
|
06c35472d3 | ||
|
|
f3e14fc18f | ||
|
|
2274766a8f | ||
|
|
a346175d24 | ||
|
|
6f93cba3da | ||
|
|
76b2761d62 | ||
|
|
6ff9a01cc8 | ||
|
|
a2b9b157e5 | ||
|
|
144f86e2dc | ||
|
|
0f1f6590c5 | ||
|
|
2bcb77a9f9 | ||
|
|
c58d6e3423 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
playwright-report
|
||||
coverage
|
||||
dist
|
||||
test-results
|
||||
@@ -10,5 +10,12 @@ module.exports = {
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
'vue/no-empty-component-block': ['error'],
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@vueuse/core',
|
||||
importNames: ['useClipboard'],
|
||||
message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
16
.github/workflows/docker-nightly-release.yml
vendored
16
.github/workflows/docker-nightly-release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
outputs:
|
||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
- name: print latest_commit
|
||||
run: echo ${{ github.sha }}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -54,29 +54,29 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
17
.github/workflows/e2e-tests.yml
vendored
17
.github/workflows/e2e-tests.yml
vendored
@@ -1,18 +1,18 @@
|
||||
name: E2E tests
|
||||
on: [deployment_status]
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.deployment_status.state == 'success'
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BASE_URL: ${{ github.event.deployment_status.target_url }}
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1/3, 2/3, 3/3]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- run: corepack enable
|
||||
|
||||
@@ -28,6 +28,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build app
|
||||
run: pnpm build
|
||||
|
||||
- name: Restore Playwright browsers from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
|
||||
14
.github/workflows/releases.yml
vendored
14
.github/workflows/releases.yml
vendored
@@ -13,29 +13,29 @@ jobs:
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- run: corepack enable
|
||||
|
||||
|
||||
75
CHANGELOG.md
75
CHANGELOG.md
@@ -2,6 +2,81 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## Version 2023.11.01-e164afb
|
||||
|
||||
### Features
|
||||
- **command-palette**: clear prompt on palette close (#708) (d013696)
|
||||
- **command-palette**: added about page in command palette (99b1eb9)
|
||||
- **new tool**: random MAC address generator (#657) (cc3425d)
|
||||
- **case-converter**: added mocking case (#705) (681f7bf)
|
||||
- **date-converter**: added excel date time format (#704) (f5eb7a8)
|
||||
- **i18n**: token generator (#688) (02e68d3)
|
||||
- **i18n**: home page (#687) (00562ed)
|
||||
- **i18n**: support for i18n in .ts files (#683) (ebb4ec4)
|
||||
- **i18n**: tool card (#682) (84a4a64)
|
||||
- **i18n**: about page (#680) (a2b53c2)
|
||||
- **i18n**: 404 page (#679) (35563b8)
|
||||
- **new tool**: text to ascii converter (#669) (b2ad4f7)
|
||||
- **new tool**: ULID generator (#623) (5c4d775)
|
||||
- **new tool**: add wifi qr code generator (#599) (0eedce6)
|
||||
- **new tool**: iban validation and parser (#591) (3a63837)
|
||||
- **new tool**: text diff and comparator (#588) (81bfe57)
|
||||
|
||||
### Bug fixes
|
||||
- **deps**: fix issue on slugify (#593) (#673) (720201a)
|
||||
- **deps**: update dependency monaco-editor to ^0.43.0 (#620) (e371ef7)
|
||||
- **deps**: update dependency sql-formatter to v13 (#606) (c7d4562)
|
||||
|
||||
### Refactoring
|
||||
- **ui**: better ui demo preview menu (#664) (015c673)
|
||||
- **color-converter**: improved color-converter UX (#701) (abb8335)
|
||||
- **docker**: improved docker config (#700) (020e9cb)
|
||||
- **c-table**: added description on c-table for accessibility (b408df8)
|
||||
- **ci**: reduced timeout in e2e (#666) (88b8818)
|
||||
- **ui**: new c-table ui component (#665) (ee4c853)
|
||||
- **ui**: removed n-page-header component in user-agent parser (#663) (cbf58fd)
|
||||
- **ui**: removed n-p components in about page (#662) (a757a51)
|
||||
- **ui**: switched naive tooltip components to custom ones (#661) (025f556)
|
||||
- **spelling**: minor corrections to phrasing/spelling (#596) (8a30b6b)
|
||||
- **i18n**: merge tools scoped locales with global ones (#612) (233d556)
|
||||
- **c-key-value-list**: got rid of table for layout (#611) (7ab9204)
|
||||
- **CI**: run e2e against built app and no longer vercel (#610) (18dd140)
|
||||
- **bcrypt**: fix typo (#604) (e18bae1)
|
||||
|
||||
### Chores
|
||||
- **deps**: clean unused dependencies (#709) (e164afb)
|
||||
- **deps**: update docker/setup-qemu-action action to v3 (#627) (4365226)
|
||||
- **deps**: update docker/setup-buildx-action action to v3 (#626) (57ecda1)
|
||||
- **deps**: update docker/login-action action to v3 (#625) (d8d7a3b)
|
||||
- **deps**: update docker/build-push-action action to v5 (#624) (d36b18f)
|
||||
- **deps**: update dependency node to v18.18.2 (#674) (eea9f91)
|
||||
- **deps**: update dependency node to v18.18.0 (#636) (2d2dffb)
|
||||
- **deps**: update actions/checkout action to v4 (#613) (4972159)
|
||||
- **deps**: update dependency unplugin-icons to ^0.17.0 (#609) (f035f48)
|
||||
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.13.0 (#597) (d1dff42)
|
||||
- **deps**: update dependency @antfu/eslint-config to ^0.41.0 (#585) (a9cd91c)
|
||||
- **deps**: update dependency typescript to ~5.2.0 (#587) (f3e14fc)
|
||||
|
||||
### Doc
|
||||
- **readme**: added contributors list (#622) (557b304)
|
||||
- **hosting**: added cloudron in the other hosting solutions section (#589) (06c3547)
|
||||
|
||||
## Version 2023.08.21-6f93cba
|
||||
|
||||
### Features
|
||||
- **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba)
|
||||
- **new tool**: string obfuscator (#575) (c58d6e3)
|
||||
|
||||
### Bug fixes
|
||||
- **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a)
|
||||
|
||||
### Chores
|
||||
- **deps**: switched to fucking typescript v5 (#501) (76b2761)
|
||||
- **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01)
|
||||
- **deps**: update dependency prettier to v3 (#564) (a2b9b15)
|
||||
- **deps**: removed @typescript-eslint/parser (#563) (144f86e)
|
||||
- **deps**: removed ts-pattern (#565) (0f1f659)
|
||||
|
||||
## Version 2023.08.16-9bd4ad4
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# build stage
|
||||
FROM node:lts-alpine AS build-stage
|
||||
# Set environment variables for non-interactive npm installs
|
||||
ENV NPM_CONFIG_LOGLEVEL warn
|
||||
ENV CI true
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm && pnpm i --frozen-lockfile
|
||||
COPY . .
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm i --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
# production stage
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
FROM nginxinc/nginx-unprivileged:stable-alpine AS production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
|
||||
19
README.md
19
README.md
@@ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
||||
|
||||
**Other solutions:**
|
||||
|
||||
- [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html)
|
||||
- [Tipi](https://www.runtipi.io/docs/apps-available)
|
||||
- [Unraid](https://unraid.net/community/apps?q=it-tools)
|
||||
|
||||
@@ -34,23 +35,21 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
||||
### Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) with the following extensions:
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
|
||||
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
|
||||
- [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally)
|
||||
|
||||
with the following settings:
|
||||
|
||||
```json5
|
||||
```json
|
||||
{
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales",
|
||||
"src/tools/*/locales"
|
||||
],
|
||||
"i18n-ally.localesPaths": ["locales", "src/tools/*/locales"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
```
|
||||
@@ -106,12 +105,20 @@ pnpm run script:create-new-tool my-tool-name
|
||||
|
||||
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool.
|
||||
|
||||
## Contributors
|
||||
|
||||
Big thanks to all the people who have already contributed!
|
||||
|
||||
[](https://github.com/corentinth/it-tools/graphs/contributors)
|
||||
|
||||
## Credits
|
||||
|
||||
Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
|
||||
|
||||
This project is continuously deployed using [vercel.com](https://vercel.com).
|
||||
|
||||
Contributor graph is generated using [contrib.rocks](https://contrib.rocks/preview?repo=corentinth/it-tools).
|
||||
|
||||
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
|
||||
6
_templates/generator/ui-component/component.demo.ejs.t
Normal file
6
_templates/generator/ui-component/component.demo.ejs.t
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.demo.vue
|
||||
---
|
||||
<template>
|
||||
<<%= h.changeCase.param(name) %> />
|
||||
</template>
|
||||
13
_templates/generator/ui-component/component.ejs.t
Normal file
13
_templates/generator/ui-component/component.ejs.t
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.vue
|
||||
---
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{ prop?: string }>(), { prop: '' });
|
||||
const { prop } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ prop }}
|
||||
</div>
|
||||
</template>
|
||||
32
components.d.ts
vendored
32
components.d.ts
vendored
@@ -25,12 +25,17 @@ declare module '@vue/runtime-core' {
|
||||
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
|
||||
CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
|
||||
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
|
||||
CButtonsSelect: typeof import('./src/ui/c-buttons-select/c-buttons-select.vue')['default']
|
||||
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
|
||||
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
||||
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
||||
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
||||
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
||||
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
|
||||
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
|
||||
CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default']
|
||||
CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default']
|
||||
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
|
||||
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
|
||||
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
|
||||
@@ -44,8 +49,13 @@ declare module '@vue/runtime-core' {
|
||||
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
|
||||
CSelect: typeof import('./src/ui/c-select/c-select.vue')['default']
|
||||
'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
|
||||
CTable: typeof import('./src/ui/c-table/c-table.vue')['default']
|
||||
'CTable.demo': typeof import('./src/ui/c-table/c-table.demo.vue')['default']
|
||||
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
|
||||
'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default']
|
||||
CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default']
|
||||
'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default']
|
||||
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
|
||||
'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default']
|
||||
'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default']
|
||||
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
|
||||
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
|
||||
@@ -68,12 +78,12 @@ declare module '@vue/runtime-core' {
|
||||
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
|
||||
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
|
||||
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
|
||||
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
|
||||
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
|
||||
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
||||
IconMdiCameraOutline: typeof import('~icons/mdi/camera-outline')['default']
|
||||
IconMdiCameraVideoOff: typeof import('~icons/mdi/camera-video-off')['default']
|
||||
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
||||
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||
IconMdiClose: typeof import('~icons/mdi/close')['default']
|
||||
@@ -82,14 +92,12 @@ declare module '@vue/runtime-core' {
|
||||
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||
IconMdiMagnify: typeof import('~icons/mdi/magnify')['default']
|
||||
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
|
||||
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
||||
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||
IconMdiRecord: typeof import('~icons/mdi/record')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||
IconMdiSearchRound: typeof import('~icons/mdi/search-round')['default']
|
||||
IconMdiTea: typeof import('~icons/mdi/tea')['default']
|
||||
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||
@@ -107,6 +115,7 @@ declare module '@vue/runtime-core' {
|
||||
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
|
||||
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
|
||||
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
|
||||
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
|
||||
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
|
||||
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
|
||||
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
|
||||
@@ -135,25 +144,19 @@ declare module '@vue/runtime-core' {
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTable: typeof import('naive-ui')['NTable']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||
@@ -170,10 +173,13 @@ declare module '@vue/runtime-core' {
|
||||
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
|
||||
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
|
||||
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
|
||||
StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
|
||||
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
|
||||
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
|
||||
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
||||
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
|
||||
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
|
||||
TextToBinary: typeof import('./src/tools/text-to-binary/text-to-binary.vue')['default']
|
||||
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
|
||||
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
|
||||
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
|
||||
@@ -181,11 +187,13 @@ declare module '@vue/runtime-core' {
|
||||
TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default']
|
||||
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
|
||||
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
|
||||
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
|
||||
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
|
||||
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
|
||||
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
|
||||
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
|
||||
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
|
||||
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
|
||||
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
|
||||
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
|
||||
YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
|
||||
|
||||
@@ -1,4 +1,51 @@
|
||||
home:
|
||||
categories:
|
||||
newestTools: Newest tools
|
||||
|
||||
favoriteTools: 'Your favorite tools'
|
||||
allTools: 'All the tools'
|
||||
subtitle: 'Handy tools for developers'
|
||||
toggleMenu: 'Toggle menu'
|
||||
home: Home
|
||||
uiLib: 'UI Lib'
|
||||
buyMeACoffee: 'Buy me a coffee'
|
||||
follow:
|
||||
title: 'You like it-tools?'
|
||||
p1: 'Give us a star on'
|
||||
githubRepository: 'IT-Tools GitHub repository'
|
||||
p2: 'or follow us on'
|
||||
twitterAccount: 'IT-Tools Twitter account'
|
||||
thankYou: 'Thank you !'
|
||||
nav:
|
||||
github: 'GitHub repository'
|
||||
githubRepository: 'IT-Tools GitHub repository'
|
||||
twitter: 'Twitter account'
|
||||
twitterAccount: 'IT Tools Twitter account'
|
||||
about: 'About IT-Tools'
|
||||
aboutLabel: 'About'
|
||||
darkMode: 'Dark mode'
|
||||
lightMode: 'Light mode'
|
||||
mode: 'Toggle dark/light mode'
|
||||
about:
|
||||
h1: 'About IT-Tools'
|
||||
h1p1: 'This wonderful website, made with ❤ by'
|
||||
h1p2: ', aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don''''t forget to bookmark it in your shortcut bar!'
|
||||
h1p3: 'IT Tools is open-source (under the MIT license) and free, and will always be, but it costs me money to host and renew the domain name. If you want to support my work, and encourage me to add more tools, please consider supporting by'
|
||||
h1p4: 'sponsoring me'
|
||||
h2: Technologies
|
||||
h2p1: 'IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the'
|
||||
h2p2: 'file of the repository.'
|
||||
h3: 'Found a bug? A tool is missing?'
|
||||
h3p1: 'If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a feature request in the'
|
||||
h3p2: 'issues section'
|
||||
h3p3: 'in the GitHub repository.'
|
||||
h3p4: 'And if you found a bug, or something doesn''''t work as expected, please file a bug report in the'
|
||||
h3p5: 'issues section'
|
||||
h3p6: 'in the GitHub repository.'
|
||||
404:
|
||||
notFound: '404 Not Found'
|
||||
sorry: 'Sorry, this page does not seem to exist'
|
||||
maybe: 'Maybe the cache is doing tricky things, try force-refreshing?'
|
||||
backHome: 'Back home'
|
||||
toolCard:
|
||||
new: New
|
||||
|
||||
57
package.json
57
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "it-tools",
|
||||
"version": "2023.8.16-9bd4ad4",
|
||||
"version": "2023.11.1-e164afb",
|
||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||
"keywords": [
|
||||
"productivity",
|
||||
@@ -21,27 +21,29 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build",
|
||||
"preview": "vite preview --port 5050",
|
||||
"test": "npm run test:unit",
|
||||
"test:unit": "vitest --environment jsdom",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
|
||||
"script:create-new-tool": "node scripts/create-tool.mjs",
|
||||
"script:create:tool": "node scripts/create-tool.mjs",
|
||||
"script:create:ui": "hygen generator ui-component",
|
||||
"release": "node ./scripts/release.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@it-tools/bip39": "^0.0.4",
|
||||
"@it-tools/oggen": "^1.3.0",
|
||||
"@sindresorhus/slugify": "^2.2.0",
|
||||
"@tiptap/pm": "^2.0.3",
|
||||
"@tiptap/starter-kit": "^2.0.3",
|
||||
"@tiptap/vue-3": "^2.0.3",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@tiptap/pm": "2.1.6",
|
||||
"@tiptap/starter-kit": "2.1.6",
|
||||
"@tiptap/vue-3": "2.0.3",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vicons/tabler": "^0.12.0",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/head": "^1.0.0",
|
||||
"@vueuse/router": "^10.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -58,23 +60,24 @@
|
||||
"fuse.js": "^6.6.2",
|
||||
"highlight.js": "^11.7.0",
|
||||
"iarna-toml-esm": "^3.0.5",
|
||||
"ibantools": "^4.3.3",
|
||||
"json5": "^2.2.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
"lodash": "^4.17.21",
|
||||
"mathjs": "^11.0.0",
|
||||
"mathjs": "^11.9.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"naive-ui": "^2.34.3",
|
||||
"monaco-editor": "^0.43.0",
|
||||
"naive-ui": "^2.35.0",
|
||||
"netmask": "^2.0.2",
|
||||
"node-forge": "^1.3.1",
|
||||
"oui": "^12.0.52",
|
||||
"pinia": "^2.0.34",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"qrcode": "^1.5.1",
|
||||
"randombytes": "^2.1.0",
|
||||
"sql-formatter": "^8.2.0",
|
||||
"ts-pattern": "^4.2.2",
|
||||
"sql-formatter": "^13.0.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"ulid": "^2.3.0",
|
||||
"unicode-emoji-json": "^0.4.0",
|
||||
"unplugin-auto-import": "^0.16.4",
|
||||
"uuid": "^9.0.0",
|
||||
@@ -86,44 +89,42 @@
|
||||
"yaml": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.39.3",
|
||||
"@antfu/eslint-config": "^0.41.0",
|
||||
"@iconify-json/mdi": "^1.1.50",
|
||||
"@intlify/unplugin-vue-i18n": "^0.12.0",
|
||||
"@intlify/unplugin-vue-i18n": "^0.13.0",
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/jsdom": "^21.0.0",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/netmask": "^2.0.0",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node-forge": "^1.3.2",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/randombytes": "^2.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@unocss/eslint-config": "^0.55.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@vitejs/plugin-vue": "^4.3.2",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"@vue/compiler-sfc": "^3.2.47",
|
||||
"@vue/runtime-dom": "^3.3.4",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"c8": "^8.0.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"consola": "^3.0.2",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint": "^8.47.0",
|
||||
"hygen": "^6.2.11",
|
||||
"jsdom": "^22.0.0",
|
||||
"less": "^4.1.3",
|
||||
"prettier": "^2.8.7",
|
||||
"typescript": "~4.9.0",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "~5.2.0",
|
||||
"unocss": "^0.55.0",
|
||||
"unocss-preset-scrollbar": "^0.2.1",
|
||||
"unplugin-icons": "^0.16.1",
|
||||
"unplugin-icons": "^0.17.0",
|
||||
"unplugin-vue-components": "^0.25.0",
|
||||
"vite": "^4.0.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-pwa": "^0.16.0",
|
||||
"vite-plugin-vue-markdown": "^0.23.5",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const isCI = !!process.env.CI;
|
||||
const baseUrl = process.env.BASE_URL || 'http://localhost:5050';
|
||||
const useWebServer = process.env.NO_WEB_SERVER !== 'true';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
@@ -52,13 +53,13 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
|
||||
...(isCI
|
||||
? {}
|
||||
: {
|
||||
webServer: {
|
||||
command: 'npm run preview',
|
||||
url: 'http://127.0.0.1:5050',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
}),
|
||||
...(useWebServer
|
||||
&& {
|
||||
webServer: {
|
||||
command: 'npm run preview',
|
||||
url: 'http://127.0.0.1:5050',
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
3938
pnpm-lock.yaml
generated
3938
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { FavoriteFilled } from '@vicons/material';
|
||||
|
||||
import { useToolStore } from '@/tools/tools.store';
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
|
||||
@@ -26,18 +24,15 @@ function toggleFavorite(event: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button
|
||||
variant="text"
|
||||
circle
|
||||
:type="buttonType"
|
||||
:style="{ opacity: isFavorite ? 1 : 0.2 }"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
<n-icon :component="FavoriteFilled" />
|
||||
</c-button>
|
||||
</template>
|
||||
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
|
||||
</n-tooltip>
|
||||
<c-tooltip :tooltip="isFavorite ? 'Remove from favorites' : 'Add to favorites' ">
|
||||
<c-button
|
||||
variant="text"
|
||||
circle
|
||||
:type="buttonType"
|
||||
:style="{ opacity: isFavorite ? 1 : 0.2 }"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
<icon-mdi-heart />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard, useVModel } from '@vueuse/core';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = defineProps<{ value: string }>();
|
||||
const emit = defineEmits(['update:value']);
|
||||
|
||||
const value = useVModel(props, 'value', emit);
|
||||
const tooltipText = ref('Copy to clipboard');
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
|
||||
function onCopyClicked() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied!';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltipText.value = 'Copy to clipboard';
|
||||
}, 2000);
|
||||
}
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-input-text v-model:value="value">
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" size="small" @click="onCopyClicked">
|
||||
<icon-mdi-content-copy />
|
||||
</c-button>
|
||||
</template>
|
||||
{{ tooltipText }}
|
||||
</n-tooltip>
|
||||
<c-tooltip :tooltip="tooltipText">
|
||||
<c-button circle variant="text" size="small" @click="copy()">
|
||||
<icon-mdi-content-copy />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
</c-input-text>
|
||||
</template>
|
||||
|
||||
@@ -7,56 +7,43 @@ const { isDarkTheme } = toRefs(styleStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="IT-Tools' GitHub repository"
|
||||
>
|
||||
<n-icon size="25" :component="BrandGithub" />
|
||||
</c-button>
|
||||
</template>
|
||||
Github repository
|
||||
</n-tooltip>
|
||||
<c-tooltip :tooltip="$t('home.nav.github')" position="bottom">
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="$t('home.nav.githubRepository')"
|
||||
>
|
||||
<n-icon size="25" :component="BrandGithub" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT Tools' Twitter account"
|
||||
>
|
||||
<n-icon size="25" :component="BrandTwitter" />
|
||||
</c-button>
|
||||
</template>
|
||||
IT Tools' Twitter account
|
||||
</n-tooltip>
|
||||
<c-tooltip :tooltip="$t('home.nav.twitter')" position="bottom">
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
:aria-label="$t('home.nav.twitterAccount')"
|
||||
>
|
||||
<n-icon size="25" :component="BrandTwitter" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" to="/about" aria-label="About">
|
||||
<n-icon size="25" :component="InfoCircle" />
|
||||
</c-button>
|
||||
</template>
|
||||
About
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" aria-label="Toggle dark/light mode" @click="() => styleStore.toggleDark()">
|
||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||
<n-icon v-else size="25" :component="Moon" />
|
||||
</c-button>
|
||||
</template>
|
||||
<span v-if="isDarkTheme">Light mode</span>
|
||||
<span v-else>Dark mode</span>
|
||||
</n-tooltip>
|
||||
<c-tooltip :tooltip="$t('home.nav.about')" position="bottom">
|
||||
<c-button circle variant="text" to="/about" :aria-label="$t('home.nav.aboutLabel')">
|
||||
<n-icon size="25" :component="InfoCircle" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
<c-tooltip :tooltip="isDarkTheme ? $t('home.nav.lightMode') : $t('home.nav.darkMode')" position="bottom">
|
||||
<c-button circle variant="text" :aria-label="$t('home.nav.mode')" @click="() => styleStore.toggleDark()">
|
||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||
<n-icon v-else size="25" :component="Moon" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
|
||||
const { value } = toRefs(props);
|
||||
|
||||
const initialText = 'Copy to clipboard';
|
||||
const tooltipText = ref(initialText);
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
|
||||
function handleClick() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied!';
|
||||
|
||||
setTimeout(() => (tooltipText.value = initialText), 1000);
|
||||
}
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<span class="value" @click="handleClick">{{ value }}</span>
|
||||
</template>
|
||||
{{ tooltipText }}
|
||||
</n-tooltip>
|
||||
<c-tooltip :tooltip="tooltipText">
|
||||
<span cursor-pointer font-mono @click="copy()">{{ value }}</span>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.value {
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Copy } from '@vicons/tabler';
|
||||
import { useClipboard, useElementSize } from '@vueuse/core';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import jsonHljs from 'highlight.js/lib/languages/json';
|
||||
import sqlHljs from 'highlight.js/lib/languages/sql';
|
||||
import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
||||
import iniHljs from 'highlight.js/lib/languages/ini';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -33,22 +34,13 @@ hljs.registerLanguage('toml', iniHljs);
|
||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
const tooltipText = ref(copyMessage.value);
|
||||
|
||||
function onCopyClicked() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied !';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltipText.value = copyMessage.value;
|
||||
}, 2000);
|
||||
}
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="overflow-x: hidden; width: 100%">
|
||||
<c-card class="result-card">
|
||||
<c-card relative>
|
||||
<n-scrollbar
|
||||
x-scrollable
|
||||
trigger="none"
|
||||
@@ -58,19 +50,16 @@ function onCopyClicked() {
|
||||
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
||||
</n-config-provider>
|
||||
</n-scrollbar>
|
||||
<n-tooltip v-if="value" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="copy-button" :class="[copyPlacement]">
|
||||
<c-button circle important:h-10 important:w-10 @click="onCopyClicked">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</c-button>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ tooltipText }}</span>
|
||||
</n-tooltip>
|
||||
<div absolute right-10px top-10px>
|
||||
<c-tooltip v-if="value" :tooltip="tooltipText" position="left">
|
||||
<c-button circle important:h-10 important:w-10 @click="copy()">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
</c-card>
|
||||
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
|
||||
<c-button @click="onCopyClicked">
|
||||
<c-button @click="copy()">
|
||||
{{ tooltipText }}
|
||||
</c-button>
|
||||
</div>
|
||||
@@ -82,25 +71,4 @@ function onCopyClicked() {
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
.result-card {
|
||||
position: relative;
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
|
||||
&.top-right {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
&.bottom-right {
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
&.outside,
|
||||
&.none {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,7 @@ const appTheme = useAppTheme();
|
||||
:bordered="false"
|
||||
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
|
||||
>
|
||||
New
|
||||
{{ $t('toolCard.new') }}
|
||||
</n-tag>
|
||||
|
||||
<FavoriteButton :tool="tool" />
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { type MaybeRef, get, useClipboard } from '@vueuse/core';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import type { MaybeRefOrGetter } from 'vue';
|
||||
|
||||
export function useCopy({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<string>; text?: string; createToast?: boolean } = {}) {
|
||||
const { copy, copied, ...rest } = useClipboard({
|
||||
source,
|
||||
legacy: true,
|
||||
});
|
||||
|
||||
export function useCopy({ source, text = 'Copied to the clipboard' }: { source?: MaybeRef<unknown>; text?: string } = {}) {
|
||||
const { copy } = useClipboard(source ? { source: computed(() => String(get(source))) } : {});
|
||||
const message = useMessage();
|
||||
|
||||
return {
|
||||
...rest,
|
||||
isJustCopied: copied,
|
||||
async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) {
|
||||
if (source) {
|
||||
await copy();
|
||||
@@ -14,7 +22,9 @@ export function useCopy({ source, text = 'Copied to the clipboard' }: { source?:
|
||||
await copy(content);
|
||||
}
|
||||
|
||||
message.success(notificationMessage ?? text);
|
||||
if (createToast) {
|
||||
message.success(notificationMessage ?? text);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const tools = computed<ToolCategory[]>(() => [
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="subtitle">
|
||||
Handy tools for developers
|
||||
{{ $t('home.subtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
@@ -88,24 +88,23 @@ const tools = computed<ToolCategory[]>(() => [
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
aria-label="Toggle menu"
|
||||
:aria-label="$t('home.toggleMenu')"
|
||||
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
||||
>
|
||||
<NIcon size="25" :component="Menu2" />
|
||||
</c-button>
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button to="/" circle variant="text" aria-label="Home">
|
||||
<NIcon size="25" :component="Home2" />
|
||||
</c-button>
|
||||
</template>
|
||||
Home
|
||||
</n-tooltip>
|
||||
<c-tooltip tooltip="Home" position="bottom">
|
||||
<c-button to="/" circle variant="text" :aria-label="$t('home.home')">
|
||||
<NIcon size="25" :component="Home2" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" aria-label="UI Lib">
|
||||
<icon-mdi:brush-variant text-20px />
|
||||
</c-button>
|
||||
<c-tooltip tooltip="UI Lib" position="bottom">
|
||||
<c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" :aria-label="$t('home.uiLib')">
|
||||
<icon-mdi:brush-variant text-20px />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<command-palette />
|
||||
|
||||
@@ -113,23 +112,20 @@ const tools = computed<ToolCategory[]>(() => [
|
||||
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
||||
</div>
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button
|
||||
round
|
||||
href="https://www.buymeacoffee.com/cthmsst"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
class="support-button"
|
||||
:bordered="false"
|
||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||
>
|
||||
Buy me a coffee
|
||||
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
||||
</c-button>
|
||||
</template>
|
||||
❤ Support IT Tools development !
|
||||
</n-tooltip>
|
||||
<c-tooltip position="bottom" tooltip="Support IT Tools development">
|
||||
<c-button
|
||||
round
|
||||
href="https://www.buymeacoffee.com/cthmsst"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
class="support-button"
|
||||
:bordered="false"
|
||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||
>
|
||||
{{ $t('home.buyMeACoffee') }}
|
||||
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,7 @@ import SunIcon from '~icons/mdi/white-balance-sunny';
|
||||
import GithubIcon from '~icons/mdi/github';
|
||||
import BugIcon from '~icons/mdi/bug-outline';
|
||||
import DiceIcon from '~icons/mdi/dice-5';
|
||||
import InfoIcon from '~icons/mdi/information-outline';
|
||||
|
||||
export const useCommandPaletteStore = defineStore('command-palette', () => {
|
||||
const toolStore = useToolStore();
|
||||
@@ -61,6 +62,14 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
|
||||
keywords: ['report', 'issue', 'bug', 'problem', 'error'],
|
||||
icon: BugIcon,
|
||||
},
|
||||
{
|
||||
name: 'About',
|
||||
description: 'Learn more about IT-Tools.',
|
||||
to: '/about',
|
||||
category: 'Pages',
|
||||
keywords: ['about', 'learn', 'more', 'info', 'information'],
|
||||
icon: InfoIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const { searchResult } = useFuzzySearch({
|
||||
|
||||
@@ -37,6 +37,7 @@ function open() {
|
||||
|
||||
function close() {
|
||||
isModalOpen.value = false;
|
||||
searchPrompt.value = '';
|
||||
}
|
||||
|
||||
const selectedOptionIndex = ref(0);
|
||||
|
||||
@@ -11,17 +11,17 @@ useHead({ title: 'Page not found - IT Tools' });
|
||||
</span>
|
||||
|
||||
<h1 m-0 mt-3>
|
||||
404 Not Found
|
||||
{{ $t('404.notFound') }}
|
||||
</h1>
|
||||
<div mt-4 op-60>
|
||||
Sorry, this page does not seem to exist
|
||||
{{ $t('404.sorry') }}
|
||||
</div>
|
||||
<div mb-8 op-60>
|
||||
Maybe the cache is doing tricky things, try force-refreshing?
|
||||
{{ $t('404.maybe') }}
|
||||
</div>
|
||||
|
||||
<c-button to="/">
|
||||
Back home
|
||||
{{ $t('404.backHome') }}
|
||||
</c-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,79 +7,57 @@ const { tracker } = useTracker();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="about-page">
|
||||
<n-h1>About</n-h1>
|
||||
<n-p>
|
||||
This wonderful website, made with ❤ by
|
||||
<div mx-auto mt-50px max-w-600px>
|
||||
<h1>{{ $t('about.h1') }}</h1>
|
||||
<p text-justify>
|
||||
{{ $t('about.h1p1') }}
|
||||
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener">
|
||||
Corentin Thomasset
|
||||
</c-link>,
|
||||
aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share
|
||||
it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar!
|
||||
</n-p>
|
||||
<n-p>
|
||||
IT Tools is open-source (under the MIT license) and free, and will always be, but it costs me money to host and
|
||||
renew the domain name. If you want to support my work, and encourage me to add more tools, please consider
|
||||
supporting by
|
||||
</c-link>{{ $t('about.h1p2') }}
|
||||
</p>
|
||||
<p text-justify>
|
||||
{{ $t('about.h1p3') }}
|
||||
<c-link
|
||||
href="https://www.buymeacoffee.com/cthmsst"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||
>
|
||||
sponsoring me
|
||||
{{ $t('about.h1p4') }}
|
||||
</c-link>.
|
||||
</n-p>
|
||||
</p>
|
||||
|
||||
<n-h2>Technologies</n-h2>
|
||||
<n-p>
|
||||
IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed
|
||||
by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the
|
||||
<h2>{{ $t('about.h2') }}</h2>
|
||||
<p text-justify>
|
||||
{{ $t('about.h2p1') }}
|
||||
<c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank">
|
||||
package.json
|
||||
</c-link>
|
||||
file of the repository.
|
||||
</n-p>
|
||||
{{ $t('about.h2p2') }}
|
||||
</p>
|
||||
|
||||
<n-h2>Found a bug? A tool is missing?</n-h2>
|
||||
<n-p>
|
||||
If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a
|
||||
feature request in the
|
||||
<h2>{{ $t('about.h3') }}</h2>
|
||||
<p text-justify>
|
||||
{{ $t('about.h3p1') }}
|
||||
<c-link
|
||||
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
issues section
|
||||
{{ $t('about.h3p2') }}
|
||||
</c-link>
|
||||
in the GitHub repository.
|
||||
</n-p>
|
||||
<n-p>
|
||||
And if you found a bug, or something doesn't work as expected, please file a bug report in the
|
||||
{{ $t('about.h3p3') }}
|
||||
</p>
|
||||
<p text-justify>
|
||||
{{ $t('about.h3p4') }}
|
||||
<c-link
|
||||
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
issues section
|
||||
{{ $t('about.h3p5') }}
|
||||
</c-link>
|
||||
in the GitHub repository.
|
||||
</n-p>
|
||||
{{ $t('about.h3p6') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.about-page {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
.n-h2 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.n-p {
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,21 +17,21 @@ const { t } = useI18n();
|
||||
<div class="grid-wrapper">
|
||||
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi>
|
||||
<ColoredCard title="You like it-tools?" :icon="Heart">
|
||||
Give us a star on
|
||||
<ColoredCard :title="$t('home.follow.title')" :icon="Heart">
|
||||
{{ $t('home.follow.p1') }}
|
||||
<a
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' GitHub repository"
|
||||
:aria-label="$t('home.follow.githubRepository')"
|
||||
>GitHub</a>
|
||||
or follow us on
|
||||
{{ $t('home.follow.p2') }}
|
||||
<a
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' Twitter account"
|
||||
>Twitter</a>! Thank you
|
||||
:aria-label="$t('home.follow.twitterAccount')"
|
||||
>Twitter</a>{{ $t('home.follow.thankYou') }}
|
||||
<n-icon :component="Heart" />
|
||||
</ColoredCard>
|
||||
</n-gi>
|
||||
@@ -39,7 +39,7 @@ const { t } = useI18n();
|
||||
|
||||
<transition name="height">
|
||||
<div v-if="toolStore.favoriteTools.length > 0">
|
||||
<n-h3>Your favorite tools</n-h3>
|
||||
<n-h3>{{ $t('home.categories.favoriteTools') }}</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
||||
<ToolCard :tool="tool" />
|
||||
@@ -49,7 +49,7 @@ const { t } = useI18n();
|
||||
</transition>
|
||||
|
||||
<div v-if="toolStore.newTools.length > 0">
|
||||
<n-h3>{{ t('home.categories.newestTools', 'Newest tools') }}</n-h3>
|
||||
<n-h3>{{ t('home.categories.newestTools') }}</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
|
||||
<ToolCard :tool="tool" />
|
||||
@@ -57,7 +57,7 @@ const { t } = useI18n();
|
||||
</n-grid>
|
||||
</div>
|
||||
|
||||
<n-h3>All the tools</n-h3>
|
||||
<n-h3>{{ $t('home.categories.allTools') }}</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
|
||||
<transition>
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import type { App } from 'vue';
|
||||
import type { Plugin } from 'vue';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
||||
import baseMessages from '@intlify/unplugin-vue-i18n/messages';
|
||||
import _ from 'lodash';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' });
|
||||
|
||||
const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => {
|
||||
const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? [];
|
||||
const content = parseYaml(await fileDescriptor());
|
||||
|
||||
return { [locale]: content };
|
||||
}));
|
||||
|
||||
const messages = _.merge(
|
||||
baseMessages,
|
||||
_.merge({}, ...messagesByTools),
|
||||
);
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -8,8 +24,14 @@ const i18n = createI18n({
|
||||
messages,
|
||||
});
|
||||
|
||||
export const i18nPlugin = {
|
||||
install: (app: App) => {
|
||||
export const i18nPlugin: Plugin = {
|
||||
install: (app) => {
|
||||
app.use(i18n);
|
||||
},
|
||||
};
|
||||
|
||||
export const translate = function (localeKey: string) {
|
||||
// @ts-expect-error global
|
||||
const hasKey = i18n.global.te(localeKey, i18n.global.locale);
|
||||
return hasKey ? i18n.global.t(localeKey) : localeKey;
|
||||
};
|
||||
|
||||
28
src/shims.d.ts
vendored
28
src/shims.d.ts
vendored
@@ -1,21 +1,35 @@
|
||||
declare module '*.vue' {
|
||||
import type { ComponentOptions, ComponentOptions } from 'vue';
|
||||
import type { ComponentOptions } from 'vue';
|
||||
const Component: ComponentOptions;
|
||||
export default Component;
|
||||
}
|
||||
|
||||
declare module '*.md' {
|
||||
import type { ComponentOptions } from 'vue';
|
||||
const Component: ComponentOptions;
|
||||
export default Component;
|
||||
}
|
||||
|
||||
declare module '~icons/*' {
|
||||
import { FunctionalComponent, SVGAttributes } from 'vue';
|
||||
const component: FunctionalComponent<SVGAttributes>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module 'iarna-toml-esm' {
|
||||
export const parse: (toml: string) => any;
|
||||
export const stringify: (obj: any) => string;
|
||||
}
|
||||
|
||||
declare module 'emojilib' {
|
||||
const lib: Record<string, string[]>;
|
||||
export default lib;
|
||||
}
|
||||
|
||||
declare module 'unicode-emoji-json' {
|
||||
const emoji: Record<string, {
|
||||
name: string;
|
||||
slug: string;
|
||||
group: string;
|
||||
emoji_version: string;
|
||||
unicode_version: string;
|
||||
skin_tone_support: boolean;
|
||||
skin_tone_support_unicode_version: string;
|
||||
}>;
|
||||
|
||||
export default emoji;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash
|
||||
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
|
||||
</n-form-item>
|
||||
<n-form-item label="Your hash: " label-placement="left">
|
||||
<c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text />
|
||||
<c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text />
|
||||
</n-form-item>
|
||||
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
||||
<div class="compare-result" :class="{ positive: compareMatch }">
|
||||
|
||||
@@ -18,7 +18,7 @@ function computeVariance({ data }: { data: number[] }) {
|
||||
return computeAverage({ data: squaredDiffs });
|
||||
}
|
||||
|
||||
function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) {
|
||||
function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Plus, Trash } from '@vicons/tabler';
|
||||
import { useClipboard, useStorage } from '@vueuse/core';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
|
||||
import DynamicValues from './dynamic-values.vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const suites = useStorage('benchmark-builder:suites', [
|
||||
{ title: 'Suite 1', data: [5, 10] },
|
||||
@@ -47,14 +48,14 @@ const results = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const { copy } = useCopy({ createToast: false });
|
||||
|
||||
const header = {
|
||||
position: 'Position',
|
||||
title: 'Suite',
|
||||
size: 'Samples',
|
||||
mean: 'Mean',
|
||||
variance: 'Variance',
|
||||
position: 'Position',
|
||||
};
|
||||
|
||||
function copyAsMarkdown() {
|
||||
@@ -130,26 +131,8 @@ function copyAsBulletList() {
|
||||
</c-button>
|
||||
</div>
|
||||
|
||||
<n-table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ header.position }}</th>
|
||||
<th>{{ header.title }}</th>
|
||||
<th>{{ header.size }}</th>
|
||||
<th>{{ header.mean }}</th>
|
||||
<th>{{ header.variance }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
|
||||
<td>{{ position }}</td>
|
||||
<td>{{ title }}</td>
|
||||
<td>{{ size }}</td>
|
||||
<td>{{ mean }}</td>
|
||||
<td>{{ variance }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
<c-table :data="results" :headers="header" />
|
||||
|
||||
<div mt-5 flex justify-center gap-3>
|
||||
<c-button @click="copyAsMarkdown()">
|
||||
Copy as markdown table
|
||||
|
||||
@@ -39,14 +39,11 @@ function onInputEnter(index: number) {
|
||||
autofocus
|
||||
@keydown.enter="onInputEnter(index)"
|
||||
/>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" @click="values.splice(index, 1)">
|
||||
<n-icon :component="Trash" depth="3" size="18" />
|
||||
</c-button>
|
||||
</template>
|
||||
Delete value
|
||||
</n-tooltip>
|
||||
<c-tooltip tooltip="Delete this value">
|
||||
<c-button circle variant="text" @click="values.splice(index, 1)">
|
||||
<n-icon :component="Trash" depth="3" size="18" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
|
||||
<c-button @click="addValue">
|
||||
|
||||
@@ -73,6 +73,13 @@ const formats = computed(() => [
|
||||
label: 'Snakecase:',
|
||||
value: snakeCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Mockingcase:',
|
||||
value: noCase(input.value, baseConfig)
|
||||
.split('')
|
||||
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
|
||||
.join(''),
|
||||
},
|
||||
]);
|
||||
|
||||
const inputLabelAlignmentConfig = {
|
||||
@@ -92,7 +99,7 @@ const inputLabelAlignmentConfig = {
|
||||
v-bind="inputLabelAlignmentConfig"
|
||||
/>
|
||||
|
||||
<div divider my-16px />
|
||||
<div my-16px divider />
|
||||
|
||||
<InputCopyable
|
||||
v-for="format in formats"
|
||||
|
||||
23
src/tools/color-converter/color-converter.e2e.spec.ts
Normal file
23
src/tools/color-converter/color-converter.e2e.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - Color converter', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/color-converter');
|
||||
});
|
||||
|
||||
test('Has title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Color converter - IT Tools');
|
||||
});
|
||||
|
||||
test('Color is converted from its name to other formats', async ({ page }) => {
|
||||
await page.getByTestId('input-name').fill('olive');
|
||||
|
||||
expect(await page.getByTestId('input-name').inputValue()).toEqual('olive');
|
||||
expect(await page.getByTestId('input-hex').inputValue()).toEqual('#808000');
|
||||
expect(await page.getByTestId('input-rgb').inputValue()).toEqual('rgb(128, 128, 0)');
|
||||
expect(await page.getByTestId('input-hsl').inputValue()).toEqual('hsl(60, 100%, 25%)');
|
||||
expect(await page.getByTestId('input-hwb').inputValue()).toEqual('hwb(60 0% 50%)');
|
||||
expect(await page.getByTestId('input-cmyk').inputValue()).toEqual('device-cmyk(0% 0% 100% 50%)');
|
||||
expect(await page.getByTestId('input-lch').inputValue()).toEqual('lch(52.15% 56.81 99.57)');
|
||||
});
|
||||
});
|
||||
13
src/tools/color-converter/color-converter.models.test.ts
Normal file
13
src/tools/color-converter/color-converter.models.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { removeAlphaChannelWhenOpaque } from './color-converter.models';
|
||||
|
||||
describe('color-converter models', () => {
|
||||
describe('removeAlphaChannelWhenOpaque', () => {
|
||||
it('remove alpha channel of an hex color when it is opaque (alpha = 1)', () => {
|
||||
expect(removeAlphaChannelWhenOpaque('#000000ff')).toBe('#000000');
|
||||
expect(removeAlphaChannelWhenOpaque('#ffffffFF')).toBe('#ffffff');
|
||||
expect(removeAlphaChannelWhenOpaque('#000000FE')).toBe('#000000FE');
|
||||
expect(removeAlphaChannelWhenOpaque('#00000000')).toBe('#00000000');
|
||||
});
|
||||
});
|
||||
});
|
||||
52
src/tools/color-converter/color-converter.models.ts
Normal file
52
src/tools/color-converter/color-converter.models.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { type Colord, colord } from 'colord';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
|
||||
export { removeAlphaChannelWhenOpaque, buildColorFormat };
|
||||
|
||||
function removeAlphaChannelWhenOpaque(hexColor: string) {
|
||||
return hexColor.replace(/^(#(?:[0-9a-f]{3}){1,2})ff$/i, '$1');
|
||||
}
|
||||
|
||||
function buildColorFormat({
|
||||
label,
|
||||
parse = value => colord(value),
|
||||
format,
|
||||
placeholder,
|
||||
invalidMessage = `Invalid ${label.toLowerCase()} format.`,
|
||||
type = 'text',
|
||||
}: {
|
||||
label: string
|
||||
parse?: (value: string) => Colord
|
||||
format: (value: Colord) => string
|
||||
placeholder?: string
|
||||
invalidMessage?: string
|
||||
type?: 'text' | 'color-picker'
|
||||
}) {
|
||||
const value = ref('');
|
||||
|
||||
return {
|
||||
type,
|
||||
label,
|
||||
parse: (v: string) => withDefaultOnError(() => parse(v), undefined),
|
||||
format,
|
||||
placeholder,
|
||||
value,
|
||||
validation: useValidation({
|
||||
source: value,
|
||||
rules: [
|
||||
{
|
||||
message: invalidMessage,
|
||||
validator: v => withDefaultOnError(() => {
|
||||
if (v === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parse(v).isValid();
|
||||
}, false),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
};
|
||||
}
|
||||
@@ -1,87 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import type { Colord } from 'colord';
|
||||
import { colord, extend } from 'colord';
|
||||
|
||||
import _ from 'lodash';
|
||||
import cmykPlugin from 'colord/plugins/cmyk';
|
||||
import hwbPlugin from 'colord/plugins/hwb';
|
||||
import namesPlugin from 'colord/plugins/names';
|
||||
import lchPlugin from 'colord/plugins/lch';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { buildColorFormat } from './color-converter.models';
|
||||
|
||||
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
||||
|
||||
const name = ref('');
|
||||
const hex = ref('#1ea54cff');
|
||||
const rgb = ref('');
|
||||
const hsl = ref('');
|
||||
const hwb = ref('');
|
||||
const cmyk = ref('');
|
||||
const lch = ref('');
|
||||
const formats = {
|
||||
picker: buildColorFormat({
|
||||
label: 'color picker',
|
||||
format: (v: Colord) => v.toHex(),
|
||||
type: 'color-picker',
|
||||
}),
|
||||
hex: buildColorFormat({
|
||||
label: 'hex',
|
||||
format: (v: Colord) => v.toHex(),
|
||||
placeholder: 'e.g. #ff0000',
|
||||
}),
|
||||
rgb: buildColorFormat({
|
||||
label: 'rgb',
|
||||
format: (v: Colord) => v.toRgbString(),
|
||||
placeholder: 'e.g. rgb(255, 0, 0)',
|
||||
}),
|
||||
hsl: buildColorFormat({
|
||||
label: 'hsl',
|
||||
format: (v: Colord) => v.toHslString(),
|
||||
placeholder: 'e.g. hsl(0, 100%, 50%)',
|
||||
}),
|
||||
hwb: buildColorFormat({
|
||||
label: 'hwb',
|
||||
format: (v: Colord) => v.toHwbString(),
|
||||
placeholder: 'e.g. hwb(0, 0%, 0%)',
|
||||
}),
|
||||
lch: buildColorFormat({
|
||||
label: 'lch',
|
||||
format: (v: Colord) => v.toLchString(),
|
||||
placeholder: 'e.g. lch(53.24, 104.55, 40.85)',
|
||||
}),
|
||||
cmyk: buildColorFormat({
|
||||
label: 'cmyk',
|
||||
format: (v: Colord) => v.toCmykString(),
|
||||
placeholder: 'e.g. cmyk(0, 100%, 100%, 0)',
|
||||
}),
|
||||
name: buildColorFormat({
|
||||
label: 'name',
|
||||
format: (v: Colord) => v.toName({ closest: true }) ?? 'Unknown',
|
||||
placeholder: 'e.g. red',
|
||||
}),
|
||||
};
|
||||
|
||||
function onInputUpdated(value: string, omit: string) {
|
||||
try {
|
||||
const color = colord(value);
|
||||
updateColorValue(colord('#1ea54c'));
|
||||
|
||||
if (omit !== 'name') {
|
||||
name.value = color.toName({ closest: true }) ?? '';
|
||||
}
|
||||
if (omit !== 'hex') {
|
||||
hex.value = color.toHex();
|
||||
}
|
||||
if (omit !== 'rgb') {
|
||||
rgb.value = color.toRgbString();
|
||||
}
|
||||
if (omit !== 'hsl') {
|
||||
hsl.value = color.toHslString();
|
||||
}
|
||||
if (omit !== 'hwb') {
|
||||
hwb.value = color.toHwbString();
|
||||
}
|
||||
if (omit !== 'cmyk') {
|
||||
cmyk.value = color.toCmykString();
|
||||
}
|
||||
if (omit !== 'lch') {
|
||||
lch.value = color.toLchString();
|
||||
}
|
||||
function updateColorValue(value: Colord | undefined, omitLabel?: string) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
catch {
|
||||
//
|
||||
|
||||
if (!value.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.forEach(formats, ({ value: valueRef, format }, key) => {
|
||||
if (key !== omitLabel) {
|
||||
valueRef.value = format(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onInputUpdated(hex.value, 'hex');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<n-form label-width="100" label-placement="left">
|
||||
<n-form-item label="color picker:">
|
||||
<template v-for="({ label, parse, placeholder, validation, type }, key) in formats" :key="key">
|
||||
<input-copyable
|
||||
v-if="type === 'text'"
|
||||
v-model:value="formats[key].value.value"
|
||||
:test-id="`input-${key}`"
|
||||
:label="`${label}:`"
|
||||
label-position="left"
|
||||
label-width="100px"
|
||||
label-align="right"
|
||||
:placeholder="placeholder"
|
||||
:validation="validation"
|
||||
raw-text
|
||||
clearable
|
||||
mt-2
|
||||
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||
/>
|
||||
|
||||
<n-form-item v-else-if="type === 'color-picker'" :label="`${label}:`" label-width="100" label-placement="left" :show-feedback="false">
|
||||
<n-color-picker
|
||||
v-model:value="hex"
|
||||
v-model:value="formats[key].value.value"
|
||||
placement="bottom-end"
|
||||
@update:value="(v: string) => onInputUpdated(v, 'hex')"
|
||||
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="color name:">
|
||||
<InputCopyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="hex:">
|
||||
<InputCopyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="rgb:">
|
||||
<InputCopyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="hsl:">
|
||||
<InputCopyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="hwb:">
|
||||
<InputCopyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="lch:">
|
||||
<InputCopyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="cmyk:">
|
||||
<InputCopyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
@@ -167,34 +167,8 @@ const cronValidationRules = [
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
<n-table v-else size="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left" scope="col">
|
||||
Symbol
|
||||
</th>
|
||||
<th class="text-left" scope="col">
|
||||
Meaning
|
||||
</th>
|
||||
<th class="text-left" scope="col">
|
||||
Example
|
||||
</th>
|
||||
<th class="text-left" scope="col">
|
||||
Equivalent
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol">
|
||||
<td>{{ symbol }}</td>
|
||||
<td>{{ meaning }}</td>
|
||||
<td>
|
||||
<code>{{ example }}</code>
|
||||
</td>
|
||||
<td>{{ equivalent }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
|
||||
<c-table v-else :data="helpers" />
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -29,5 +29,6 @@ test.describe('Date time converter - json to yaml', () => {
|
||||
expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
|
||||
expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
|
||||
expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
|
||||
expect((await page.getByTestId('Excel date/time').inputValue()).trim()).toEqual('45028.88222222222');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import {
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
isISO8601DateTimeString,
|
||||
isISO9075DateString,
|
||||
isMongoObjectId,
|
||||
@@ -139,4 +142,39 @@ describe('date-time-converter models', () => {
|
||||
expect(isMongoObjectId('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExcelFormat', () => {
|
||||
test('an Excel format string is a floating number that can be negative', () => {
|
||||
expect(isExcelFormat('0')).toBe(true);
|
||||
expect(isExcelFormat('1')).toBe(true);
|
||||
expect(isExcelFormat('1.1')).toBe(true);
|
||||
expect(isExcelFormat('-1.1')).toBe(true);
|
||||
expect(isExcelFormat('-1')).toBe(true);
|
||||
|
||||
expect(isExcelFormat('')).toBe(false);
|
||||
expect(isExcelFormat('foo')).toBe(false);
|
||||
expect(isExcelFormat('1.1.1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateToExcelFormat', () => {
|
||||
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||
expect(dateToExcelFormat(new Date('2016-05-20T00:00:00.000Z'))).toBe('42510');
|
||||
expect(dateToExcelFormat(new Date('2016-05-20T12:00:00.000Z'))).toBe('42510.5');
|
||||
expect(dateToExcelFormat(new Date('2023-10-31T09:26:06.421Z'))).toBe('45230.39312987268');
|
||||
expect(dateToExcelFormat(new Date('1970-01-01T00:00:00.000Z'))).toBe('25569');
|
||||
expect(dateToExcelFormat(new Date('1800-01-01T00:00:00.000Z'))).toBe('-36522');
|
||||
});
|
||||
});
|
||||
|
||||
describe('excelFormatToDate', () => {
|
||||
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||
expect(excelFormatToDate('0')).toEqual(new Date('1899-12-30T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('1')).toEqual(new Date('1899-12-31T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('2')).toEqual(new Date('1900-01-01T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('4242.4242')).toEqual(new Date('1911-08-12T10:10:50.880Z'));
|
||||
expect(excelFormatToDate('42738.22626859954')).toEqual(new Date('2017-01-03T05:25:49.607Z'));
|
||||
expect(excelFormatToDate('-1000')).toEqual(new Date('1897-04-04T00:00:00.000Z'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,9 @@ export {
|
||||
isTimestamp,
|
||||
isUTCDateString,
|
||||
isMongoObjectId,
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
};
|
||||
|
||||
const ISO8601_REGEX
|
||||
@@ -21,6 +24,8 @@ const RFC3339_REGEX
|
||||
|
||||
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
|
||||
|
||||
const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;
|
||||
|
||||
function createRegexMatcher(regex: RegExp) {
|
||||
return (date?: string) => !_.isNil(date) && regex.test(date);
|
||||
}
|
||||
@@ -33,6 +38,8 @@ const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
|
||||
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
|
||||
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
|
||||
|
||||
const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);
|
||||
|
||||
function isUTCDateString(date?: string) {
|
||||
if (_.isNil(date)) {
|
||||
return false;
|
||||
@@ -45,3 +52,11 @@ function isUTCDateString(date?: string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function dateToExcelFormat(date: Date) {
|
||||
return String(((date.getTime()) / (1000 * 60 * 60 * 24)) + 25569);
|
||||
}
|
||||
|
||||
function excelFormatToDate(excelFormat: string | number) {
|
||||
return new Date((Number(excelFormat) - 25569) * 86400 * 1000);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
} from 'date-fns';
|
||||
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
||||
import {
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
isISO8601DateTimeString,
|
||||
isISO9075DateString,
|
||||
isMongoObjectId,
|
||||
@@ -82,9 +85,15 @@ const formats: DateFormat[] = [
|
||||
{
|
||||
name: 'Mongo ObjectID',
|
||||
fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`,
|
||||
toDate: objectId => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
formatMatcher: date => isMongoObjectId(date),
|
||||
},
|
||||
{
|
||||
name: 'Excel date/time',
|
||||
fromDate: date => dateToExcelFormat(date),
|
||||
toDate: excelFormatToDate,
|
||||
formatMatcher: isExcelFormat,
|
||||
},
|
||||
];
|
||||
|
||||
const formatIndex = ref(6);
|
||||
@@ -146,7 +155,7 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date
|
||||
<c-input-text
|
||||
v-model:value="inputDate"
|
||||
autofocus
|
||||
placeholder="Put you date string here..."
|
||||
placeholder="Put your date string here..."
|
||||
clearable
|
||||
test-id="date-time-converter-input"
|
||||
:validation="validation"
|
||||
|
||||
@@ -29,7 +29,7 @@ const { copy } = useCopy();
|
||||
Unicode: <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span>
|
||||
</div> -->
|
||||
|
||||
<div flex gap-2 font-mono text-xs op-70>
|
||||
<div flex gap-2 text-xs font-mono op-70>
|
||||
<span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })">
|
||||
{{ emojiInfo.codePoints }}
|
||||
</span>
|
||||
|
||||
@@ -5,4 +5,4 @@ export type EmojiInfo = {
|
||||
emoji: string
|
||||
codePoints: string | undefined
|
||||
unicode: string
|
||||
} & typeof emojiUnicodeData['\uD83E\uDD10'];
|
||||
} & typeof emojiUnicodeData[string];
|
||||
|
||||
@@ -2,6 +2,6 @@ export function convertHexToBin(hex: string) {
|
||||
return hex
|
||||
.trim()
|
||||
.split('')
|
||||
.map(byte => parseInt(byte, 16).toString(2).padStart(4, '0'))
|
||||
.map(byte => Number.parseInt(byte, 16).toString(2).padStart(4, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -6,13 +6,9 @@ const { icon, title, action, isActive } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
|
||||
<n-icon :component="icon" />
|
||||
</c-button>
|
||||
</template>
|
||||
|
||||
{{ title }}
|
||||
</n-tooltip>
|
||||
<c-tooltip :tooltip="title">
|
||||
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
|
||||
<n-icon :component="icon" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { format } from 'prettier';
|
||||
import htmlParser from 'prettier/parser-html';
|
||||
import htmlParser from 'prettier/plugins/html';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import Editor from './editor/editor.vue';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
|
||||
const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>');
|
||||
|
||||
const formattedHtml = asyncComputed(() => format(html.value, { parser: 'html', plugins: [htmlParser] }), '');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Editor v-model:html="html" />
|
||||
<TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" />
|
||||
<TextareaCopyable :value="formattedHtml" language="html" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
|
||||
async function extractIbanInfo({ page }: { page: Page }) {
|
||||
const itemsLines = await page
|
||||
.locator('.c-key-value-list__item').all();
|
||||
|
||||
return await Promise.all(
|
||||
itemsLines.map(async item => [
|
||||
(await item.locator('.c-key-value-list__key').textContent() ?? '').trim(),
|
||||
(await item.locator('.c-key-value-list__value').textContent() ?? '').trim(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('Tool - Iban validator and parser', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/iban-validator-and-parser');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('IBAN validator and parser - IT Tools');
|
||||
});
|
||||
|
||||
test('iban info are extracted from a valid iban', async ({ page }) => {
|
||||
await page.getByTestId('iban-input').fill('DE89370400440532013000');
|
||||
|
||||
const ibanInfo = await extractIbanInfo({ page });
|
||||
|
||||
expect(ibanInfo).toEqual([
|
||||
['Is IBAN valid ?', 'Yes'],
|
||||
['Is IBAN a QR-IBAN ?', 'No'],
|
||||
['Country code', 'DE'],
|
||||
['BBAN', '370400440532013000'],
|
||||
['IBAN friendly format', 'DE89 3704 0044 0532 0130 00'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('invalid iban errors are displayed', async ({ page }) => {
|
||||
await page.getByTestId('iban-input').fill('FR7630006060011234567890189');
|
||||
|
||||
const ibanInfo = await extractIbanInfo({ page });
|
||||
|
||||
expect(ibanInfo).toEqual([
|
||||
['Is IBAN valid ?', 'No'],
|
||||
['IBAN errors', 'Wrong account bank branch checksum Wrong IBAN checksum'],
|
||||
['Is IBAN a QR-IBAN ?', 'No'],
|
||||
['Country code', 'N/A'],
|
||||
['BBAN', 'N/A'],
|
||||
['IBAN friendly format', 'FR76 3000 6060 0112 3456 7890 189'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ValidationErrorsIBAN } from 'ibantools';
|
||||
|
||||
export { getFriendlyErrors };
|
||||
|
||||
const ibanErrorToMessage = {
|
||||
[ValidationErrorsIBAN.NoIBANProvided]: 'No IBAN provided',
|
||||
[ValidationErrorsIBAN.NoIBANCountry]: 'No IBAN country',
|
||||
[ValidationErrorsIBAN.WrongBBANLength]: 'Wrong BBAN length',
|
||||
[ValidationErrorsIBAN.WrongBBANFormat]: 'Wrong BBAN format',
|
||||
[ValidationErrorsIBAN.ChecksumNotNumber]: 'Checksum is not a number',
|
||||
[ValidationErrorsIBAN.WrongIBANChecksum]: 'Wrong IBAN checksum',
|
||||
[ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: 'Wrong account bank branch checksum',
|
||||
[ValidationErrorsIBAN.QRIBANNotAllowed]: 'QR-IBAN not allowed',
|
||||
};
|
||||
|
||||
function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) {
|
||||
return errorCodes.map(errorCode => ibanErrorToMessage[errorCode]).filter(Boolean);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { extractIBAN, friendlyFormatIBAN, isQRIBAN, validateIBAN } from 'ibantools';
|
||||
import { getFriendlyErrors } from './iban-validator-and-parser.service';
|
||||
import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types';
|
||||
|
||||
const rawIban = ref('');
|
||||
|
||||
const ibanInfo = computed<CKeyValueListItems>(() => {
|
||||
const iban = rawIban.value.toUpperCase().replace(/\s/g, '').replace(/-/g, '');
|
||||
|
||||
if (iban === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { valid: isIbanValid, errorCodes } = validateIBAN(iban);
|
||||
const { countryCode, bban } = extractIBAN(iban);
|
||||
const errors = getFriendlyErrors(errorCodes);
|
||||
|
||||
return [
|
||||
|
||||
{
|
||||
label: 'Is IBAN valid ?',
|
||||
value: isIbanValid,
|
||||
showCopyButton: false,
|
||||
},
|
||||
{
|
||||
label: 'IBAN errors',
|
||||
value: errors.length === 0 ? undefined : errors,
|
||||
hideOnNil: true,
|
||||
showCopyButton: false,
|
||||
},
|
||||
{
|
||||
label: 'Is IBAN a QR-IBAN ?',
|
||||
value: isQRIBAN(iban),
|
||||
showCopyButton: false,
|
||||
},
|
||||
{
|
||||
label: 'Country code',
|
||||
value: countryCode,
|
||||
},
|
||||
{
|
||||
label: 'BBAN',
|
||||
value: bban,
|
||||
},
|
||||
{
|
||||
label: 'IBAN friendly format',
|
||||
value: friendlyFormatIBAN(iban),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const ibanExamples = [
|
||||
'FR7630006000011234567890189',
|
||||
'DE89370400440532013000',
|
||||
'GB29NWBK60161331926819',
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" />
|
||||
|
||||
<c-key-value-list :items="ibanInfo" my-5 data-test-id="iban-info" />
|
||||
|
||||
<c-card title="Valid IBAN examples">
|
||||
<div v-for="iban in ibanExamples" :key="iban">
|
||||
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
||||
12
src/tools/iban-validator-and-parser/index.ts
Normal file
12
src/tools/iban-validator-and-parser/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineTool } from '../tool';
|
||||
import Bank from '~icons/mdi/bank';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'IBAN validator and parser',
|
||||
path: '/iban-validator-and-parser',
|
||||
description: 'Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.',
|
||||
keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'],
|
||||
component: () => import('./iban-validator-and-parser.vue'),
|
||||
icon: Bank,
|
||||
createdAt: new Date('2023-08-26'),
|
||||
});
|
||||
@@ -1,6 +1,12 @@
|
||||
import { tool as base64FileConverter } from './base64-file-converter';
|
||||
import { tool as base64StringConverter } from './base64-string-converter';
|
||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||
import { tool as textToBinary } from './text-to-binary';
|
||||
import { tool as ulidGenerator } from './ulid-generator';
|
||||
import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
|
||||
import { tool as stringObfuscator } from './string-obfuscator';
|
||||
import { tool as textDiff } from './text-diff';
|
||||
import { tool as emojiPicker } from './emoji-picker';
|
||||
import { tool as passwordStrengthAnalyser } from './password-strength-analyser';
|
||||
import { tool as yamlToToml } from './yaml-to-toml';
|
||||
@@ -53,6 +59,7 @@ import { tool as metaTagGenerator } from './meta-tag-generator';
|
||||
import { tool as mimeTypes } from './mime-types';
|
||||
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
|
||||
import { tool as qrCodeGenerator } from './qr-code-generator';
|
||||
import { tool as wifiQrCodeGenerator } from './wifi-qr-code-generator';
|
||||
import { tool as randomPortGenerator } from './random-port-generator';
|
||||
import { tool as romanNumeralConverter } from './roman-numeral-converter';
|
||||
import { tool as sqlPrettify } from './sql-prettify';
|
||||
@@ -70,7 +77,7 @@ import { tool as xmlFormatter } from './xml-formatter';
|
||||
export const toolsByCategory: ToolCategory[] = [
|
||||
{
|
||||
name: 'Crypto',
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
|
||||
},
|
||||
{
|
||||
name: 'Converter',
|
||||
@@ -83,6 +90,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
colorConverter,
|
||||
caseConverter,
|
||||
textToNatoAlphabet,
|
||||
textToBinary,
|
||||
yamlToJson,
|
||||
yamlToToml,
|
||||
jsonToYaml,
|
||||
@@ -114,7 +122,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
},
|
||||
{
|
||||
name: 'Images and videos',
|
||||
components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
||||
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
||||
},
|
||||
{
|
||||
name: 'Development',
|
||||
@@ -133,7 +141,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator],
|
||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator],
|
||||
},
|
||||
{
|
||||
name: 'Math',
|
||||
@@ -145,11 +153,11 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
components: [loremIpsumGenerator, textStatistics, emojiPicker],
|
||||
components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff],
|
||||
},
|
||||
{
|
||||
name: 'Data',
|
||||
components: [phoneParserAndFormatter],
|
||||
components: [phoneParserAndFormatter, ibanValidatorAndParser],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str
|
||||
+ _.chain(ip)
|
||||
.trim()
|
||||
.split('.')
|
||||
.map(part => parseInt(part).toString(16).padStart(2, '0'))
|
||||
.map(part => Number.parseInt(part).toString(16).padStart(2, '0'))
|
||||
.chunk(2)
|
||||
.map(blocks => blocks.join(''))
|
||||
.join(':')
|
||||
|
||||
@@ -13,7 +13,7 @@ function getRangesize(start: string, end: string) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1 + parseInt(end, 2) - parseInt(start, 2);
|
||||
return 1 + Number.parseInt(end, 2) - Number.parseInt(start, 2);
|
||||
}
|
||||
|
||||
function getCidr(start: string, end: string) {
|
||||
@@ -55,8 +55,8 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
|
||||
const cidr = getCidr(start, end);
|
||||
if (cidr != null) {
|
||||
const result: Ipv4RangeExpanderResult = {};
|
||||
result.newEnd = bits2ip(parseInt(cidr.end, 2));
|
||||
result.newStart = bits2ip(parseInt(cidr.start, 2));
|
||||
result.newEnd = bits2ip(Number.parseInt(cidr.end, 2));
|
||||
result.newStart = bits2ip(Number.parseInt(cidr.start, 2));
|
||||
result.newCidr = `${result.newStart}/${cidr.mask}`;
|
||||
result.newSize = getRangesize(cidr.start, cidr.end);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode';
|
||||
import _ from 'lodash';
|
||||
import { match } from 'ts-pattern';
|
||||
import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants';
|
||||
|
||||
export { decodeJwt };
|
||||
@@ -32,10 +31,15 @@ function parseClaims({ claim, value }: { claim: string; value: unknown }) {
|
||||
}
|
||||
|
||||
function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) {
|
||||
return match(claim)
|
||||
.with('exp', 'nbf', 'iat', () => dateFormatter(value))
|
||||
.with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined))
|
||||
.otherwise(() => undefined);
|
||||
if (['exp', 'nbf', 'iat'].includes(claim)) {
|
||||
return dateFormatter(value);
|
||||
}
|
||||
|
||||
if (claim === 'alg' && _.isString(value)) {
|
||||
return ALGORITHM_DESCRIPTIONS[value];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function dateFormatter(value: unknown) {
|
||||
|
||||
12
src/tools/mac-address-generator/index.ts
Normal file
12
src/tools/mac-address-generator/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Devices } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'MAC address generator',
|
||||
path: '/mac-address-generator',
|
||||
description: 'Enter the quantity and prefix. MAC addresses will be generated in your chosen case (uppercase or lowercase)',
|
||||
keywords: ['mac', 'address', 'generator', 'random', 'prefix'],
|
||||
component: () => import('./mac-address-generator.vue'),
|
||||
icon: Devices,
|
||||
createdAt: new Date('2023-11-31'),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - MAC address generator', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/mac-address-generator');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('MAC address generator - IT Tools');
|
||||
});
|
||||
});
|
||||
103
src/tools/mac-address-generator/mac-address-generator.vue
Normal file
103
src/tools/mac-address-generator/mac-address-generator.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { generateRandomMacAddress } from './mac-adress-generator.models';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { usePartialMacAddressValidation } from '@/utils/macAddress';
|
||||
|
||||
const amount = useStorage('mac-address-generator-amount', 1);
|
||||
const macAddressPrefix = useStorage('mac-address-generator-prefix', '64:16:7F');
|
||||
|
||||
const prefixValidation = usePartialMacAddressValidation(macAddressPrefix);
|
||||
|
||||
const casesTransformers = [
|
||||
{ label: 'Uppercase', value: (value: string) => value.toUpperCase() },
|
||||
{ label: 'Lowercase', value: (value: string) => value.toLowerCase() },
|
||||
];
|
||||
const caseTransformer = ref(casesTransformers[0].value);
|
||||
|
||||
const separators = [
|
||||
{
|
||||
label: ':',
|
||||
value: ':',
|
||||
},
|
||||
{
|
||||
label: '-',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
label: '.',
|
||||
value: '.',
|
||||
},
|
||||
{
|
||||
label: 'None',
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
const separator = useStorage('mac-address-generator-separator', separators[0].value);
|
||||
|
||||
const [macAddresses, refreshMacAddresses] = computedRefreshable(() => {
|
||||
if (!prefixValidation.isValid) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const ids = _.times(amount.value, () => caseTransformer.value(generateRandomMacAddress({
|
||||
prefix: macAddressPrefix.value,
|
||||
separator: separator.value,
|
||||
})));
|
||||
return ids.join('\n');
|
||||
});
|
||||
|
||||
const { copy } = useCopy({ source: macAddresses, text: 'MAC addresses copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col justify-center gap-2>
|
||||
<div flex items-center>
|
||||
<label w-150px pr-12px text-right> Quantity:</label>
|
||||
<n-input-number v-model:value="amount" min="1" max="100" flex-1 />
|
||||
</div>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="macAddressPrefix"
|
||||
label="MAC address prefix:"
|
||||
placeholder="Set a prefix, e.g. 64:16:7F"
|
||||
clearable
|
||||
label-position="left"
|
||||
spellcheck="false"
|
||||
:validation="prefixValidation"
|
||||
raw-text
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
/>
|
||||
|
||||
<c-buttons-select
|
||||
v-model:value="caseTransformer"
|
||||
:options="casesTransformers"
|
||||
label="Case:"
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
/>
|
||||
|
||||
<c-buttons-select
|
||||
v-model:value="separator"
|
||||
:options="separators"
|
||||
label="Separator:"
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
/>
|
||||
|
||||
<c-card mt-5 flex data-test-id="ulids">
|
||||
<pre m-0 m-x-auto>{{ macAddresses }}</pre>
|
||||
</c-card>
|
||||
|
||||
<div flex justify-center gap-2>
|
||||
<c-button data-test-id="refresh" @click="refreshMacAddresses()">
|
||||
Refresh
|
||||
</c-button>
|
||||
<c-button @click="copy()">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateRandomMacAddress, splitPrefix } from './mac-adress-generator.models';
|
||||
|
||||
describe('mac-adress-generator models', () => {
|
||||
describe('splitPrefix', () => {
|
||||
it('a mac address prefix is splitted around non hex characters', () => {
|
||||
expect(splitPrefix('')).toEqual([]);
|
||||
expect(splitPrefix('01')).toEqual(['01']);
|
||||
expect(splitPrefix('01:')).toEqual(['01']);
|
||||
expect(splitPrefix('01:23')).toEqual(['01', '23']);
|
||||
expect(splitPrefix('01-23')).toEqual(['01', '23']);
|
||||
});
|
||||
|
||||
it('when a prefix contains only hex characters, they are grouped by 2', () => {
|
||||
expect(splitPrefix('0123')).toEqual(['01', '23']);
|
||||
expect(splitPrefix('012345')).toEqual(['01', '23', '45']);
|
||||
expect(splitPrefix('0123456')).toEqual(['01', '23', '45', '06']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateRandomMacAddress', () => {
|
||||
const createRandomByteGenerator = () => {
|
||||
let i = 0;
|
||||
return () => (i++).toString(16).padStart(2, '0');
|
||||
};
|
||||
|
||||
it('generates a random mac address', () => {
|
||||
expect(generateRandomMacAddress({ getRandomByte: createRandomByteGenerator() })).toBe('00:01:02:03:04:05');
|
||||
});
|
||||
|
||||
it('generates a random mac address with a prefix', () => {
|
||||
expect(generateRandomMacAddress({ prefix: 'ff:ee:aa', getRandomByte: createRandomByteGenerator() })).toBe('ff:ee:aa:00:01:02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff:ee:a', getRandomByte: createRandomByteGenerator() })).toBe('ff:ee:0a:00:01:02');
|
||||
});
|
||||
|
||||
it('generates a random mac address with a prefix and a different separator', () => {
|
||||
expect(generateRandomMacAddress({ prefix: 'ff-ee-aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff:ee:aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff-ee:aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff ee:aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export { splitPrefix, generateRandomMacAddress };
|
||||
|
||||
function splitPrefix(prefix: string): string[] {
|
||||
const base = prefix.match(/[^0-9a-f]/i) === null ? prefix.match(/.{1,2}/g) ?? [] : prefix.split(/[^0-9a-f]/i);
|
||||
|
||||
return base.filter(Boolean).map(byte => byte.padStart(2, '0'));
|
||||
}
|
||||
|
||||
function generateRandomMacAddress({ prefix: rawPrefix = '', separator = ':', getRandomByte = () => _.random(0, 255).toString(16).padStart(2, '0') }: { prefix?: string; separator?: string; getRandomByte?: () => string } = {}) {
|
||||
const prefix = splitPrefix(rawPrefix);
|
||||
|
||||
const randomBytes = _.times(6 - prefix.length, getRandomByte);
|
||||
const bytes = [...prefix, ...randomBytes];
|
||||
|
||||
return bytes.join(separator);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '')
|
||||
const macAddress = ref('20:37:06:12:34:56');
|
||||
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]);
|
||||
|
||||
const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' });
|
||||
const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -61,19 +61,16 @@ const secretValidationRules = [
|
||||
:validation-rules="secretValidationRules"
|
||||
>
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" size="small" @click="refreshSecret">
|
||||
<icon-mdi-refresh />
|
||||
</c-button>
|
||||
</template>
|
||||
Generate secret token
|
||||
</n-tooltip>
|
||||
<c-tooltip tooltip="Generate a new random secret">
|
||||
<c-button circle variant="text" size="small" @click="refreshSecret">
|
||||
<icon-mdi-refresh />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
</c-input-text>
|
||||
|
||||
<div>
|
||||
<TokenDisplay :tokens="tokens" style="margin-top: 2px" />
|
||||
<TokenDisplay :tokens="tokens" />
|
||||
|
||||
<n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" />
|
||||
<div style="text-align: center">
|
||||
|
||||
@@ -15,7 +15,7 @@ export {
|
||||
};
|
||||
|
||||
function hexToBytes(hex: string) {
|
||||
return (hex.match(/.{1,2}/g) ?? []).map(char => parseInt(char, 16));
|
||||
return (hex.match(/.{1,2}/g) ?? []).map(char => Number.parseInt(char, 16));
|
||||
}
|
||||
|
||||
function computeHMACSha1(message: string, key: string) {
|
||||
@@ -32,7 +32,7 @@ function base32toHex(base32: string) {
|
||||
.map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0'))
|
||||
.join('');
|
||||
|
||||
const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
|
||||
const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => Number.parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>();
|
||||
const { copy: copyPrevious, copied: previousCopied } = useClipboard();
|
||||
const { copy: copyCurrent, copied: currentCopied } = useClipboard();
|
||||
const { copy: copyNext, copied: nextCopied } = useClipboard();
|
||||
const { copy: copyPrevious, isJustCopied: previousCopied } = useCopy({ createToast: false });
|
||||
const { copy: copyCurrent, isJustCopied: currentCopied } = useCopy({ createToast: false });
|
||||
const { copy: copyNext, isJustCopied: nextCopied } = useCopy({ createToast: false });
|
||||
|
||||
const { tokens } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="labels" w-full flex items-center>
|
||||
<div mb-5px w-full flex items-center>
|
||||
<div flex-1 text-left>
|
||||
Previous
|
||||
</div>
|
||||
@@ -22,60 +22,24 @@ const { tokens } = toRefs(props);
|
||||
Next
|
||||
</div>
|
||||
</div>
|
||||
<n-input-group>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<c-button important:h-12 data-test-id="previous-otp" @click.prevent="copyPrevious(tokens.previous)">
|
||||
{{ tokens.previous }}
|
||||
</c-button>
|
||||
</template>
|
||||
<div>{{ previousCopied ? 'Copied !' : 'Copy previous OTP' }}</div>
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<c-button
|
||||
data-test-id="current-otp"
|
||||
class="current-otp"
|
||||
important:h-12
|
||||
@click.prevent="copyCurrent(tokens.current)"
|
||||
>
|
||||
{{ tokens.current }}
|
||||
</c-button>
|
||||
</template>
|
||||
<div>{{ currentCopied ? 'Copied !' : 'Copy current OTP' }}</div>
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<c-button important:h-12 data-test-id="next-otp" @click.prevent="copyNext(tokens.next)">
|
||||
{{
|
||||
tokens.next
|
||||
}}
|
||||
</c-button>
|
||||
</template>
|
||||
<div>{{ nextCopied ? 'Copied !' : 'Copy next OTP' }}</div>
|
||||
</n-tooltip>
|
||||
</n-input-group>
|
||||
<div flex items-center>
|
||||
<c-tooltip :tooltip="previousCopied ? 'Copied !' : 'Copy previous OTP'" position="bottom" flex-1>
|
||||
<c-button data-test-id="previous-otp" w-full important:h-12 important:rounded-r-none important:font-mono @click.prevent="copyPrevious(tokens.previous)">
|
||||
{{ tokens.previous }}
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
<c-tooltip :tooltip="currentCopied ? 'Copied !' : 'Copy current OTP'" position="bottom" flex-1 flex-basis-5xl>
|
||||
<c-button
|
||||
data-test-id="current-otp" w-full important:border-x="1px solid gray op-40" important:h-12 important:rounded-0 important:text-22px @click.prevent="copyCurrent(tokens.current)"
|
||||
>
|
||||
{{ tokens.current }}
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
<c-tooltip :tooltip="nextCopied ? 'Copied !' : 'Copy next OTP'" position="bottom" flex-1>
|
||||
<c-button data-test-id="next-otp" w-full important:h-12 important:rounded-l-none @click.prevent="copyNext(tokens.next)">
|
||||
{{ tokens.next }}
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.current-otp {
|
||||
font-size: 22px;
|
||||
flex: 1 0 35% !important;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.labels {
|
||||
div {
|
||||
padding: 0 2px 6px 2px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
.n-input-group > * {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,6 @@ test.describe('Tool - Password strength analyser', () => {
|
||||
|
||||
const crackDuration = await page.getByTestId('crack-duration').textContent();
|
||||
|
||||
expect(crackDuration).toEqual('15,091 milleniums, 3 centurys');
|
||||
expect(crackDuration).toEqual('15,091 millennia, 3 centuries');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export { getPasswordCrackTimeEstimation, getCharsetLength };
|
||||
|
||||
function prettifyExponentialNotation(exponentialNotation: number) {
|
||||
const [base, exponent] = exponentialNotation.toString().split('e');
|
||||
const baseAsNumber = parseFloat(base);
|
||||
const baseAsNumber = Number.parseFloat(base);
|
||||
const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2);
|
||||
return exponent ? `${prettyBase}e${exponent}` : prettyBase;
|
||||
}
|
||||
@@ -19,20 +19,20 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
|
||||
}
|
||||
|
||||
const timeUnits = [
|
||||
{ unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation },
|
||||
{ unit: 'century', secondsInUnit: 3153600000 },
|
||||
{ unit: 'decade', secondsInUnit: 315360000 },
|
||||
{ unit: 'year', secondsInUnit: 31536000 },
|
||||
{ unit: 'month', secondsInUnit: 2592000 },
|
||||
{ unit: 'week', secondsInUnit: 604800 },
|
||||
{ unit: 'day', secondsInUnit: 86400 },
|
||||
{ unit: 'hour', secondsInUnit: 3600 },
|
||||
{ unit: 'minute', secondsInUnit: 60 },
|
||||
{ unit: 'second', secondsInUnit: 1 },
|
||||
{ unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' },
|
||||
{ unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' },
|
||||
{ unit: 'decade', secondsInUnit: 315360000, plural: 'decades' },
|
||||
{ unit: 'year', secondsInUnit: 31536000, plural: 'years' },
|
||||
{ unit: 'month', secondsInUnit: 2592000, plural: 'months' },
|
||||
{ unit: 'week', secondsInUnit: 604800, plural: 'weeks' },
|
||||
{ unit: 'day', secondsInUnit: 86400, plural: 'days' },
|
||||
{ unit: 'hour', secondsInUnit: 3600, plural: 'hours' },
|
||||
{ unit: 'minute', secondsInUnit: 60, plural: 'minutes' },
|
||||
{ unit: 'second', secondsInUnit: 1, plural: 'seconds' },
|
||||
];
|
||||
|
||||
return _.chain(timeUnits)
|
||||
.map(({ unit, secondsInUnit, format = _.identity }) => {
|
||||
.map(({ unit, secondsInUnit, plural, format = _.identity }) => {
|
||||
const quantity = Math.floor(seconds / secondsInUnit);
|
||||
seconds %= secondsInUnit;
|
||||
|
||||
@@ -41,7 +41,7 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
|
||||
}
|
||||
|
||||
const formattedQuantity = format(quantity);
|
||||
return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`;
|
||||
return `${formattedQuantity} ${quantity > 1 ? plural : unit}`;
|
||||
})
|
||||
.compact()
|
||||
.take(2)
|
||||
|
||||
@@ -36,7 +36,7 @@ const validationRoman = useValidation({
|
||||
});
|
||||
|
||||
const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' });
|
||||
const { copy: copyArabic } = useCopy({ source: outputNumeral, text: 'Arabic number copied to the clipboard' });
|
||||
const { copy: copyArabic } = useCopy({ source: () => String(outputNumeral), text: 'Arabic number copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { type FormatFnOptions, format as formatSQL } from 'sql-formatter';
|
||||
import { type FormatOptionsWithLanguage, format as formatSQL } from 'sql-formatter';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const inputElement = ref<HTMLElement>();
|
||||
const styleStore = useStyleStore();
|
||||
const config = reactive<Partial<FormatFnOptions>>({
|
||||
const config = reactive<FormatOptionsWithLanguage>({
|
||||
keywordCase: 'upper',
|
||||
useTabs: false,
|
||||
language: 'sql',
|
||||
|
||||
12
src/tools/string-obfuscator/index.ts
Normal file
12
src/tools/string-obfuscator/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { EyeOff } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'String obfuscator',
|
||||
path: '/string-obfuscator',
|
||||
description: 'Obfuscate a string (like a secret, an IBAN, or a token) to make it shareable and identifiable without revealing its content.',
|
||||
keywords: ['string', 'obfuscator', 'secret', 'token', 'hide', 'obscure', 'mask', 'masking'],
|
||||
component: () => import('./string-obfuscator.vue'),
|
||||
icon: EyeOff,
|
||||
createdAt: new Date('2023-08-16'),
|
||||
});
|
||||
20
src/tools/string-obfuscator/string-obfuscator.model.test.ts
Normal file
20
src/tools/string-obfuscator/string-obfuscator.model.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { obfuscateString } from './string-obfuscator.model';
|
||||
|
||||
describe('string-obfuscator model', () => {
|
||||
describe('obfuscateString', () => {
|
||||
it('the characters in the middle of the string are replaced by the replacement character', () => {
|
||||
expect(obfuscateString('1234567890')).toBe('1234******');
|
||||
expect(obfuscateString('1234567890', { replacementChar: 'x' })).toBe('1234xxxxxx');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 5 })).toBe('12345*****');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 0, keepLast: 5 })).toBe('*****67890');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 5, keepLast: 5 })).toBe('1234567890');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 2, keepLast: 2, replacementChar: 'x' })).toBe('12xxxxxx90');
|
||||
});
|
||||
|
||||
it('by default, the spaces are kept, they can be removed with the keepSpace option', () => {
|
||||
expect(obfuscateString('12345 67890')).toBe('1234* *****');
|
||||
expect(obfuscateString('12345 67890', { keepSpace: false })).toBe('1234*******');
|
||||
});
|
||||
});
|
||||
});
|
||||
35
src/tools/string-obfuscator/string-obfuscator.model.ts
Normal file
35
src/tools/string-obfuscator/string-obfuscator.model.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { get } from '@vueuse/core';
|
||||
import { type MaybeRef, computed } from 'vue';
|
||||
|
||||
export { obfuscateString, useObfuscateString };
|
||||
|
||||
function obfuscateString(
|
||||
str: string,
|
||||
{ replacementChar = '*', keepFirst = 4, keepLast = 0, keepSpace = true }: { replacementChar?: string; keepFirst?: number; keepLast?: number; keepSpace?: boolean } = {}): string {
|
||||
return str
|
||||
.split('')
|
||||
.map((char, index, array) => {
|
||||
if (keepSpace && char === ' ') {
|
||||
return char;
|
||||
}
|
||||
|
||||
return (index < keepFirst || index >= array.length - keepLast) ? char : replacementChar;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function useObfuscateString(
|
||||
str: MaybeRef<string>,
|
||||
config: { replacementChar?: MaybeRef<string>; keepFirst?: MaybeRef<number>; keepLast?: MaybeRef<number>; keepSpace?: MaybeRef<boolean> } = {},
|
||||
|
||||
) {
|
||||
return computed(() => obfuscateString(
|
||||
get(str),
|
||||
{
|
||||
replacementChar: get(config.replacementChar),
|
||||
keepFirst: get(config.keepFirst),
|
||||
keepLast: get(config.keepLast),
|
||||
keepSpace: get(config.keepSpace),
|
||||
},
|
||||
));
|
||||
}
|
||||
47
src/tools/string-obfuscator/string-obfuscator.vue
Normal file
47
src/tools/string-obfuscator/string-obfuscator.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { useObfuscateString } from './string-obfuscator.model';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const str = ref('Lorem ipsum dolor sit amet');
|
||||
const keepFirst = ref(4);
|
||||
const keepLast = ref(4);
|
||||
const keepSpace = ref(true);
|
||||
|
||||
const obfuscatedString = useObfuscateString(str, { keepFirst, keepLast, keepSpace });
|
||||
const { copy } = useCopy({ source: obfuscatedString });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text v-model:value="str" raw-text placeholder="Enter string to obfuscate" label="String to obfuscate:" clearable multiline />
|
||||
|
||||
<div mt-4 flex gap-10px>
|
||||
<div>
|
||||
<div>Keep first:</div>
|
||||
<n-input-number v-model:value="keepFirst" min="0" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>Keep last:</div>
|
||||
<n-input-number v-model:value="keepLast" min="0" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div mb-5px>
|
||||
Keep spaces:
|
||||
</div>
|
||||
<n-switch v-model:value="keepSpace" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<c-card v-if="obfuscatedString" mt-60px max-w-600px flex items-center gap-5px font-mono>
|
||||
<div break-anywhere text-wrap>
|
||||
{{ obfuscatedString }}
|
||||
</div>
|
||||
|
||||
<c-button @click="copy()">
|
||||
<icon-mdi:content-copy />
|
||||
</c-button>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
||||
12
src/tools/text-diff/index.ts
Normal file
12
src/tools/text-diff/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { FileDiff } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Text diff',
|
||||
path: '/text-diff',
|
||||
description: 'Compare two texts and see the differences between them.',
|
||||
keywords: ['text', 'diff', 'compare', 'string', 'text diff', 'code'],
|
||||
component: () => import('./text-diff.vue'),
|
||||
icon: FileDiff,
|
||||
createdAt: new Date('2023-08-16'),
|
||||
});
|
||||
5
src/tools/text-diff/text-diff.vue
Normal file
5
src/tools/text-diff/text-diff.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<c-card w-full important:flex-1 important:pa-0>
|
||||
<c-diff-editor />
|
||||
</c-card>
|
||||
</template>
|
||||
12
src/tools/text-to-binary/index.ts
Normal file
12
src/tools/text-to-binary/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Binary } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Text to ASCII binary',
|
||||
path: '/text-to-binary',
|
||||
description: 'Convert text to its ASCII binary representation and vice versa.',
|
||||
keywords: ['text', 'to', 'binary', 'converter', 'encode', 'decode', 'ascii'],
|
||||
component: () => import('./text-to-binary.vue'),
|
||||
icon: Binary,
|
||||
createdAt: new Date('2023-10-15'),
|
||||
});
|
||||
25
src/tools/text-to-binary/text-to-binary.e2e.spec.ts
Normal file
25
src/tools/text-to-binary/text-to-binary.e2e.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - Text to ASCII binary', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/text-to-binary');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Text to ASCII binary - IT Tools');
|
||||
});
|
||||
|
||||
test('Text to binary conversion', async ({ page }) => {
|
||||
await page.getByTestId('text-to-binary-input').fill('it-tools');
|
||||
const binary = await page.getByTestId('text-to-binary-output').inputValue();
|
||||
|
||||
expect(binary).toEqual('01101001 01110100 00101101 01110100 01101111 01101111 01101100 01110011');
|
||||
});
|
||||
|
||||
test('Binary to text conversion', async ({ page }) => {
|
||||
await page.getByTestId('binary-to-text-input').fill('01101001 01110100 00101101 01110100 01101111 01101111 01101100 01110011');
|
||||
const text = await page.getByTestId('binary-to-text-output').inputValue();
|
||||
|
||||
expect(text).toEqual('it-tools');
|
||||
});
|
||||
});
|
||||
32
src/tools/text-to-binary/text-to-binary.models.test.ts
Normal file
32
src/tools/text-to-binary/text-to-binary.models.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { convertAsciiBinaryToText, convertTextToAsciiBinary } from './text-to-binary.models';
|
||||
|
||||
describe('text-to-binary', () => {
|
||||
describe('convertTextToAsciiBinary', () => {
|
||||
it('a text string is converted to its ascii binary representation', () => {
|
||||
expect(convertTextToAsciiBinary('A')).toBe('01000001');
|
||||
expect(convertTextToAsciiBinary('hello')).toBe('01101000 01100101 01101100 01101100 01101111');
|
||||
expect(convertTextToAsciiBinary('')).toBe('');
|
||||
});
|
||||
it('the separator between octets can be changed', () => {
|
||||
expect(convertTextToAsciiBinary('hello', { separator: '' })).toBe('0110100001100101011011000110110001101111');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertAsciiBinaryToText', () => {
|
||||
it('an ascii binary string is converted to its text representation', () => {
|
||||
expect(convertAsciiBinaryToText('01101000 01100101 01101100 01101100 01101111')).toBe('hello');
|
||||
expect(convertAsciiBinaryToText('01000001')).toBe('A');
|
||||
expect(convertTextToAsciiBinary('')).toBe('');
|
||||
});
|
||||
|
||||
it('the given binary string is cleaned before conversion', () => {
|
||||
expect(convertAsciiBinaryToText(' 01000 001garbage')).toBe('A');
|
||||
});
|
||||
|
||||
it('throws an error if the given binary string as no complete octet', () => {
|
||||
expect(() => convertAsciiBinaryToText('010000011')).toThrow('Invalid binary string');
|
||||
expect(() => convertAsciiBinaryToText('1')).toThrow('Invalid binary string');
|
||||
});
|
||||
});
|
||||
});
|
||||
22
src/tools/text-to-binary/text-to-binary.models.ts
Normal file
22
src/tools/text-to-binary/text-to-binary.models.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export { convertTextToAsciiBinary, convertAsciiBinaryToText };
|
||||
|
||||
function convertTextToAsciiBinary(text: string, { separator = ' ' }: { separator?: string } = {}): string {
|
||||
return text
|
||||
.split('')
|
||||
.map(char => char.charCodeAt(0).toString(2).padStart(8, '0'))
|
||||
.join(separator);
|
||||
}
|
||||
|
||||
function convertAsciiBinaryToText(binary: string): string {
|
||||
const cleanBinary = binary.replace(/[^01]/g, '');
|
||||
|
||||
if (cleanBinary.length % 8) {
|
||||
throw new Error('Invalid binary string');
|
||||
}
|
||||
|
||||
return cleanBinary
|
||||
.split(/(\d{8})/)
|
||||
.filter(Boolean)
|
||||
.map(binary => String.fromCharCode(Number.parseInt(binary, 2)))
|
||||
.join('');
|
||||
}
|
||||
42
src/tools/text-to-binary/text-to-binary.vue
Normal file
42
src/tools/text-to-binary/text-to-binary.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { convertAsciiBinaryToText, convertTextToAsciiBinary } from './text-to-binary.models';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
|
||||
const inputText = ref('');
|
||||
const binaryFromText = computed(() => convertTextToAsciiBinary(inputText.value));
|
||||
const { copy: copyBinary } = useCopy({ source: binaryFromText });
|
||||
|
||||
const inputBinary = ref('');
|
||||
const textFromBinary = computed(() => withDefaultOnError(() => convertAsciiBinaryToText(inputBinary.value), ''));
|
||||
const inputBinaryValidationRules = [
|
||||
{
|
||||
validator: (value: string) => isNotThrowing(() => convertAsciiBinaryToText(value)),
|
||||
message: 'Binary should be a valid ASCII binary string with multiples of 8 bits',
|
||||
},
|
||||
];
|
||||
const { copy: copyText } = useCopy({ source: textFromBinary });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card title="Text to ASCII binary">
|
||||
<c-input-text v-model:value="inputText" multiline placeholder="e.g. 'Hello world'" label="Enter text to convert to binary" autosize autofocus raw-text test-id="text-to-binary-input" />
|
||||
<c-input-text v-model:value="binaryFromText" label="Binary from your text" multiline raw-text readonly mt-2 placeholder="The binary representation of your text will be here" test-id="text-to-binary-output" />
|
||||
<div mt-2 flex justify-center>
|
||||
<c-button :disabled="!binaryFromText" @click="copyBinary()">
|
||||
Copy binary to clipboard
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<c-card title="ASCII binary to text">
|
||||
<c-input-text v-model:value="inputBinary" multiline placeholder="e.g. '01001000 01100101 01101100 01101100 01101111'" label="Enter binary to convert to text" autosize raw-text :validation-rules="inputBinaryValidationRules" test-id="binary-to-text-input" />
|
||||
<c-input-text v-model:value="textFromBinary" label="Text from your binary" multiline raw-text readonly mt-2 placeholder="The text representation of your binary will be here" test-id="binary-to-text-output" />
|
||||
<div mt-2 flex justify-center>
|
||||
<c-button :disabled="!textFromBinary" @click="copyText()">
|
||||
Copy text to clipboard
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ArrowsShuffle } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
import { translate } from '@/plugins/i18n.plugin';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Token generator',
|
||||
name: translate('tools.token-generator.title'),
|
||||
path: '/token-generator',
|
||||
description:
|
||||
'Generate random string with the chars you want: uppercase or lowercase letters, numbers and/or symbols.',
|
||||
description: translate('tools.token-generator.description'),
|
||||
keywords: ['token', 'random', 'string', 'alphanumeric', 'symbols', 'number', 'letters', 'lowercase', 'uppercase'],
|
||||
component: () => import('./token-generator.tool.vue'),
|
||||
icon: ArrowsShuffle,
|
||||
|
||||
@@ -6,4 +6,10 @@ tools:
|
||||
uppercase: Uppercase (ABC...)
|
||||
lowercase: Lowercase (abc...)
|
||||
numbers: Numbers (123...)
|
||||
symbols: Symbols (!-;...)
|
||||
symbols: Symbols (!-;...)
|
||||
length: Length
|
||||
tokenPlaceholder: 'The token...'
|
||||
copied: Token copied to the clipboard
|
||||
button:
|
||||
copy: Copy
|
||||
refresh: Refresh
|
||||
@@ -15,14 +15,12 @@ export function createToken({
|
||||
length?: number
|
||||
alphabet?: string
|
||||
}) {
|
||||
const allAlphabet
|
||||
= alphabet
|
||||
?? [
|
||||
...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''),
|
||||
...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''),
|
||||
...(withNumbers ? '0123456789' : ''),
|
||||
...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''),
|
||||
].join('');
|
||||
const allAlphabet = alphabet ?? [
|
||||
withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '',
|
||||
withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
|
||||
withNumbers ? '0123456789' : '',
|
||||
withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
|
||||
].join(''); ;
|
||||
|
||||
return shuffleString(allAlphabet.repeat(length)).substring(0, length);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const [token, refreshToken] = computedRefreshable(() =>
|
||||
}),
|
||||
);
|
||||
|
||||
const { copy } = useCopy({ source: token, text: 'Token copied to the clipboard' });
|
||||
const { copy } = useCopy({ source: token, text: t('tools.token-generator.copied') });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -51,14 +51,14 @@ const { copy } = useCopy({ source: token, text: 'Token copied to the clipboard'
|
||||
</div>
|
||||
</n-form>
|
||||
|
||||
<n-form-item :label="`Length (${length})`" label-placement="left">
|
||||
<n-form-item :label="`${t('tools.token-generator.length')} (${length})`" label-placement="left">
|
||||
<n-slider v-model:value="length" :step="1" :min="1" :max="512" />
|
||||
</n-form-item>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="token"
|
||||
multiline
|
||||
placeholder="The token..."
|
||||
:placeholder="t('tools.token-generator.tokenPlaceholder')"
|
||||
readonly
|
||||
rows="3"
|
||||
autosize
|
||||
@@ -67,10 +67,10 @@ const { copy } = useCopy({ source: token, text: 'Token copied to the clipboard'
|
||||
|
||||
<div mt-5 flex justify-center gap-3>
|
||||
<c-button @click="copy()">
|
||||
Copy
|
||||
{{ t('tools.token-generator.button.copy') }}
|
||||
</c-button>
|
||||
<c-button @click="refreshToken">
|
||||
Refresh
|
||||
{{ t('tools.token-generator.button.refresh') }}
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
12
src/tools/ulid-generator/index.ts
Normal file
12
src/tools/ulid-generator/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SortDescendingNumbers } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'ULID generator',
|
||||
path: '/ulid-generator',
|
||||
description: 'Generate random Universally Unique Lexicographically Sortable Identifier (ULID).',
|
||||
keywords: ['ulid', 'generator', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
|
||||
component: () => import('./ulid-generator.vue'),
|
||||
icon: SortDescendingNumbers,
|
||||
createdAt: new Date('2023-09-11'),
|
||||
});
|
||||
23
src/tools/ulid-generator/ulid-generator.e2e.spec.ts
Normal file
23
src/tools/ulid-generator/ulid-generator.e2e.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const ULID_REGEX = /[0-9A-Z]{26}/;
|
||||
|
||||
test.describe('Tool - ULID generator', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/ulid-generator');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('ULID generator - IT Tools');
|
||||
});
|
||||
|
||||
test('the refresh button generates a new ulid', async ({ page }) => {
|
||||
const ulid = await page.getByTestId('ulids').textContent();
|
||||
expect(ulid?.trim()).toMatch(ULID_REGEX);
|
||||
|
||||
await page.getByTestId('refresh').click();
|
||||
const newUlid = await page.getByTestId('ulids').textContent();
|
||||
expect(ulid?.trim()).not.toBe(newUlid?.trim());
|
||||
expect(newUlid?.trim()).toMatch(ULID_REGEX);
|
||||
});
|
||||
});
|
||||
46
src/tools/ulid-generator/ulid-generator.vue
Normal file
46
src/tools/ulid-generator/ulid-generator.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ulid } from 'ulid';
|
||||
import _ from 'lodash';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const amount = useStorage('ulid-generator-amount', 1);
|
||||
const formats = [{ label: 'Raw', value: 'raw' }, { label: 'JSON', value: 'json' }] as const;
|
||||
const format = useStorage<typeof formats[number]['value']>('ulid-generator-format', formats[0].value);
|
||||
|
||||
const [ulids, refreshUlids] = computedRefreshable(() => {
|
||||
const ids = _.times(amount.value, () => ulid());
|
||||
|
||||
if (format.value === 'json') {
|
||||
return JSON.stringify(ids, null, 2);
|
||||
}
|
||||
|
||||
return ids.join('\n');
|
||||
});
|
||||
|
||||
const { copy } = useCopy({ source: ulids, text: 'ULIDs copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col justify-center gap-2>
|
||||
<div flex items-center>
|
||||
<label w-75px> Quantity:</label>
|
||||
<n-input-number v-model:value="amount" min="1" max="100" flex-1 />
|
||||
</div>
|
||||
|
||||
<c-buttons-select v-model:value="format" :options="formats" label="Format: " label-width="75px" />
|
||||
|
||||
<c-card mt-5 flex data-test-id="ulids">
|
||||
<pre m-0 m-x-auto>{{ ulids }}</pre>
|
||||
</c-card>
|
||||
|
||||
<div flex justify-center gap-2>
|
||||
<c-button data-test-id="refresh" @click="refreshUlids()">
|
||||
Refresh
|
||||
</c-button>
|
||||
<c-button @click="copy()">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,25 +14,18 @@ const { userAgentInfo, sections } = toRefs(props);
|
||||
<n-grid :x-gap="12" :y-gap="8" cols="1 s:2" responsive="screen">
|
||||
<n-gi v-for="{ heading, icon, content } in sections" :key="heading">
|
||||
<c-card h-full>
|
||||
<n-page-header>
|
||||
<template #title>
|
||||
{{ heading }}
|
||||
</template>
|
||||
<template v-if="icon" #avatar>
|
||||
<n-icon size="30" :component="icon" :depth="3" />
|
||||
</template>
|
||||
</n-page-header>
|
||||
<div flex items-center gap-3>
|
||||
<n-icon size="30" :component="icon" :depth="3" />
|
||||
<span text-lg>{{ heading }}</span>
|
||||
</div>
|
||||
|
||||
<div mt-5 flex gap-2>
|
||||
<span v-for="{ label, getValue } in content" :key="label">
|
||||
<n-tooltip v-if="getValue(userAgentInfo)" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-tag type="success" size="large" round :bordered="false">
|
||||
{{ getValue(userAgentInfo) }}
|
||||
</n-tag>
|
||||
</template>
|
||||
{{ label }}
|
||||
</n-tooltip>
|
||||
<c-tooltip v-if="getValue(userAgentInfo)" :tooltip="label">
|
||||
<n-tag type="success" size="large" round :bordered="false">
|
||||
{{ getValue(userAgentInfo) }}
|
||||
</n-tag>
|
||||
</c-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div flex flex-col>
|
||||
|
||||
@@ -5,7 +5,7 @@ export const tool = defineTool({
|
||||
name: 'UUIDs v4 generator',
|
||||
path: '/uuid-generator',
|
||||
description:
|
||||
'A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot !).',
|
||||
'A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).',
|
||||
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
|
||||
component: () => import('./uuid-generator.vue'),
|
||||
icon: Fingerprint,
|
||||
|
||||
@@ -34,7 +34,7 @@ const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard'
|
||||
/>
|
||||
|
||||
<div flex justify-center gap-3>
|
||||
<c-button autofocus @click="copy">
|
||||
<c-button autofocus @click="copy()">
|
||||
Copy
|
||||
</c-button>
|
||||
<c-button @click="refreshUUIDs">
|
||||
|
||||
13
src/tools/wifi-qr-code-generator/index.ts
Normal file
13
src/tools/wifi-qr-code-generator/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Qrcode } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'WiFi QR Code generator',
|
||||
path: '/wifi-qrcode-generator',
|
||||
description:
|
||||
'Generate and download QR-codes for quick connections to WiFi networks.',
|
||||
keywords: ['qr', 'code', 'generator', 'square', 'color', 'link', 'low', 'medium', 'quartile', 'high', 'transparent', 'wifi'],
|
||||
component: () => import('./wifi-qr-code-generator.vue'),
|
||||
icon: Qrcode,
|
||||
createdAt: new Date('2023-09-06'),
|
||||
});
|
||||
146
src/tools/wifi-qr-code-generator/useQRCode.ts
Normal file
146
src/tools/wifi-qr-code-generator/useQRCode.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { type MaybeRef, get } from '@vueuse/core';
|
||||
import QRCode, { type QRCodeToDataURLOptions } from 'qrcode';
|
||||
import { isRef, ref, watch } from 'vue';
|
||||
|
||||
export const wifiEncryptions = ['WEP', 'WPA', 'nopass', 'WPA2-EAP'] as const;
|
||||
export type WifiEncryption = typeof wifiEncryptions[number];
|
||||
|
||||
// @see https://en.wikipedia.org/wiki/Extensible_Authentication_Protocol
|
||||
// for a list of available EAP methods. There are a lot (40!) of them.
|
||||
export const EAPMethods = [
|
||||
'MD5',
|
||||
'POTP',
|
||||
'GTC',
|
||||
'TLS',
|
||||
'IKEv2',
|
||||
'SIM',
|
||||
'AKA',
|
||||
'AKA\'',
|
||||
'TTLS',
|
||||
'PWD',
|
||||
'LEAP',
|
||||
'PSK',
|
||||
'FAST',
|
||||
'TEAP',
|
||||
'EKE',
|
||||
'NOOB',
|
||||
'PEAP',
|
||||
] as const;
|
||||
export type EAPMethod = typeof EAPMethods[number];
|
||||
|
||||
export const EAPPhase2Methods = [
|
||||
'None',
|
||||
'MSCHAPV2',
|
||||
] as const;
|
||||
export type EAPPhase2Method = typeof EAPPhase2Methods[number];
|
||||
|
||||
interface IWifiQRCodeOptions {
|
||||
ssid: MaybeRef<string>
|
||||
password: MaybeRef<string>
|
||||
eapMethod: MaybeRef<EAPMethod>
|
||||
isHiddenSSID: MaybeRef<boolean>
|
||||
eapAnonymous: MaybeRef<boolean>
|
||||
eapIdentity: MaybeRef<string>
|
||||
eapPhase2Method: MaybeRef<EAPPhase2Method>
|
||||
color: { foreground: MaybeRef<string>; background: MaybeRef<string> }
|
||||
options?: QRCodeToDataURLOptions
|
||||
}
|
||||
|
||||
interface GetQrCodeTextOptions {
|
||||
ssid: string
|
||||
password: string
|
||||
encryption: WifiEncryption
|
||||
eapMethod: EAPMethod
|
||||
isHiddenSSID: boolean
|
||||
eapAnonymous: boolean
|
||||
eapIdentity: string
|
||||
eapPhase2Method: EAPPhase2Method
|
||||
}
|
||||
|
||||
function escapeString(str: string) {
|
||||
// replaces \, ;, ,, " and : with the same character preceded by a backslash
|
||||
return str.replace(/([\\;,:"])/g, '\\$1');
|
||||
}
|
||||
|
||||
function getQrCodeText(options: GetQrCodeTextOptions): string | null {
|
||||
const { ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method } = options;
|
||||
if (!ssid) {
|
||||
return null;
|
||||
}
|
||||
if (encryption === 'nopass') {
|
||||
return `WIFI:S:${escapeString(ssid)};;`; // type can be omitted in that case, and password is not needed, makes the QR Code smaller
|
||||
}
|
||||
if (encryption !== 'WPA2-EAP' && password) {
|
||||
// EAP has a lot of options, so we'll handle it separately
|
||||
// WPA and WEP are pretty simple though.
|
||||
return `WIFI:S:${escapeString(ssid)};T:${encryption};P:${escapeString(password)};${isHiddenSSID ? 'H:true' : ''};`;
|
||||
}
|
||||
if (encryption === 'WPA2-EAP' && password && eapMethod) {
|
||||
// WPA2-EAP string is a lot more complex, first off, we drop the text if there is no identity, and it's not anonymous.
|
||||
if (!eapIdentity && !eapAnonymous) {
|
||||
return null;
|
||||
}
|
||||
// From reading, I could only find that a phase 2 is required for the PEAP method, I may be wrong though, I didn't read the whole spec.
|
||||
if (eapMethod === 'PEAP' && !eapPhase2Method) {
|
||||
return null;
|
||||
}
|
||||
// The string is built in the following order:
|
||||
// 1. SSID
|
||||
// 2. Authentication type
|
||||
// 3. Password
|
||||
// 4. EAP method
|
||||
// 5. EAP phase 2 method
|
||||
// 6. Identity or anonymous if checked
|
||||
// 7. Hidden SSID if checked
|
||||
const identity = eapAnonymous ? 'A:anon' : `I:${escapeString(eapIdentity)}`;
|
||||
const phase2 = eapPhase2Method !== 'None' ? `PH2:${eapPhase2Method};` : '';
|
||||
return `WIFI:S:${escapeString(ssid)};T:WPA2-EAP;P:${escapeString(password)};E:${eapMethod};${phase2}${identity};${isHiddenSSID ? 'H:true' : ''};`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useWifiQRCode({
|
||||
ssid,
|
||||
password,
|
||||
eapMethod,
|
||||
isHiddenSSID,
|
||||
eapAnonymous,
|
||||
eapIdentity,
|
||||
eapPhase2Method,
|
||||
color: { background, foreground },
|
||||
options,
|
||||
}: IWifiQRCodeOptions) {
|
||||
const qrcode = ref('');
|
||||
const encryption = ref<WifiEncryption>('WPA');
|
||||
|
||||
watch(
|
||||
[ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method, background, foreground].filter(isRef),
|
||||
async () => {
|
||||
// @see https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
|
||||
// This is the full spec, there's quite a bit of logic to generate the string embeddedin the QR code.
|
||||
const text = getQrCodeText({
|
||||
ssid: get(ssid),
|
||||
password: get(password),
|
||||
encryption: get(encryption),
|
||||
eapMethod: get(eapMethod),
|
||||
isHiddenSSID: get(isHiddenSSID),
|
||||
eapAnonymous: get(eapAnonymous),
|
||||
eapIdentity: get(eapIdentity),
|
||||
eapPhase2Method: get(eapPhase2Method),
|
||||
});
|
||||
if (text) {
|
||||
qrcode.value = await QRCode.toDataURL(get(text).trim(), {
|
||||
color: {
|
||||
dark: get(foreground),
|
||||
light: get(background),
|
||||
...options?.color,
|
||||
},
|
||||
errorCorrectionLevel: 'M',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
return { qrcode, encryption };
|
||||
}
|
||||
153
src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
Normal file
153
src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EAPMethods,
|
||||
EAPPhase2Methods,
|
||||
useWifiQRCode,
|
||||
} from './useQRCode';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
|
||||
const foreground = ref('#000000ff');
|
||||
const background = ref('#ffffffff');
|
||||
|
||||
const ssid = ref();
|
||||
const password = ref();
|
||||
const eapMethod = ref();
|
||||
const isHiddenSSID = ref(false);
|
||||
const eapAnonymous = ref(false);
|
||||
const eapIdentity = ref();
|
||||
const eapPhase2Method = ref();
|
||||
|
||||
const { qrcode, encryption } = useWifiQRCode({
|
||||
ssid,
|
||||
password,
|
||||
eapMethod,
|
||||
isHiddenSSID,
|
||||
eapAnonymous,
|
||||
eapIdentity,
|
||||
eapPhase2Method,
|
||||
color: {
|
||||
background,
|
||||
foreground,
|
||||
},
|
||||
options: { width: 1024 },
|
||||
});
|
||||
|
||||
const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<div grid grid-cols-1 gap-12>
|
||||
<div>
|
||||
<c-select
|
||||
v-model:value="encryption"
|
||||
mb-4
|
||||
label="Encryption method"
|
||||
default-value="WPA"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
:options="[
|
||||
{
|
||||
label: 'No password',
|
||||
value: 'nopass',
|
||||
},
|
||||
{
|
||||
label: 'WPA/WPA2',
|
||||
value: 'WPA',
|
||||
},
|
||||
{
|
||||
label: 'WEP',
|
||||
value: 'WEP',
|
||||
},
|
||||
{
|
||||
label: 'WPA2-EAP',
|
||||
value: 'WPA2-EAP',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="mb-6 flex flex-row items-center gap-2">
|
||||
<c-input-text
|
||||
v-model:value="ssid"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
label="SSID:"
|
||||
rows="1"
|
||||
autosize
|
||||
placeholder="Your WiFi SSID..."
|
||||
mb-6
|
||||
/>
|
||||
<n-checkbox v-model:checked="isHiddenSSID">
|
||||
Hidden SSID
|
||||
</n-checkbox>
|
||||
</div>
|
||||
<c-input-text
|
||||
v-if="encryption !== 'nopass'"
|
||||
v-model:value="password"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
label="Password:"
|
||||
rows="1"
|
||||
autosize
|
||||
type="password"
|
||||
placeholder="Your WiFi Password..."
|
||||
mb-6
|
||||
/>
|
||||
<c-select
|
||||
v-if="encryption === 'WPA2-EAP'"
|
||||
v-model:value="eapMethod"
|
||||
label="EAP method"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
:options="EAPMethods.map((method) => ({ label: method, value: method }))"
|
||||
searchable mb-4
|
||||
/>
|
||||
<div v-if="encryption === 'WPA2-EAP'" class="mb-6 flex flex-row items-center gap-2">
|
||||
<c-input-text
|
||||
v-model:value="eapIdentity"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
label="Identity:"
|
||||
rows="1"
|
||||
autosize
|
||||
placeholder="Your EAP Identity..."
|
||||
mb-6
|
||||
/>
|
||||
<n-checkbox v-model:checked="eapAnonymous">
|
||||
Anonymous?
|
||||
</n-checkbox>
|
||||
</div>
|
||||
<c-select
|
||||
v-if="encryption === 'WPA2-EAP'"
|
||||
v-model:value="eapPhase2Method"
|
||||
label="EAP Phase 2 method"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
:options="EAPPhase2Methods.map((method) => ({ label: method, value: method }))"
|
||||
searchable mb-4
|
||||
/>
|
||||
<n-form label-width="130" label-placement="left">
|
||||
<n-form-item label="Foreground color:">
|
||||
<n-color-picker v-model:value="foreground" :modes="['hex']" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Background color:">
|
||||
<n-color-picker v-model:value="background" :modes="['hex']" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
<div v-if="qrcode">
|
||||
<div flex flex-col items-center gap-3>
|
||||
<img alt="wifi-qrcode" :src="qrcode" width="200">
|
||||
<c-button @click="download">
|
||||
Download qr-code
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
14
src/ui/c-buttons-select/c-buttons-select.demo.vue
Normal file
14
src/ui/c-buttons-select/c-buttons-select.demo.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
const optionsA = [
|
||||
{ label: 'Option A', value: 'a' },
|
||||
{ label: 'Option B', value: 'b', tooltip: 'This is a tooltip' },
|
||||
{ label: 'Option C', value: 'c' },
|
||||
];
|
||||
const valueA = ref('a');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " />
|
||||
<c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " label-position="left" mt-2 />
|
||||
<c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " label-position="left" mt-2 />
|
||||
</template>
|
||||
5
src/ui/c-buttons-select/c-buttons-select.types.ts
Normal file
5
src/ui/c-buttons-select/c-buttons-select.types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { CSelectOption } from '../c-select/c-select.types';
|
||||
|
||||
export type CButtonSelectOption<T> = CSelectOption<T> & {
|
||||
tooltip?: string
|
||||
};
|
||||
59
src/ui/c-buttons-select/c-buttons-select.vue
Normal file
59
src/ui/c-buttons-select/c-buttons-select.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts" generic="T extends unknown">
|
||||
import type { CLabelProps } from '../c-label/c-label.types';
|
||||
import type { CButtonSelectOption } from './c-buttons-select.types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
options?: CButtonSelectOption<T>[] | string[]
|
||||
value?: T
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
} & CLabelProps >(),
|
||||
{
|
||||
options: () => [],
|
||||
value: undefined,
|
||||
labelPosition: 'left',
|
||||
size: 'medium',
|
||||
},
|
||||
);
|
||||
|
||||
const emits = defineEmits(['update:value']);
|
||||
|
||||
const { options: rawOptions, size } = toRefs(props);
|
||||
|
||||
const options = computed(() => {
|
||||
return rawOptions.value.map((option: string | CButtonSelectOption<T>) => {
|
||||
if (typeof option === 'string') {
|
||||
return { label: option, value: option };
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
});
|
||||
|
||||
const value = useVModel(props, 'value', emits);
|
||||
|
||||
function selectOption(option: CButtonSelectOption<T>) {
|
||||
// @ts-expect-error vue template generic is a bit flacky thanks to withDefaults
|
||||
value.value = option.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-label v-bind="props">
|
||||
<div class="flex gap-2">
|
||||
<c-tooltip
|
||||
v-for="option in options" :key="option.value"
|
||||
:tooltip="option.tooltip"
|
||||
>
|
||||
<c-button
|
||||
:test-id="option.value"
|
||||
:size="size"
|
||||
:type="option.value === value ? 'primary' : 'default'"
|
||||
@click="selectOption(option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
</c-label>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user