mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-10-23 04:52:14 +00:00
Compare commits
177 Commits
update-dep
...
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 | ||
|
0eedce69a6 | ||
|
8a30b6bdb3 | ||
|
233d5565f6 | ||
|
7ab9204e96 | ||
|
c7d4562d3b | ||
|
18dd1400bd | ||
|
f035f485c0 | ||
|
e18bae1fca | ||
|
d1dff428d8 | ||
|
3a63837d3d | ||
|
81bfe57cb8 | ||
|
a9cd91ca9c | ||
|
06c35472d3 | ||
|
f3e14fc18f | ||
|
2274766a8f | ||
|
a346175d24 | ||
|
6f93cba3da | ||
|
76b2761d62 | ||
|
6ff9a01cc8 | ||
|
a2b9b157e5 | ||
|
144f86e2dc | ||
|
0f1f6590c5 | ||
|
2bcb77a9f9 | ||
|
c58d6e3423 | ||
|
f235dcd6c1 | ||
|
ba2c589f0f | ||
|
9bd4ad4dfd | ||
|
5e12991bcd | ||
|
65a9474078 | ||
|
85cc7a8447 | ||
|
4268e255de | ||
|
d1c888019b | ||
|
7924456cec | ||
|
99bc84c37e | ||
|
ea0f27cf4c | ||
|
cd5a503fc0 | ||
|
86e964a274 | ||
|
7b6232a151 | ||
|
56d74d07a8 | ||
|
e5d0ba7073 | ||
|
1a602365be | ||
|
a2494219a8 | ||
|
93f7cf0e98 | ||
|
dfa1ba8554 | ||
|
6498c9b0fa | ||
|
f40d7ecddf | ||
|
2e28c5073e | ||
|
d12dd40841 | ||
|
cf382b5a10 | ||
|
01525838e0 | ||
|
38cb61da4d | ||
|
354aed6e6f | ||
|
6b8682fd23 | ||
|
9e8349dbc4 | ||
|
f44050742d | ||
|
b0d9a3e6c7 | ||
|
30f88fc6a8 | ||
|
72c98a3c5e | ||
|
05ea545475 | ||
|
5c3bebfe62 | ||
|
8df7cd0f19 | ||
|
a9c7b89193 | ||
|
6bda2caa04 | ||
|
994a1c3401 | ||
|
05edaf423c | ||
|
8c72e692a7 | ||
|
cec9dea9e0 | ||
|
49eacea195 | ||
|
3f7d469e9f | ||
|
5f2190887d | ||
|
6cb0845336 | ||
|
12d9e5d377 | ||
|
e29b258e90 | ||
|
ea50a3fc65 | ||
|
746e5bdccc | ||
|
c7d4f112c0 | ||
|
9125dcf9c6 | ||
|
38710dce56 | ||
|
282cfc4c4b | ||
|
ba4876d0d5 | ||
|
363c2e47e6 | ||
|
9526ed8324 | ||
|
7068610438 | ||
|
847323ccba | ||
|
1b038c7826 | ||
|
ec4c533718 | ||
|
63045951e1 | ||
|
c4cec9e18f | ||
|
bcb98b359c | ||
|
732da08157 | ||
|
b9406a492d | ||
|
69f0bd079f | ||
|
4cbd7ac145 | ||
|
ebfb872fae | ||
|
a6bbeaebd8 | ||
|
f771e7a99f | ||
|
cf7b1f000a | ||
|
1e2a35b892 | ||
|
45c2474279 | ||
|
fe61f0f2f2 | ||
|
93799af83c | ||
|
962a6d6ec4 | ||
|
d2956b66fe | ||
|
105b21badc | ||
|
33c9b6643f | ||
|
4d2b037dbe | ||
|
34d8e5ce2c | ||
|
8515c24264 | ||
|
0b20f1c16a | ||
|
8c92d56318 | ||
|
7aed9c56fd | ||
|
f7fc779e63 | ||
|
b3b6b7c46b | ||
|
141c12455e | ||
|
77f2efc0b9 | ||
|
aad8d84e13 | ||
|
401f13f7e3 | ||
|
edae4c6915 | ||
|
a43c546e34 | ||
|
83a7b3bae9 | ||
|
ce3150c65d | ||
|
3f6c8f0edd | ||
|
daf2cf0285 | ||
|
b7aaea1b58 | ||
|
92bd83536f | ||
|
e88c1d5f2c | ||
|
362f2fa280 | ||
|
61ece2387f | ||
|
f080933d2a | ||
|
bb32513bd3 | ||
|
c311e3824d | ||
|
74073f5038 | ||
|
c45bce36f9 | ||
|
df989e24b3 | ||
|
6d2202597c |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
playwright-report
|
||||
coverage
|
||||
dist
|
||||
test-results
|
@@ -22,7 +22,9 @@
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createReusableTemplate": true,
|
||||
"createSharedComposable": true,
|
||||
"createTemplatePromise": true,
|
||||
"createUnrefFn": true,
|
||||
"customRef": true,
|
||||
"debouncedRef": true,
|
||||
@@ -42,9 +44,6 @@
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"logicAnd": true,
|
||||
"logicNot": true,
|
||||
"logicOr": true,
|
||||
"makeDestructurable": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
@@ -107,6 +106,19 @@
|
||||
"unrefElement": true,
|
||||
"until": true,
|
||||
"useActiveElement": true,
|
||||
"useAnimate": true,
|
||||
"useArrayDifference": true,
|
||||
"useArrayEvery": true,
|
||||
"useArrayFilter": true,
|
||||
"useArrayFind": true,
|
||||
"useArrayFindIndex": true,
|
||||
"useArrayFindLast": true,
|
||||
"useArrayIncludes": true,
|
||||
"useArrayJoin": true,
|
||||
"useArrayMap": true,
|
||||
"useArrayReduce": true,
|
||||
"useArraySome": true,
|
||||
"useArrayUnique": true,
|
||||
"useAsyncQueue": true,
|
||||
"useAsyncState": true,
|
||||
"useAttrs": true,
|
||||
@@ -117,8 +129,8 @@
|
||||
"useBroadcastChannel": true,
|
||||
"useBrowserLocation": true,
|
||||
"useCached": true,
|
||||
"useClamp": true,
|
||||
"useClipboard": true,
|
||||
"useCloned": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCounter": true,
|
||||
@@ -160,6 +172,7 @@
|
||||
"useFullscreen": true,
|
||||
"useGamepad": true,
|
||||
"useGeolocation": true,
|
||||
"useI18n": true,
|
||||
"useIdle": true,
|
||||
"useImage": true,
|
||||
"useInfiniteScroll": true,
|
||||
@@ -192,12 +205,18 @@
|
||||
"useOnline": true,
|
||||
"usePageLeave": true,
|
||||
"useParallax": true,
|
||||
"useParentElement": true,
|
||||
"usePerformanceObserver": true,
|
||||
"usePermission": true,
|
||||
"usePointer": true,
|
||||
"usePointerLock": true,
|
||||
"usePointerSwipe": true,
|
||||
"usePreferredColorScheme": true,
|
||||
"usePreferredContrast": true,
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
@@ -211,14 +230,17 @@
|
||||
"useSessionStorage": true,
|
||||
"useShare": true,
|
||||
"useSlots": true,
|
||||
"useSorted": true,
|
||||
"useSpeechRecognition": true,
|
||||
"useSpeechSynthesis": true,
|
||||
"useStepper": true,
|
||||
"useStorage": true,
|
||||
"useStorageAsync": true,
|
||||
"useStyleTag": true,
|
||||
"useSupported": true,
|
||||
"useSwipe": true,
|
||||
"useTemplateRefsList": true,
|
||||
"useTextDirection": true,
|
||||
"useTextSelection": true,
|
||||
"useTextareaAutosize": true,
|
||||
"useThrottle": true,
|
||||
@@ -230,6 +252,8 @@
|
||||
"useTimeoutPoll": true,
|
||||
"useTimestamp": true,
|
||||
"useTitle": true,
|
||||
"useToNumber": true,
|
||||
"useToString": true,
|
||||
"useToggle": true,
|
||||
"useTransition": true,
|
||||
"useUrlSearchParams": true,
|
||||
@@ -250,8 +274,10 @@
|
||||
"watchArray": true,
|
||||
"watchAtMost": true,
|
||||
"watchDebounced": true,
|
||||
"watchDeep": true,
|
||||
"watchEffect": true,
|
||||
"watchIgnorable": true,
|
||||
"watchImmediate": true,
|
||||
"watchOnce": true,
|
||||
"watchPausable": true,
|
||||
"watchPostEffect": true,
|
||||
@@ -259,6 +285,7 @@
|
||||
"watchThrottled": true,
|
||||
"watchTriggerable": true,
|
||||
"watchWithFilter": true,
|
||||
"whenever": true
|
||||
"whenever": true,
|
||||
"toValue": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,39 +1,21 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||
|
||||
/**
|
||||
* @type {import('eslint').Linter.Config}
|
||||
*/
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/eslint-config-prettier',
|
||||
'plugin:import/recommended',
|
||||
'./.eslintrc-auto-import.json',
|
||||
'@unocss',
|
||||
],
|
||||
extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'],
|
||||
|
||||
settings: {
|
||||
'import/resolver': { typescript: { project: './tsconfig.app.json' } },
|
||||
},
|
||||
env: {
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': ['off'],
|
||||
'prettier/prettier': ['error'],
|
||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
||||
'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }],
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
js: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
|
||||
'curly': ['error', 'all'],
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
'vue/no-empty-component-block': ['error'],
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@vueuse/core',
|
||||
importNames: ['useClipboard'],
|
||||
message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
4
.github/ISSUE_TEMPLATE/new-tool-request.md
vendored
4
.github/ISSUE_TEMPLATE/new-tool-request.md
vendored
@@ -6,8 +6,8 @@ labels: new tool
|
||||
assignees: CorentinTh
|
||||
---
|
||||
|
||||
**Which tool is impacted?**
|
||||
Example: the token generator
|
||||
**What tool do you want?**
|
||||
Example: a token generator
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -27,5 +27,8 @@ jobs:
|
||||
- name: Run unit test
|
||||
run: pnpm test
|
||||
|
||||
- name: Type check
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -37,11 +37,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -66,4 +66,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
16
.github/workflows/docker-nightly-release.yml
vendored
16
.github/workflows/docker-nightly-release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
outputs:
|
||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
- name: print latest_commit
|
||||
run: echo ${{ github.sha }}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -54,29 +54,29 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
47
.github/workflows/e2e-tests.yml
vendored
Normal file
47
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: E2E tests
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1/3, 2/3, 3/3]
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- run: corepack enable
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get Playwright version
|
||||
id: playwright-version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build app
|
||||
run: pnpm build
|
||||
|
||||
- name: Restore Playwright browsers from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-${{ hashFiles('**/playwright.config.ts') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm run test:e2e --shard=${{ matrix.shard }}
|
23
.github/workflows/playwright.yml
vendored
23
.github/workflows/playwright.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: E2E tests
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: pnpm exec playwright test
|
14
.github/workflows/releases.yml
vendored
14
.github/workflows/releases.yml
vendored
@@ -13,29 +13,29 @@ jobs:
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
|
||||
- run: corepack enable
|
||||
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -31,3 +31,5 @@ coverage
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
# Webkit with playwright creates a salt file
|
||||
salt
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint"]
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint", "lokalise.i18n-ally"]
|
||||
}
|
||||
|
229
CHANGELOG.md
229
CHANGELOG.md
@@ -2,6 +2,235 @@
|
||||
|
||||
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
|
||||
|
||||
### Features
|
||||
- **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba)
|
||||
- **new tool**: string obfuscator (#575) (c58d6e3)
|
||||
|
||||
### Bug fixes
|
||||
- **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a)
|
||||
|
||||
### Chores
|
||||
- **deps**: switched to fucking typescript v5 (#501) (76b2761)
|
||||
- **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01)
|
||||
- **deps**: update dependency prettier to v3 (#564) (a2b9b15)
|
||||
- **deps**: removed @typescript-eslint/parser (#563) (144f86e)
|
||||
- **deps**: removed ts-pattern (#565) (0f1f659)
|
||||
|
||||
## Version 2023.08.16-9bd4ad4
|
||||
|
||||
### Features
|
||||
- **Case Converter**: Add lowercase and uppercase (#534) (7b6232a)
|
||||
- **new tool**: emoji picker (#551) (93f7cf0)
|
||||
- **ui**: added c-select in the ui lib (#550) (dfa1ba8)
|
||||
- **new-tool**: password strength analyzer (#502) (a9c7b89)
|
||||
- **new-tool**: yaml to toml (e29b258)
|
||||
- **new-tool**: json to toml (ea50a3f)
|
||||
- **new-tool**: toml to yaml (746e5bd)
|
||||
- **new-tool**: toml to json (c7d4f11)
|
||||
- **command-palette**: random tool action (ec4c533)
|
||||
- **config**: allow app to run in a subfolder via BASE_URL (#461) (6304595)
|
||||
- **new-tool**: percentage calculator (#456) (b9406a4)
|
||||
- **new-tool**: json to csv converter (69f0bd0)
|
||||
- **new tool**: xml formatter (#457) (a6bbeae)
|
||||
- **chmod-calculator**: added symbolic representation (#455) (f771e7a)
|
||||
- **enhancement**: use system dark mode (#458) (cf7b1f0)
|
||||
- **phone-parser**: searchable country code select (d2956b6)
|
||||
- **new tool**: camera screenshot and recorder (34d8e5c)
|
||||
- **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c)
|
||||
|
||||
### Bug fixes
|
||||
- **deps**: update dependency uuid to v9 (#566) (5e12991)
|
||||
- **deps**: update dependency mathjs to v11 (#519) (7924456)
|
||||
- **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c)
|
||||
- **copy**: prevent shorthand copy if source is present in useCopy (#559) (86e964a)
|
||||
- **c-lib**: hide component library shortcut link in non-dev (#557) (56d74d0)
|
||||
- **emoji picker**: fix copy button (#556) (e5d0ba7)
|
||||
- **deps**: update dependency @vueuse/head to v1 (#515) (d12dd40)
|
||||
- **deps**: update dependency country-code-lookup to ^0.1.0 (#493) (8c72e69)
|
||||
- **deps**: update dependency @vueuse/head to ^0.9.0 (#492) (cec9dea)
|
||||
- **i18n**: fallback for demo i18n (12d9e5d)
|
||||
- **typos**: fixed more typos & uppercase JSON (#475) (9526ed8)
|
||||
- **about**: typos and wording (#474) (7068610)
|
||||
- **mime-types**: typos (#470) (c4cec9e)
|
||||
- **sonar**: took down minor sonar warning (4cbd7ac)
|
||||
- **readme**: typo (105b21b)
|
||||
- **ipv4-range-expander**: calculate correct for ip addresses where the first octet is lower than 128 (#405) (8c92d56)
|
||||
- **ipv4-converter**: removed readonly on input (7aed9c5)
|
||||
|
||||
### Refactoring
|
||||
- **navbar**: consistent spacing in navbar buttons (#507) (30f88fc)
|
||||
- **ui**: remove n-text (#506) (72c98a3)
|
||||
- **ui**: replaced some n-input to c-input (#505) (05ea545)
|
||||
- **json-viewer**: input monospace font (#485) (9125dcf)
|
||||
- **search**: command palette design (#463) (bcb98b3)
|
||||
- **c-input-text**: force usage of props with default (1e2a35b)
|
||||
- **naming**: prevent auto import conflicts for git memo (45c2474)
|
||||
- **imports**: removed unnecessary imports to vue (fe61f0f)
|
||||
- **ui**: removed all n-space (4d2b037)
|
||||
- **ui**: replaced some n-input with c-input-text (f7fc779)
|
||||
|
||||
### Chores
|
||||
- **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4)
|
||||
- **deps**: update dependency node to v18.17.1 (#560) (65a9474)
|
||||
- **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8)
|
||||
- **deps**: update dependency @unocss/eslint-config to ^0.55.0 (#553) (4268e25)
|
||||
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526) (d1c8880)
|
||||
- **deps**: update docker/login-action action to v2 (#512) (99bc84c)
|
||||
- **deps**: update dependency jsdom to v22 (#499) (cd5a503)
|
||||
- **deps**: update dependency @vitejs/plugin-vue-jsx to v3 (#497) (1a60236)
|
||||
- **deps**: update dependency @vitejs/plugin-vue to v4 (#496) (a249421)
|
||||
- **deps**: update dependency vite-plugin-pwa to ^0.16.0 (#488) (6498c9b)
|
||||
- **deps**: update dependency vite to v4 (#503) (f40d7ec)
|
||||
- **ci**: e2e against vercel deployement (#518) (2e28c50)
|
||||
- **e2e**: execute e2e against built app (#511) (cf382b5)
|
||||
- **deps**: update github/codeql-action action to v2 (#513) (0152583)
|
||||
- **deps**: update node.js to v18 (#514) (38cb61d)
|
||||
- **deps**: switched from vite-plugin-md to vite-plugin-vue-markdown (#510) (354aed6)
|
||||
- **deps**: update dependency workbox-window to v7 (#509) (6b8682f)
|
||||
- **deps**: update dependency vite-svg-loader to v4 (#508) (9e8349d)
|
||||
- **deps**: update dependency typescript to ~4.9.0 (#481) (f440507)
|
||||
- **deps**: update dependency vue-tsc to ^0.40.0 (#490) (b0d9a3e)
|
||||
- **deps**: updated unplugin-auto-import (#504) (5c3bebf)
|
||||
- **deps**: removed start-server-and-test dependency (8df7cd0)
|
||||
- **deps**: update dependency c8 to v8 (#498) (6bda2ca)
|
||||
- **deps**: update dependency @types/jsdom to v21 (#495) (994a1c3)
|
||||
- **deps**: update node.js to v16.20.1 (#491) (05edaf4)
|
||||
- **deps**: update dependency vitest to ^0.32.0 (#489) (49eacea)
|
||||
- **deps**: update actions/checkout action to v3 (#494) (3f7d469)
|
||||
- **deps**: update dependency unplugin-vue-components to ^0.25.0 (#484) (5f21908)
|
||||
- **deps**: update dependency unplugin-auto-import to ^0.16.0 (#483) (6cb0845)
|
||||
- **deps**: update dependency unocss to ^0.53.0 (#482) (38710dc)
|
||||
- **deps**: update dependency @unocss/eslint-config to ^0.53.0 (#478) (282cfc4)
|
||||
- **deps**: added renovate.json (#477) (363c2e4)
|
||||
- **i18n**: tool scoped locales (#471) (1b038c7)
|
||||
- **wysiwyg-editor**: update tiptap dependencies (732da08)
|
||||
- **i18n**: setup i18n plugin config (ebfb872)
|
||||
- **config**: netlify deployment support (#443) (93799af)
|
||||
- **ci**: shard e2e tests (962a6d6)
|
||||
- **lint**: switched to a better lint config (33c9b66)
|
||||
|
||||
### Refacor
|
||||
- **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d)
|
||||
|
||||
### Documentation
|
||||
- **ide**: updated vscode extensions settings (#472) (847323c)
|
||||
|
||||
### Chors
|
||||
- **deps**: updated vueuse dependency version (8515c24)
|
||||
|
||||
## Version 2023.05.14-77f2efc
|
||||
|
||||
### Features
|
||||
- **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b)
|
||||
- **new tool**: phone parser and normalizer (ce3150c)
|
||||
|
||||
### Bug fixes
|
||||
- **phone-parser**: use default country code (a43c546)
|
||||
- **home**: prevent weird blue border on card (3f6c8f0)
|
||||
|
||||
### Refactoring
|
||||
- **ui**: replaced some n-input with c-input-text (77f2efc)
|
||||
|
||||
### Chores
|
||||
- **issues**: updated new tool request issue template (edae4c6)
|
||||
|
||||
### Ui-lib
|
||||
- **new-component**: added text input component in the c-lib (aad8d84)
|
||||
- **button**: size variants (401f13f)
|
||||
|
||||
## Version 2023.04.23-92bd835
|
||||
|
||||
### Features
|
||||
- **ui-lib**: demo pages for c-lib components (92bd835)
|
||||
- **new-tool**: diff of two json objects (362f2fa)
|
||||
- **ipv4-range-expander**: expands a given IPv4 start and end address to a valid IPv4 subnet (#366) (df989e2)
|
||||
- **date converter**: auto focus main input (6d22025)
|
||||
|
||||
### Bug fixes
|
||||
- **ts**: cleaned legacy typechecking warning (e88c1d5)
|
||||
- **mac-address-lookup**: added copy handler on button click (c311e38)
|
||||
|
||||
### Refactoring
|
||||
- **ui-lib**: prevent c-button to shrink (61ece23)
|
||||
- **ui**: replaced naive ui cards with custom ones (f080933)
|
||||
- **clean**: removed unused lodash import (bb32513)
|
||||
- **clean**: removed useless br tags (74073f5)
|
||||
- **ui**: getting ride of naive ui buttons (c45bce3)
|
||||
|
||||
## Version 2023.04.14-dbad773
|
||||
|
||||
### Features
|
||||
|
@@ -1,9 +1,12 @@
|
||||
# build stage
|
||||
FROM node:lts-alpine AS build-stage
|
||||
# Set environment variables for non-interactive npm installs
|
||||
ENV NPM_CONFIG_LOGLEVEL warn
|
||||
ENV CI true
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm && pnpm i --frozen-lockfile
|
||||
COPY . .
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm i --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
# production stage
|
||||
@@ -11,4 +14,4 @@ FROM nginx:stable-alpine AS production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
33
README.md
33
README.md
@@ -6,7 +6,7 @@ Useful tools for developer and people working in IT. [Have a look !](https://it-
|
||||
|
||||
Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented.
|
||||
|
||||
You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new?assignees=corentinth&labels=&template=feature_request.md&title=)!
|
||||
You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new/choose)!
|
||||
|
||||
## Self host
|
||||
|
||||
@@ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
||||
|
||||
**Other solutions:**
|
||||
|
||||
- [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html)
|
||||
- [Tipi](https://www.runtipi.io/docs/apps-available)
|
||||
- [Unraid](https://unraid.net/community/apps?q=it-tools)
|
||||
|
||||
@@ -33,7 +34,25 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
||||
|
||||
### Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
[VSCode](https://code.visualstudio.com/) with the following extensions:
|
||||
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
|
||||
- [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally)
|
||||
|
||||
with the following settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"i18n-ally.localesPaths": ["locales", "src/tools/*/locales"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
```
|
||||
|
||||
### Type Support for `.vue` Imports in TS
|
||||
|
||||
@@ -84,7 +103,13 @@ To create a new tool, there is a script that generate the boilerplate of the new
|
||||
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 inported 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
|
||||
|
||||
@@ -92,6 +117,8 @@ Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
|
||||
|
||||
This project is continuously deployed using [vercel.com](https://vercel.com).
|
||||
|
||||
Contributor graph is generated using [contrib.rocks](https://contrib.rocks/preview?repo=corentinth/it-tools).
|
||||
|
||||
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
|
6
_templates/generator/ui-component/component.demo.ejs.t
Normal file
6
_templates/generator/ui-component/component.demo.ejs.t
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.demo.vue
|
||||
---
|
||||
<template>
|
||||
<<%= h.changeCase.param(name) %> />
|
||||
</template>
|
13
_templates/generator/ui-component/component.ejs.t
Normal file
13
_templates/generator/ui-component/component.ejs.t
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.vue
|
||||
---
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{ prop?: string }>(), { prop: '' });
|
||||
const { prop } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ prop }}
|
||||
</div>
|
||||
</template>
|
354
auto-imports.d.ts
vendored
354
auto-imports.d.ts
vendored
@@ -19,7 +19,9 @@ declare global {
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
@@ -39,9 +41,6 @@ declare global {
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
@@ -94,6 +93,7 @@ declare global {
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
@@ -104,6 +104,19 @@ declare global {
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
@@ -114,8 +127,8 @@ declare global {
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
@@ -157,6 +170,7 @@ declare global {
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
@@ -189,12 +203,18 @@ declare global {
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
@@ -208,14 +228,17 @@ declare global {
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
@@ -227,6 +250,8 @@ declare global {
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||
const useToString: typeof import('@vueuse/core')['useToString']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
@@ -247,8 +272,10 @@ declare global {
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
@@ -282,7 +309,9 @@ declare module 'vue' {
|
||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||
@@ -302,9 +331,6 @@ declare module 'vue' {
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']>
|
||||
readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']>
|
||||
readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
@@ -357,6 +383,7 @@ declare module 'vue' {
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
@@ -367,6 +394,19 @@ declare module 'vue' {
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
@@ -377,8 +417,8 @@ declare module 'vue' {
|
||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||
readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
@@ -420,6 +460,7 @@ declare module 'vue' {
|
||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||
@@ -452,12 +493,18 @@ declare module 'vue' {
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
@@ -471,14 +518,17 @@ declare module 'vue' {
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||
@@ -490,6 +540,8 @@ declare module 'vue' {
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||
@@ -510,8 +562,294 @@ declare module 'vue' {
|
||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
|
||||
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
|
||||
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
|
||||
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
|
||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
|
||||
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
|
||||
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
||||
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
||||
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
||||
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
||||
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
|
||||
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
|
||||
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
|
||||
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
|
||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
|
||||
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
|
||||
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
|
||||
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
|
||||
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
|
||||
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
|
||||
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
|
||||
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||
readonly useDialog: UnwrapRef<typeof import('naive-ui')['useDialog']>
|
||||
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
|
||||
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
|
||||
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
|
||||
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
|
||||
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
|
||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
|
||||
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
|
||||
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
|
||||
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
|
||||
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
|
||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
|
||||
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
|
||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
|
||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||
readonly useNotification: UnwrapRef<typeof import('naive-ui')['useNotification']>
|
||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
|
||||
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
|
||||
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
|
||||
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
|
||||
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
|
||||
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
|
||||
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
|
||||
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
|
||||
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
|
||||
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
|
||||
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
|
||||
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
|
||||
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
|
158
components.d.ts
vendored
158
components.d.ts
vendored
@@ -9,18 +9,125 @@ export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
'404.page': typeof import('./src/pages/404.page.vue')['default']
|
||||
About: typeof import('./src/pages/About.vue')['default']
|
||||
App: typeof import('./src/App.vue')['default']
|
||||
'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
|
||||
Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
|
||||
Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
|
||||
BasicAuthGenerator: typeof import('./src/tools/basic-auth-generator/basic-auth-generator.vue')['default']
|
||||
Bcrypt: typeof import('./src/tools/bcrypt/bcrypt.vue')['default']
|
||||
BenchmarkBuilder: typeof import('./src/tools/benchmark-builder/benchmark-builder.vue')['default']
|
||||
Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default']
|
||||
CAlert: typeof import('./src/ui/c-alert/c-alert.vue')['default']
|
||||
'CAlert.demo': typeof import('./src/ui/c-alert/c-alert.demo.vue')['default']
|
||||
CameraRecorder: typeof import('./src/tools/camera-recorder/camera-recorder.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.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
|
||||
CButtonsSelect: typeof import('./src/ui/c-buttons-select/c-buttons-select.vue')['default']
|
||||
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
|
||||
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
||||
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
||||
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
||||
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
||||
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
|
||||
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
|
||||
CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default']
|
||||
CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default']
|
||||
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
|
||||
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
|
||||
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
|
||||
CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default']
|
||||
'CModal.demo': typeof import('./src/ui/c-modal/c-modal.demo.vue')['default']
|
||||
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
||||
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
|
||||
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
|
||||
CommandPalette: typeof import('./src/modules/command-palette/command-palette.vue')['default']
|
||||
CommandPaletteOption: typeof import('./src/modules/command-palette/components/command-palette-option.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.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
|
||||
CTable: typeof import('./src/ui/c-table/c-table.vue')['default']
|
||||
'CTable.demo': typeof import('./src/ui/c-table/c-table.demo.vue')['default']
|
||||
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
|
||||
'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default']
|
||||
CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default']
|
||||
'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default']
|
||||
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
|
||||
'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default']
|
||||
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
|
||||
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
|
||||
DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default']
|
||||
DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default']
|
||||
DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default']
|
||||
Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default']
|
||||
EmojiCard: typeof import('./src/tools/emoji-picker/emoji-card.vue')['default']
|
||||
EmojiGrid: typeof import('./src/tools/emoji-picker/emoji-grid.vue')['default']
|
||||
EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default']
|
||||
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
|
||||
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
|
||||
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
|
||||
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
|
||||
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
|
||||
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
|
||||
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
|
||||
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
|
||||
'Home.page': typeof import('./src/pages/Home.page.vue')['default']
|
||||
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
|
||||
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
|
||||
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
|
||||
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
|
||||
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
|
||||
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
||||
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
||||
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||
IconMdiClose: typeof import('~icons/mdi/close')['default']
|
||||
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
|
||||
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
||||
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
|
||||
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
||||
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||
IconMdiRecord: typeof import('~icons/mdi/record')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
|
||||
Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default']
|
||||
Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default']
|
||||
Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default']
|
||||
JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default']
|
||||
JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
|
||||
JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default']
|
||||
JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default']
|
||||
JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
|
||||
JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
|
||||
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
|
||||
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.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']
|
||||
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
|
||||
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
|
||||
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
|
||||
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
|
||||
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
|
||||
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
|
||||
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
|
||||
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
|
||||
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
|
||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
@@ -37,37 +144,60 @@ declare module '@vue/runtime-core' {
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH2: typeof import('naive-ui')['NH2']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NH4: typeof import('naive-ui')['NH4']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTable: typeof import('naive-ui')['NTable']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
|
||||
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
|
||||
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
|
||||
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
||||
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
|
||||
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
|
||||
RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
||||
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
|
||||
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
|
||||
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
|
||||
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
|
||||
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
|
||||
StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
|
||||
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
|
||||
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
|
||||
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
||||
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
|
||||
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
|
||||
TextToBinary: typeof import('./src/tools/text-to-binary/text-to-binary.vue')['default']
|
||||
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
|
||||
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
|
||||
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
|
||||
TomlToJson: typeof import('./src/tools/toml-to-json/toml-to-json.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']
|
||||
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
|
||||
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
|
||||
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
|
||||
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
|
||||
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
|
||||
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
|
||||
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
|
||||
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
|
||||
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
|
||||
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
|
||||
YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
|
||||
}
|
||||
}
|
||||
|
12
index.html
12
index.html
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>IT Tools - Handy online tools for developers</title>
|
||||
<meta itemprop="name" content="IT Tools - Handy online tools for developers" />
|
||||
@@ -14,13 +14,13 @@
|
||||
itemprop="description"
|
||||
content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
|
||||
/>
|
||||
<link rel="author" href="/humans.txt" />
|
||||
<link rel="author" href="humans.txt" />
|
||||
<link rel="canonical" href="https://it-tools.tech" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#18a058" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#18a058" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
|
65
locales/en.yml
Normal file
65
locales/en.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
home:
|
||||
categories:
|
||||
newestTools: Newest tools
|
||||
favoriteTools: 'Your favorite tools'
|
||||
allTools: 'All the tools'
|
||||
subtitle: 'Handy tools for developers'
|
||||
toggleMenu: 'Toggle menu'
|
||||
home: Home
|
||||
uiLib: 'UI Lib'
|
||||
buyMeACoffee: 'Buy me a coffee'
|
||||
follow:
|
||||
title: 'You like it-tools?'
|
||||
p1: 'Give us a star on'
|
||||
githubRepository: 'IT-Tools GitHub repository'
|
||||
p2: 'or follow us on'
|
||||
twitterAccount: 'IT-Tools Twitter account'
|
||||
thankYou: 'Thank you !'
|
||||
nav:
|
||||
github: 'GitHub repository'
|
||||
githubRepository: 'IT-Tools GitHub repository'
|
||||
twitter: 'Twitter account'
|
||||
twitterAccount: 'IT Tools Twitter account'
|
||||
about: 'About IT-Tools'
|
||||
aboutLabel: 'About'
|
||||
darkMode: 'Dark mode'
|
||||
lightMode: 'Light mode'
|
||||
mode: 'Toggle dark/light mode'
|
||||
about:
|
||||
h1: 'About IT-Tools'
|
||||
h1p1: 'This wonderful website, made with ❤ by'
|
||||
h1p2: ", aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don''t forget to bookmark it in your shortcut bar!"
|
||||
h1p3: 'IT Tools is open-source (under the MIT license) and free, and will always be, but it costs me money to host and renew the domain name. If you want to support my work, and encourage me to add more tools, please consider supporting by'
|
||||
h1p4: 'sponsoring me'
|
||||
h2: Technologies
|
||||
h2p1: 'IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the'
|
||||
h2p2: 'file of the repository.'
|
||||
h3: 'Found a bug? A tool is missing?'
|
||||
h3p1: 'If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a feature request in the'
|
||||
h3p2: 'issues section'
|
||||
h3p3: 'in the GitHub repository.'
|
||||
h3p4: "And if you found a bug, or something doesn''t work as expected, please file a bug report in the"
|
||||
h3p5: 'issues section'
|
||||
h3p6: 'in the GitHub repository.'
|
||||
404:
|
||||
notFound: '404 Not Found'
|
||||
sorry: 'Sorry, this page does not seem to exist'
|
||||
maybe: 'Maybe the cache is doing tricky things, try force-refreshing?'
|
||||
backHome: 'Back home'
|
||||
toolCard:
|
||||
new: New
|
||||
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
|
49
locales/fr.yml
Normal file
49
locales/fr.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
home:
|
||||
categories:
|
||||
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
|
4
netlify.toml
Normal file
4
netlify.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
109
package.json
109
package.json
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "it-tools",
|
||||
"version": "2023.4.14-dbad773",
|
||||
"version": "2023.11.2-7d94e11",
|
||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"productivity",
|
||||
"converter",
|
||||
@@ -22,107 +21,115 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build",
|
||||
"preview": "vite preview --port 5050",
|
||||
"test": "npm run test:unit",
|
||||
"test:unit": "vitest --environment jsdom",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
|
||||
"script:create-new-tool": "node scripts/create-tool.mjs",
|
||||
"script:create:tool": "node scripts/create-tool.mjs",
|
||||
"script:create:ui": "hygen generator ui-component",
|
||||
"release": "node ./scripts/release.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@it-tools/bip39": "^0.0.4",
|
||||
"@it-tools/oggen": "^1.3.0",
|
||||
"@sindresorhus/slugify": "^2.2.0",
|
||||
"@tiptap/pm": "2.0.0-beta.220",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.220",
|
||||
"@tiptap/vue-3": "2.0.0-beta.220",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@tiptap/pm": "2.1.6",
|
||||
"@tiptap/starter-kit": "2.1.6",
|
||||
"@tiptap/vue-3": "2.0.3",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vicons/tabler": "^0.12.0",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"@vueuse/head": "^0.7.13",
|
||||
"@vueuse/router": "^9.13.0",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/head": "^1.0.0",
|
||||
"@vueuse/router": "^10.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"change-case": "^4.1.2",
|
||||
"colord": "^2.9.3",
|
||||
"composerize-ts": "^0.6.2",
|
||||
"country-code-lookup": "^0.1.0",
|
||||
"cron-validator": "^1.3.1",
|
||||
"cronstrue": "^2.26.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"emojilib": "^3.0.10",
|
||||
"figue": "^1.2.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"highlight.js": "^11.7.0",
|
||||
"iarna-toml-esm": "^3.0.5",
|
||||
"ibantools": "^4.3.3",
|
||||
"json5": "^2.2.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
"lodash": "^4.17.21",
|
||||
"mathjs": "^10.6.4",
|
||||
"mathjs": "^11.9.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"naive-ui": "^2.34.3",
|
||||
"monaco-editor": "^0.43.0",
|
||||
"naive-ui": "^2.35.0",
|
||||
"netmask": "^2.0.2",
|
||||
"node-forge": "^1.3.1",
|
||||
"oui": "^12.0.52",
|
||||
"pinia": "^2.0.34",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"qrcode": "^1.5.1",
|
||||
"randombytes": "^2.1.0",
|
||||
"sql-formatter": "^8.2.0",
|
||||
"ts-pattern": "^4.2.2",
|
||||
"sql-formatter": "^13.0.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.2.47",
|
||||
"ulid": "^2.3.0",
|
||||
"unicode-emoji-json": "^0.4.0",
|
||||
"unplugin-auto-import": "^0.16.4",
|
||||
"uuid": "^9.0.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-tsc": "^1.8.1",
|
||||
"xml-formatter": "^3.3.2",
|
||||
"yaml": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.41.0",
|
||||
"@iconify-json/mdi": "^1.1.50",
|
||||
"@intlify/unplugin-vue-i18n": "^0.13.0",
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/jsdom": "^16.2.15",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/jsdom": "^21.0.0",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/netmask": "^2.0.0",
|
||||
"@types/node": "^16.18.23",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node-forge": "^1.3.2",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/randombytes": "^2.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@unocss/eslint-config": "^0.50.8",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@unocss/eslint-config": "^0.55.0",
|
||||
"@vitejs/plugin-vue": "^4.3.2",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"@vue/compiler-sfc": "^3.2.47",
|
||||
"@vue/runtime-dom": "^3.3.4",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"c8": "^7.13.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"consola": "^3.0.2",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"jsdom": "^19.0.0",
|
||||
"eslint": "^8.47.0",
|
||||
"hygen": "^6.2.11",
|
||||
"jsdom": "^22.0.0",
|
||||
"less": "^4.1.3",
|
||||
"prettier": "^2.8.7",
|
||||
"start-server-and-test": "^1.15.4",
|
||||
"typescript": "~4.5.5",
|
||||
"unocss": "^0.50.8",
|
||||
"unplugin-auto-import": "^0.15.3",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^4.2.1",
|
||||
"vite-plugin-md": "^0.21.5",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-svg-loader": "^3.6.0",
|
||||
"vitest": "^0.30.1",
|
||||
"vue-tsc": "^1.2.0",
|
||||
"workbox-window": "^6.5.4",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "~5.2.0",
|
||||
"unocss": "^0.55.0",
|
||||
"unocss-preset-scrollbar": "^0.2.1",
|
||||
"unplugin-icons": "^0.17.0",
|
||||
"unplugin-vue-components": "^0.25.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-pwa": "^0.16.0",
|
||||
"vite-plugin-vue-markdown": "^0.23.5",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vitest": "^0.34.0",
|
||||
"workbox-window": "^7.0.0",
|
||||
"zx": "^7.2.1"
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
const isCI = !!process.env.CI;
|
||||
const baseUrl = process.env.BASE_URL || 'http://localhost:5050';
|
||||
const useWebServer = process.env.NO_WEB_SERVER !== 'true';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
@@ -15,17 +13,17 @@ export default defineConfig({
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
forbidOnly: isCI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
retries: isCI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: isCI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://127.0.0.1:3000',
|
||||
baseURL: baseUrl,
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
@@ -51,32 +49,17 @@ export default defineConfig({
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
...(useWebServer
|
||||
&& {
|
||||
webServer: {
|
||||
command: 'npm run preview',
|
||||
url: 'http://127.0.0.1:5050',
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
7761
pnpm-lock.yaml
generated
7761
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
renovate.json
Normal file
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
@@ -29,9 +29,9 @@ createToolFile(
|
||||
`${toolName}.vue`,
|
||||
`
|
||||
<template>
|
||||
<n-card>
|
||||
<div>
|
||||
Lorem ipsum
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
24
src/App.vue
24
src/App.vue
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, RouterView } from 'vue-router';
|
||||
import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui';
|
||||
import { RouterView, useRoute } from 'vue-router';
|
||||
import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui';
|
||||
import { darkThemeOverrides, lightThemeOverrides } from './themes';
|
||||
import { layouts } from './layouts';
|
||||
import { useStyleStore } from './stores/style.store';
|
||||
@@ -12,18 +11,25 @@ const styleStore = useStyleStore();
|
||||
|
||||
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
|
||||
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
syncRef(
|
||||
locale,
|
||||
useStorage('locale', locale),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
|
||||
<n-global-style />
|
||||
<n-message-provider placement="bottom">
|
||||
<n-notification-provider placement="bottom-right">
|
||||
<NGlobalStyle />
|
||||
<NMessageProvider placement="bottom">
|
||||
<NNotificationProvider placement="bottom-right">
|
||||
<component :is="layout">
|
||||
<router-view />
|
||||
<RouterView />
|
||||
</component>
|
||||
</n-notification-provider>
|
||||
</n-message-provider>
|
||||
</NNotificationProvider>
|
||||
</NMessageProvider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
|
@@ -1,39 +1,9 @@
|
||||
<template>
|
||||
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
|
||||
<n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })">
|
||||
<n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" />
|
||||
|
||||
<span>
|
||||
{{ name }}
|
||||
</span>
|
||||
</n-text>
|
||||
|
||||
<n-collapse-transition :show="!isCollapsed">
|
||||
<div class="menu-wrapper">
|
||||
<div class="toggle-bar" @click="toggleCategoryCollapse({ name })" />
|
||||
|
||||
<n-menu
|
||||
class="menu"
|
||||
:value="(route.name as string)"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="tools"
|
||||
:indent="8"
|
||||
:default-expand-all="true"
|
||||
/>
|
||||
</div>
|
||||
</n-collapse-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Tool, ToolCategory } from '@/tools/tools.types';
|
||||
import { ChevronRight } from '@vicons/tabler';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { toRefs, computed, h } from 'vue';
|
||||
import { RouterLink, useRoute } from 'vue-router';
|
||||
import MenuIconItem from './MenuIconItem.vue';
|
||||
import type { Tool, ToolCategory } from '@/tools/tools.types';
|
||||
|
||||
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
|
||||
const { toolsByCategory } = toRefs(props);
|
||||
@@ -49,8 +19,8 @@ const collapsedCategories = useStorage<Record<string, boolean>>(
|
||||
{
|
||||
deep: true,
|
||||
serializer: {
|
||||
read: (v) => (v ? JSON.parse(v) : null),
|
||||
write: (v) => JSON.stringify(v),
|
||||
read: v => (v ? JSON.parse(v) : null),
|
||||
write: v => JSON.stringify(v),
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -61,12 +31,12 @@ function toggleCategoryCollapse({ name }: { name: string }) {
|
||||
|
||||
const menuOptions = computed(() =>
|
||||
toolsByCategory.value.map(({ name, components }) => ({
|
||||
name: name,
|
||||
name,
|
||||
isCollapsed: collapsedCategories.value[name],
|
||||
tools: components.map((tool) => ({
|
||||
tools: components.map(tool => ({
|
||||
label: makeLabel(tool),
|
||||
icon: makeIcon(tool),
|
||||
key: tool.name,
|
||||
key: tool.path,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
@@ -74,27 +44,37 @@ const menuOptions = computed(() =>
|
||||
const themeVars = useThemeVars();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
|
||||
<div ml-6px mt-12px flex cursor-pointer items-center op-60 @click="toggleCategoryCollapse({ name })">
|
||||
<span :class="{ 'rotate-0': isCollapsed, 'rotate-90': !isCollapsed }" text-16px lh-1 op-50 transition-transform>
|
||||
<icon-mdi-chevron-right />
|
||||
</span>
|
||||
|
||||
<span ml-8px text-13px>
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<n-collapse-transition :show="!isCollapsed">
|
||||
<div class="menu-wrapper">
|
||||
<div class="toggle-bar" @click="toggleCategoryCollapse({ name })" />
|
||||
|
||||
<n-menu
|
||||
class="menu"
|
||||
:value="route.path"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="tools"
|
||||
:indent="8"
|
||||
:default-expand-all="true"
|
||||
/>
|
||||
</div>
|
||||
</n-collapse-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.category-name {
|
||||
font-size: 0.93em;
|
||||
padding: 12px 0 0px 0;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
transition: transform ease 0.5s;
|
||||
transform: rotate(90deg);
|
||||
margin: 0 10px 0 7px;
|
||||
opacity: 0.5;
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@@ -1,8 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
|
||||
const props = defineProps<{ icon: Component; title: string }>();
|
||||
const { icon, title } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card class="colored-card">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-icon class="icon" size="40" :component="icon" />
|
||||
</n-space>
|
||||
<c-card class="colored-card">
|
||||
<n-icon class="icon" size="40" :component="icon" />
|
||||
<n-h3 class="title">
|
||||
<n-ellipsis>{{ title }}</n-ellipsis>
|
||||
</n-h3>
|
||||
@@ -12,16 +17,9 @@
|
||||
<slot />
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</n-card>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs, type Component } from 'vue';
|
||||
|
||||
const props = defineProps<{ icon: Component; title: string }>();
|
||||
const { icon, title } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.colored-card {
|
||||
background: rgb(37, 99, 108);
|
||||
|
@@ -1,25 +1,11 @@
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite">
|
||||
<template #icon>
|
||||
<n-icon :component="FavoriteFilled" />
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FavoriteFilled } from '@vicons/material';
|
||||
import { useToolStore } from '@/tools/tools.store';
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
import { computed, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{ tool: Tool }>();
|
||||
|
||||
const toolStore = useToolStore();
|
||||
|
||||
const props = defineProps<{ tool: Tool }>();
|
||||
const { tool } = toRefs(props);
|
||||
|
||||
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
|
||||
@@ -36,3 +22,17 @@ function toggleFavorite(event: MouseEvent) {
|
||||
toolStore.addToolToFavorites({ tool });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-tooltip :tooltip="isFavorite ? 'Remove from favorites' : 'Add to favorites' ">
|
||||
<c-button
|
||||
variant="text"
|
||||
circle
|
||||
:type="buttonType"
|
||||
:style="{ opacity: isFavorite ? 1 : 0.2 }"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
<icon-mdi-heart />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
|
@@ -1,36 +1,17 @@
|
||||
<template>
|
||||
<n-form-item :label="inputLabel" v-bind="validationAttrs">
|
||||
<n-input
|
||||
ref="inputElement"
|
||||
v-model:value="input"
|
||||
:placeholder="inputPlaceholder"
|
||||
type="textarea"
|
||||
rows="20"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
:input-props="{ 'data-test-id': 'input' }"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="outputLabel">
|
||||
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" />
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useValidation, type UseValidationRule } from '@/composable/validation';
|
||||
import _ from 'lodash';
|
||||
import type { UseValidationRule } from '@/composable/validation';
|
||||
import CInputText from '@/ui/c-input-text/c-input-text.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
transformer?: (v: string) => string;
|
||||
inputValidationRules?: UseValidationRule<string>[];
|
||||
inputLabel?: string;
|
||||
inputPlaceholder?: string;
|
||||
inputDefault?: string;
|
||||
outputLabel?: string;
|
||||
outputLanguage?: string;
|
||||
transformer?: (v: string) => string
|
||||
inputValidationRules?: UseValidationRule<string>[]
|
||||
inputLabel?: string
|
||||
inputPlaceholder?: string
|
||||
inputDefault?: string
|
||||
outputLabel?: string
|
||||
outputLanguage?: string
|
||||
}>(),
|
||||
{
|
||||
transformer: _.identity,
|
||||
@@ -43,15 +24,34 @@ const props = withDefaults(
|
||||
},
|
||||
);
|
||||
|
||||
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } =
|
||||
toRefs(props);
|
||||
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault }
|
||||
= toRefs(props);
|
||||
|
||||
const inputElement = ref();
|
||||
const inputElement = ref<typeof CInputText>();
|
||||
|
||||
const input = ref(inputDefault.value);
|
||||
const output = computed(() => transformer.value(input.value));
|
||||
|
||||
const { attrs: validationAttrs } = useValidation({ source: input, rules: inputValidationRules.value });
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<template>
|
||||
<CInputText
|
||||
ref="inputElement"
|
||||
v-model:value="input"
|
||||
:placeholder="inputPlaceholder"
|
||||
:label="inputLabel"
|
||||
rows="20"
|
||||
autosize
|
||||
raw-text
|
||||
multiline
|
||||
test-id="input"
|
||||
:validation-rules="inputValidationRules"
|
||||
monospace
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div mb-5px>
|
||||
{{ outputLabel }}
|
||||
</div>
|
||||
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,43 +1,23 @@
|
||||
<template>
|
||||
<n-input v-model:value="value">
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button quaternary circle @click="onCopyClicked">
|
||||
<n-icon :component="ContentCopyFilled" />
|
||||
</n-button>
|
||||
</template>
|
||||
{{ tooltipText }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel, useClipboard } from '@vueuse/core';
|
||||
import { ContentCopyFilled } from '@vicons/material';
|
||||
import { ref } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = defineProps<{ value: string }>();
|
||||
const emit = defineEmits(['update:value']);
|
||||
|
||||
const value = useVModel(props, 'value', emit);
|
||||
const tooltipText = ref('Copy to clipboard');
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
|
||||
function onCopyClicked() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied !';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltipText.value = 'Copy to clipboard';
|
||||
}, 2000);
|
||||
}
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep(.n-input-wrapper) {
|
||||
padding-right: 5px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<c-input-text v-model:value="value">
|
||||
<template #suffix>
|
||||
<c-tooltip :tooltip="tooltipText">
|
||||
<c-button circle variant="text" size="small" @click="copy()">
|
||||
<icon-mdi-content-copy />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
</c-input-text>
|
||||
</template>
|
||||
|
@@ -1,14 +1,6 @@
|
||||
<template>
|
||||
<div class="menu-icon-item">
|
||||
<n-icon :component="tool.icon" />
|
||||
<div v-if="tool.isNew" class="badge"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { toRefs } from 'vue';
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
|
||||
const props = defineProps<{ tool: Tool }>();
|
||||
const { tool } = toRefs(props);
|
||||
@@ -16,6 +8,13 @@ const { tool } = toRefs(props);
|
||||
const theme = useThemeVars();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu-icon-item">
|
||||
<n-icon :component="tool.icon" />
|
||||
<div v-if="tool.isNew" class="badge" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.menu-icon-item {
|
||||
position: relative;
|
||||
|
@@ -1,3 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
|
||||
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout has-sider>
|
||||
<n-layout-sider
|
||||
@@ -19,15 +27,6 @@
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { toRefs, computed } from 'vue';
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
|
||||
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.overlay {
|
||||
position: absolute;
|
||||
|
@@ -1,71 +1,51 @@
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button
|
||||
size="large"
|
||||
circle
|
||||
quaternary
|
||||
tag="a"
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' GitHub repository"
|
||||
>
|
||||
<n-icon size="25" :component="BrandGithub" />
|
||||
</n-button>
|
||||
</template>
|
||||
Github repository
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button
|
||||
size="large"
|
||||
circle
|
||||
quaternary
|
||||
tag="a"
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT Tools' Twitter account"
|
||||
>
|
||||
<n-icon size="25" :component="BrandTwitter" />
|
||||
</n-button>
|
||||
</template>
|
||||
IT Tools' Twitter account
|
||||
</n-tooltip>
|
||||
|
||||
<router-link to="/about" #="{ navigate, href }" custom>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button tag="a" :href="href" circle quaternary size="large" aria-label="About" @click="navigate">
|
||||
<n-icon size="25" :component="InfoCircle" />
|
||||
</n-button>
|
||||
</template>
|
||||
About
|
||||
</n-tooltip>
|
||||
</router-link>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button size="large" circle quaternary aria-label="Toggle dark/light mode" @click="isDarkTheme = !isDarkTheme">
|
||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||
<n-icon v-else size="25" :component="Moon" />
|
||||
</n-button>
|
||||
</template>
|
||||
<span v-if="isDarkTheme">Light mode</span>
|
||||
<span v-else>Dark mode</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
|
||||
import { toRefs } from 'vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
const { isDarkTheme } = toRefs(styleStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-tooltip :tooltip="$t('home.nav.github')" position="bottom">
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="$t('home.nav.githubRepository')"
|
||||
>
|
||||
<n-icon size="25" :component="BrandGithub" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<c-tooltip :tooltip="$t('home.nav.twitter')" position="bottom">
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
:aria-label="$t('home.nav.twitterAccount')"
|
||||
>
|
||||
<n-icon size="25" :component="BrandTwitter" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<c-tooltip :tooltip="$t('home.nav.about')" position="bottom">
|
||||
<c-button circle variant="text" to="/about" :aria-label="$t('home.nav.aboutLabel')">
|
||||
<n-icon size="25" :component="InfoCircle" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
<c-tooltip :tooltip="isDarkTheme ? $t('home.nav.lightMode') : $t('home.nav.darkMode')" position="bottom">
|
||||
<c-button circle variant="text" :aria-label="$t('home.nav.mode')" @click="() => styleStore.toggleDark()">
|
||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||
<n-icon v-else size="25" :component="Moon" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-button {
|
||||
&:not(:last-child) {
|
||||
|
@@ -1,110 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||
import { tools } from '@/tools';
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
import { SearchRound } from '@vicons/material';
|
||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||
import type { NInput } from 'naive-ui';
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import SearchBarItem from './SearchBarItem.vue';
|
||||
|
||||
const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool });
|
||||
|
||||
const router = useRouter();
|
||||
const { tracker } = useTracker();
|
||||
|
||||
const queryString = ref('');
|
||||
const inputEl = ref<HTMLElement>();
|
||||
const displayDropDown = ref(true);
|
||||
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
|
||||
|
||||
const options = computed(() => {
|
||||
if (queryString.value === '') {
|
||||
return tools.map(toolToOption);
|
||||
}
|
||||
|
||||
return searchResult.value.map(toolToOption);
|
||||
});
|
||||
|
||||
const { searchResult } = useFuzzySearch({
|
||||
search: queryString,
|
||||
data: tools,
|
||||
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
|
||||
});
|
||||
|
||||
const keys = useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
whenever(keys.ctrl_k, claimFocus);
|
||||
whenever(keys.meta_k, claimFocus);
|
||||
whenever(keys.escape, releaseFocus);
|
||||
|
||||
function renderOption({ tool }: { tool: Tool }) {
|
||||
return h(SearchBarItem, { tool });
|
||||
}
|
||||
|
||||
function onSelect(path: string) {
|
||||
router.push(path);
|
||||
queryString.value = '';
|
||||
}
|
||||
|
||||
function claimFocus() {
|
||||
displayDropDown.value = true;
|
||||
|
||||
inputEl.value?.focus();
|
||||
}
|
||||
|
||||
function releaseFocus() {
|
||||
displayDropDown.value = false;
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
tracker.trackEvent({ eventName: 'Search-bar focused' });
|
||||
displayDropDown.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-bar">
|
||||
<n-auto-complete
|
||||
v-model:value="queryString"
|
||||
:options="options"
|
||||
:on-select="(value) => onSelect(String(value))"
|
||||
:render-label="renderOption"
|
||||
:default-value="'aa'"
|
||||
:get-show="() => displayDropDown"
|
||||
:on-focus="onFocus"
|
||||
@update:value="() => (displayDropDown = true)"
|
||||
>
|
||||
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
|
||||
<n-input
|
||||
ref="inputEl"
|
||||
round
|
||||
clearable
|
||||
:placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`"
|
||||
:value="slotValue"
|
||||
:input-props="{ autocomplete: 'disabled' }"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon :component="SearchRound" />
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
</n-auto-complete>
|
||||
</div>
|
||||
</template>
|
@@ -1,45 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{ tool: Tool }>();
|
||||
const { tool } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-bar-item">
|
||||
<n-icon class="icon" :component="tool.icon" />
|
||||
|
||||
<div>
|
||||
<div class="name">{{ tool.name }}</div>
|
||||
<div class="description">{{ tool.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-bar-item {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
margin-right: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: 0.7;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
17
src/components/SpanCopyable.vue
Normal file
17
src/components/SpanCopyable.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
|
||||
const { value } = toRefs(props);
|
||||
|
||||
const initialText = 'Copy to clipboard';
|
||||
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-tooltip :tooltip="tooltipText">
|
||||
<span cursor-pointer font-mono @click="copy()">{{ value }}</span>
|
||||
</c-tooltip>
|
||||
</template>
|
@@ -1,6 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { Copy } from '@vicons/tabler';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import jsonHljs from 'highlight.js/lib/languages/json';
|
||||
import sqlHljs from 'highlight.js/lib/languages/sql';
|
||||
import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
||||
import iniHljs from 'highlight.js/lib/languages/ini';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: string
|
||||
followHeightOf?: HTMLElement | null
|
||||
language?: string
|
||||
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'
|
||||
copyMessage?: string
|
||||
}>(),
|
||||
{
|
||||
followHeightOf: null,
|
||||
language: 'txt',
|
||||
copyPlacement: 'top-right',
|
||||
copyMessage: 'Copy to clipboard',
|
||||
},
|
||||
);
|
||||
hljs.registerLanguage('sql', sqlHljs);
|
||||
hljs.registerLanguage('json', jsonHljs);
|
||||
hljs.registerLanguage('html', xmlHljs);
|
||||
hljs.registerLanguage('xml', xmlHljs);
|
||||
hljs.registerLanguage('yaml', yamlHljs);
|
||||
hljs.registerLanguage('toml', iniHljs);
|
||||
|
||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="overflow-x: hidden; width: 100%">
|
||||
<n-card class="result-card">
|
||||
<c-card relative>
|
||||
<n-scrollbar
|
||||
x-scrollable
|
||||
trigger="none"
|
||||
@@ -10,93 +50,25 @@
|
||||
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
||||
</n-config-provider>
|
||||
</n-scrollbar>
|
||||
<n-tooltip v-if="value" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="copy-button" :class="[copyPlacement]">
|
||||
<n-button circle secondary size="large" @click="onCopyClicked">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ tooltipText }}</span>
|
||||
</n-tooltip>
|
||||
</n-card>
|
||||
<n-space v-if="copyPlacement === 'outside'" justify="center" mt-4>
|
||||
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
|
||||
</n-space>
|
||||
<div absolute right-10px top-10px>
|
||||
<c-tooltip v-if="value" :tooltip="tooltipText" position="left">
|
||||
<c-button circle important:h-10 important:w-10 @click="copy()">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
</c-card>
|
||||
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
|
||||
<c-button @click="copy()">
|
||||
{{ tooltipText }}
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Copy } from '@vicons/tabler';
|
||||
import { useClipboard, useElementSize } from '@vueuse/core';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import jsonHljs from 'highlight.js/lib/languages/json';
|
||||
import sqlHljs from 'highlight.js/lib/languages/sql';
|
||||
import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
||||
import { ref, toRefs } from 'vue';
|
||||
|
||||
hljs.registerLanguage('sql', sqlHljs);
|
||||
hljs.registerLanguage('json', jsonHljs);
|
||||
hljs.registerLanguage('html', xmlHljs);
|
||||
hljs.registerLanguage('yaml', yamlHljs);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: string;
|
||||
followHeightOf?: HTMLElement | null;
|
||||
language?: string;
|
||||
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none';
|
||||
copyMessage?: string;
|
||||
}>(),
|
||||
{
|
||||
followHeightOf: null,
|
||||
language: 'txt',
|
||||
copyPlacement: 'top-right',
|
||||
copyMessage: 'Copy to clipboard',
|
||||
},
|
||||
);
|
||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||
const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
const tooltipText = ref(copyMessage.value);
|
||||
|
||||
function onCopyClicked() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied !';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltipText.value = copyMessage.value;
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
::v-deep(.n-scrollbar) {
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
.result-card {
|
||||
position: relative;
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
|
||||
&.top-right {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
&.bottom-right {
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
&.outside,
|
||||
&.none {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,9 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import FavoriteButton from './FavoriteButton.vue';
|
||||
import { useAppTheme } from '@/ui/theme/themes';
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
|
||||
const props = defineProps<{ tool: Tool & { category: string } }>();
|
||||
const { tool } = toRefs(props);
|
||||
const theme = useThemeVars();
|
||||
|
||||
const appTheme = useAppTheme();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link :to="tool.path">
|
||||
<n-card class="tool-card">
|
||||
<n-space justify="space-between" align="center">
|
||||
<c-card class="tool-card">
|
||||
<div flex items-center justify-between>
|
||||
<n-icon class="icon" size="40" :component="tool.icon" />
|
||||
<n-space align="center">
|
||||
<div flex items-center gap-8px>
|
||||
<n-tag
|
||||
v-if="tool.isNew"
|
||||
size="small"
|
||||
@@ -13,12 +26,12 @@
|
||||
:bordered="false"
|
||||
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
|
||||
>
|
||||
New
|
||||
{{ $t('toolCard.new') }}
|
||||
</n-tag>
|
||||
|
||||
<favorite-button :tool="tool" />
|
||||
</n-space>
|
||||
</n-space>
|
||||
<FavoriteButton :tool="tool" />
|
||||
</div>
|
||||
</div>
|
||||
<n-h3 class="title">
|
||||
<n-ellipsis>{{ tool.name }}</n-ellipsis>
|
||||
</n-h3>
|
||||
@@ -26,32 +39,25 @@
|
||||
<div class="description">
|
||||
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
|
||||
{{ tool.description }}
|
||||
<br />
|
||||
<br>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</n-card>
|
||||
</c-card>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { toRefs } from 'vue';
|
||||
import FavoriteButton from './FavoriteButton.vue';
|
||||
|
||||
const props = defineProps<{ tool: Tool & { category: string } }>();
|
||||
const { tool } = toRefs(props);
|
||||
const theme = useThemeVars();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
transition: border-color ease 0.5s;
|
||||
border-width: 2px !important;
|
||||
color: transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--n-color-target);
|
||||
border-color: v-bind('appTheme.primary.colorHover');
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
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;
|
||||
}
|
@@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb
|
||||
|
||||
if (throttle) {
|
||||
watchThrottled(getter, update, { throttle });
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
watch(getter, update);
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,30 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import type { Ref } from 'vue';
|
||||
import type { MaybeRefOrGetter } from 'vue';
|
||||
|
||||
export function useCopy({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<string>; text?: string; createToast?: boolean } = {}) {
|
||||
const { copy, copied, ...rest } = useClipboard({
|
||||
source,
|
||||
legacy: true,
|
||||
});
|
||||
|
||||
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: Ref; text?: string }) {
|
||||
const { copy } = useClipboard({ source });
|
||||
const message = useMessage();
|
||||
|
||||
return {
|
||||
async copy() {
|
||||
await copy();
|
||||
message.success(text);
|
||||
...rest,
|
||||
isJustCopied: copied,
|
||||
async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) {
|
||||
if (source) {
|
||||
await copy();
|
||||
}
|
||||
else {
|
||||
await copy(content);
|
||||
}
|
||||
|
||||
if (createToast) {
|
||||
message.success(notificationMessage ?? text);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -5,8 +5,8 @@ function getFileExtensionFromBase64({
|
||||
base64String,
|
||||
defaultExtension = 'txt',
|
||||
}: {
|
||||
base64String: string;
|
||||
defaultExtension?: string;
|
||||
base64String: string
|
||||
defaultExtension?: string
|
||||
}) {
|
||||
const hasMimeType = base64String.match(/data:(.*?);base64/i);
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { get, type MaybeRef } from '@vueuse/core';
|
||||
import { type MaybeRef, get } from '@vueuse/core';
|
||||
import Fuse from 'fuse.js';
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -9,14 +9,21 @@ function useFuzzySearch<Data>({
|
||||
data,
|
||||
options = {},
|
||||
}: {
|
||||
search: MaybeRef<string>;
|
||||
data: Data[];
|
||||
options?: Fuse.IFuseOptions<Data>;
|
||||
search: MaybeRef<string>
|
||||
data: Data[]
|
||||
options?: Fuse.IFuseOptions<Data> & { filterEmpty?: boolean }
|
||||
}) {
|
||||
const fuse = new Fuse(data, options);
|
||||
const filterEmpty = options.filterEmpty ?? true;
|
||||
|
||||
const searchResult = computed(() => {
|
||||
return fuse.search(get(search)).map(({ item }) => item);
|
||||
const searchResult = computed<Data[]>(() => {
|
||||
const query = get(search);
|
||||
|
||||
if (!filterEmpty && query === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return fuse.search(query).map(({ item }) => item);
|
||||
});
|
||||
|
||||
return { searchResult };
|
||||
|
@@ -26,7 +26,7 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
|
||||
|
||||
return computed<T>({
|
||||
get() {
|
||||
return transformer.fromQuery(proxy.value) as T;
|
||||
return transformer.fromQuery(proxy.value) as unknown as T;
|
||||
},
|
||||
set(value) {
|
||||
proxy.value = transformer.toQuery(value as never);
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isFalsyOrHasThrown } from './validation';
|
||||
|
||||
@@ -11,7 +10,7 @@ describe('useValidation', () => {
|
||||
expect(isFalsyOrHasThrown(() => {})).toBe(true);
|
||||
expect(
|
||||
isFalsyOrHasThrown(() => {
|
||||
throw new Error();
|
||||
throw new Error('message');
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
@@ -1,44 +1,48 @@
|
||||
import { type MaybeRef, get } from '@vueuse/core';
|
||||
import _ from 'lodash';
|
||||
import { reactive, watch, type Ref } from 'vue';
|
||||
import { type Ref, reactive, watch } from 'vue';
|
||||
|
||||
type ValidatorReturnType = unknown;
|
||||
|
||||
export interface UseValidationRule<T> {
|
||||
validator: (value: T) => ValidatorReturnType;
|
||||
message: string;
|
||||
validator: (value: T) => ValidatorReturnType
|
||||
message: string
|
||||
}
|
||||
|
||||
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
|
||||
try {
|
||||
const returnValue = cb();
|
||||
|
||||
if (_.isNil(returnValue)) return true;
|
||||
if (_.isNil(returnValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return returnValue === false;
|
||||
} catch (_) {
|
||||
}
|
||||
catch (_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidationAttrs = {
|
||||
feedback: string;
|
||||
validationStatus: string | undefined;
|
||||
};
|
||||
export interface ValidationAttrs {
|
||||
feedback: string
|
||||
validationStatus: string | undefined
|
||||
}
|
||||
|
||||
export function useValidation<T>({
|
||||
source,
|
||||
rules,
|
||||
watch: watchRefs = [],
|
||||
}: {
|
||||
source: Ref<T>;
|
||||
rules: UseValidationRule<T>[];
|
||||
watch?: Ref<unknown>[];
|
||||
source: Ref<T>
|
||||
rules: MaybeRef<UseValidationRule<T>[]>
|
||||
watch?: Ref<unknown>[]
|
||||
}) {
|
||||
const state = reactive<{
|
||||
message: string;
|
||||
status: undefined | 'error';
|
||||
isValid: boolean;
|
||||
attrs: ValidationAttrs;
|
||||
message: string
|
||||
status: undefined | 'error'
|
||||
isValid: boolean
|
||||
attrs: ValidationAttrs
|
||||
}>({
|
||||
message: '',
|
||||
status: undefined,
|
||||
@@ -55,7 +59,7 @@ export function useValidation<T>({
|
||||
state.message = '';
|
||||
state.status = undefined;
|
||||
|
||||
for (const rule of rules) {
|
||||
for (const rule of get(rules)) {
|
||||
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
||||
state.message = rule.message;
|
||||
state.status = 'error';
|
||||
|
@@ -23,9 +23,9 @@ export const config = figue({
|
||||
env: {
|
||||
doc: 'Application current env',
|
||||
format: 'enum',
|
||||
values: ['production', 'development', 'test'],
|
||||
values: ['production', 'development', 'preview', 'test'],
|
||||
default: 'development',
|
||||
env: 'MODE',
|
||||
env: 'VITE_VERCEL_ENV',
|
||||
},
|
||||
},
|
||||
plausible: {
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { NIcon, useThemeVars } from 'naive-ui';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { Heart, Menu2, Home2 } from '@vicons/tabler';
|
||||
import { toolsByCategory } from '@/tools';
|
||||
import { Heart, Home2, Menu2 } from '@vicons/tabler';
|
||||
|
||||
import { storeToRefs } from 'pinia';
|
||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||
import MenuLayout from '../components/MenuLayout.vue';
|
||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { config } from '@/config';
|
||||
import type { ToolCategory } from '@/tools/tools.types';
|
||||
import { useToolStore } from '@/tools/tools.store';
|
||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||
import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue';
|
||||
import SearchBar from '../components/SearchBar.vue';
|
||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||
import MenuLayout from '../components/MenuLayout.vue';
|
||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
const styleStore = useStyleStore();
|
||||
@@ -21,133 +21,123 @@ const version = config.app.version;
|
||||
const commitSha = config.app.lastCommitSha.slice(0, 7);
|
||||
|
||||
const { tracker } = useTracker();
|
||||
const { t } = useI18n();
|
||||
|
||||
const toolStore = useToolStore();
|
||||
const { favoriteTools, toolsByCategory } = storeToRefs(toolStore);
|
||||
|
||||
const tools = computed<ToolCategory[]>(() => [
|
||||
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
|
||||
...toolsByCategory,
|
||||
...(favoriteTools.value.length > 0 ? [{ name: t('tools.categories.favorite-tools'), components: favoriteTools.value }] : []),
|
||||
...toolsByCategory.value,
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
|
||||
<MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
|
||||
<template #sider>
|
||||
<router-link to="/" class="hero-wrapper">
|
||||
<hero-gradient class="gradient" />
|
||||
<RouterLink to="/" class="hero-wrapper">
|
||||
<HeroGradient class="gradient" />
|
||||
<div class="text-wrapper">
|
||||
<div class="title">IT - TOOLS</div>
|
||||
<div class="title">
|
||||
IT - TOOLS
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="subtitle">Handy tools for developers</div>
|
||||
<div class="subtitle">
|
||||
{{ $t('home.subtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</RouterLink>
|
||||
|
||||
<div class="sider-content">
|
||||
<n-space v-if="styleStore.isSmallScreen" justify="center">
|
||||
<navbar-buttons />
|
||||
</n-space>
|
||||
<div v-if="styleStore.isSmallScreen" flex flex-col items-center>
|
||||
<locale-selector w="90%" />
|
||||
|
||||
<collapsible-tool-menu :tools-by-category="tools" />
|
||||
<div flex justify-center>
|
||||
<NavbarButtons />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleToolMenu :tools-by-category="tools" />
|
||||
|
||||
<div class="footer">
|
||||
<div>
|
||||
IT-Tools
|
||||
|
||||
<n-button
|
||||
text
|
||||
tag="a"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
type="primary"
|
||||
depth="3"
|
||||
:href="`https://github.com/CorentinTh/it-tools/tree/v${version}`"
|
||||
>
|
||||
<c-link target="_blank" rel="noopener" :href="`https://github.com/CorentinTh/it-tools/tree/v${version}`">
|
||||
v{{ version }}
|
||||
</n-button>
|
||||
</c-link>
|
||||
|
||||
<template v-if="commitSha && commitSha.length > 0">
|
||||
-
|
||||
<n-button
|
||||
text
|
||||
tag="a"
|
||||
<c-link
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
type="primary"
|
||||
depth="3"
|
||||
:href="`https://github.com/CorentinTh/it-tools/tree/${commitSha}`"
|
||||
>
|
||||
{{ commitSha }}
|
||||
</n-button>
|
||||
</c-link>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
© {{ new Date().getFullYear() }}
|
||||
<n-button text tag="a" target="_blank" rel="noopener" type="primary" href="https://github.com/CorentinTh">
|
||||
<c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh">
|
||||
Corentin Thomasset
|
||||
</n-button>
|
||||
</c-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="navigation">
|
||||
<n-button
|
||||
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
||||
<div flex items-center justify-center gap-2>
|
||||
<c-button
|
||||
circle
|
||||
quaternary
|
||||
aria-label="Toggle menu"
|
||||
variant="text"
|
||||
:aria-label="$t('home.toggleMenu')"
|
||||
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
||||
>
|
||||
<n-icon size="25" :component="Menu2" />
|
||||
</n-button>
|
||||
<NIcon size="25" :component="Menu2" />
|
||||
</c-button>
|
||||
|
||||
<router-link to="/" #="{ navigate, href }" custom>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button
|
||||
tag="a"
|
||||
:href="href"
|
||||
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
||||
circle
|
||||
quaternary
|
||||
aria-label="Home"
|
||||
@click="navigate"
|
||||
>
|
||||
<n-icon size="25" :component="Home2" />
|
||||
</n-button>
|
||||
</template>
|
||||
Home
|
||||
</n-tooltip>
|
||||
</router-link>
|
||||
<c-tooltip tooltip="Home" position="bottom">
|
||||
<c-button to="/" circle variant="text" :aria-label="$t('home.home')">
|
||||
<NIcon size="25" :component="Home2" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<search-bar />
|
||||
<c-tooltip tooltip="UI Lib" position="bottom">
|
||||
<c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" :aria-label="$t('home.uiLib')">
|
||||
<icon-mdi:brush-variant text-20px />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<navbar-buttons v-if="!styleStore.isSmallScreen" />
|
||||
<command-palette />
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button
|
||||
round
|
||||
type="primary"
|
||||
tag="a"
|
||||
href="https://www.buymeacoffee.com/cthmsst"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
class="support-button"
|
||||
:bordered="false"
|
||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||
>
|
||||
Buy me a coffee
|
||||
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
||||
</n-button>
|
||||
</template>
|
||||
❤ Support IT Tools development !
|
||||
</n-tooltip>
|
||||
<locale-selector v-if="!styleStore.isSmallScreen" />
|
||||
|
||||
<div>
|
||||
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
||||
</div>
|
||||
|
||||
<c-tooltip position="bottom" tooltip="Support IT Tools development">
|
||||
<c-button
|
||||
round
|
||||
href="https://www.buymeacoffee.com/cthmsst"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
class="support-button"
|
||||
:bordered="false"
|
||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||
>
|
||||
{{ $t('home.buyMeACoffee') }}
|
||||
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
<slot />
|
||||
</template>
|
||||
</menu-layout>
|
||||
</MenuLayout>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -165,8 +155,8 @@ const tools = computed<ToolCategory[]>(() => [
|
||||
.support-button {
|
||||
background: rgb(37, 99, 108);
|
||||
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
|
||||
color: #fff;
|
||||
transition: all ease 0.2s;
|
||||
color: #fff !important;
|
||||
transition: padding ease 0.2s !important;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
@@ -225,25 +215,4 @@ const tools = computed<ToolCategory[]>(() => [
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ::v-deep(.n-menu-item-content-header) {
|
||||
// overflow: visible !important;
|
||||
// // overflow-x: hidden !important;
|
||||
// }
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
// width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,10 +2,10 @@
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useHead } from '@vueuse/head';
|
||||
import type { HeadObject } from '@vueuse/head';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import BaseLayout from './base.layout.vue';
|
||||
import FavoriteButton from '@/components/FavoriteButton.vue';
|
||||
import type { Tool } from '@/tools/tools.types';
|
||||
import BaseLayout from './base.layout.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -23,26 +23,31 @@ const head = computed<HeadObject>(() => ({
|
||||
],
|
||||
}));
|
||||
useHead(head);
|
||||
const { t } = useI18n();
|
||||
|
||||
const i18nKey = computed<string>(() => route.path.trim().replace('/', ''));
|
||||
const toolTitle = computed<string>(() => t(`tools.${i18nKey.value}.title`, String(route.meta.name)));
|
||||
const toolDescription = computed<string>(() => t(`tools.${i18nKey.value}.description`, String(route.meta.description)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<base-layout>
|
||||
<BaseLayout>
|
||||
<div class="tool-layout">
|
||||
<div class="tool-header">
|
||||
<n-space align="center" justify="space-between" :wrap="false">
|
||||
<div flex flex-nowrap items-center justify-between>
|
||||
<n-h1>
|
||||
{{ route.meta.name }}
|
||||
{{ toolTitle }}
|
||||
</n-h1>
|
||||
|
||||
<div>
|
||||
<favorite-button :tool="{name: route.meta.name} as Tool" />
|
||||
<FavoriteButton :tool="{ name: route.meta.name } as Tool" />
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<div class="separator" />
|
||||
|
||||
<div class="description">
|
||||
{{ route.meta.description }}
|
||||
{{ toolDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +55,7 @@ useHead(head);
|
||||
<div class="tool-content">
|
||||
<slot />
|
||||
</div>
|
||||
</base-layout>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@@ -1,23 +1,25 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createHead } from '@vueuse/head';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
import { plausible } from './plugins/plausible.plugin';
|
||||
|
||||
import 'virtual:uno.css';
|
||||
|
||||
registerSW();
|
||||
|
||||
import { naive } from './plugins/naive.plugin';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import { i18nPlugin } from './plugins/i18n.plugin';
|
||||
|
||||
registerSW();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(createHead());
|
||||
app.use(i18nPlugin);
|
||||
app.use(router);
|
||||
app.use(naive);
|
||||
app.use(plausible);
|
||||
|
91
src/modules/command-palette/command-palette.store.ts
Normal file
91
src/modules/command-palette/command-palette.store.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import _ from 'lodash';
|
||||
import type { PaletteOption } from './command-palette.types';
|
||||
import { useToolStore } from '@/tools/tools.store';
|
||||
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
import SunIcon from '~icons/mdi/white-balance-sunny';
|
||||
import GithubIcon from '~icons/mdi/github';
|
||||
import BugIcon from '~icons/mdi/bug-outline';
|
||||
import DiceIcon from '~icons/mdi/dice-5';
|
||||
import InfoIcon from '~icons/mdi/information-outline';
|
||||
|
||||
export const useCommandPaletteStore = defineStore('command-palette', () => {
|
||||
const toolStore = useToolStore();
|
||||
const styleStore = useStyleStore();
|
||||
const router = useRouter();
|
||||
const searchPrompt = ref('');
|
||||
|
||||
const toolsOptions = toolStore.tools.map(tool => ({
|
||||
...tool,
|
||||
to: tool.path,
|
||||
toolCategory: tool.category,
|
||||
category: 'Tools',
|
||||
}));
|
||||
|
||||
const searchOptions: PaletteOption[] = [
|
||||
...toolsOptions,
|
||||
{
|
||||
name: 'Random tool',
|
||||
description: 'Get a random tool from the list.',
|
||||
action: () => {
|
||||
const { path } = _.sample(toolStore.tools)!;
|
||||
router.push(path);
|
||||
},
|
||||
icon: DiceIcon,
|
||||
category: 'Tools',
|
||||
keywords: ['random', 'tool', 'pick', 'choose', 'select'],
|
||||
closeOnSelect: true,
|
||||
},
|
||||
{
|
||||
name: 'Toggle dark mode',
|
||||
description: 'Toggle dark mode on or off.',
|
||||
action: () => styleStore.toggleDark(),
|
||||
icon: SunIcon,
|
||||
category: 'Actions',
|
||||
keywords: ['dark', 'theme', 'toggle', 'mode', 'light', 'system'],
|
||||
},
|
||||
{
|
||||
name: 'Github repository',
|
||||
href: 'https://github.com/CorentinTh/it-tools',
|
||||
category: 'External',
|
||||
description: 'View the source code of it-tools on Github.',
|
||||
keywords: ['github', 'repo', 'repository', 'source', 'code'],
|
||||
icon: GithubIcon,
|
||||
},
|
||||
{
|
||||
name: 'Report a bug or an issue',
|
||||
description: 'Report a bug or an issue to help improve it-tools.',
|
||||
href: 'https://github.com/CorentinTh/it-tools/issues/new/choose',
|
||||
category: 'Actions',
|
||||
keywords: ['report', 'issue', 'bug', 'problem', 'error'],
|
||||
icon: BugIcon,
|
||||
},
|
||||
{
|
||||
name: 'About',
|
||||
description: 'Learn more about IT-Tools.',
|
||||
to: '/about',
|
||||
category: 'Pages',
|
||||
keywords: ['about', 'learn', 'more', 'info', 'information'],
|
||||
icon: InfoIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const { searchResult } = useFuzzySearch({
|
||||
search: searchPrompt,
|
||||
data: searchOptions,
|
||||
options: {
|
||||
keys: [{ name: 'name', weight: 2 }, 'description', 'keywords', 'category'],
|
||||
threshold: 0.3,
|
||||
},
|
||||
});
|
||||
|
||||
const filteredSearchResult = computed(() =>
|
||||
_.chain(searchResult.value).groupBy('category').mapValues(categoryOptions => _.take(categoryOptions, 5)).value());
|
||||
|
||||
return {
|
||||
filteredSearchResult,
|
||||
searchPrompt,
|
||||
};
|
||||
});
|
14
src/modules/command-palette/command-palette.types.ts
Normal file
14
src/modules/command-palette/command-palette.types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Component } from 'vue';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
export interface PaletteOption {
|
||||
name: string
|
||||
description?: string
|
||||
icon?: Component
|
||||
action?: () => void
|
||||
to?: RouteLocationRaw
|
||||
category: string
|
||||
keywords?: string[]
|
||||
href?: string
|
||||
closeOnSelect?: boolean
|
||||
}
|
154
src/modules/command-palette/command-palette.vue
Normal file
154
src/modules/command-palette/command-palette.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import _ from 'lodash';
|
||||
import { useCommandPaletteStore } from './command-palette.store';
|
||||
import type { PaletteOption } from './command-palette.types';
|
||||
|
||||
const isModalOpen = ref(false);
|
||||
const inputRef = ref();
|
||||
const router = useRouter();
|
||||
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
|
||||
|
||||
const commandPaletteStore = useCommandPaletteStore();
|
||||
const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore);
|
||||
|
||||
const keys = useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
whenever(isModalOpen, () => inputRef.value?.focus());
|
||||
|
||||
whenever(keys.ctrl_k, open);
|
||||
whenever(keys.meta_k, open);
|
||||
whenever(keys.escape, close);
|
||||
|
||||
function open() {
|
||||
return isModalOpen.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
isModalOpen.value = false;
|
||||
searchPrompt.value = '';
|
||||
}
|
||||
|
||||
const selectedOptionIndex = ref(0);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
const { key } = event;
|
||||
const isEnterPressed = key === 'Enter';
|
||||
const isArrowUpOrDown = ['ArrowUp', 'ArrowDown'].includes(key);
|
||||
const isArrowDown = key === 'ArrowDown';
|
||||
|
||||
if (isArrowUpOrDown) {
|
||||
const increment = isArrowDown ? 1 : -1;
|
||||
const maxIndex = Math.max(_.chain(filteredSearchResult.value).values().flatten().size().value() - 1, 0);
|
||||
|
||||
selectedOptionIndex.value = Math.min(Math.max(selectedOptionIndex.value + increment, 0), maxIndex);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEnterPressed) {
|
||||
const option = _.chain(filteredSearchResult.value)
|
||||
.values()
|
||||
.flatten()
|
||||
.nth(selectedOptionIndex.value)
|
||||
.value();
|
||||
|
||||
activateOption(option);
|
||||
}
|
||||
}
|
||||
|
||||
function getOptionIndex(option: PaletteOption) {
|
||||
return _.chain(filteredSearchResult.value)
|
||||
.values()
|
||||
.flatten()
|
||||
.findIndex(o => o === option)
|
||||
.value();
|
||||
}
|
||||
|
||||
function activateOption(option: PaletteOption) {
|
||||
const { closeOnSelect } = option;
|
||||
|
||||
if (option.action) {
|
||||
option.action();
|
||||
|
||||
if (closeOnSelect) {
|
||||
close();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const closeAfterNavigation = closeOnSelect || _.isUndefined(closeOnSelect);
|
||||
|
||||
if (option.to) {
|
||||
router.push(option.to);
|
||||
|
||||
if (closeAfterNavigation) {
|
||||
close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.href) {
|
||||
window.open(option.href, '_blank');
|
||||
|
||||
if (closeAfterNavigation) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex-1>
|
||||
<c-button w-full important:justify-start @click="isModalOpen = true">
|
||||
<span flex items-center gap-3 op-40>
|
||||
|
||||
<icon-mdi-search />
|
||||
{{ $t('search.label') }}
|
||||
|
||||
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
|
||||
{{ isMac ? 'Cmd' : 'Ctrl' }} + K
|
||||
</span>
|
||||
</span>
|
||||
</c-button>
|
||||
|
||||
<c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown">
|
||||
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />
|
||||
|
||||
<div v-for="(options, category) in filteredSearchResult" :key="category">
|
||||
<div ml-3 mt-3 text-sm font-bold text-primary op-60>
|
||||
{{ category }}
|
||||
</div>
|
||||
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />
|
||||
</div>
|
||||
</c-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.c-input-text {
|
||||
font-size: 18px;
|
||||
|
||||
::v-deep(.input-wrapper) {
|
||||
padding: 4px;
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.c-modal--overlay {
|
||||
align-items: flex-start !important;
|
||||
padding-top: 80px;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaletteOption } from '../command-palette.types';
|
||||
|
||||
const props = withDefaults(defineProps<{ option: PaletteOption; selected?: boolean }>(), {
|
||||
selected: false,
|
||||
});
|
||||
const emit = defineEmits(['activated']);
|
||||
const { option } = toRefs(props);
|
||||
|
||||
const { selected } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="option"
|
||||
:aria-selected="selected"
|
||||
:class="{
|
||||
'text-white': selected,
|
||||
'bg-primary': selected,
|
||||
}"
|
||||
w-full flex cursor-pointer items-center overflow-hidden rounded pa-3 transition hover:bg-primary hover:text-white
|
||||
@click="() => emit('activated', option)"
|
||||
>
|
||||
<component :is="option.icon" v-if="option.icon" mr-3 h-30px w-30px shrink-0 op-50 />
|
||||
|
||||
<div flex-1 overflow-hidden>
|
||||
<div truncate font-bold lh-tight op-90>
|
||||
{{ option.name }}
|
||||
</div>
|
||||
|
||||
<div v-if="option.description" truncate lh-tight op-60>
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
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>
|
7
src/modules/shared/date.models.ts
Normal file
7
src/modules/shared/date.models.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export { getUrlFriendlyDateTime };
|
||||
|
||||
function getUrlFriendlyDateTime({ date = new Date() }: { date?: Date } = {}) {
|
||||
return format(date, 'yyyy-MM-dd-HH-mm-ss');
|
||||
}
|
5
src/modules/shared/number.models.ts
Normal file
5
src/modules/shared/number.models.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
function clamp({ value, min = 0, max = 100 }: { value: number; min?: number; max?: number }) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export { clamp };
|
@@ -16,7 +16,7 @@ function useTracker() {
|
||||
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
|
||||
|
||||
if (_.isNil(plausible)) {
|
||||
throw new Error('Plausible must be instantiated');
|
||||
throw new TypeError('Plausible must be instantiated');
|
||||
}
|
||||
|
||||
const tracker = createTrackerService({ plausible });
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { Coffee } from '@vicons/tabler';
|
||||
import { useHead } from '@vueuse/head';
|
||||
|
||||
useHead({ title: 'Page not found - IT Tools' });
|
||||
@@ -7,14 +6,22 @@ useHead({ title: 'Page not found - IT Tools' });
|
||||
|
||||
<template>
|
||||
<div mt-20 flex flex-col items-center>
|
||||
<n-icon :component="Coffee" size="100" depth="3" />
|
||||
<span text-90px lh-1 op-50>
|
||||
<icon-mdi:kettle-steam-outline />
|
||||
</span>
|
||||
|
||||
<n-h1 m-0 mt-3>404 Not Found</n-h1>
|
||||
<n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text>
|
||||
<n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text>
|
||||
<h1 m-0 mt-3>
|
||||
{{ $t('404.notFound') }}
|
||||
</h1>
|
||||
<div mt-4 op-60>
|
||||
{{ $t('404.sorry') }}
|
||||
</div>
|
||||
<div mb-8 op-60>
|
||||
{{ $t('404.maybe') }}
|
||||
</div>
|
||||
|
||||
<router-link to="/" #="{ navigate, href }" custom>
|
||||
<n-button tag="a" :href="href" secondary @click="navigate"> Back home </n-button>
|
||||
</router-link>
|
||||
<c-button to="/">
|
||||
{{ $t('404.backHome') }}
|
||||
</c-button>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,100 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||
import { useHead } from '@vueuse/head';
|
||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||
|
||||
useHead({ title: 'About - IT Tools' });
|
||||
const { tracker } = useTracker();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="about-page">
|
||||
<n-h1>About</n-h1>
|
||||
<n-p>
|
||||
This wonderful website, made with ❤ by
|
||||
<n-button text tag="a" href="https://github.com/CorentinTh" target="_blank" rel="noopener" type="primary">
|
||||
Corentin Thomasset </n-button
|
||||
>, aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to
|
||||
share it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
|
||||
</n-p>
|
||||
<n-p>
|
||||
IT Tools is open-source (under the MIT license) and free, and will always be, but it cost 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
|
||||
<n-button
|
||||
type="primary"
|
||||
tag="a"
|
||||
text
|
||||
<div mx-auto mt-50px max-w-600px>
|
||||
<h1>{{ $t('about.h1') }}</h1>
|
||||
<p text-justify>
|
||||
{{ $t('about.h1p1') }}
|
||||
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener">
|
||||
Corentin Thomasset
|
||||
</c-link>{{ $t('about.h1p2') }}
|
||||
</p>
|
||||
<p text-justify>
|
||||
{{ $t('about.h1p3') }}
|
||||
<c-link
|
||||
href="https://www.buymeacoffee.com/cthmsst"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||
>
|
||||
sponsoring me </n-button
|
||||
>.
|
||||
</n-p>
|
||||
{{ $t('about.h1p4') }}
|
||||
</c-link>.
|
||||
</p>
|
||||
|
||||
<n-h2>Technologies</n-h2>
|
||||
<n-p>
|
||||
IT Tools is made in Vue JS (vue 3) with the the naive-ui component library and is hosted and continuously deployed
|
||||
by Vercel. Third party open-source libraries are used in some tools, you may find the complete list in the
|
||||
<n-button
|
||||
type="primary"
|
||||
tag="a"
|
||||
text
|
||||
href="https://github.com/CorentinTh/it-tools/blob/main/package.json"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<h2>{{ $t('about.h2') }}</h2>
|
||||
<p text-justify>
|
||||
{{ $t('about.h2p1') }}
|
||||
<c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank">
|
||||
package.json
|
||||
</n-button>
|
||||
file of the repository.
|
||||
</n-p>
|
||||
</c-link>
|
||||
{{ $t('about.h2p2') }}
|
||||
</p>
|
||||
|
||||
<n-h2>Found a bug? A tool is missing?</n-h2>
|
||||
<n-p>
|
||||
If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a
|
||||
feature request in the
|
||||
<n-button
|
||||
type="primary"
|
||||
tag="a"
|
||||
text
|
||||
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D%20My%20feature"
|
||||
<h2>{{ $t('about.h3') }}</h2>
|
||||
<p text-justify>
|
||||
{{ $t('about.h3p1') }}
|
||||
<c-link
|
||||
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
issues section
|
||||
</n-button>
|
||||
in the GitHub repository.
|
||||
</n-p>
|
||||
<n-p>
|
||||
And if you found a bug, or something broken that doesn't work as expected, please fill a bug report in the
|
||||
<n-button
|
||||
type="primary"
|
||||
tag="a"
|
||||
text
|
||||
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=bug&template=bug_report.md&title=%5BBUG%5D%20My%20bug"
|
||||
{{ $t('about.h3p2') }}
|
||||
</c-link>
|
||||
{{ $t('about.h3p3') }}
|
||||
</p>
|
||||
<p text-justify>
|
||||
{{ $t('about.h3p4') }}
|
||||
<c-link
|
||||
href="https://github.com/CorentinTh/it-tools/issues/new/choose"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
issues section
|
||||
</n-button>
|
||||
in the GitHub repository.
|
||||
</n-p>
|
||||
{{ $t('about.h3p5') }}
|
||||
</c-link>
|
||||
{{ $t('about.h3p6') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.about-page {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
.n-h2 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.n-p {
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { config } from '@/config';
|
||||
import { useToolStore } from '@/tools/tools.store';
|
||||
import { Heart } from '@vicons/tabler';
|
||||
import { useHead } from '@vueuse/head';
|
||||
import ColoredCard from '../components/ColoredCard.vue';
|
||||
import ToolCard from '../components/ToolCard.vue';
|
||||
import { useToolStore } from '@/tools/tools.store';
|
||||
import { config } from '@/config';
|
||||
|
||||
const toolStore = useToolStore();
|
||||
|
||||
useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,53 +17,52 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||
<div class="grid-wrapper">
|
||||
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi>
|
||||
<colored-card title="You like it-tools?" :icon="Heart">
|
||||
Give us a star on
|
||||
<ColoredCard :title="$t('home.follow.title')" :icon="Heart">
|
||||
{{ $t('home.follow.p1') }}
|
||||
<a
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' GitHub repository"
|
||||
>GitHub</a
|
||||
>
|
||||
or follow us on
|
||||
:aria-label="$t('home.follow.githubRepository')"
|
||||
>GitHub</a>
|
||||
{{ $t('home.follow.p2') }}
|
||||
<a
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' Twitter account"
|
||||
>Twitter</a
|
||||
>! Thank you
|
||||
:aria-label="$t('home.follow.twitterAccount')"
|
||||
>Twitter</a>.
|
||||
{{ $t('home.follow.thankYou') }}
|
||||
<n-icon :component="Heart" />
|
||||
</colored-card>
|
||||
</ColoredCard>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<transition name="height">
|
||||
<div v-if="toolStore.favoriteTools.length > 0">
|
||||
<n-h3>Your favorite tools</n-h3>
|
||||
<n-h3>{{ $t('home.categories.favoriteTools') }}</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
||||
<tool-card :tool="tool" />
|
||||
<ToolCard :tool="tool" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-if="toolStore.newTools.length > 0">
|
||||
<n-h3>Newest tools</n-h3>
|
||||
<n-h3>{{ t('home.categories.newestTools') }}</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
|
||||
<tool-card :tool="tool" />
|
||||
<ToolCard :tool="tool" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
|
||||
<n-h3>All the tools</n-h3>
|
||||
<n-h3>{{ $t('home.categories.allTools') }}</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
|
||||
<transition>
|
||||
<tool-card :tool="tool" />
|
||||
<ToolCard :tool="tool" />
|
||||
</transition>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
37
src/plugins/i18n.plugin.ts
Normal file
37
src/plugins/i18n.plugin.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Plugin } from 'vue';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import baseMessages from '@intlify/unplugin-vue-i18n/messages';
|
||||
import _ from 'lodash';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' });
|
||||
|
||||
const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => {
|
||||
const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? [];
|
||||
const content = parseYaml(await fileDescriptor());
|
||||
|
||||
return { [locale]: content };
|
||||
}));
|
||||
|
||||
const messages = _.merge(
|
||||
baseMessages,
|
||||
_.merge({}, ...messagesByTools),
|
||||
);
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages,
|
||||
});
|
||||
|
||||
export const i18nPlugin: Plugin = {
|
||||
install: (app) => {
|
||||
app.use(i18n);
|
||||
},
|
||||
};
|
||||
|
||||
export const translate = function (localeKey: string) {
|
||||
// @ts-expect-error global
|
||||
const hasKey = i18n.global.te(localeKey, i18n.global.locale);
|
||||
return hasKey ? i18n.global.t(localeKey) : localeKey;
|
||||
};
|
@@ -1,8 +1,8 @@
|
||||
import { config } from '@/config';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import Plausible from 'plausible-tracker';
|
||||
import type { App } from 'vue';
|
||||
import { config } from '@/config';
|
||||
|
||||
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
|
||||
return {
|
||||
@@ -15,11 +15,11 @@ function createPlausibleInstance({
|
||||
config,
|
||||
}: {
|
||||
config: {
|
||||
isTrackerEnabled: boolean;
|
||||
domain: string;
|
||||
apiHost: string;
|
||||
trackLocalhost: boolean;
|
||||
};
|
||||
isTrackerEnabled: boolean
|
||||
domain: string
|
||||
apiHost: string
|
||||
trackLocalhost: boolean
|
||||
}
|
||||
}) {
|
||||
if (config.isTrackerEnabled) {
|
||||
return Plausible(config);
|
||||
|
@@ -4,6 +4,7 @@ import HomePage from './pages/Home.page.vue';
|
||||
import NotFound from './pages/404.page.vue';
|
||||
import { tools } from './tools';
|
||||
import { config } from './config';
|
||||
import { routes as demoRoutes } from './ui/demo/demo.routes';
|
||||
|
||||
const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
||||
path,
|
||||
@@ -14,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
||||
const toolsRedirectRoutes = tools
|
||||
.filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0)
|
||||
.flatMap(
|
||||
({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [],
|
||||
({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [],
|
||||
);
|
||||
|
||||
const router = createRouter({
|
||||
@@ -32,6 +33,7 @@ const router = createRouter({
|
||||
},
|
||||
...toolsRoutes,
|
||||
...toolsRedirectRoutes,
|
||||
...(config.app.env === 'development' ? demoRoutes : []),
|
||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
|
||||
],
|
||||
});
|
||||
|
27
src/shims.d.ts
vendored
27
src/shims.d.ts
vendored
@@ -1,10 +1,35 @@
|
||||
declare module '*.vue' {
|
||||
import type { ComponentOptions, ComponentOptions } from 'vue';
|
||||
import type { ComponentOptions } from 'vue';
|
||||
const Component: ComponentOptions;
|
||||
export default Component;
|
||||
}
|
||||
|
||||
declare module '*.md' {
|
||||
import type { ComponentOptions } from 'vue';
|
||||
const Component: ComponentOptions;
|
||||
export default Component;
|
||||
}
|
||||
|
||||
declare module 'iarna-toml-esm' {
|
||||
export const parse: (toml: string) => any;
|
||||
export const stringify: (obj: any) => string;
|
||||
}
|
||||
|
||||
declare module 'emojilib' {
|
||||
const lib: Record<string, string[]>;
|
||||
export default lib;
|
||||
}
|
||||
|
||||
declare module 'unicode-emoji-json' {
|
||||
const emoji: Record<string, {
|
||||
name: string;
|
||||
slug: string;
|
||||
group: string;
|
||||
emoji_version: string;
|
||||
unicode_version: string;
|
||||
skin_tone_support: boolean;
|
||||
skin_tone_support_unicode_version: string;
|
||||
}>;
|
||||
|
||||
export default emoji;
|
||||
}
|
@@ -1,17 +1,19 @@
|
||||
import { useMediaQuery, useStorage } from '@vueuse/core';
|
||||
import { useDark, useMediaQuery, useStorage, useToggle } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import { watch, type Ref } from 'vue';
|
||||
import { type Ref, watch } from 'vue';
|
||||
|
||||
export const useStyleStore = defineStore('style', {
|
||||
state: () => {
|
||||
const isDarkTheme = useStorage('isDarkTheme', true) as Ref<boolean>;
|
||||
const isDarkTheme = useDark();
|
||||
const toggleDark = useToggle(isDarkTheme);
|
||||
const isSmallScreen = useMediaQuery('(max-width: 700px)');
|
||||
const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>;
|
||||
|
||||
watch(isSmallScreen, (v) => (isMenuCollapsed.value = v));
|
||||
watch(isSmallScreen, v => (isMenuCollapsed.value = v));
|
||||
|
||||
return {
|
||||
isDarkTheme,
|
||||
toggleDark,
|
||||
isMenuCollapsed,
|
||||
isSmallScreen,
|
||||
};
|
||||
|
@@ -1,45 +1,12 @@
|
||||
<template>
|
||||
<n-card title="Base64 to file">
|
||||
<n-form-item
|
||||
:feedback="base64InputValidation.message"
|
||||
:validation-status="base64InputValidation.status"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" />
|
||||
</n-form-item>
|
||||
<n-space justify="center">
|
||||
<n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()">
|
||||
Download file
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="File to base64">
|
||||
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
|
||||
<n-upload-dragger>
|
||||
<div mb-2>
|
||||
<n-icon size="35" :depth="3" :component="Upload" />
|
||||
</div>
|
||||
<n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" />
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copyFileBase64()"> Copy </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Upload } from '@vicons/tabler';
|
||||
import { useBase64 } from '@vueuse/core';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import type { Ref } from 'vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isValidBase64 } from '@/utils/base64';
|
||||
import { Upload } from '@vicons/tabler';
|
||||
import { useBase64 } from '@vueuse/core';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import { ref, type Ref } from 'vue';
|
||||
|
||||
const base64Input = ref('');
|
||||
const { download } = useDownloadFileFromBase64({ source: base64Input });
|
||||
@@ -48,17 +15,20 @@ const base64InputValidation = useValidation({
|
||||
rules: [
|
||||
{
|
||||
message: 'Invalid base 64 string',
|
||||
validator: (value) => isValidBase64(value.trim()),
|
||||
validator: value => isValidBase64(value.trim()),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function downloadFile() {
|
||||
if (!base64InputValidation.isValid) return;
|
||||
if (!base64InputValidation.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
download();
|
||||
} catch (_) {
|
||||
}
|
||||
catch (_) {
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -76,12 +46,47 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-input,
|
||||
.n-upload {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
<template>
|
||||
<c-card title="Base64 to file">
|
||||
<c-input-text
|
||||
v-model:value="base64Input"
|
||||
multiline
|
||||
placeholder="Put your base64 file string here..."
|
||||
rows="5"
|
||||
:validation="base64InputValidation"
|
||||
mb-2
|
||||
/>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
|
||||
Download file
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<c-card title="File to base64">
|
||||
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
|
||||
<n-upload-dragger>
|
||||
<div mb-2>
|
||||
<n-icon size="35" :depth="3" :component="Upload" />
|
||||
</div>
|
||||
<div op-60>
|
||||
Click or drag a file to this area to upload
|
||||
</div>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 />
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button @click="copyFileBase64()">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
::v-deep(.n-upload-trigger) {
|
||||
width: 100%;
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import { defineTool } from '../tool';
|
||||
export const tool = defineTool({
|
||||
name: 'Base64 file converter',
|
||||
path: '/base64-file-converter',
|
||||
description: "Convert string, files or images into a it's base64 representation.",
|
||||
description: 'Convert string, files or images into a it\'s base64 representation.',
|
||||
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
|
||||
component: () => import('./base64-file-converter.vue'),
|
||||
icon: FileDigit,
|
||||
|
@@ -1,55 +1,90 @@
|
||||
<template>
|
||||
<n-card title="String to base64">
|
||||
<n-form-item label="String to encode">
|
||||
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="Base64 of string">
|
||||
<n-input
|
||||
:value="base64Output"
|
||||
type="textarea"
|
||||
readonly
|
||||
placeholder="The base64 encoding of your string will be here"
|
||||
rows="5"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copyTextBase64()"> Copy base64 </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="Base64 to string">
|
||||
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs">
|
||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="Decoded string">
|
||||
<n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copyText()"> Copy decoded string </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false);
|
||||
const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false);
|
||||
|
||||
const textInput = ref('');
|
||||
const base64Output = computed(() => textToBase64(textInput.value));
|
||||
const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value }));
|
||||
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
|
||||
|
||||
const base64Input = ref('');
|
||||
const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), ''));
|
||||
const textOutput = computed(() =>
|
||||
withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''),
|
||||
);
|
||||
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
|
||||
const b64Validation = useValidation({
|
||||
source: base64Input,
|
||||
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
|
||||
});
|
||||
const b64ValidationRules = [
|
||||
{
|
||||
message: 'Invalid base64 string',
|
||||
validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }),
|
||||
},
|
||||
];
|
||||
const b64ValidationWatch = [decodeUrlSafe];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card title="String to base64">
|
||||
<n-form-item label="Encode URL safe" label-placement="left">
|
||||
<n-switch v-model:value="encodeUrlSafe" />
|
||||
</n-form-item>
|
||||
<c-input-text
|
||||
v-model:value="textInput"
|
||||
multiline
|
||||
placeholder="Put your string here..."
|
||||
rows="5"
|
||||
label="String to encode"
|
||||
raw-text
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<c-input-text
|
||||
label="Base64 of string"
|
||||
:value="base64Output"
|
||||
multiline
|
||||
readonly
|
||||
placeholder="The base64 encoding of your string will be here"
|
||||
rows="5"
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button @click="copyTextBase64()">
|
||||
Copy base64
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<c-card title="Base64 to string">
|
||||
<n-form-item label="Decode URL safe" label-placement="left">
|
||||
<n-switch v-model:value="decodeUrlSafe" />
|
||||
</n-form-item>
|
||||
<c-input-text
|
||||
v-model:value="base64Input"
|
||||
multiline
|
||||
placeholder="Your base64 string..."
|
||||
rows="5"
|
||||
:validation-rules="b64ValidationRules"
|
||||
:validation-watch="b64ValidationWatch"
|
||||
label="Base64 string to decode"
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="textOutput"
|
||||
label="Decoded string"
|
||||
placeholder="The decoded string will be here"
|
||||
multiline
|
||||
rows="5"
|
||||
readonly
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button @click="copyText()">
|
||||
Copy decoded string
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
|
@@ -1,37 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item label="Username">
|
||||
<n-input v-model:value="username" placeholder="Your username..." clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="Password">
|
||||
<n-input
|
||||
v-model:value="password"
|
||||
placeholder="Your password..."
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<br />
|
||||
<n-card>
|
||||
<n-statistic label="Authorization header:" class="header">
|
||||
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
|
||||
{{ header }}
|
||||
</n-scrollbar>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<br />
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copy">Copy header</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { textToBase64 } from '@/utils/base64';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
@@ -40,6 +9,34 @@ const header = computed(() => `Authorization: Basic ${textToBase64(`${username.v
|
||||
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
|
||||
<c-input-text
|
||||
v-model:value="password"
|
||||
label="Password"
|
||||
placeholder="Your password..."
|
||||
clearable
|
||||
raw-text
|
||||
mb-2
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<c-card>
|
||||
<n-statistic label="Authorization header:" class="header">
|
||||
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
|
||||
{{ header }}
|
||||
</n-scrollbar>
|
||||
</n-statistic>
|
||||
</c-card>
|
||||
<div mt-5 flex justify-center>
|
||||
<c-button @click="copy()">
|
||||
Copy header
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
::v-deep(.n-statistic-value__content) {
|
||||
font-family: monospace;
|
||||
|
@@ -1,63 +1,7 @@
|
||||
<template>
|
||||
<n-card title="Hash">
|
||||
<n-form label-width="120">
|
||||
<n-form-item label="Your string: " label-placement="left">
|
||||
<n-input
|
||||
v-model:value="input"
|
||||
placeholder="Your string to bcrypt..."
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Salt count: " label-placement="left">
|
||||
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
|
||||
</n-form-item>
|
||||
<n-input :value="hashed" readonly style="text-align: center" />
|
||||
</n-form>
|
||||
<br />
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copy"> Copy hash </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="Compare string with hash">
|
||||
<n-form label-width="120">
|
||||
<n-form-item label="Your string: " label-placement="left">
|
||||
<n-input
|
||||
v-model:value="compareString"
|
||||
placeholder="Your string to compare..."
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Your hash: " label-placement="left">
|
||||
<n-input
|
||||
v-model:value="compareHash"
|
||||
placeholder="Your hahs to compare..."
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
||||
<div class="compare-result" :class="{ positive: compareMatch }">
|
||||
{{ compareMatch ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { hashSync, compareSync } from 'bcryptjs';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { compareSync, hashSync } from 'bcryptjs';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
@@ -71,6 +15,47 @@ const compareHash = ref('');
|
||||
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card title="Hash">
|
||||
<c-input-text
|
||||
v-model:value="input"
|
||||
placeholder="Your string to bcrypt..."
|
||||
raw-text
|
||||
label="Your string: "
|
||||
label-position="left"
|
||||
label-width="120px"
|
||||
mb-2
|
||||
/>
|
||||
<n-form-item label="Salt count: " label-placement="left" label-width="120">
|
||||
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
|
||||
</n-form-item>
|
||||
|
||||
<c-input-text :value="hashed" readonly text-center />
|
||||
|
||||
<div mt-5 flex justify-center>
|
||||
<c-button @click="copy()">
|
||||
Copy hash
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<c-card title="Compare string with hash">
|
||||
<n-form label-width="120">
|
||||
<n-form-item label="Your string: " label-placement="left">
|
||||
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
|
||||
</n-form-item>
|
||||
<n-form-item label="Your hash: " label-placement="left">
|
||||
<c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text />
|
||||
</n-form-item>
|
||||
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
||||
<div class="compare-result" :class="{ positive: compareMatch }">
|
||||
{{ compareMatch ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.compare-result {
|
||||
color: v-bind('themeVars.errorColor');
|
||||
|
@@ -13,22 +13,22 @@ function computeAverage({ data }: { data: number[] }) {
|
||||
function computeVariance({ data }: { data: number[] }) {
|
||||
const mean = computeAverage({ data });
|
||||
|
||||
const squaredDiffs = data.map((value) => Math.pow(value - mean, 2));
|
||||
const squaredDiffs = data.map(value => (value - mean) ** 2);
|
||||
|
||||
return computeAverage({ data: squaredDiffs });
|
||||
}
|
||||
|
||||
function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) {
|
||||
function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const rows = data.map((obj) => Object.values(obj));
|
||||
const rows = data.map(obj => Object.values(obj));
|
||||
|
||||
const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`;
|
||||
const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`;
|
||||
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
|
||||
const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
|
||||
const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n');
|
||||
|
||||
return `${headerRow}\n${separatorRow}\n${dataRows}`;
|
||||
}
|
||||
|
@@ -1,92 +1,11 @@
|
||||
<template>
|
||||
<n-scrollbar style="flex: 1" x-scrollable>
|
||||
<n-space :wrap="false" style="flex: 1" justify="center" :size="0">
|
||||
<div v-for="(suite, index) of suites" :key="index">
|
||||
<n-card style="width: 292px; margin: 0 8px 5px">
|
||||
<n-form-item label="Suite name:" :show-feedback="false" label-placement="left">
|
||||
<n-input v-model:value="suite.title" placeholder="Suite name..." />
|
||||
</n-form-item>
|
||||
|
||||
<n-divider></n-divider>
|
||||
<n-form-item label="Suite values" :show-feedback="false">
|
||||
<dynamic-values v-model:values="suite.data" />
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button v-if="suites.length > 1" quaternary @click="suites.splice(index, 1)">
|
||||
<template #icon>
|
||||
<n-icon :component="Trash" depth="3" />
|
||||
</template>
|
||||
Delete suite
|
||||
</n-button>
|
||||
<n-button quaternary @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })">
|
||||
<template #icon>
|
||||
<n-icon :component="Plus" depth="3" />
|
||||
</template>
|
||||
Add suite
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-space>
|
||||
<br />
|
||||
</n-scrollbar>
|
||||
|
||||
<div style="flex: 0 0 100%">
|
||||
<div style="max-width: 600px; margin: 0 auto">
|
||||
<n-space justify="center">
|
||||
<n-form-item label="Unit:" label-placement="left">
|
||||
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
|
||||
</n-form-item>
|
||||
|
||||
<n-button
|
||||
tertiary
|
||||
@click="
|
||||
suites = [
|
||||
{ title: 'Suite 1', data: [] },
|
||||
{ title: 'Suite 2', data: [] },
|
||||
]
|
||||
"
|
||||
>Reset suites</n-button
|
||||
>
|
||||
</n-space>
|
||||
|
||||
<n-table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ header.position }}</th>
|
||||
<th>{{ header.title }}</th>
|
||||
<th>{{ header.size }}</th>
|
||||
<th>{{ header.mean }}</th>
|
||||
<th>{{ header.variance }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
|
||||
<td>{{ position }}</td>
|
||||
<td>{{ title }}</td>
|
||||
<td>{{ size }}</td>
|
||||
<td>{{ mean }}</td>
|
||||
<td>{{ variance }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
<br />
|
||||
<n-space justify="center">
|
||||
<n-button tertiary @click="copyAsMarkdown">Copy as markdown table</n-button>
|
||||
<n-button tertiary @click="copyAsBulletList">Copy as bullet list</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Trash, Plus } from '@vicons/tabler';
|
||||
import { useClipboard, useStorage } from '@vueuse/core';
|
||||
import { Plus, Trash } from '@vicons/tabler';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import _ from 'lodash';
|
||||
import { computed } from 'vue';
|
||||
import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models';
|
||||
|
||||
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
|
||||
import DynamicValues from './dynamic-values.vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const suites = useStorage('benchmark-builder:suites', [
|
||||
{ title: 'Suite 1', data: [5, 10] },
|
||||
@@ -116,8 +35,8 @@ const results = computed(() => {
|
||||
const deltaWithBestMean = mean - bestMean;
|
||||
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
|
||||
|
||||
const comparisonValues: string =
|
||||
index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
|
||||
const comparisonValues: string
|
||||
= (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
|
||||
|
||||
return {
|
||||
position: index + 1,
|
||||
@@ -129,14 +48,14 @@ const results = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const { copy } = useCopy({ createToast: false });
|
||||
|
||||
const header = {
|
||||
position: 'Position',
|
||||
title: 'Suite',
|
||||
size: 'Samples',
|
||||
mean: 'Mean',
|
||||
variance: 'Variance',
|
||||
position: 'Position',
|
||||
};
|
||||
|
||||
function copyAsMarkdown() {
|
||||
@@ -159,4 +78,69 @@ function copyAsBulletList() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<n-scrollbar style="flex: 1" x-scrollable>
|
||||
<div mb-5 flex flex-1 flex-nowrap justify-center gap-12px>
|
||||
<div v-for="(suite, index) of suites" :key="index">
|
||||
<c-card style="width: 294px">
|
||||
<c-input-text
|
||||
v-model:value="suite.title"
|
||||
label-position="left"
|
||||
label="Suite name"
|
||||
placeholder="Suite name..."
|
||||
clearable
|
||||
/>
|
||||
|
||||
<n-divider />
|
||||
<n-form-item label="Suite values" :show-feedback="false">
|
||||
<DynamicValues v-model:values="suite.data" />
|
||||
</n-form-item>
|
||||
</c-card>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)">
|
||||
<n-icon :component="Trash" depth="3" mr-2 size="18" />
|
||||
Delete suite
|
||||
</c-button>
|
||||
<c-button
|
||||
variant="text"
|
||||
@click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"
|
||||
>
|
||||
<n-icon :component="Plus" depth="3" mr-2 size="18" />
|
||||
Add suite
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
|
||||
<div style="flex: 0 0 100%">
|
||||
<div style="max-width: 600px; margin: 0 auto">
|
||||
<div mx-auto max-w-sm flex justify-center gap-3>
|
||||
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
|
||||
|
||||
<c-button
|
||||
@click="
|
||||
suites = [
|
||||
{ title: 'Suite 1', data: [] },
|
||||
{ title: 'Suite 2', data: [] },
|
||||
]
|
||||
"
|
||||
>
|
||||
Reset suites
|
||||
</c-button>
|
||||
</div>
|
||||
|
||||
<c-table :data="results" :headers="header" />
|
||||
|
||||
<div mt-5 flex justify-center gap-3>
|
||||
<c-button @click="copyAsMarkdown()">
|
||||
Copy as markdown table
|
||||
</c-button>
|
||||
<c-button @click="copyAsBulletList()">
|
||||
Copy as bullet list
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,45 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5">
|
||||
<n-input-number
|
||||
:ref="refs.set"
|
||||
v-model:value="values[index]"
|
||||
:show-button="false"
|
||||
placeholder="Set your measure..."
|
||||
autofocus
|
||||
@keydown.enter="onInputEnter(index)"
|
||||
/>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button circle quaternary @click="values.splice(index, 1)">
|
||||
<template #icon>
|
||||
<n-icon :component="Trash" depth="3" />
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
Delete value
|
||||
</n-tooltip>
|
||||
</n-space>
|
||||
|
||||
<n-button tertiary @click="addValue">
|
||||
<template #icon>
|
||||
<n-icon :component="Plus" />
|
||||
</template>
|
||||
Add a measure
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Trash, Plus } from '@vicons/tabler';
|
||||
import { Plus, Trash } from '@vicons/tabler';
|
||||
import { useTemplateRefsList, useVModel } from '@vueuse/core';
|
||||
import { NInputNumber } from 'naive-ui';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const props = defineProps<{ values: (number | null)[] }>();
|
||||
|
||||
const emit = defineEmits(['update:values']);
|
||||
|
||||
const refs = useTemplateRefsList<typeof NInputNumber>();
|
||||
|
||||
const props = defineProps<{ values: (number | null)[] }>();
|
||||
const emit = defineEmits(['update:values']);
|
||||
const values = useVModel(props, 'values', emit);
|
||||
|
||||
async function addValue() {
|
||||
@@ -58,4 +28,27 @@ function onInputEnter(index: number) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2>
|
||||
<NInputNumber
|
||||
:ref="refs.set"
|
||||
v-model:value="values[index]"
|
||||
:show-button="false"
|
||||
placeholder="Set your measure..."
|
||||
autofocus
|
||||
@keydown.enter="onInputEnter(index)"
|
||||
/>
|
||||
<c-tooltip tooltip="Delete this value">
|
||||
<c-button circle variant="text" @click="values.splice(index, 1)">
|
||||
<n-icon :component="Trash" depth="3" size="18" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
|
||||
<c-button @click="addValue">
|
||||
<n-icon :component="Plus" depth="3" mr-2 size="18" />
|
||||
Add a measure
|
||||
</c-button>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,67 +1,4 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card>
|
||||
<n-grid cols="3" x-gap="12">
|
||||
<n-gi span="1">
|
||||
<n-form-item label="Language:">
|
||||
<n-select
|
||||
v-model:value="language"
|
||||
:options="Object.keys(languages).map((label) => ({ label, value: label }))"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-form-item
|
||||
label="Entropy (seed):"
|
||||
:feedback="entropyValidation.message"
|
||||
:validation-status="entropyValidation.status"
|
||||
>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="entropy" placeholder="Your string..." />
|
||||
<n-button @click="refreshEntropy">
|
||||
<n-icon size="22">
|
||||
<Refresh />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
<n-button @click="copyEntropy">
|
||||
<n-icon size="22">
|
||||
<Copy />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-form-item
|
||||
label="Passphrase (mnemonic):"
|
||||
:feedback="mnemonicValidation.message"
|
||||
:validation-status="mnemonicValidation.status"
|
||||
>
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="passphrase"
|
||||
style="text-align: center; flex: 1"
|
||||
placeholder="Your mnemonic..."
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
|
||||
<n-button @click="copyPassphrase">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import {
|
||||
chineseSimplifiedWordList,
|
||||
chineseTraditionalWordList,
|
||||
@@ -78,19 +15,23 @@ import {
|
||||
spanishWordList,
|
||||
} from '@it-tools/bip39';
|
||||
import { Copy, Refresh } from '@vicons/tabler';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
|
||||
const languages = {
|
||||
English: englishWordList,
|
||||
'English': englishWordList,
|
||||
'Chinese simplified': chineseSimplifiedWordList,
|
||||
'Chinese traditional': chineseTraditionalWordList,
|
||||
Czech: czechWordList,
|
||||
French: frenchWordList,
|
||||
Italian: italianWordList,
|
||||
Japanese: japaneseWordList,
|
||||
Korean: koreanWordList,
|
||||
Portuguese: portugueseWordList,
|
||||
Spanish: spanishWordList,
|
||||
'Czech': czechWordList,
|
||||
'French': frenchWordList,
|
||||
'Italian': italianWordList,
|
||||
'Japanese': japaneseWordList,
|
||||
'Korean': koreanWordList,
|
||||
'Portuguese': portugueseWordList,
|
||||
'Spanish': spanishWordList,
|
||||
};
|
||||
|
||||
const entropy = ref(generateEntropy());
|
||||
@@ -111,11 +52,11 @@ const entropyValidation = useValidation({
|
||||
source: entropy,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
|
||||
validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
|
||||
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
|
||||
},
|
||||
{
|
||||
validator: (value) => /^[a-fA-F0-9]*$/.test(value),
|
||||
validator: value => /^[a-fA-F0-9]*$/.test(value),
|
||||
message: 'Entropy should be an hexadecimal string',
|
||||
},
|
||||
],
|
||||
@@ -125,7 +66,7 @@ const mnemonicValidation = useValidation({
|
||||
source: passphrase,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
|
||||
validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
|
||||
message: 'Invalid mnemonic',
|
||||
},
|
||||
],
|
||||
@@ -138,3 +79,53 @@ function refreshEntropy() {
|
||||
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
|
||||
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-grid cols="3" x-gap="12">
|
||||
<n-gi span="1">
|
||||
<c-select
|
||||
v-model:value="language"
|
||||
searchable
|
||||
label="Language:"
|
||||
:options="Object.keys(languages)"
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-form-item
|
||||
label="Entropy (seed):"
|
||||
:feedback="entropyValidation.message"
|
||||
:validation-status="entropyValidation.status"
|
||||
>
|
||||
<n-input-group>
|
||||
<c-input-text v-model:value="entropy" placeholder="Your string..." />
|
||||
|
||||
<c-button @click="refreshEntropy()">
|
||||
<n-icon size="22">
|
||||
<Refresh />
|
||||
</n-icon>
|
||||
</c-button>
|
||||
<c-button @click="copyEntropy()">
|
||||
<n-icon size="22">
|
||||
<Copy />
|
||||
</n-icon>
|
||||
</c-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-form-item
|
||||
label="Passphrase (mnemonic):"
|
||||
:feedback="mnemonicValidation.message"
|
||||
:validation-status="mnemonicValidation.status"
|
||||
>
|
||||
<n-input-group>
|
||||
<c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
|
||||
|
||||
<c-button @click="copyPassphrase()">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</c-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
214
src/tools/camera-recorder/camera-recorder.vue
Normal file
214
src/tools/camera-recorder/camera-recorder.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useMediaRecorder } from './useMediaRecorder';
|
||||
|
||||
interface Media { type: 'image' | 'video'; value: string; createdAt: Date }
|
||||
|
||||
const {
|
||||
videoInputs: cameras,
|
||||
audioInputs: microphones,
|
||||
permissionGranted,
|
||||
isSupported,
|
||||
ensurePermissions,
|
||||
} = useDevicesList({
|
||||
requestPermissions: true,
|
||||
constraints: { video: true, audio: true },
|
||||
onUpdated() {
|
||||
refreshCurrentDevices();
|
||||
},
|
||||
});
|
||||
|
||||
const video = ref<HTMLVideoElement>();
|
||||
const medias = ref<Media[]>([]);
|
||||
const currentCamera = ref(cameras.value[0]?.deviceId);
|
||||
const currentMicrophone = ref(microphones.value[0]?.deviceId);
|
||||
const permissionCannotBePrompted = ref(false);
|
||||
|
||||
const {
|
||||
stream,
|
||||
start,
|
||||
enabled: isMediaStreamAvailable,
|
||||
} = useUserMedia({
|
||||
constraints: computed(() => ({
|
||||
video: { deviceId: currentCamera.value },
|
||||
...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}),
|
||||
})),
|
||||
autoSwitch: true,
|
||||
});
|
||||
|
||||
const {
|
||||
isRecordingSupported,
|
||||
onRecordAvailable,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
pauseRecording,
|
||||
recordingState,
|
||||
resumeRecording,
|
||||
} = useMediaRecorder({
|
||||
stream,
|
||||
});
|
||||
|
||||
onRecordAvailable((value) => {
|
||||
medias.value.unshift({ type: 'video', value, createdAt: new Date() });
|
||||
});
|
||||
|
||||
function refreshCurrentDevices() {
|
||||
if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) {
|
||||
currentCamera.value = cameras.value[0]?.deviceId;
|
||||
}
|
||||
|
||||
if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) {
|
||||
currentMicrophone.value = microphones.value[0]?.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
function takeScreenshot() {
|
||||
if (!video.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.value.videoWidth;
|
||||
canvas.height = video.value.videoHeight;
|
||||
canvas.getContext('2d')?.drawImage(video.value, 0, 0);
|
||||
const image = canvas.toDataURL('image/png');
|
||||
|
||||
medias.value.unshift({ type: 'image', value: image, createdAt: new Date() });
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (video.value && stream.value) {
|
||||
video.value.srcObject = stream.value;
|
||||
}
|
||||
});
|
||||
|
||||
async function requestPermissions() {
|
||||
try {
|
||||
await ensurePermissions();
|
||||
}
|
||||
catch (e) {
|
||||
permissionCannotBePrompted.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadMedia({ type, value, createdAt }: Media) {
|
||||
const link = document.createElement('a');
|
||||
link.href = value;
|
||||
link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`;
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-card v-if="!isSupported">
|
||||
Your browser does not support recording video from camera
|
||||
</c-card>
|
||||
|
||||
<c-card v-else-if="!permissionGranted" text-center>
|
||||
You need to grant permission to use your camera and microphone
|
||||
|
||||
<c-alert v-if="permissionCannotBePrompted" mt-4 text-left>
|
||||
Your browser has blocked permission request or does not support it. You need to grant permission manually in
|
||||
your browser settings (usually the lock icon in the address bar).
|
||||
</c-alert>
|
||||
|
||||
<div v-else mt-4 flex justify-center>
|
||||
<c-button @click="requestPermissions">
|
||||
Grant permission
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<c-card v-else>
|
||||
<div flex flex-col gap-2>
|
||||
<c-select
|
||||
v-model:value="currentCamera"
|
||||
label-position="left"
|
||||
label-width="60px"
|
||||
label="Video:"
|
||||
:options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
||||
placeholder="Select camera"
|
||||
/>
|
||||
<c-select
|
||||
v-if="currentMicrophone && microphones.length > 0"
|
||||
v-model:value="currentMicrophone"
|
||||
label="Audio:"
|
||||
label-position="left"
|
||||
label-width="60px"
|
||||
:options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
||||
placeholder="Select microphone"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!isMediaStreamAvailable" mt-3 flex justify-center>
|
||||
<c-button type="primary" @click="start">
|
||||
Start webcam
|
||||
</c-button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div my-2>
|
||||
<video ref="video" autoplay controls playsinline max-h-full w-full />
|
||||
</div>
|
||||
|
||||
<div flex items-center justify-between gap-2>
|
||||
<c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot">
|
||||
<span mr-2> <icon-mdi-camera /></span>
|
||||
Take screenshot
|
||||
</c-button>
|
||||
|
||||
<div v-if="isRecordingSupported" flex justify-center gap-2>
|
||||
<c-button v-if="recordingState === 'stopped'" @click="startRecording">
|
||||
<span mr-2> <icon-mdi-video /></span>
|
||||
Start recording
|
||||
</c-button>
|
||||
|
||||
<c-button v-if="recordingState === 'recording'" @click="pauseRecording">
|
||||
<span mr-2> <icon-mdi-pause /></span>
|
||||
Pause
|
||||
</c-button>
|
||||
|
||||
<c-button v-if="recordingState === 'paused'" @click="resumeRecording">
|
||||
<span mr-2> <icon-mdi-play /></span>
|
||||
Resume
|
||||
</c-button>
|
||||
|
||||
<c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording">
|
||||
<span mr-2> <icon-mdi-record /></span>
|
||||
Stop
|
||||
</c-button>
|
||||
</div>
|
||||
<div v-else italic op-60>
|
||||
Video recording is not supported in your browser
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<div grid grid-cols-2 mt-5 gap-2>
|
||||
<c-card v-for="({ type, value, createdAt }, index) in medias" :key="index">
|
||||
<img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot">
|
||||
|
||||
<video v-else :src="value" controls max-h-full w-full />
|
||||
|
||||
<div flex items-center justify-between>
|
||||
<div font-bold>
|
||||
{{ type === 'image' ? 'Screenshot' : 'Video' }}
|
||||
</div>
|
||||
|
||||
<div flex gap-2>
|
||||
<c-button @click="downloadMedia({ type, value, createdAt })">
|
||||
<icon-mdi-download />
|
||||
</c-button>
|
||||
|
||||
<c-button @click="medias = medias.filter((_ignored, i) => i !== index)">
|
||||
<icon-mdi-delete-outline />
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
12
src/tools/camera-recorder/index.ts
Normal file
12
src/tools/camera-recorder/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Camera } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Camera recorder',
|
||||
path: '/camera-recorder',
|
||||
description: 'Take a picture or record a video from your webcam or camera.',
|
||||
keywords: ['camera', 'recoder'],
|
||||
component: () => import('./camera-recorder.vue'),
|
||||
icon: Camera,
|
||||
createdAt: new Date('2023-05-15'),
|
||||
});
|
114
src/tools/camera-recorder/useMediaRecorder.ts
Normal file
114
src/tools/camera-recorder/useMediaRecorder.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { type Ref, computed, ref } from 'vue';
|
||||
|
||||
export { useMediaRecorder };
|
||||
|
||||
function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): {
|
||||
isRecordingSupported: Ref<boolean>
|
||||
recordingState: Ref<'stopped' | 'recording' | 'paused'>
|
||||
startRecording: () => void
|
||||
stopRecording: () => void
|
||||
pauseRecording: () => void
|
||||
resumeRecording: () => void
|
||||
onRecordAvailable: (cb: (url: string) => void) => void
|
||||
} {
|
||||
const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm'));
|
||||
const mediaRecorder = ref<MediaRecorder | null>(null);
|
||||
const recordedChunks = ref<Blob[]>([]);
|
||||
const recordAvailable = createEventHook();
|
||||
const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped');
|
||||
|
||||
const createVideo = () => {
|
||||
const blob = new Blob(recordedChunks.value, { type: 'video/webm' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
recordedChunks.value = [];
|
||||
return url;
|
||||
};
|
||||
|
||||
const startRecording = () => {
|
||||
if (!isRecordingSupported.value) {
|
||||
return;
|
||||
}
|
||||
if (!stream.value) {
|
||||
return;
|
||||
}
|
||||
if (recordingState.value !== 'stopped') {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' });
|
||||
|
||||
mediaRecorder.value.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
recordedChunks.value.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.value.onstop = () => {
|
||||
recordAvailable.trigger(createVideo());
|
||||
};
|
||||
|
||||
if (mediaRecorder.value.state !== 'inactive') {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaRecorder.value.start();
|
||||
recordingState.value = 'recording';
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (!isRecordingSupported.value) {
|
||||
return;
|
||||
}
|
||||
if (!mediaRecorder.value) {
|
||||
return;
|
||||
}
|
||||
if (recordingState.value === 'stopped') {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaRecorder.value.stop();
|
||||
recordingState.value = 'stopped';
|
||||
};
|
||||
|
||||
const pauseRecording = () => {
|
||||
if (!isRecordingSupported.value) {
|
||||
return;
|
||||
}
|
||||
if (!mediaRecorder.value) {
|
||||
return;
|
||||
}
|
||||
if (recordingState.value !== 'recording') {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaRecorder.value.pause();
|
||||
recordingState.value = 'paused';
|
||||
};
|
||||
|
||||
const resumeRecording = () => {
|
||||
if (!isRecordingSupported.value) {
|
||||
return;
|
||||
}
|
||||
if (!mediaRecorder.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordingState.value !== 'paused') {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaRecorder.value.resume();
|
||||
recordingState.value = 'recording';
|
||||
};
|
||||
|
||||
return {
|
||||
isRecordingSupported,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
pauseRecording,
|
||||
resumeRecording,
|
||||
recordingState,
|
||||
|
||||
onRecordAvailable: recordAvailable.on,
|
||||
};
|
||||
}
|
@@ -1,51 +1,4 @@
|
||||
<template>
|
||||
<n-card>
|
||||
<n-form label-width="120" label-placement="left" :show-feedback="false">
|
||||
<n-form-item label="Your string:">
|
||||
<n-input v-model:value="input" />
|
||||
</n-form-item>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<n-form-item label="Camelcase:">
|
||||
<input-copyable :value="camelCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Capitalcase:">
|
||||
<input-copyable :value="capitalCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Constantcase:">
|
||||
<input-copyable :value="constantCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Dotcase:">
|
||||
<input-copyable :value="dotCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Headercase:">
|
||||
<input-copyable :value="headerCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Nocase:">
|
||||
<input-copyable :value="noCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Paramcase:">
|
||||
<input-copyable :value="paramCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Pascalcase:">
|
||||
<input-copyable :value="pascalCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Pathcase:">
|
||||
<input-copyable :value="pathCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Sentencecase:">
|
||||
<input-copyable :value="sentenceCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Snakecase:">
|
||||
<input-copyable :value="snakeCase(input, baseConfig)" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
camelCase,
|
||||
capitalCase,
|
||||
@@ -66,10 +19,95 @@ const baseConfig = {
|
||||
};
|
||||
|
||||
const input = ref('lorem ipsum dolor sit amet');
|
||||
|
||||
const formats = computed(() => [
|
||||
{
|
||||
label: 'Lowercase:',
|
||||
value: noCase(input.value, baseConfig).toLocaleLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Uppercase:',
|
||||
value: noCase(input.value, baseConfig).toLocaleUpperCase(),
|
||||
},
|
||||
{
|
||||
label: 'Camelcase:',
|
||||
value: camelCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Capitalcase:',
|
||||
value: capitalCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Constantcase:',
|
||||
value: constantCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Dotcase:',
|
||||
value: dotCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Headercase:',
|
||||
value: headerCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Nocase:',
|
||||
value: noCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Paramcase:',
|
||||
value: paramCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Pascalcase:',
|
||||
value: pascalCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Pathcase:',
|
||||
value: pathCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Sentencecase:',
|
||||
value: sentenceCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Snakecase:',
|
||||
value: snakeCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Mockingcase:',
|
||||
value: noCase(input.value, baseConfig)
|
||||
.split('')
|
||||
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
|
||||
.join(''),
|
||||
},
|
||||
]);
|
||||
|
||||
const inputLabelAlignmentConfig = {
|
||||
labelPosition: 'left',
|
||||
labelWidth: '120px',
|
||||
labelAlign: 'right',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-form-item {
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<c-card>
|
||||
<c-input-text
|
||||
v-model:value="input"
|
||||
label="Your string:"
|
||||
placeholder="Your string..."
|
||||
raw-text
|
||||
v-bind="inputLabelAlignmentConfig"
|
||||
/>
|
||||
|
||||
<div my-16px divider />
|
||||
|
||||
<InputCopyable
|
||||
v-for="format in formats"
|
||||
:key="format.label"
|
||||
:value="format.value"
|
||||
:label="format.label"
|
||||
v-bind="inputLabelAlignmentConfig"
|
||||
mb-1
|
||||
/>
|
||||
</c-card>
|
||||
</template>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
|
||||
|
||||
describe('chmod-calculator', () => {
|
||||
describe('computeChmodOctalRepresentation', () => {
|
||||
@@ -64,5 +64,67 @@ describe('chmod-calculator', () => {
|
||||
}),
|
||||
).to.eql('222');
|
||||
});
|
||||
|
||||
it('get the symbolic representation from permissions', () => {
|
||||
expect(
|
||||
computeChmodSymbolicRepresentation({
|
||||
permissions: {
|
||||
owner: { read: true, write: true, execute: true },
|
||||
group: { read: true, write: true, execute: true },
|
||||
public: { read: true, write: true, execute: true },
|
||||
},
|
||||
}),
|
||||
).to.eql('rwxrwxrwx');
|
||||
|
||||
expect(
|
||||
computeChmodSymbolicRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: false, execute: false },
|
||||
group: { read: false, write: false, execute: false },
|
||||
public: { read: false, write: false, execute: false },
|
||||
},
|
||||
}),
|
||||
).to.eql('---------');
|
||||
|
||||
expect(
|
||||
computeChmodSymbolicRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: true, execute: false },
|
||||
group: { read: false, write: true, execute: true },
|
||||
public: { read: true, write: false, execute: true },
|
||||
},
|
||||
}),
|
||||
).to.eql('-w--wxr-x');
|
||||
|
||||
expect(
|
||||
computeChmodSymbolicRepresentation({
|
||||
permissions: {
|
||||
owner: { read: true, write: false, execute: false },
|
||||
group: { read: false, write: true, execute: false },
|
||||
public: { read: false, write: false, execute: true },
|
||||
},
|
||||
}),
|
||||
).to.eql('r---w---x');
|
||||
|
||||
expect(
|
||||
computeChmodSymbolicRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: false, execute: true },
|
||||
group: { read: false, write: true, execute: false },
|
||||
public: { read: true, write: false, execute: false },
|
||||
},
|
||||
}),
|
||||
).to.eql('--x-w-r--');
|
||||
|
||||
expect(
|
||||
computeChmodSymbolicRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: true, execute: false },
|
||||
group: { read: false, write: true, execute: false },
|
||||
public: { read: false, write: true, execute: false },
|
||||
},
|
||||
}),
|
||||
).to.eql('-w--w--w-');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import type { GroupPermissions, Permissions } from './chmod-calculator.types';
|
||||
|
||||
export { computeChmodOctalRepresentation };
|
||||
export { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation };
|
||||
|
||||
function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
|
||||
const permissionValue = { read: 4, write: 2, execute: 1 };
|
||||
@@ -15,3 +15,16 @@ function computeChmodOctalRepresentation({ permissions }: { permissions: Permiss
|
||||
getGroupPermissionValue(permissions.public),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string {
|
||||
const permissionValue = { read: 'r', write: 'w', execute: 'x' };
|
||||
|
||||
const getGroupPermissionValue = (permission: GroupPermissions) =>
|
||||
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, '') : '-'), '');
|
||||
|
||||
return [
|
||||
getGroupPermissionValue(permissions.owner),
|
||||
getGroupPermissionValue(permissions.group),
|
||||
getGroupPermissionValue(permissions.public),
|
||||
].join('');
|
||||
}
|
||||
|
@@ -1,38 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" scope="col"></th>
|
||||
<th class="text-center" scope="col">Owner (u)</th>
|
||||
<th class="text-center" scope="col">Group (g)</th>
|
||||
<th class="text-center" scope="col">Public (o)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ scope, title } of scopes" :key="scope">
|
||||
<td class="line-header">{{ title }}</td>
|
||||
<td v-for="group of groups" :key="group" class="text-center">
|
||||
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
|
||||
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
|
||||
<div class="octal-result">
|
||||
{{ octal }}
|
||||
</div>
|
||||
|
||||
<input-copyable :value="`chmod ${octal} path`" readonly />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
||||
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
|
||||
|
||||
import type { Group, Scope } from './chmod-calculator.types';
|
||||
|
||||
@@ -52,8 +22,50 @@ const permissions = ref({
|
||||
});
|
||||
|
||||
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
|
||||
const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions: permissions.value }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" scope="col" />
|
||||
<th class="text-center" scope="col">
|
||||
Owner (u)
|
||||
</th>
|
||||
<th class="text-center" scope="col">
|
||||
Group (g)
|
||||
</th>
|
||||
<th class="text-center" scope="col">
|
||||
Public (o)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ scope, title } of scopes" :key="scope">
|
||||
<td class="line-header">
|
||||
{{ title }}
|
||||
</td>
|
||||
<td v-for="group of groups" :key="group" class="text-center">
|
||||
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
|
||||
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
|
||||
<div class="octal-result">
|
||||
{{ octal }}
|
||||
</div>
|
||||
<div class="octal-result">
|
||||
{{ symbolic }}
|
||||
</div>
|
||||
|
||||
<InputCopyable :value="`chmod ${octal} path`" readonly />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.octal-result {
|
||||
text-align: center;
|
||||
|
@@ -1,21 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card>
|
||||
<div class="duration">{{ formatMs(counter) }}</div>
|
||||
</n-card>
|
||||
<br />
|
||||
<n-space justify="center">
|
||||
<n-button v-if="!isRunning" secondary type="primary" @click="resume">Start</n-button>
|
||||
<n-button v-else secondary type="warning" @click="pause">Stop</n-button>
|
||||
|
||||
<n-button secondary @click="counter = 0">Reset</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRafFn } from '@vueuse/core';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { formatMs } from './chronometer.service';
|
||||
|
||||
const isRunning = ref(false);
|
||||
@@ -43,6 +28,28 @@ function pause() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-card>
|
||||
<div class="duration">
|
||||
{{ formatMs(counter) }}
|
||||
</div>
|
||||
</c-card>
|
||||
<div mt-5 flex justify-center gap-3>
|
||||
<c-button v-if="!isRunning" type="primary" @click="resume">
|
||||
Start
|
||||
</c-button>
|
||||
<c-button v-else type="warning" @click="pause">
|
||||
Stop
|
||||
</c-button>
|
||||
|
||||
<c-button @click="counter = 0">
|
||||
Reset
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.duration {
|
||||
text-align: center;
|
||||
|
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,69 +1,103 @@
|
||||
<template>
|
||||
<n-card>
|
||||
<n-form label-width="100" label-placement="left">
|
||||
<n-form-item label="color picker:">
|
||||
<n-color-picker
|
||||
v-model:value="hex"
|
||||
placement="bottom-end"
|
||||
@update:value="(v: string) => onInputUpdated(v, 'hex')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="color name:">
|
||||
<input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="hex:">
|
||||
<input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="rgb:">
|
||||
<input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="hsl:">
|
||||
<input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="hwb:">
|
||||
<input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="lch:">
|
||||
<input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" />
|
||||
</n-form-item>
|
||||
<n-form-item label="cmyk:">
|
||||
<input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { Colord } from 'colord';
|
||||
import { colord, extend } from 'colord';
|
||||
|
||||
import _ from 'lodash';
|
||||
import cmykPlugin from 'colord/plugins/cmyk';
|
||||
import hwbPlugin from 'colord/plugins/hwb';
|
||||
import namesPlugin from 'colord/plugins/names';
|
||||
import lchPlugin from 'colord/plugins/lch';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { buildColorFormat } from './color-converter.models';
|
||||
|
||||
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
||||
|
||||
const name = ref('');
|
||||
const hex = ref('#1ea54cff');
|
||||
const rgb = ref('');
|
||||
const hsl = ref('');
|
||||
const hwb = ref('');
|
||||
const cmyk = ref('');
|
||||
const lch = ref('');
|
||||
const formats = {
|
||||
picker: buildColorFormat({
|
||||
label: 'color picker',
|
||||
format: (v: Colord) => v.toHex(),
|
||||
type: 'color-picker',
|
||||
}),
|
||||
hex: buildColorFormat({
|
||||
label: 'hex',
|
||||
format: (v: Colord) => v.toHex(),
|
||||
placeholder: 'e.g. #ff0000',
|
||||
}),
|
||||
rgb: buildColorFormat({
|
||||
label: 'rgb',
|
||||
format: (v: Colord) => v.toRgbString(),
|
||||
placeholder: 'e.g. rgb(255, 0, 0)',
|
||||
}),
|
||||
hsl: buildColorFormat({
|
||||
label: 'hsl',
|
||||
format: (v: Colord) => v.toHslString(),
|
||||
placeholder: 'e.g. hsl(0, 100%, 50%)',
|
||||
}),
|
||||
hwb: buildColorFormat({
|
||||
label: 'hwb',
|
||||
format: (v: Colord) => v.toHwbString(),
|
||||
placeholder: 'e.g. hwb(0, 0%, 0%)',
|
||||
}),
|
||||
lch: buildColorFormat({
|
||||
label: 'lch',
|
||||
format: (v: Colord) => v.toLchString(),
|
||||
placeholder: 'e.g. lch(53.24, 104.55, 40.85)',
|
||||
}),
|
||||
cmyk: buildColorFormat({
|
||||
label: 'cmyk',
|
||||
format: (v: Colord) => v.toCmykString(),
|
||||
placeholder: 'e.g. cmyk(0, 100%, 100%, 0)',
|
||||
}),
|
||||
name: buildColorFormat({
|
||||
label: 'name',
|
||||
format: (v: Colord) => v.toName({ closest: true }) ?? 'Unknown',
|
||||
placeholder: 'e.g. red',
|
||||
}),
|
||||
};
|
||||
|
||||
function onInputUpdated(value: string, omit: string) {
|
||||
const color = colord(value);
|
||||
updateColorValue(colord('#1ea54c'));
|
||||
|
||||
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
|
||||
if (omit !== 'hex') hex.value = color.toHex();
|
||||
if (omit !== 'rgb') rgb.value = color.toRgbString();
|
||||
if (omit !== 'hsl') hsl.value = color.toHslString();
|
||||
if (omit !== 'hwb') hwb.value = color.toHwbString();
|
||||
if (omit !== 'cmyk') cmyk.value = color.toCmykString();
|
||||
if (omit !== 'lch') lch.value = color.toLchString();
|
||||
function updateColorValue(value: Colord | undefined, omitLabel?: string) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.forEach(formats, ({ value: valueRef, format }, key) => {
|
||||
if (key !== omitLabel) {
|
||||
valueRef.value = format(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onInputUpdated(hex.value, 'hex');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<template v-for="({ label, parse, placeholder, validation, type }, key) in formats" :key="key">
|
||||
<input-copyable
|
||||
v-if="type === 'text'"
|
||||
v-model:value="formats[key].value.value"
|
||||
:test-id="`input-${key}`"
|
||||
:label="`${label}:`"
|
||||
label-position="left"
|
||||
label-width="100px"
|
||||
label-align="right"
|
||||
:placeholder="placeholder"
|
||||
:validation="validation"
|
||||
raw-text
|
||||
clearable
|
||||
mt-2
|
||||
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||
/>
|
||||
|
||||
<n-form-item v-else-if="type === 'color-picker'" :label="`${label}:`" label-width="100" label-placement="left" :show-feedback="false">
|
||||
<n-color-picker
|
||||
v-model:value="formats[key].value.value"
|
||||
placement="bottom-end"
|
||||
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
</c-card>
|
||||
</template>
|
||||
|
@@ -1,92 +1,6 @@
|
||||
<template>
|
||||
<n-card>
|
||||
<n-form-item
|
||||
class="cron"
|
||||
:show-label="false"
|
||||
:feedback="cronValidation.message"
|
||||
:validation-status="cronValidation.status"
|
||||
>
|
||||
<n-input v-model:value="cron" size="large" placeholder="* * * * *" />
|
||||
</n-form-item>
|
||||
<div class="cron-string">
|
||||
{{ cronString }}
|
||||
</div>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<n-space justify="center">
|
||||
<n-form :show-feedback="false" label-width="170" label-placement="left">
|
||||
<n-form-item label="Verbose">
|
||||
<n-switch v-model:value="cronstrueConfig.verbose" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Use 24 hour time format">
|
||||
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Days start at 0">
|
||||
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<n-card>
|
||||
<pre>
|
||||
┌──────────── [optional] seconds (0 - 59)
|
||||
| ┌────────── minute (0 - 59)
|
||||
| | ┌──────── hour (0 - 23)
|
||||
| | | ┌────── day of month (1 - 31)
|
||||
| | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ...
|
||||
| | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ...
|
||||
| | | | | |
|
||||
* * * * * * command</pre
|
||||
>
|
||||
|
||||
<n-space v-if="styleStore.isSmallScreen" vertical>
|
||||
<n-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" embedded :bordered="false">
|
||||
<div>
|
||||
Symbol: <strong>{{ symbol }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Meaning: <strong>{{ meaning }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Example:
|
||||
<strong
|
||||
><code>{{ example }}</code></strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
Equivalent: <strong>{{ equivalent }}</strong>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
<n-table v-else size="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left" scope="col">Symbol</th>
|
||||
<th class="text-left" scope="col">Meaning</th>
|
||||
<th class="text-left" scope="col">Example</th>
|
||||
<th class="text-left" scope="col">Equivalent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol">
|
||||
<td>{{ symbol }}</td>
|
||||
<td>{{ meaning }}</td>
|
||||
<td>
|
||||
<code>{{ example }}</code>
|
||||
</td>
|
||||
<td>{{ equivalent }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import cronstrue from 'cronstrue';
|
||||
import { isValidCron } from 'cron-validator';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
function isCronValid(v: string) {
|
||||
@@ -185,30 +99,85 @@ const cronString = computed(() => {
|
||||
return ' ';
|
||||
});
|
||||
|
||||
const cronValidation = useValidation({
|
||||
source: cron,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => isCronValid(value),
|
||||
message: 'This cron is invalid',
|
||||
},
|
||||
],
|
||||
});
|
||||
const cronValidationRules = [
|
||||
{
|
||||
validator: (value: string) => isCronValid(value),
|
||||
message: 'This cron is invalid',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<div mx-auto max-w-sm>
|
||||
<c-input-text
|
||||
v-model:value="cron"
|
||||
size="large"
|
||||
placeholder="* * * * *"
|
||||
:validation-rules="cronValidationRules"
|
||||
mb-3
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="cron-string">
|
||||
{{ cronString }}
|
||||
</div>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<div flex justify-center>
|
||||
<n-form :show-feedback="false" label-width="170" label-placement="left">
|
||||
<n-form-item label="Verbose">
|
||||
<n-switch v-model:value="cronstrueConfig.verbose" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Use 24 hour time format">
|
||||
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Days start at 0">
|
||||
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</c-card>
|
||||
<c-card>
|
||||
<pre>
|
||||
┌──────────── [optional] seconds (0 - 59)
|
||||
| ┌────────── minute (0 - 59)
|
||||
| | ┌──────── hour (0 - 23)
|
||||
| | | ┌────── day of month (1 - 31)
|
||||
| | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ...
|
||||
| | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ...
|
||||
| | | | | |
|
||||
* * * * * * command</pre>
|
||||
|
||||
<div v-if="styleStore.isSmallScreen">
|
||||
<c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none>
|
||||
<div>
|
||||
Symbol: <strong>{{ symbol }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Meaning: <strong>{{ meaning }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Example:
|
||||
<strong><code>{{ example }}</code></strong>
|
||||
</div>
|
||||
<div>
|
||||
Equivalent: <strong>{{ equivalent }}</strong>
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
|
||||
<c-table v-else :data="helpers" />
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.cron {
|
||||
::v-deep(input) {
|
||||
font-size: 30px;
|
||||
font-family: monospace;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
|
||||
margin: auto;
|
||||
max-width: 400px;
|
||||
display: block;
|
||||
|
||||
.n-input {
|
||||
font-size: 30px;
|
||||
font-family: monospace;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.cron-string {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Date time converter - json to yaml', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -29,5 +29,6 @@ test.describe('Date time converter - json to yaml', () => {
|
||||
expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
|
||||
expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
|
||||
expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
|
||||
expect((await page.getByTestId('Excel date/time').inputValue()).trim()).toEqual('45028.88222222222');
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,16 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import {
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
isISO8601DateTimeString,
|
||||
isISO9075DateString,
|
||||
isMongoObjectId,
|
||||
isRFC3339DateString,
|
||||
isRFC7231DateString,
|
||||
isUnixTimestamp,
|
||||
isTimestamp,
|
||||
isUTCDateString,
|
||||
isMongoObjectId,
|
||||
isUnixTimestamp,
|
||||
} from './date-time-converter.models';
|
||||
|
||||
describe('date-time-converter models', () => {
|
||||
@@ -139,4 +142,39 @@ describe('date-time-converter models', () => {
|
||||
expect(isMongoObjectId('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExcelFormat', () => {
|
||||
test('an Excel format string is a floating number that can be negative', () => {
|
||||
expect(isExcelFormat('0')).toBe(true);
|
||||
expect(isExcelFormat('1')).toBe(true);
|
||||
expect(isExcelFormat('1.1')).toBe(true);
|
||||
expect(isExcelFormat('-1.1')).toBe(true);
|
||||
expect(isExcelFormat('-1')).toBe(true);
|
||||
|
||||
expect(isExcelFormat('')).toBe(false);
|
||||
expect(isExcelFormat('foo')).toBe(false);
|
||||
expect(isExcelFormat('1.1.1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateToExcelFormat', () => {
|
||||
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||
expect(dateToExcelFormat(new Date('2016-05-20T00:00:00.000Z'))).toBe('42510');
|
||||
expect(dateToExcelFormat(new Date('2016-05-20T12:00:00.000Z'))).toBe('42510.5');
|
||||
expect(dateToExcelFormat(new Date('2023-10-31T09:26:06.421Z'))).toBe('45230.39312987268');
|
||||
expect(dateToExcelFormat(new Date('1970-01-01T00:00:00.000Z'))).toBe('25569');
|
||||
expect(dateToExcelFormat(new Date('1800-01-01T00:00:00.000Z'))).toBe('-36522');
|
||||
});
|
||||
});
|
||||
|
||||
describe('excelFormatToDate', () => {
|
||||
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||
expect(excelFormatToDate('0')).toEqual(new Date('1899-12-30T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('1')).toEqual(new Date('1899-12-31T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('2')).toEqual(new Date('1900-01-01T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('4242.4242')).toEqual(new Date('1911-08-12T10:10:50.880Z'));
|
||||
expect(excelFormatToDate('42738.22626859954')).toEqual(new Date('2017-01-03T05:25:49.607Z'));
|
||||
expect(excelFormatToDate('-1000')).toEqual(new Date('1897-04-04T00:00:00.000Z'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -9,18 +9,23 @@ export {
|
||||
isTimestamp,
|
||||
isUTCDateString,
|
||||
isMongoObjectId,
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
};
|
||||
|
||||
const ISO8601_REGEX =
|
||||
/^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
|
||||
const ISO9075_REGEX =
|
||||
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
|
||||
const ISO8601_REGEX
|
||||
= /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
|
||||
const ISO9075_REGEX
|
||||
= /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
|
||||
|
||||
const RFC3339_REGEX =
|
||||
/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
|
||||
const RFC3339_REGEX
|
||||
= /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
|
||||
|
||||
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
|
||||
|
||||
const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;
|
||||
|
||||
function createRegexMatcher(regex: RegExp) {
|
||||
return (date?: string) => !_.isNil(date) && regex.test(date);
|
||||
}
|
||||
@@ -33,6 +38,8 @@ const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
|
||||
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
|
||||
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
|
||||
|
||||
const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);
|
||||
|
||||
function isUTCDateString(date?: string) {
|
||||
if (_.isNil(date)) {
|
||||
return false;
|
||||
@@ -40,7 +47,16 @@ function isUTCDateString(date?: string) {
|
||||
|
||||
try {
|
||||
return new Date(date).toUTCString() === date;
|
||||
} catch (_ignored) {
|
||||
}
|
||||
catch (_ignored) {
|
||||
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);
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
export type ToDateMapper = (value: string) => Date;
|
||||
|
||||
export type DateFormat = {
|
||||
name: string;
|
||||
fromDate: (date: Date) => string;
|
||||
toDate: (value: string) => Date;
|
||||
formatMatcher: (dateString: string) => boolean;
|
||||
};
|
||||
export interface DateFormat {
|
||||
name: string
|
||||
fromDate: (date: Date) => string
|
||||
toDate: (value: string) => Date
|
||||
formatMatcher: (dateString: string) => boolean
|
||||
}
|
||||
|
@@ -1,37 +1,3 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item :show-label="false" v-bind="validation.attrs">
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="inputDate"
|
||||
:on-input="onDateInputChanged"
|
||||
placeholder="Put you date string here..."
|
||||
clearable
|
||||
:input-props="{ 'data-test-id': 'date-time-converter-input' }"
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-model:value="formatIndex"
|
||||
style="flex: 0 0 170px"
|
||||
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
|
||||
data-test-id="date-time-converter-format-select"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-divider style="margin-top: 0" />
|
||||
<div v-for="{ name, fromDate } in formats" :key="name" mt-1>
|
||||
<n-input-group>
|
||||
<n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label>
|
||||
<input-copyable
|
||||
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
|
||||
placeholder="Invalid date..."
|
||||
:input-props="{ 'data-test-id': name }"
|
||||
/>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
formatISO,
|
||||
@@ -41,41 +7,36 @@ import {
|
||||
fromUnixTime,
|
||||
getTime,
|
||||
getUnixTime,
|
||||
parseISO,
|
||||
parseJSON,
|
||||
isDate,
|
||||
isValid,
|
||||
parseISO,
|
||||
parseJSON,
|
||||
} from 'date-fns';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
||||
import {
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
isISO8601DateTimeString,
|
||||
isISO9075DateString,
|
||||
isMongoObjectId,
|
||||
isRFC3339DateString,
|
||||
isRFC7231DateString,
|
||||
isTimestamp,
|
||||
isUTCDateString,
|
||||
isUnixTimestamp,
|
||||
isMongoObjectId,
|
||||
} from './date-time-converter.models';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
|
||||
const inputDate = ref('');
|
||||
|
||||
const toDate: ToDateMapper = (date) => new Date(date);
|
||||
|
||||
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
|
||||
if (!date || !validation.isValid) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return withDefaultOnError(() => formatter(date), '');
|
||||
}
|
||||
const toDate: ToDateMapper = date => new Date(date);
|
||||
|
||||
const formats: DateFormat[] = [
|
||||
{
|
||||
name: 'JS locale date string',
|
||||
fromDate: (date) => date.toString(),
|
||||
fromDate: date => date.toString(),
|
||||
toDate,
|
||||
formatMatcher: () => false,
|
||||
},
|
||||
@@ -83,49 +44,55 @@ const formats: DateFormat[] = [
|
||||
name: 'ISO 8601',
|
||||
fromDate: formatISO,
|
||||
toDate: parseISO,
|
||||
formatMatcher: (date) => isISO8601DateTimeString(date),
|
||||
formatMatcher: date => isISO8601DateTimeString(date),
|
||||
},
|
||||
{
|
||||
name: 'ISO 9075',
|
||||
fromDate: formatISO9075,
|
||||
toDate: parseISO,
|
||||
formatMatcher: (date) => isISO9075DateString(date),
|
||||
formatMatcher: date => isISO9075DateString(date),
|
||||
},
|
||||
{
|
||||
name: 'RFC 3339',
|
||||
fromDate: formatRFC3339,
|
||||
toDate,
|
||||
formatMatcher: (date) => isRFC3339DateString(date),
|
||||
formatMatcher: date => isRFC3339DateString(date),
|
||||
},
|
||||
{
|
||||
name: 'RFC 7231',
|
||||
fromDate: formatRFC7231,
|
||||
toDate,
|
||||
formatMatcher: (date) => isRFC7231DateString(date),
|
||||
formatMatcher: date => isRFC7231DateString(date),
|
||||
},
|
||||
{
|
||||
name: 'Unix timestamp',
|
||||
fromDate: (date) => String(getUnixTime(date)),
|
||||
toDate: (sec) => fromUnixTime(+sec),
|
||||
formatMatcher: (date) => isUnixTimestamp(date),
|
||||
fromDate: date => String(getUnixTime(date)),
|
||||
toDate: sec => fromUnixTime(+sec),
|
||||
formatMatcher: date => isUnixTimestamp(date),
|
||||
},
|
||||
{
|
||||
name: 'Timestamp',
|
||||
fromDate: (date) => String(getTime(date)),
|
||||
toDate: (ms) => parseJSON(+ms),
|
||||
formatMatcher: (date) => isTimestamp(date),
|
||||
fromDate: date => String(getTime(date)),
|
||||
toDate: ms => parseJSON(+ms),
|
||||
formatMatcher: date => isTimestamp(date),
|
||||
},
|
||||
{
|
||||
name: 'UTC format',
|
||||
fromDate: (date) => date.toUTCString(),
|
||||
fromDate: date => date.toUTCString(),
|
||||
toDate,
|
||||
formatMatcher: (date) => isUTCDateString(date),
|
||||
formatMatcher: date => isUTCDateString(date),
|
||||
},
|
||||
{
|
||||
name: 'Mongo ObjectID',
|
||||
fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000',
|
||||
toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
formatMatcher: (date) => isMongoObjectId(date),
|
||||
fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`,
|
||||
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
formatMatcher: date => isMongoObjectId(date),
|
||||
},
|
||||
{
|
||||
name: 'Excel date/time',
|
||||
fromDate: date => dateToExcelFormat(date),
|
||||
toDate: excelFormatToDate,
|
||||
formatMatcher: isExcelFormat,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -141,7 +108,8 @@ const normalizedDate = computed(() => {
|
||||
|
||||
try {
|
||||
return toDate(inputDate.value);
|
||||
} catch (_ignored) {
|
||||
}
|
||||
catch (_ignored) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
@@ -159,9 +127,11 @@ const validation = useValidation({
|
||||
rules: [
|
||||
{
|
||||
message: 'This date is invalid for this format',
|
||||
validator: (value) =>
|
||||
validator: value =>
|
||||
withDefaultOnError(() => {
|
||||
if (value === '') return true;
|
||||
if (value === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const maybeDate = formats[formatIndex.value].toDate(value);
|
||||
return isDate(maybeDate) && isValid(maybeDate);
|
||||
@@ -169,4 +139,51 @@ const validation = useValidation({
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
|
||||
if (!date || !validation.isValid) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return withDefaultOnError(() => formatter(date), '');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div flex gap-2>
|
||||
<c-input-text
|
||||
v-model:value="inputDate"
|
||||
autofocus
|
||||
placeholder="Put your date string here..."
|
||||
clearable
|
||||
test-id="date-time-converter-input"
|
||||
:validation="validation"
|
||||
@update:value="onDateInputChanged"
|
||||
/>
|
||||
|
||||
<c-select
|
||||
v-model:value="formatIndex"
|
||||
style="flex: 0 0 170px"
|
||||
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
|
||||
data-test-id="date-time-converter-format-select"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<input-copyable
|
||||
v-for="{ name, fromDate } in formats"
|
||||
:key="name"
|
||||
:label="name"
|
||||
label-width="150px"
|
||||
label-position="left"
|
||||
label-align="right"
|
||||
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
|
||||
placeholder="Invalid date..."
|
||||
:test-id="name"
|
||||
readonly
|
||||
mt-2
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,27 +1,5 @@
|
||||
<template>
|
||||
<n-card v-for="{ name, information } in sections" :key="name" :title="name">
|
||||
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
|
||||
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
|
||||
<n-card :bordered="false" embedded>
|
||||
<div class="label">
|
||||
{{ label }}
|
||||
</div>
|
||||
|
||||
<div class="value">
|
||||
<n-ellipsis v-if="value">
|
||||
{{ value }}
|
||||
</n-ellipsis>
|
||||
<div v-else class="undefined-value">unknown</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const { width, height } = useWindowSize();
|
||||
|
||||
@@ -79,8 +57,33 @@ const sections = [
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card v-for="{ name, information } in sections" :key="name" :title="name">
|
||||
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
|
||||
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
|
||||
<div class="label">
|
||||
{{ label }}
|
||||
</div>
|
||||
|
||||
<div class="value">
|
||||
<n-ellipsis v-if="value">
|
||||
{{ value }}
|
||||
</n-ellipsis>
|
||||
<div v-else class="undefined-value">
|
||||
unknown
|
||||
</div>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.information {
|
||||
padding: 14px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: #aaaaaa11;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
|
@@ -1,84 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item label="Your docker run command:" :show-feedback="false">
|
||||
<n-input
|
||||
v-model:value="dockerRun"
|
||||
style="font-family: monospace"
|
||||
type="textarea"
|
||||
placeholder="Your docker run command to convert..."
|
||||
rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<textarea-copyable :value="dockerCompose" language="yaml" />
|
||||
<br />
|
||||
<br />
|
||||
<n-space justify="center">
|
||||
<n-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </n-button>
|
||||
</n-space>
|
||||
|
||||
<div v-if="notComposable.length > 0">
|
||||
<br />
|
||||
<n-alert title="This options are not translatable to docker-compose" type="info">
|
||||
<ul>
|
||||
<li v-for="(message, index) of notComposable" :key="index">{{ message }}</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="notImplemented.length > 0">
|
||||
<br />
|
||||
<n-alert
|
||||
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
|
||||
type="warning"
|
||||
>
|
||||
<ul>
|
||||
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="errors.length > 0">
|
||||
<br />
|
||||
<n-alert title="The following errors occured" type="error">
|
||||
<ul>
|
||||
<li v-for="(message, index) of errors" :key="index">{{ message }}</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { MessageType, composerize } from 'composerize-ts';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { textToBase64 } from '@/utils/base64';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
|
||||
import { composerize, MessageType } from 'composerize-ts';
|
||||
|
||||
const dockerRun = ref(
|
||||
'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx',
|
||||
);
|
||||
|
||||
const conversionResult = computed(() =>
|
||||
withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }),
|
||||
withDefaultOnError(() => composerize(dockerRun.value.trim()), { yaml: '', messages: [] }),
|
||||
);
|
||||
const dockerCompose = computed(() => conversionResult.value.yaml);
|
||||
const notImplemented = computed(() =>
|
||||
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value),
|
||||
conversionResult.value.messages.filter(msg => msg.type === MessageType.notImplemented).map(msg => msg.value),
|
||||
);
|
||||
const notComposable = computed(() =>
|
||||
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value),
|
||||
conversionResult.value.messages.filter(msg => msg.type === MessageType.notTranslatable).map(msg => msg.value),
|
||||
);
|
||||
const errors = computed(() =>
|
||||
conversionResult.value.messages
|
||||
.filter((msg) => msg.type === MessageType.errorDuringConversion)
|
||||
.map((msg) => msg.value),
|
||||
.filter(msg => msg.type === MessageType.errorDuringConversion)
|
||||
.map(msg => msg.value),
|
||||
);
|
||||
const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value));
|
||||
const dockerComposeBase64 = computed(() => `data:application/yaml;base64,${textToBase64(dockerCompose.value)}`);
|
||||
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text
|
||||
v-model:value="dockerRun"
|
||||
label="Your docker run command:"
|
||||
style="font-family: monospace"
|
||||
multiline
|
||||
raw-text
|
||||
monospace
|
||||
placeholder="Your docker run command to convert..."
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<TextareaCopyable :value="dockerCompose" language="yaml" />
|
||||
|
||||
<div mt-5 flex justify-center>
|
||||
<c-button :disabled="dockerCompose === ''" secondary @click="download">
|
||||
Download docker-compose.yml
|
||||
</c-button>
|
||||
</div>
|
||||
|
||||
<div v-if="notComposable.length > 0">
|
||||
<n-alert title="This options are not translatable to docker-compose" type="info" mt-5>
|
||||
<ul>
|
||||
<li v-for="(message, index) of notComposable" :key="index">
|
||||
{{ message }}
|
||||
</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="notImplemented.length > 0">
|
||||
<n-alert
|
||||
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
|
||||
type="warning"
|
||||
mt-5
|
||||
>
|
||||
<ul>
|
||||
<li v-for="(message, index) of notImplemented" :key="index">
|
||||
{{ message }}
|
||||
</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="errors.length > 0">
|
||||
<n-alert title="The following errors occured" type="error" mt-5>
|
||||
<ul>
|
||||
<li v-for="(message, index) of errors" :key="index">
|
||||
{{ message }}
|
||||
</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user