mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-11-02 04:53:15 +00:00
Compare commits
42 Commits
card-hover
...
v2023.11.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2614990e3 | ||
|
|
79646375f3 | ||
|
|
7d94e11cee | ||
|
|
e87f4b1837 | ||
|
|
e86fd96ae3 | ||
|
|
58de8970f5 | ||
|
|
02b0d0d1a1 | ||
|
|
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 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
playwright-report
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
test-results
|
||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- 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:
|
outputs:
|
||||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- name: print latest_commit
|
- name: print latest_commit
|
||||||
run: echo ${{ github.sha }}
|
run: echo ${{ github.sha }}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
@@ -54,29 +54,29 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
4
.github/workflows/e2e-tests.yml
vendored
4
.github/workflows/e2e-tests.yml
vendored
@@ -6,13 +6,13 @@ on:
|
|||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 10
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
shard: [1/3, 2/3, 3/3]
|
shard: [1/3, 2/3, 3/3]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
|
|||||||
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
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
|
|||||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -2,6 +2,78 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## Version 2023.11.02-7d94e11
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **i18n**: language selector (#710) (e86fd96)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **dockerfile**: revert replacement of nginx image with non-privileged one (#716) (7d94e11)
|
||||||
|
- **encryption**: alert on decryption error (#711) (02b0d0d)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **math-evaluator**: improved description (e87f4b1)
|
||||||
|
- **math-evaluator**: improved search and UX (#713) (58de897)
|
||||||
|
|
||||||
|
## 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
|
## Version 2023.08.21-6f93cba
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# build stage
|
# build stage
|
||||||
FROM node:lts-alpine AS 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
|
WORKDIR /app
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN npm install -g pnpm && pnpm i --frozen-lockfile
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install -g pnpm
|
|
||||||
RUN pnpm i --frozen-lockfile
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# production stage
|
# production stage
|
||||||
@@ -11,4 +14,4 @@ FROM nginx:stable-alpine AS production-stage
|
|||||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -105,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.
|
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
|
## Credits
|
||||||
|
|
||||||
Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
|
Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
|
||||||
|
|
||||||
This project is continuously deployed using [vercel.com](https://vercel.com).
|
This project is continuously deployed using [vercel.com](https://vercel.com).
|
||||||
|
|
||||||
|
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-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>
|
<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>
|
||||||
13
components.d.ts
vendored
13
components.d.ts
vendored
@@ -25,6 +25,8 @@ declare module '@vue/runtime-core' {
|
|||||||
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
|
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
|
||||||
CButton: typeof import('./src/ui/c-button/c-button.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']
|
'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: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||||
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.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']
|
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
||||||
@@ -47,6 +49,8 @@ declare module '@vue/runtime-core' {
|
|||||||
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
|
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
|
||||||
CSelect: typeof import('./src/ui/c-select/c-select.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']
|
'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: 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']
|
'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: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default']
|
||||||
@@ -88,11 +92,13 @@ declare module '@vue/runtime-core' {
|
|||||||
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||||
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||||
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||||
|
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
|
||||||
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
||||||
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||||
IconMdiRecord: typeof import('~icons/mdi/record')['default']
|
IconMdiRecord: typeof import('~icons/mdi/record')['default']
|
||||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||||
|
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||||
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||||
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||||
@@ -109,7 +115,9 @@ declare module '@vue/runtime-core' {
|
|||||||
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
|
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
|
||||||
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
|
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
|
||||||
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
|
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
|
||||||
|
LocaleSelector: typeof import('./src/modules/i18n/components/locale-selector.vue')['default']
|
||||||
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.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']
|
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
|
||||||
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.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']
|
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
|
||||||
@@ -144,8 +152,6 @@ declare module '@vue/runtime-core' {
|
|||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||||
NMenu: typeof import('naive-ui')['NMenu']
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
NP: typeof import('naive-ui')['NP']
|
|
||||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
@@ -153,7 +159,6 @@ declare module '@vue/runtime-core' {
|
|||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTable: typeof import('naive-ui')['NTable']
|
NTable: typeof import('naive-ui')['NTable']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
|
||||||
NUpload: typeof import('naive-ui')['NUpload']
|
NUpload: typeof import('naive-ui')['NUpload']
|
||||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||||
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||||
@@ -176,6 +181,7 @@ declare module '@vue/runtime-core' {
|
|||||||
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
||||||
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
|
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
|
||||||
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.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']
|
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']
|
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']
|
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
|
||||||
@@ -183,6 +189,7 @@ declare module '@vue/runtime-core' {
|
|||||||
TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default']
|
TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default']
|
||||||
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
|
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
|
||||||
ToolCard: typeof import('./src/components/ToolCard.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']
|
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
|
||||||
UrlParser: typeof import('./src/tools/url-parser/url-parser.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']
|
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
|
||||||
|
|||||||
@@ -1,4 +1,65 @@
|
|||||||
home:
|
home:
|
||||||
categories:
|
categories:
|
||||||
newestTools: Newest tools
|
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
|
||||||
|
search:
|
||||||
|
label: Search
|
||||||
|
tools:
|
||||||
|
categories:
|
||||||
|
favorite-tools: 'Your favorite tools'
|
||||||
|
crypto: Crypto
|
||||||
|
converter: Converter
|
||||||
|
web: Web
|
||||||
|
images and videos: 'Images & Videos'
|
||||||
|
development: Development
|
||||||
|
network: Network
|
||||||
|
math: Math
|
||||||
|
measurement: Measurement
|
||||||
|
text: Text
|
||||||
|
data: Data
|
||||||
|
|||||||
@@ -1,3 +1,49 @@
|
|||||||
home:
|
home:
|
||||||
categories:
|
categories:
|
||||||
newestTools: "Nouveaux outils"
|
newestTools: 'Les nouveaux outils'
|
||||||
|
favoriteTools: 'Vos outils favoris'
|
||||||
|
allTools: 'Tous les outils'
|
||||||
|
subtitle: 'Outils pour les développeurs'
|
||||||
|
toggleMenu: 'Menu'
|
||||||
|
home: Accueil
|
||||||
|
uiLib: 'UI Lib'
|
||||||
|
buyMeACoffee: 'Soutenez IT-Tools'
|
||||||
|
follow:
|
||||||
|
title: 'Vous aimez it-tools ?'
|
||||||
|
p1: 'Soutenez-nous avec une star sur'
|
||||||
|
githubRepository: "le dépôt GitHub d'IT-Tools"
|
||||||
|
p2: 'ou suivez-nous sur'
|
||||||
|
twitterAccount: "le compte Twitter d'IT-Tools"
|
||||||
|
thankYou: 'Merci !'
|
||||||
|
nav:
|
||||||
|
github: 'Dépôt GitHub'
|
||||||
|
githubRepository: "Dépôt GitHub d'IT-Tools"
|
||||||
|
twitter: 'Compte Twitter'
|
||||||
|
twitterAccount: "Compte Twitter d'IT-Tools"
|
||||||
|
about: "À propos d'IT-Tools"
|
||||||
|
aboutLabel: 'À propos'
|
||||||
|
darkMode: 'Mode sombre'
|
||||||
|
lightMode: 'Mode clair'
|
||||||
|
mode: 'Basculer le mode sombre/clair'
|
||||||
|
404:
|
||||||
|
notFound: '404 Not Found'
|
||||||
|
sorry: "Désolé, cette page n'existe pas"
|
||||||
|
maybe: 'Peut-être que le cache fait des siennes, essayez de forcer le rafraîchissement ?'
|
||||||
|
backHome: "Retour à l'accueil"
|
||||||
|
toolCard:
|
||||||
|
new: Nouveau
|
||||||
|
search:
|
||||||
|
label: Rechercher
|
||||||
|
tools:
|
||||||
|
categories:
|
||||||
|
favorite-tools: 'Vos outils favoris'
|
||||||
|
crypto: Cryptographie
|
||||||
|
converter: Convertisseur
|
||||||
|
web: Web
|
||||||
|
images and videos: 'Images & Vidéos'
|
||||||
|
development: Développement
|
||||||
|
network: Réseau
|
||||||
|
math: Math
|
||||||
|
measurement: Mesure
|
||||||
|
text: Texte
|
||||||
|
data: Données
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "it-tools",
|
"name": "it-tools",
|
||||||
"version": "2023.8.21-6f93cba",
|
"version": "2023.11.2-7d94e11",
|
||||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"productivity",
|
"productivity",
|
||||||
@@ -30,16 +30,17 @@
|
|||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage",
|
||||||
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
"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",
|
"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"
|
"release": "node ./scripts/release.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@it-tools/bip39": "^0.0.4",
|
"@it-tools/bip39": "^0.0.4",
|
||||||
"@it-tools/oggen": "^1.3.0",
|
"@it-tools/oggen": "^1.3.0",
|
||||||
"@sindresorhus/slugify": "^2.2.0",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@tiptap/pm": "^2.1.6",
|
"@tiptap/pm": "2.1.6",
|
||||||
"@tiptap/starter-kit": "^2.1.6",
|
"@tiptap/starter-kit": "2.1.6",
|
||||||
"@tiptap/vue-3": "^2.0.3",
|
"@tiptap/vue-3": "2.0.3",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vicons/tabler": "^0.12.0",
|
"@vicons/tabler": "^0.12.0",
|
||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.3.0",
|
||||||
@@ -66,17 +67,17 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mathjs": "^11.9.1",
|
"mathjs": "^11.9.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"monaco-editor": "^0.41.0",
|
"monaco-editor": "^0.43.0",
|
||||||
"naive-ui": "^2.34.3",
|
"naive-ui": "^2.35.0",
|
||||||
"netmask": "^2.0.2",
|
"netmask": "^2.0.2",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"oui": "^12.0.52",
|
"oui": "^12.0.52",
|
||||||
"pinia": "^2.0.34",
|
"pinia": "^2.0.34",
|
||||||
"plausible-tracker": "^0.3.8",
|
"plausible-tracker": "^0.3.8",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"randombytes": "^2.1.0",
|
|
||||||
"sql-formatter": "^13.0.0",
|
"sql-formatter": "^13.0.0",
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
|
"ulid": "^2.3.0",
|
||||||
"unicode-emoji-json": "^0.4.0",
|
"unicode-emoji-json": "^0.4.0",
|
||||||
"unplugin-auto-import": "^0.16.4",
|
"unplugin-auto-import": "^0.16.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
@@ -103,7 +104,6 @@
|
|||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"@types/node-forge": "^1.3.2",
|
"@types/node-forge": "^1.3.2",
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/randombytes": "^2.0.0",
|
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"@unocss/eslint-config": "^0.55.0",
|
"@unocss/eslint-config": "^0.55.0",
|
||||||
@@ -113,9 +113,9 @@
|
|||||||
"@vue/runtime-dom": "^3.3.4",
|
"@vue/runtime-dom": "^3.3.4",
|
||||||
"@vue/test-utils": "^2.3.2",
|
"@vue/test-utils": "^2.3.2",
|
||||||
"@vue/tsconfig": "^0.4.0",
|
"@vue/tsconfig": "^0.4.0",
|
||||||
"c8": "^8.0.0",
|
|
||||||
"consola": "^3.0.2",
|
"consola": "^3.0.2",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.47.0",
|
||||||
|
"hygen": "^6.2.11",
|
||||||
"jsdom": "^22.0.0",
|
"jsdom": "^22.0.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
|
|||||||
2154
pnpm-lock.yaml
generated
2154
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,13 @@ const styleStore = useStyleStore();
|
|||||||
|
|
||||||
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
|
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
|
||||||
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
|
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
syncRef(
|
||||||
|
locale,
|
||||||
|
useStorage('locale', locale),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const menuOptions = computed(() =>
|
|||||||
tools: components.map(tool => ({
|
tools: components.map(tool => ({
|
||||||
label: makeLabel(tool),
|
label: makeLabel(tool),
|
||||||
icon: makeIcon(tool),
|
icon: makeIcon(tool),
|
||||||
key: tool.name,
|
key: tool.path,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@@ -62,7 +62,7 @@ const themeVars = useThemeVars();
|
|||||||
|
|
||||||
<n-menu
|
<n-menu
|
||||||
class="menu"
|
class="menu"
|
||||||
:value="route.name as string"
|
:value="route.path"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:collapsed-icon-size="22"
|
:collapsed-icon-size="22"
|
||||||
:options="tools"
|
:options="tools"
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FavoriteFilled } from '@vicons/material';
|
|
||||||
|
|
||||||
import { useToolStore } from '@/tools/tools.store';
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
import type { Tool } from '@/tools/tools.types';
|
import type { Tool } from '@/tools/tools.types';
|
||||||
|
|
||||||
@@ -26,18 +24,15 @@ function toggleFavorite(event: MouseEvent) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip :tooltip="isFavorite ? 'Remove from favorites' : 'Add to favorites' ">
|
||||||
<template #trigger>
|
<c-button
|
||||||
<c-button
|
variant="text"
|
||||||
variant="text"
|
circle
|
||||||
circle
|
:type="buttonType"
|
||||||
:type="buttonType"
|
:style="{ opacity: isFavorite ? 1 : 0.2 }"
|
||||||
:style="{ opacity: isFavorite ? 1 : 0.2 }"
|
@click="toggleFavorite"
|
||||||
@click="toggleFavorite"
|
>
|
||||||
>
|
<icon-mdi-heart />
|
||||||
<n-icon :component="FavoriteFilled" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,14 +13,11 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to cli
|
|||||||
<template>
|
<template>
|
||||||
<c-input-text v-model:value="value">
|
<c-input-text v-model:value="value">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip :tooltip="tooltipText">
|
||||||
<template #trigger>
|
<c-button circle variant="text" size="small" @click="copy()">
|
||||||
<c-button circle variant="text" size="small" @click="copy()">
|
<icon-mdi-content-copy />
|
||||||
<icon-mdi-content-copy />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
{{ tooltipText }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
</c-input-text>
|
</c-input-text>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,56 +7,43 @@ const { isDarkTheme } = toRefs(styleStore);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip :tooltip="$t('home.nav.github')" position="bottom">
|
||||||
<template #trigger>
|
<c-button
|
||||||
<c-button
|
circle
|
||||||
circle
|
variant="text"
|
||||||
variant="text"
|
href="https://github.com/CorentinTh/it-tools"
|
||||||
href="https://github.com/CorentinTh/it-tools"
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
:aria-label="$t('home.nav.githubRepository')"
|
||||||
aria-label="IT-Tools' GitHub repository"
|
>
|
||||||
>
|
<n-icon size="25" :component="BrandGithub" />
|
||||||
<n-icon size="25" :component="BrandGithub" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
Github repository
|
|
||||||
</n-tooltip>
|
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip :tooltip="$t('home.nav.twitter')" position="bottom">
|
||||||
<template #trigger>
|
<c-button
|
||||||
<c-button
|
circle
|
||||||
circle
|
variant="text"
|
||||||
variant="text"
|
href="https://twitter.com/ittoolsdottech"
|
||||||
href="https://twitter.com/ittoolsdottech"
|
rel="noopener"
|
||||||
rel="noopener"
|
target="_blank"
|
||||||
target="_blank"
|
:aria-label="$t('home.nav.twitterAccount')"
|
||||||
aria-label="IT Tools' Twitter account"
|
>
|
||||||
>
|
<n-icon size="25" :component="BrandTwitter" />
|
||||||
<n-icon size="25" :component="BrandTwitter" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
IT Tools' Twitter account
|
|
||||||
</n-tooltip>
|
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip :tooltip="$t('home.nav.about')" position="bottom">
|
||||||
<template #trigger>
|
<c-button circle variant="text" to="/about" :aria-label="$t('home.nav.aboutLabel')">
|
||||||
<c-button circle variant="text" to="/about" aria-label="About">
|
<n-icon size="25" :component="InfoCircle" />
|
||||||
<n-icon size="25" :component="InfoCircle" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
<c-tooltip :tooltip="isDarkTheme ? $t('home.nav.lightMode') : $t('home.nav.darkMode')" position="bottom">
|
||||||
About
|
<c-button circle variant="text" :aria-label="$t('home.nav.mode')" @click="() => styleStore.toggleDark()">
|
||||||
</n-tooltip>
|
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||||
<n-tooltip trigger="hover">
|
<n-icon v-else size="25" :component="Moon" />
|
||||||
<template #trigger>
|
</c-button>
|
||||||
<c-button circle variant="text" aria-label="Toggle dark/light mode" @click="() => styleStore.toggleDark()">
|
</c-tooltip>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@@ -11,17 +11,7 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip :tooltip="tooltipText">
|
||||||
<template #trigger>
|
<span cursor-pointer font-mono @click="copy()">{{ value }}</span>
|
||||||
<span class="value" @click="copy()">{{ value }}</span>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
{{ tooltipText }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.value {
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="overflow-x: hidden; width: 100%">
|
<div style="overflow-x: hidden; width: 100%">
|
||||||
<c-card class="result-card">
|
<c-card relative>
|
||||||
<n-scrollbar
|
<n-scrollbar
|
||||||
x-scrollable
|
x-scrollable
|
||||||
trigger="none"
|
trigger="none"
|
||||||
@@ -50,16 +50,13 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.
|
|||||||
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
<n-tooltip v-if="value" trigger="hover">
|
<div absolute right-10px top-10px>
|
||||||
<template #trigger>
|
<c-tooltip v-if="value" :tooltip="tooltipText" position="left">
|
||||||
<div class="copy-button" :class="[copyPlacement]">
|
<c-button circle important:h-10 important:w-10 @click="copy()">
|
||||||
<c-button circle important:h-10 important:w-10 @click="copy()">
|
<n-icon size="22" :component="Copy" />
|
||||||
<n-icon size="22" :component="Copy" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<span>{{ tooltipText }}</span>
|
|
||||||
</n-tooltip>
|
|
||||||
</c-card>
|
</c-card>
|
||||||
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
|
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
|
||||||
<c-button @click="copy()">
|
<c-button @click="copy()">
|
||||||
@@ -74,25 +71,4 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
margin-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>
|
</style>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const appTheme = useAppTheme();
|
|||||||
:bordered="false"
|
:bordered="false"
|
||||||
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
|
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
|
||||||
>
|
>
|
||||||
New
|
{{ $t('toolCard.new') }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
|
||||||
<FavoriteButton :tool="tool" />
|
<FavoriteButton :tool="tool" />
|
||||||
|
|||||||
22
src/composable/computed/catchedComputed.ts
Normal file
22
src/composable/computed/catchedComputed.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { type Ref, ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
export { computedCatch };
|
||||||
|
|
||||||
|
function computedCatch<T, D>(getter: () => T, { defaultValue }: { defaultValue: D; defaultErrorMessage?: string }): [Ref<T | D>, Ref<string | undefined>];
|
||||||
|
function computedCatch<T, D>(getter: () => T, { defaultValue, defaultErrorMessage = 'Unknown error' }: { defaultValue?: D; defaultErrorMessage?: string } = {}) {
|
||||||
|
const error = ref<string | undefined>();
|
||||||
|
const value = ref<T | D | undefined>();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
try {
|
||||||
|
error.value = undefined;
|
||||||
|
value.value = getter();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : err?.toString() ?? defaultErrorMessage;
|
||||||
|
value.value = defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [value, error] as const;
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ import { NIcon, useThemeVars } from 'naive-ui';
|
|||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
import { Heart, Home2, Menu2 } from '@vicons/tabler';
|
import { Heart, Home2, Menu2 } from '@vicons/tabler';
|
||||||
|
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||||
import MenuLayout from '../components/MenuLayout.vue';
|
import MenuLayout from '../components/MenuLayout.vue';
|
||||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||||
import { toolsByCategory } from '@/tools';
|
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import type { ToolCategory } from '@/tools/tools.types';
|
import type { ToolCategory } from '@/tools/tools.types';
|
||||||
@@ -21,12 +21,14 @@ const version = config.app.version;
|
|||||||
const commitSha = config.app.lastCommitSha.slice(0, 7);
|
const commitSha = config.app.lastCommitSha.slice(0, 7);
|
||||||
|
|
||||||
const { tracker } = useTracker();
|
const { tracker } = useTracker();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const toolStore = useToolStore();
|
const toolStore = useToolStore();
|
||||||
|
const { favoriteTools, toolsByCategory } = storeToRefs(toolStore);
|
||||||
|
|
||||||
const tools = computed<ToolCategory[]>(() => [
|
const tools = computed<ToolCategory[]>(() => [
|
||||||
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
|
...(favoriteTools.value.length > 0 ? [{ name: t('tools.categories.favorite-tools'), components: favoriteTools.value }] : []),
|
||||||
...toolsByCategory,
|
...toolsByCategory.value,
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -41,14 +43,18 @@ const tools = computed<ToolCategory[]>(() => [
|
|||||||
</div>
|
</div>
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
Handy tools for developers
|
{{ $t('home.subtitle') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<div class="sider-content">
|
<div class="sider-content">
|
||||||
<div v-if="styleStore.isSmallScreen" flex justify-center>
|
<div v-if="styleStore.isSmallScreen" flex flex-col items-center>
|
||||||
<NavbarButtons />
|
<locale-selector w="90%" />
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<NavbarButtons />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CollapsibleToolMenu :tools-by-category="tools" />
|
<CollapsibleToolMenu :tools-by-category="tools" />
|
||||||
@@ -88,48 +94,46 @@ const tools = computed<ToolCategory[]>(() => [
|
|||||||
<c-button
|
<c-button
|
||||||
circle
|
circle
|
||||||
variant="text"
|
variant="text"
|
||||||
aria-label="Toggle menu"
|
:aria-label="$t('home.toggleMenu')"
|
||||||
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
||||||
>
|
>
|
||||||
<NIcon size="25" :component="Menu2" />
|
<NIcon size="25" :component="Menu2" />
|
||||||
</c-button>
|
</c-button>
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip tooltip="Home" position="bottom">
|
||||||
<template #trigger>
|
<c-button to="/" circle variant="text" :aria-label="$t('home.home')">
|
||||||
<c-button to="/" circle variant="text" aria-label="Home">
|
<NIcon size="25" :component="Home2" />
|
||||||
<NIcon size="25" :component="Home2" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
Home
|
|
||||||
</n-tooltip>
|
|
||||||
|
|
||||||
<c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" aria-label="UI Lib">
|
<c-tooltip tooltip="UI Lib" position="bottom">
|
||||||
<icon-mdi:brush-variant text-20px />
|
<c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" :aria-label="$t('home.uiLib')">
|
||||||
</c-button>
|
<icon-mdi:brush-variant text-20px />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
|
||||||
<command-palette />
|
<command-palette />
|
||||||
|
|
||||||
|
<locale-selector v-if="!styleStore.isSmallScreen" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip position="bottom" tooltip="Support IT Tools development">
|
||||||
<template #trigger>
|
<c-button
|
||||||
<c-button
|
round
|
||||||
round
|
href="https://www.buymeacoffee.com/cthmsst"
|
||||||
href="https://www.buymeacoffee.com/cthmsst"
|
rel="noopener"
|
||||||
rel="noopener"
|
target="_blank"
|
||||||
target="_blank"
|
class="support-button"
|
||||||
class="support-button"
|
:bordered="false"
|
||||||
:bordered="false"
|
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
>
|
||||||
>
|
{{ $t('home.buyMeACoffee') }}
|
||||||
Buy me a coffee
|
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
||||||
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
❤ Support IT Tools development !
|
|
||||||
</n-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SunIcon from '~icons/mdi/white-balance-sunny';
|
|||||||
import GithubIcon from '~icons/mdi/github';
|
import GithubIcon from '~icons/mdi/github';
|
||||||
import BugIcon from '~icons/mdi/bug-outline';
|
import BugIcon from '~icons/mdi/bug-outline';
|
||||||
import DiceIcon from '~icons/mdi/dice-5';
|
import DiceIcon from '~icons/mdi/dice-5';
|
||||||
|
import InfoIcon from '~icons/mdi/information-outline';
|
||||||
|
|
||||||
export const useCommandPaletteStore = defineStore('command-palette', () => {
|
export const useCommandPaletteStore = defineStore('command-palette', () => {
|
||||||
const toolStore = useToolStore();
|
const toolStore = useToolStore();
|
||||||
@@ -61,6 +62,14 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
|
|||||||
keywords: ['report', 'issue', 'bug', 'problem', 'error'],
|
keywords: ['report', 'issue', 'bug', 'problem', 'error'],
|
||||||
icon: BugIcon,
|
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({
|
const { searchResult } = useFuzzySearch({
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ function open() {
|
|||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
isModalOpen.value = false;
|
isModalOpen.value = false;
|
||||||
|
searchPrompt.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOptionIndex = ref(0);
|
const selectedOptionIndex = ref(0);
|
||||||
@@ -115,7 +116,7 @@ function activateOption(option: PaletteOption) {
|
|||||||
<span flex items-center gap-3 op-40>
|
<span flex items-center gap-3 op-40>
|
||||||
|
|
||||||
<icon-mdi-search />
|
<icon-mdi-search />
|
||||||
Search...
|
{{ $t('search.label') }}
|
||||||
|
|
||||||
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
|
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
|
||||||
{{ isMac ? 'Cmd' : 'Ctrl' }} + K
|
{{ isMac ? 'Cmd' : 'Ctrl' }} + K
|
||||||
|
|||||||
28
src/modules/i18n/components/locale-selector.vue
Normal file
28
src/modules/i18n/components/locale-selector.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { availableLocales, locale } = useI18n();
|
||||||
|
|
||||||
|
const localesLong: Record<string, string> = {
|
||||||
|
en: 'English',
|
||||||
|
es: 'Español',
|
||||||
|
fr: 'Français',
|
||||||
|
pt: 'Português',
|
||||||
|
ru: 'Русский',
|
||||||
|
zh: '中文',
|
||||||
|
};
|
||||||
|
|
||||||
|
const localeOptions = computed(() =>
|
||||||
|
availableLocales.map(locale => ({
|
||||||
|
label: localesLong[locale] ?? locale,
|
||||||
|
value: locale,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-select
|
||||||
|
v-model:value="locale"
|
||||||
|
:options="localeOptions"
|
||||||
|
placeholder="Select a language"
|
||||||
|
w-100px
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -11,17 +11,17 @@ useHead({ title: 'Page not found - IT Tools' });
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h1 m-0 mt-3>
|
<h1 m-0 mt-3>
|
||||||
404 Not Found
|
{{ $t('404.notFound') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div mt-4 op-60>
|
<div mt-4 op-60>
|
||||||
Sorry, this page does not seem to exist
|
{{ $t('404.sorry') }}
|
||||||
</div>
|
</div>
|
||||||
<div mb-8 op-60>
|
<div mb-8 op-60>
|
||||||
Maybe the cache is doing tricky things, try force-refreshing?
|
{{ $t('404.maybe') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<c-button to="/">
|
<c-button to="/">
|
||||||
Back home
|
{{ $t('404.backHome') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,79 +7,57 @@ const { tracker } = useTracker();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="about-page">
|
<div mx-auto mt-50px max-w-600px>
|
||||||
<n-h1>About</n-h1>
|
<h1>{{ $t('about.h1') }}</h1>
|
||||||
<n-p>
|
<p text-justify>
|
||||||
This wonderful website, made with ❤ by
|
{{ $t('about.h1p1') }}
|
||||||
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener">
|
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener">
|
||||||
Corentin Thomasset
|
Corentin Thomasset
|
||||||
</c-link>,
|
</c-link>{{ $t('about.h1p2') }}
|
||||||
aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share
|
</p>
|
||||||
it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar!
|
<p text-justify>
|
||||||
</n-p>
|
{{ $t('about.h1p3') }}
|
||||||
<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
|
<c-link
|
||||||
href="https://www.buymeacoffee.com/cthmsst"
|
href="https://www.buymeacoffee.com/cthmsst"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||||
>
|
>
|
||||||
sponsoring me
|
{{ $t('about.h1p4') }}
|
||||||
</c-link>.
|
</c-link>.
|
||||||
</n-p>
|
</p>
|
||||||
|
|
||||||
<n-h2>Technologies</n-h2>
|
<h2>{{ $t('about.h2') }}</h2>
|
||||||
<n-p>
|
<p text-justify>
|
||||||
IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed
|
{{ $t('about.h2p1') }}
|
||||||
by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the
|
|
||||||
<c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank">
|
<c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank">
|
||||||
package.json
|
package.json
|
||||||
</c-link>
|
</c-link>
|
||||||
file of the repository.
|
{{ $t('about.h2p2') }}
|
||||||
</n-p>
|
</p>
|
||||||
|
|
||||||
<n-h2>Found a bug? A tool is missing?</n-h2>
|
<h2>{{ $t('about.h3') }}</h2>
|
||||||
<n-p>
|
<p text-justify>
|
||||||
If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a
|
{{ $t('about.h3p1') }}
|
||||||
feature request in the
|
|
||||||
<c-link
|
<c-link
|
||||||
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
issues section
|
{{ $t('about.h3p2') }}
|
||||||
</c-link>
|
</c-link>
|
||||||
in the GitHub repository.
|
{{ $t('about.h3p3') }}
|
||||||
</n-p>
|
</p>
|
||||||
<n-p>
|
<p text-justify>
|
||||||
And if you found a bug, or something doesn't work as expected, please file a bug report in the
|
{{ $t('about.h3p4') }}
|
||||||
<c-link
|
<c-link
|
||||||
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
issues section
|
{{ $t('about.h3p5') }}
|
||||||
</c-link>
|
</c-link>
|
||||||
in the GitHub repository.
|
{{ $t('about.h3p6') }}
|
||||||
</n-p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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,22 @@ const { t } = useI18n();
|
|||||||
<div class="grid-wrapper">
|
<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-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<ColoredCard title="You like it-tools?" :icon="Heart">
|
<ColoredCard :title="$t('home.follow.title')" :icon="Heart">
|
||||||
Give us a star on
|
{{ $t('home.follow.p1') }}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/CorentinTh/it-tools"
|
href="https://github.com/CorentinTh/it-tools"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="IT-Tools' GitHub repository"
|
:aria-label="$t('home.follow.githubRepository')"
|
||||||
>GitHub</a>
|
>GitHub</a>
|
||||||
or follow us on
|
{{ $t('home.follow.p2') }}
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ittoolsdottech"
|
href="https://twitter.com/ittoolsdottech"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="IT-Tools' Twitter account"
|
:aria-label="$t('home.follow.twitterAccount')"
|
||||||
>Twitter</a>! Thank you
|
>Twitter</a>.
|
||||||
|
{{ $t('home.follow.thankYou') }}
|
||||||
<n-icon :component="Heart" />
|
<n-icon :component="Heart" />
|
||||||
</ColoredCard>
|
</ColoredCard>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
@@ -39,7 +40,7 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
<transition name="height">
|
<transition name="height">
|
||||||
<div v-if="toolStore.favoriteTools.length > 0">
|
<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-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">
|
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
||||||
<ToolCard :tool="tool" />
|
<ToolCard :tool="tool" />
|
||||||
@@ -57,7 +58,7 @@ const { t } = useI18n();
|
|||||||
</n-grid>
|
</n-grid>
|
||||||
</div>
|
</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-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">
|
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
|
||||||
<transition>
|
<transition>
|
||||||
|
|||||||
@@ -29,3 +29,9 @@ export const i18nPlugin: Plugin = {
|
|||||||
app.use(i18n);
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ const results = computed(() => {
|
|||||||
const { copy } = useCopy({ createToast: false });
|
const { copy } = useCopy({ createToast: false });
|
||||||
|
|
||||||
const header = {
|
const header = {
|
||||||
|
position: 'Position',
|
||||||
title: 'Suite',
|
title: 'Suite',
|
||||||
size: 'Samples',
|
size: 'Samples',
|
||||||
mean: 'Mean',
|
mean: 'Mean',
|
||||||
variance: 'Variance',
|
variance: 'Variance',
|
||||||
position: 'Position',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function copyAsMarkdown() {
|
function copyAsMarkdown() {
|
||||||
@@ -131,26 +131,8 @@ function copyAsBulletList() {
|
|||||||
</c-button>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-table>
|
<c-table :data="results" :headers="header" />
|
||||||
<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>
|
|
||||||
<div mt-5 flex justify-center gap-3>
|
<div mt-5 flex justify-center gap-3>
|
||||||
<c-button @click="copyAsMarkdown()">
|
<c-button @click="copyAsMarkdown()">
|
||||||
Copy as markdown table
|
Copy as markdown table
|
||||||
|
|||||||
@@ -39,14 +39,11 @@ function onInputEnter(index: number) {
|
|||||||
autofocus
|
autofocus
|
||||||
@keydown.enter="onInputEnter(index)"
|
@keydown.enter="onInputEnter(index)"
|
||||||
/>
|
/>
|
||||||
<n-tooltip>
|
<c-tooltip tooltip="Delete this value">
|
||||||
<template #trigger>
|
<c-button circle variant="text" @click="values.splice(index, 1)">
|
||||||
<c-button circle variant="text" @click="values.splice(index, 1)">
|
<n-icon :component="Trash" depth="3" size="18" />
|
||||||
<n-icon :component="Trash" depth="3" size="18" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
Delete value
|
|
||||||
</n-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<c-button @click="addValue">
|
<c-button @click="addValue">
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ const formats = computed(() => [
|
|||||||
label: 'Snakecase:',
|
label: 'Snakecase:',
|
||||||
value: snakeCase(input.value, baseConfig),
|
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 = {
|
const inputLabelAlignmentConfig = {
|
||||||
|
|||||||
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">
|
<script setup lang="ts">
|
||||||
|
import type { Colord } from 'colord';
|
||||||
import { colord, extend } from 'colord';
|
import { colord, extend } from 'colord';
|
||||||
|
import _ from 'lodash';
|
||||||
import cmykPlugin from 'colord/plugins/cmyk';
|
import cmykPlugin from 'colord/plugins/cmyk';
|
||||||
import hwbPlugin from 'colord/plugins/hwb';
|
import hwbPlugin from 'colord/plugins/hwb';
|
||||||
import namesPlugin from 'colord/plugins/names';
|
import namesPlugin from 'colord/plugins/names';
|
||||||
import lchPlugin from 'colord/plugins/lch';
|
import lchPlugin from 'colord/plugins/lch';
|
||||||
import InputCopyable from '../../components/InputCopyable.vue';
|
import { buildColorFormat } from './color-converter.models';
|
||||||
|
|
||||||
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
||||||
|
|
||||||
const name = ref('');
|
const formats = {
|
||||||
const hex = ref('#1ea54cff');
|
picker: buildColorFormat({
|
||||||
const rgb = ref('');
|
label: 'color picker',
|
||||||
const hsl = ref('');
|
format: (v: Colord) => v.toHex(),
|
||||||
const hwb = ref('');
|
type: 'color-picker',
|
||||||
const cmyk = ref('');
|
}),
|
||||||
const lch = ref('');
|
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) {
|
updateColorValue(colord('#1ea54c'));
|
||||||
try {
|
|
||||||
const color = colord(value);
|
|
||||||
|
|
||||||
if (omit !== 'name') {
|
function updateColorValue(value: Colord | undefined, omitLabel?: string) {
|
||||||
name.value = color.toName({ closest: true }) ?? '';
|
if (value === undefined) {
|
||||||
}
|
return;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
//
|
if (!value.isValid()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_.forEach(formats, ({ value: valueRef, format }, key) => {
|
||||||
|
if (key !== omitLabel) {
|
||||||
|
valueRef.value = format(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputUpdated(hex.value, 'hex');
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<c-card>
|
<c-card>
|
||||||
<n-form label-width="100" label-placement="left">
|
<template v-for="({ label, parse, placeholder, validation, type }, key) in formats" :key="key">
|
||||||
<n-form-item label="color picker:">
|
<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
|
<n-color-picker
|
||||||
v-model:value="hex"
|
v-model:value="formats[key].value.value"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@update:value="(v: string) => onInputUpdated(v, 'hex')"
|
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="color name:">
|
</template>
|
||||||
<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>
|
|
||||||
</c-card>
|
</c-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -167,34 +167,8 @@ const cronValidationRules = [
|
|||||||
</div>
|
</div>
|
||||||
</c-card>
|
</c-card>
|
||||||
</div>
|
</div>
|
||||||
<n-table v-else size="small">
|
|
||||||
<thead>
|
<c-table v-else :data="helpers" />
|
||||||
<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-card>
|
</c-card>
|
||||||
</template>
|
</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('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('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('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 { describe, expect, test } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
dateToExcelFormat,
|
||||||
|
excelFormatToDate,
|
||||||
|
isExcelFormat,
|
||||||
isISO8601DateTimeString,
|
isISO8601DateTimeString,
|
||||||
isISO9075DateString,
|
isISO9075DateString,
|
||||||
isMongoObjectId,
|
isMongoObjectId,
|
||||||
@@ -139,4 +142,39 @@ describe('date-time-converter models', () => {
|
|||||||
expect(isMongoObjectId('')).toBe(false);
|
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,
|
isTimestamp,
|
||||||
isUTCDateString,
|
isUTCDateString,
|
||||||
isMongoObjectId,
|
isMongoObjectId,
|
||||||
|
dateToExcelFormat,
|
||||||
|
excelFormatToDate,
|
||||||
|
isExcelFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ISO8601_REGEX
|
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 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) {
|
function createRegexMatcher(regex: RegExp) {
|
||||||
return (date?: string) => !_.isNil(date) && regex.test(date);
|
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 isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
|
||||||
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
|
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
|
||||||
|
|
||||||
|
const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);
|
||||||
|
|
||||||
function isUTCDateString(date?: string) {
|
function isUTCDateString(date?: string) {
|
||||||
if (_.isNil(date)) {
|
if (_.isNil(date)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -45,3 +52,11 @@ function isUTCDateString(date?: string) {
|
|||||||
return false;
|
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';
|
} from 'date-fns';
|
||||||
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
||||||
import {
|
import {
|
||||||
|
dateToExcelFormat,
|
||||||
|
excelFormatToDate,
|
||||||
|
isExcelFormat,
|
||||||
isISO8601DateTimeString,
|
isISO8601DateTimeString,
|
||||||
isISO9075DateString,
|
isISO9075DateString,
|
||||||
isMongoObjectId,
|
isMongoObjectId,
|
||||||
@@ -85,6 +88,12 @@ const formats: DateFormat[] = [
|
|||||||
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
|
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||||
formatMatcher: date => isMongoObjectId(date),
|
formatMatcher: date => isMongoObjectId(date),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Excel date/time',
|
||||||
|
fromDate: date => dateToExcelFormat(date),
|
||||||
|
toDate: excelFormatToDate,
|
||||||
|
formatMatcher: isExcelFormat,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const formatIndex = ref(6);
|
const formatIndex = ref(6);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js';
|
import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js';
|
||||||
|
import { computedCatch } from '@/composable/computed/catchedComputed';
|
||||||
|
|
||||||
const algos = { AES, TripleDES, Rabbit, RC4 };
|
const algos = { AES, TripleDES, Rabbit, RC4 };
|
||||||
|
|
||||||
@@ -11,9 +12,10 @@ const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.
|
|||||||
const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs');
|
const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs');
|
||||||
const decryptAlgo = ref<keyof typeof algos>('AES');
|
const decryptAlgo = ref<keyof typeof algos>('AES');
|
||||||
const decryptSecret = ref('my secret key');
|
const decryptSecret = ref('my secret key');
|
||||||
const decryptOutput = computed(() =>
|
const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), {
|
||||||
algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8),
|
defaultValue: '',
|
||||||
);
|
defaultErrorMessage: 'Unable to decrypt your text',
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -63,7 +65,11 @@ const decryptOutput = computed(() =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<c-alert v-if="decryptError" type="error" mt-12 title="Error while decrypting">
|
||||||
|
{{ decryptError }}
|
||||||
|
</c-alert>
|
||||||
<c-input-text
|
<c-input-text
|
||||||
|
v-else
|
||||||
label="Your decrypted text:"
|
label="Your decrypted text:"
|
||||||
:value="decryptOutput"
|
:value="decryptOutput"
|
||||||
placeholder="Your string hash"
|
placeholder="Your string hash"
|
||||||
|
|||||||
@@ -6,13 +6,9 @@ const { icon, title, action, isActive } = toRefs(props);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip :tooltip="title">
|
||||||
<template #trigger>
|
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
|
||||||
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
|
<n-icon :component="icon" />
|
||||||
<n-icon :component="icon" />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
|
|
||||||
{{ title }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
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 ibanValidatorAndParser } from './iban-validator-and-parser';
|
||||||
import { tool as stringObfuscator } from './string-obfuscator';
|
import { tool as stringObfuscator } from './string-obfuscator';
|
||||||
import { tool as textDiff } from './text-diff';
|
import { tool as textDiff } from './text-diff';
|
||||||
@@ -74,7 +77,7 @@ import { tool as xmlFormatter } from './xml-formatter';
|
|||||||
export const toolsByCategory: ToolCategory[] = [
|
export const toolsByCategory: ToolCategory[] = [
|
||||||
{
|
{
|
||||||
name: 'Crypto',
|
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',
|
name: 'Converter',
|
||||||
@@ -87,6 +90,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||||||
colorConverter,
|
colorConverter,
|
||||||
caseConverter,
|
caseConverter,
|
||||||
textToNatoAlphabet,
|
textToNatoAlphabet,
|
||||||
|
textToBinary,
|
||||||
yamlToJson,
|
yamlToJson,
|
||||||
yamlToToml,
|
yamlToToml,
|
||||||
jsonToYaml,
|
jsonToYaml,
|
||||||
@@ -137,7 +141,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Network',
|
name: 'Network',
|
||||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator],
|
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Math',
|
name: 'Math',
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
@@ -4,10 +4,13 @@ import { defineTool } from '../tool';
|
|||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'Math evaluator',
|
name: 'Math evaluator',
|
||||||
path: '/math-evaluator',
|
path: '/math-evaluator',
|
||||||
description: 'Evaluate math expression, like a calculator on steroid (you can use function like sqrt, cos, sin, abs, ...)',
|
description: 'A calculator for evaluating mathematical expressions. You can use functions like sqrt, cos, sin, abs, etc.',
|
||||||
keywords: [
|
keywords: [
|
||||||
'math',
|
'math',
|
||||||
'evaluator',
|
'evaluator',
|
||||||
|
'calculator',
|
||||||
|
'expression',
|
||||||
|
'abs',
|
||||||
'acos',
|
'acos',
|
||||||
'acosh',
|
'acosh',
|
||||||
'acot',
|
'acot',
|
||||||
@@ -31,6 +34,7 @@ export const tool = defineTool({
|
|||||||
'sech',
|
'sech',
|
||||||
'sin',
|
'sin',
|
||||||
'sinh',
|
'sinh',
|
||||||
|
'sqrt',
|
||||||
'tan',
|
'tan',
|
||||||
'tanh',
|
'tanh',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const result = computed(() => withDefaultOnError(() => evaluate(expression.value
|
|||||||
multiline
|
multiline
|
||||||
placeholder="Your math expression (ex: 2*sqrt(6) )..."
|
placeholder="Your math expression (ex: 2*sqrt(6) )..."
|
||||||
raw-text
|
raw-text
|
||||||
|
monospace
|
||||||
|
autofocus
|
||||||
|
autosize
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<c-card v-if="result !== ''" title="Result " mt-5>
|
<c-card v-if="result !== ''" title="Result " mt-5>
|
||||||
|
|||||||
@@ -61,19 +61,16 @@ const secretValidationRules = [
|
|||||||
:validation-rules="secretValidationRules"
|
:validation-rules="secretValidationRules"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-tooltip trigger="hover">
|
<c-tooltip tooltip="Generate a new random secret">
|
||||||
<template #trigger>
|
<c-button circle variant="text" size="small" @click="refreshSecret">
|
||||||
<c-button circle variant="text" size="small" @click="refreshSecret">
|
<icon-mdi-refresh />
|
||||||
<icon-mdi-refresh />
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
Generate secret token
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
</c-input-text>
|
</c-input-text>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<TokenDisplay :tokens="tokens" style="margin-top: 2px" />
|
<TokenDisplay :tokens="tokens" />
|
||||||
|
|
||||||
<n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" />
|
<n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" />
|
||||||
<div style="text-align: center">
|
<div style="text-align: center">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const { tokens } = toRefs(props);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="labels" w-full flex items-center>
|
<div mb-5px w-full flex items-center>
|
||||||
<div flex-1 text-left>
|
<div flex-1 text-left>
|
||||||
Previous
|
Previous
|
||||||
</div>
|
</div>
|
||||||
@@ -22,60 +22,24 @@ const { tokens } = toRefs(props);
|
|||||||
Next
|
Next
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n-input-group>
|
<div flex items-center>
|
||||||
<n-tooltip trigger="hover" placement="bottom">
|
<c-tooltip :tooltip="previousCopied ? 'Copied !' : 'Copy previous OTP'" position="bottom" flex-1>
|
||||||
<template #trigger>
|
<c-button data-test-id="previous-otp" w-full important:h-12 important:rounded-r-none important:font-mono @click.prevent="copyPrevious(tokens.previous)">
|
||||||
<c-button important:h-12 data-test-id="previous-otp" @click.prevent="copyPrevious(tokens.previous)">
|
{{ tokens.previous }}
|
||||||
{{ tokens.previous }}
|
</c-button>
|
||||||
</c-button>
|
</c-tooltip>
|
||||||
</template>
|
<c-tooltip :tooltip="currentCopied ? 'Copied !' : 'Copy current OTP'" position="bottom" flex-1 flex-basis-5xl>
|
||||||
<div>{{ previousCopied ? 'Copied !' : 'Copy previous OTP' }}</div>
|
<c-button
|
||||||
</n-tooltip>
|
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)"
|
||||||
<n-tooltip trigger="hover" placement="bottom">
|
>
|
||||||
<template #trigger>
|
{{ tokens.current }}
|
||||||
<c-button
|
</c-button>
|
||||||
data-test-id="current-otp"
|
</c-tooltip>
|
||||||
class="current-otp"
|
<c-tooltip :tooltip="nextCopied ? 'Copied !' : 'Copy next OTP'" position="bottom" flex-1>
|
||||||
important:h-12
|
<c-button data-test-id="next-otp" w-full important:h-12 important:rounded-l-none @click.prevent="copyNext(tokens.next)">
|
||||||
@click.prevent="copyCurrent(tokens.current)"
|
{{ tokens.next }}
|
||||||
>
|
</c-button>
|
||||||
{{ tokens.current }}
|
</c-tooltip>
|
||||||
</c-button>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</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>
|
|
||||||
|
|||||||
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 { ArrowsShuffle } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'Token generator',
|
name: translate('tools.token-generator.title'),
|
||||||
path: '/token-generator',
|
path: '/token-generator',
|
||||||
description:
|
description: translate('tools.token-generator.description'),
|
||||||
'Generate random string with the chars you want: uppercase or lowercase letters, numbers and/or symbols.',
|
|
||||||
keywords: ['token', 'random', 'string', 'alphanumeric', 'symbols', 'number', 'letters', 'lowercase', 'uppercase'],
|
keywords: ['token', 'random', 'string', 'alphanumeric', 'symbols', 'number', 'letters', 'lowercase', 'uppercase'],
|
||||||
component: () => import('./token-generator.tool.vue'),
|
component: () => import('./token-generator.tool.vue'),
|
||||||
icon: ArrowsShuffle,
|
icon: ArrowsShuffle,
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ tools:
|
|||||||
uppercase: Uppercase (ABC...)
|
uppercase: Uppercase (ABC...)
|
||||||
lowercase: Lowercase (abc...)
|
lowercase: Lowercase (abc...)
|
||||||
numbers: Numbers (123...)
|
numbers: Numbers (123...)
|
||||||
symbols: Symbols (!-;...)
|
symbols: Symbols (!-;...)
|
||||||
|
length: Length
|
||||||
|
tokenPlaceholder: 'The token...'
|
||||||
|
copied: Token copied to the clipboard
|
||||||
|
button:
|
||||||
|
copy: Copy
|
||||||
|
refresh: Refresh
|
||||||
@@ -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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -51,14 +51,14 @@ const { copy } = useCopy({ source: token, text: 'Token copied to the clipboard'
|
|||||||
</div>
|
</div>
|
||||||
</n-form>
|
</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-slider v-model:value="length" :step="1" :min="1" :max="512" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<c-input-text
|
<c-input-text
|
||||||
v-model:value="token"
|
v-model:value="token"
|
||||||
multiline
|
multiline
|
||||||
placeholder="The token..."
|
:placeholder="t('tools.token-generator.tokenPlaceholder')"
|
||||||
readonly
|
readonly
|
||||||
rows="3"
|
rows="3"
|
||||||
autosize
|
autosize
|
||||||
@@ -67,10 +67,10 @@ const { copy } = useCopy({ source: token, text: 'Token copied to the clipboard'
|
|||||||
|
|
||||||
<div mt-5 flex justify-center gap-3>
|
<div mt-5 flex justify-center gap-3>
|
||||||
<c-button @click="copy()">
|
<c-button @click="copy()">
|
||||||
Copy
|
{{ t('tools.token-generator.button.copy') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
<c-button @click="refreshToken">
|
<c-button @click="refreshToken">
|
||||||
Refresh
|
{{ t('tools.token-generator.button.refresh') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
</c-card>
|
</c-card>
|
||||||
|
|||||||
@@ -1,44 +1,57 @@
|
|||||||
import { type MaybeRef, get, useStorage } from '@vueuse/core';
|
import { type MaybeRef, get, useStorage } from '@vueuse/core';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { Tool, ToolWithCategory } from './tools.types';
|
import _ from 'lodash';
|
||||||
|
import type { Tool, ToolCategory, ToolWithCategory } from './tools.types';
|
||||||
import { toolsWithCategory } from './index';
|
import { toolsWithCategory } from './index';
|
||||||
|
|
||||||
export const useToolStore = defineStore('tools', {
|
export const useToolStore = defineStore('tools', () => {
|
||||||
state: () => ({
|
const favoriteToolsName = useStorage('favoriteToolsName', []) as Ref<string[]>;
|
||||||
favoriteToolsName: useStorage('favoriteToolsName', []) as Ref<string[]>,
|
const { t } = useI18n();
|
||||||
}),
|
|
||||||
getters: {
|
|
||||||
favoriteTools(state) {
|
|
||||||
return state.favoriteToolsName
|
|
||||||
.map(favoriteName => toolsWithCategory.find(({ name }) => name === favoriteName))
|
|
||||||
.filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
|
|
||||||
},
|
|
||||||
|
|
||||||
notFavoriteTools(state): ToolWithCategory[] {
|
const tools = computed<ToolWithCategory[]>(() => toolsWithCategory.map((tool) => {
|
||||||
return toolsWithCategory.filter(tool => !state.favoriteToolsName.includes(tool.name));
|
const toolI18nKey = tool.path.replace(/\//g, '');
|
||||||
},
|
|
||||||
|
|
||||||
tools(): ToolWithCategory[] {
|
return ({
|
||||||
return toolsWithCategory;
|
...tool,
|
||||||
},
|
name: t(`tools.${toolI18nKey}.title`, tool.name),
|
||||||
|
description: t(`tools.${toolI18nKey}.description`, tool.description),
|
||||||
|
category: t(`tools.categories.${tool.category.toLowerCase()}`, tool.category),
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
newTools(): ToolWithCategory[] {
|
const toolsByCategory = computed<ToolCategory[]>(() => {
|
||||||
return this.tools.filter(({ isNew }) => isNew);
|
return _.chain(tools.value)
|
||||||
},
|
.groupBy('category')
|
||||||
},
|
.map((components, name) => ({
|
||||||
|
name,
|
||||||
|
components,
|
||||||
|
}))
|
||||||
|
.value();
|
||||||
|
});
|
||||||
|
|
||||||
|
const favoriteTools = computed(() => {
|
||||||
|
return favoriteToolsName.value
|
||||||
|
.map(favoriteName => tools.value.find(({ name }) => name === favoriteName))
|
||||||
|
.filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
favoriteTools,
|
||||||
|
toolsByCategory,
|
||||||
|
newTools: computed(() => tools.value.filter(({ isNew }) => isNew)),
|
||||||
|
|
||||||
actions: {
|
|
||||||
addToolToFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
addToolToFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
||||||
this.favoriteToolsName.push(get(tool).name);
|
favoriteToolsName.value.push(get(tool).name);
|
||||||
},
|
},
|
||||||
|
|
||||||
removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
||||||
this.favoriteToolsName = this.favoriteToolsName.filter(name => get(tool).name !== name);
|
favoriteToolsName.value = favoriteToolsName.value.filter(name => get(tool).name !== name);
|
||||||
},
|
},
|
||||||
|
|
||||||
isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) {
|
isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) {
|
||||||
return this.favoriteToolsName.includes(get(tool).name);
|
return favoriteToolsName.value.includes(get(tool).name);
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
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-grid :x-gap="12" :y-gap="8" cols="1 s:2" responsive="screen">
|
||||||
<n-gi v-for="{ heading, icon, content } in sections" :key="heading">
|
<n-gi v-for="{ heading, icon, content } in sections" :key="heading">
|
||||||
<c-card h-full>
|
<c-card h-full>
|
||||||
<n-page-header>
|
<div flex items-center gap-3>
|
||||||
<template #title>
|
<n-icon size="30" :component="icon" :depth="3" />
|
||||||
{{ heading }}
|
<span text-lg>{{ heading }}</span>
|
||||||
</template>
|
</div>
|
||||||
<template v-if="icon" #avatar>
|
|
||||||
<n-icon size="30" :component="icon" :depth="3" />
|
|
||||||
</template>
|
|
||||||
</n-page-header>
|
|
||||||
|
|
||||||
<div mt-5 flex gap-2>
|
<div mt-5 flex gap-2>
|
||||||
<span v-for="{ label, getValue } in content" :key="label">
|
<span v-for="{ label, getValue } in content" :key="label">
|
||||||
<n-tooltip v-if="getValue(userAgentInfo)" trigger="hover">
|
<c-tooltip v-if="getValue(userAgentInfo)" :tooltip="label">
|
||||||
<template #trigger>
|
<n-tag type="success" size="large" round :bordered="false">
|
||||||
<n-tag type="success" size="large" round :bordered="false">
|
{{ getValue(userAgentInfo) }}
|
||||||
{{ getValue(userAgentInfo) }}
|
</n-tag>
|
||||||
</n-tag>
|
</c-tooltip>
|
||||||
</template>
|
|
||||||
{{ label }}
|
|
||||||
</n-tooltip>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div flex flex-col>
|
<div flex flex-col>
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const variants = ['warning'] as const;
|
const variants = ['warning', 'error'] as const;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<h2>Basic</h2>
|
||||||
<c-alert v-for="variant in variants" :key="variant" :type="variant" mb-4>
|
<c-alert v-for="variant in variants" :key="variant" :type="variant" mb-4>
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Magni reprehenderit itaque enim? Suscipit magni optio velit
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Magni reprehenderit itaque enim? Suscipit magni optio velit
|
||||||
quia, eveniet repellat pariatur quaerat laudantium dignissimos natus, beatae deleniti adipisci, atque necessitatibus
|
quia, eveniet repellat pariatur quaerat laudantium dignissimos natus, beatae deleniti adipisci, atque necessitatibus
|
||||||
odio!
|
odio!
|
||||||
</c-alert>
|
</c-alert>
|
||||||
|
|
||||||
|
<h2>With title</h2>
|
||||||
|
<c-alert v-for="variant in variants" :key="variant" :type="variant" title="This is the title" mb-4>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Magni reprehenderit itaque enim? Suscipit magni optio velit
|
||||||
|
quia, eveniet repellat pariatur quaerat laudantium dignissimos natus, beatae deleniti adipisci, atque necessitatibus
|
||||||
|
odio!
|
||||||
|
</c-alert>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { defineThemes } from '../theme/theme.models';
|
|||||||
import { appThemes } from '../theme/themes';
|
import { appThemes } from '../theme/themes';
|
||||||
|
|
||||||
import WarningIcon from '~icons/mdi/alert-circle-outline';
|
import WarningIcon from '~icons/mdi/alert-circle-outline';
|
||||||
|
import ErrorIcon from '~icons/mdi/close-circle-outline';
|
||||||
|
|
||||||
export const { useTheme } = defineThemes({
|
export const { useTheme } = defineThemes({
|
||||||
dark: {
|
dark: {
|
||||||
@@ -12,6 +13,12 @@ export const { useTheme } = defineThemes({
|
|||||||
textColor: appThemes.dark.warning.color,
|
textColor: appThemes.dark.warning.color,
|
||||||
icon: WarningIcon,
|
icon: WarningIcon,
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: appThemes.dark.error.colorFaded,
|
||||||
|
borderColor: appThemes.dark.error.color,
|
||||||
|
textColor: appThemes.dark.error.color,
|
||||||
|
icon: ErrorIcon,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
light: {
|
light: {
|
||||||
warning: {
|
warning: {
|
||||||
@@ -20,5 +27,11 @@ export const { useTheme } = defineThemes({
|
|||||||
textColor: darken(appThemes.light.warning.color, 40),
|
textColor: darken(appThemes.light.warning.color, 40),
|
||||||
icon: WarningIcon,
|
icon: WarningIcon,
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: appThemes.light.error.colorFaded,
|
||||||
|
borderColor: appThemes.light.error.color,
|
||||||
|
textColor: darken(appThemes.light.error.color, 40),
|
||||||
|
icon: ErrorIcon,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useTheme } from './c-alert.theme';
|
import { useTheme } from './c-alert.theme';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ type?: 'warning' }>(), { type: 'warning' });
|
const props = withDefaults(defineProps<{ type?: 'warning'; title?: string }>(), { type: 'warning', title: undefined });
|
||||||
const { type } = toRefs(props);
|
const { type, title } = toRefs(props);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const variantTheme = computed(() => theme.value[type.value]);
|
const variantTheme = computed(() => theme.value[type.value]);
|
||||||
@@ -17,6 +17,9 @@ const variantTheme = computed(() => theme.value[type.value]);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="c-alert--content">
|
<div class="c-alert--content">
|
||||||
|
<div v-if="title" class="c-alert--title" text-15px fw-600>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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>
|
||||||
@@ -33,4 +33,19 @@ const value = ref('');
|
|||||||
<c-select label="Label" label-position="left" label-align="left" mb-2 label-width="200px" />
|
<c-select label="Label" label-position="left" label-align="left" mb-2 label-width="200px" />
|
||||||
<c-select label="Label" label-position="left" label-align="center" mb-2 label-width="200px" />
|
<c-select label="Label" label-position="left" label-align="center" mb-2 label-width="200px" />
|
||||||
<c-select label="Label" label-position="left" label-align="right" mb-2 label-width="200px" />
|
<c-select label="Label" label-position="left" label-align="right" mb-2 label-width="200px" />
|
||||||
|
|
||||||
|
<h2>Custom displayed value</h2>
|
||||||
|
<c-select v-model:value="value" :options="optionsA" mb-2>
|
||||||
|
<template #displayed-value>
|
||||||
|
<span class="font-bold lh-normal">Hello</span>
|
||||||
|
</template>
|
||||||
|
</c-select>
|
||||||
|
|
||||||
|
<c-select v-model:value="value" :options="optionsA">
|
||||||
|
<template #displayed-value>
|
||||||
|
<span lh-normal>
|
||||||
|
<icon-mdi-translate />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</c-select>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -150,13 +150,15 @@ function onSearchInput() {
|
|||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
>
|
>
|
||||||
<div flex-1 truncate>
|
<div flex-1 truncate>
|
||||||
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
|
<slot name="displayed-value">
|
||||||
<span v-else-if="selectedOption" lh-normal>
|
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
|
||||||
{{ selectedOption.label }}
|
<span v-else-if="selectedOption" lh-normal>
|
||||||
</span>
|
{{ selectedOption.label }}
|
||||||
<span v-else class="placeholder" lh-normal>
|
</span>
|
||||||
{{ placeholder ?? 'Select an option' }}
|
<span v-else class="placeholder" lh-normal>
|
||||||
</span>
|
{{ placeholder ?? 'Select an option' }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<icon-mdi-chevron-down class="chevron" />
|
<icon-mdi-chevron-down class="chevron" />
|
||||||
|
|||||||
20
src/ui/c-table/c-table.demo.vue
Normal file
20
src/ui/c-table/c-table.demo.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const data = ref([
|
||||||
|
{ name: 'John', age: 20 },
|
||||||
|
{ name: 'Jane', age: 24 },
|
||||||
|
{ name: 'Joe', age: 30 },
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-table :data="data" mb-2 />
|
||||||
|
<c-table :data="data" hide-headers mb-2 />
|
||||||
|
<c-table :data="data" :headers="['age', 'name']" mb-2 />
|
||||||
|
<c-table :data="data" :headers="['age', { key: 'name', label: 'Full name' }]" mb-2 />
|
||||||
|
<c-table :data="data" :headers="{ name: 'full name' }" mb-2 />
|
||||||
|
<c-table :data="data" :headers="['age', 'name']">
|
||||||
|
<template #age="{ value }">
|
||||||
|
{{ value }}yo
|
||||||
|
</template>
|
||||||
|
</c-table>
|
||||||
|
</template>
|
||||||
4
src/ui/c-table/c-table.types.ts
Normal file
4
src/ui/c-table/c-table.types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type HeaderConfiguration = (string | {
|
||||||
|
key: string
|
||||||
|
label?: string
|
||||||
|
})[] | Record<string, string>;
|
||||||
65
src/ui/c-table/c-table.vue
Normal file
65
src/ui/c-table/c-table.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import _ from 'lodash';
|
||||||
|
import type { HeaderConfiguration } from './c-table.types';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ data?: Record<string, unknown>[]; headers?: HeaderConfiguration ; hideHeaders?: boolean; description?: string }>(), { data: () => [], headers: undefined, hideHeaders: false, description: 'Data table' });
|
||||||
|
const { data, headers: rawHeaders, hideHeaders } = toRefs(props);
|
||||||
|
|
||||||
|
const headers = computed(() => {
|
||||||
|
if (rawHeaders.value) {
|
||||||
|
if (Array.isArray(rawHeaders.value)) {
|
||||||
|
return rawHeaders.value.map((value) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return { key: value, label: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key, label } = value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label: label ?? key,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.map(rawHeaders.value, (value, key) => ({
|
||||||
|
key, label: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.chain(data.value)
|
||||||
|
.map(row => Object.keys(row))
|
||||||
|
.flatten()
|
||||||
|
.uniq()
|
||||||
|
.map(key => ({ key, label: key }))
|
||||||
|
.value();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative overflow-x-auto rounded">
|
||||||
|
<table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description">
|
||||||
|
<thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
|
||||||
|
{{ header.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(row, i) in data" :key="i" border-b="1px solid dark:#282828 #efeff5" class="bg-white dark:bg-#232323"
|
||||||
|
:class="{
|
||||||
|
'important:border-b-none': i === data.length - 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<td v-for="header in headers" :key="header.key" class="px-6 py-4">
|
||||||
|
<slot :name="header" :row="row" :headers="headers" :value="row[header.key]">
|
||||||
|
{{ row[header.key] }}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const positions = ['top', 'bottom', 'left', 'right'] as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<c-tooltip>
|
<c-tooltip>
|
||||||
@@ -14,4 +18,18 @@
|
|||||||
Hover me
|
Hover me
|
||||||
</c-tooltip>
|
</c-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div mt-5>
|
||||||
|
<h2>Tooltip positions</h2>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<div v-for="position in positions" :key="position">
|
||||||
|
<c-tooltip :position="position" :tooltip="`Tooltip ${position}`">
|
||||||
|
<c-button>
|
||||||
|
{{ position }}
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = withDefaults(defineProps<{ tooltip?: string }>(), { tooltip: '' });
|
const props = withDefaults(defineProps<{ tooltip?: string; position?: 'top' | 'bottom' | 'left' | 'right' }>(), {
|
||||||
const { tooltip } = toRefs(props);
|
tooltip: undefined,
|
||||||
|
position: 'top',
|
||||||
|
});
|
||||||
|
const { tooltip, position } = toRefs(props);
|
||||||
|
|
||||||
const targetRef = ref();
|
const targetRef = ref();
|
||||||
const isTargetHovered = useElementHover(targetRef);
|
const isTargetHovered = useElementHover(targetRef);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative" inline-block>
|
<div relative inline-block>
|
||||||
<div ref="targetRef">
|
<div ref="targetRef">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2"
|
v-if="tooltip || $slots.tooltip"
|
||||||
|
class="absolute z-10 whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s"
|
||||||
:class="{
|
:class="{
|
||||||
'op-0 scale-0': isTargetHovered === false,
|
'op-0 scale-0': isTargetHovered === false,
|
||||||
'op-100 scale-100': isTargetHovered,
|
'op-100 scale-100': isTargetHovered,
|
||||||
|
'bottom-100% left-50% -translate-x-1/2 mb-5px': position === 'top',
|
||||||
|
'top-100% left-50% -translate-x-1/2 mt-5px': position === 'bottom',
|
||||||
|
'right-100% top-50% -translate-y-1/2 mr-5px': position === 'left',
|
||||||
|
'left-100% top-50% -translate-y-1/2 ml-5px': position === 'right',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { demoRoutes } from './demo.routes';
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div grid grid-cols-5 gap-2>
|
<div grid grid-cols-5 gap-2>
|
||||||
<c-card v-for="{ name } of demoRoutes" :key="name" :title="String(name)">
|
<c-button v-for="{ name } of demoRoutes" :key="name" :to="{ name }">
|
||||||
<c-button :to="{ name }">
|
{{ name }}
|
||||||
{{ name }}
|
</c-button>
|
||||||
</c-button>
|
|
||||||
</c-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const componentName = computed(() => _.startCase(String(route.name).replace(/^c-
|
|||||||
<h1>c-lib components</h1>
|
<h1>c-lib components</h1>
|
||||||
|
|
||||||
<div flex>
|
<div flex>
|
||||||
<div w-30 b-r b-gray b-op-10 b-r-solid pr-4>
|
<div w-200px b-r b-gray b-op-10 b-r-solid pr-4>
|
||||||
<c-button
|
<c-button
|
||||||
v-for="{ name } of demoRoutes"
|
v-for="{ name } of demoRoutes"
|
||||||
:key="name"
|
:key="name"
|
||||||
@@ -20,6 +20,7 @@ const componentName = computed(() => _.startCase(String(route.name).replace(/^c-
|
|||||||
:to="{ name }"
|
:to="{ name }"
|
||||||
w-full
|
w-full
|
||||||
important:justify-start
|
important:justify-start
|
||||||
|
important:text-left
|
||||||
:type="route.name === name ? 'primary' : 'default'"
|
:type="route.name === name ? 'primary' : 'default'"
|
||||||
>
|
>
|
||||||
{{ name }}
|
{{ name }}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
import DemoHome from './demo-home.page.vue';
|
import DemoHome from './demo-home.page.vue';
|
||||||
|
|
||||||
const demoPages = import.meta.glob('../*/*.demo.vue');
|
const demoPages = import.meta.glob('../*/*.demo.vue', { eager: true });
|
||||||
|
|
||||||
export const demoRoutes = Object.keys(demoPages).map((path) => {
|
export const demoRoutes = Object.keys(demoPages).map((demoComponentPath) => {
|
||||||
const [, , fileName] = path.split('/');
|
const [, , fileName] = demoComponentPath.split('/');
|
||||||
const name = fileName.split('.').shift();
|
const demoComponentName = fileName.split('.').shift();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: name,
|
path: demoComponentName,
|
||||||
name,
|
name: demoComponentName,
|
||||||
component: () => import(/* @vite-ignore */ path),
|
component: () => import(/* @vite-ignore */ demoComponentPath),
|
||||||
} as RouteRecordRaw;
|
} as RouteRecordRaw;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,18 @@ function macAddressValidation(value: Ref) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { macAddressValidation, macAddressValidationRules };
|
const partialMacAddressValidationRules = [
|
||||||
|
{
|
||||||
|
message: 'Invalid partial MAC address',
|
||||||
|
validator: (value: string) => value.trim().match(/^([0-9a-f]{2}[:\-. ]){0,5}([0-9a-f]{0,2})$/i),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function usePartialMacAddressValidation(value: Ref) {
|
||||||
|
return useValidation({
|
||||||
|
source: value,
|
||||||
|
rules: partialMacAddressValidationRules,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { macAddressValidation, macAddressValidationRules, usePartialMacAddressValidation, partialMacAddressValidationRules };
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ export default defineConfig({
|
|||||||
shortcuts: {
|
shortcuts: {
|
||||||
'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868',
|
'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868',
|
||||||
'divider': 'h-1px bg-current op-10',
|
'divider': 'h-1px bg-current op-10',
|
||||||
|
'bg-surface': 'bg-#ffffff dark:bg-#232323',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user