mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-10-23 04:52:14 +00:00
Compare commits
26 Commits
v2023.8.16
...
card-hover
Author | SHA1 | Date | |
---|---|---|---|
|
e9e0884789 | ||
|
0eedce69a6 | ||
|
e0c7771e8f | ||
|
8a30b6bdb3 | ||
|
233d5565f6 | ||
|
7ab9204e96 | ||
|
c7d4562d3b | ||
|
18dd1400bd | ||
|
f035f485c0 | ||
|
e18bae1fca | ||
|
d1dff428d8 | ||
|
3a63837d3d | ||
|
81bfe57cb8 | ||
|
a9cd91ca9c | ||
|
06c35472d3 | ||
|
f3e14fc18f | ||
|
2274766a8f | ||
|
a346175d24 | ||
|
6f93cba3da | ||
|
76b2761d62 | ||
|
6ff9a01cc8 | ||
|
a2b9b157e5 | ||
|
144f86e2dc | ||
|
0f1f6590c5 | ||
|
2bcb77a9f9 | ||
|
c58d6e3423 |
@@ -10,5 +10,12 @@ module.exports = {
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
'vue/no-empty-component-block': ['error'],
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@vueuse/core',
|
||||
importNames: ['useClipboard'],
|
||||
message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
13
.github/workflows/e2e-tests.yml
vendored
13
.github/workflows/e2e-tests.yml
vendored
@@ -1,13 +1,13 @@
|
||||
name: E2E tests
|
||||
on: [deployment_status]
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.deployment_status.state == 'success'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BASE_URL: ${{ github.event.deployment_status.target_url }}
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1/3, 2/3, 3/3]
|
||||
@@ -28,6 +28,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build app
|
||||
run: pnpm build
|
||||
|
||||
- name: Restore Playwright browsers from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@@ -2,6 +2,22 @@
|
||||
|
||||
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.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
|
||||
|
11
README.md
11
README.md
@@ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
||||
|
||||
**Other solutions:**
|
||||
|
||||
- [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html)
|
||||
- [Tipi](https://www.runtipi.io/docs/apps-available)
|
||||
- [Unraid](https://unraid.net/community/apps?q=it-tools)
|
||||
|
||||
@@ -34,23 +35,21 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
||||
### Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) with the following extensions:
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
|
||||
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
|
||||
- [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally)
|
||||
|
||||
with the following settings:
|
||||
|
||||
```json5
|
||||
```json
|
||||
{
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales",
|
||||
"src/tools/*/locales"
|
||||
],
|
||||
"i18n-ally.localesPaths": ["locales", "src/tools/*/locales"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
```
|
||||
|
21
components.d.ts
vendored
21
components.d.ts
vendored
@@ -27,10 +27,13 @@ declare module '@vue/runtime-core' {
|
||||
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
|
||||
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
||||
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
||||
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
||||
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
||||
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
|
||||
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
|
||||
CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default']
|
||||
CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default']
|
||||
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
|
||||
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
|
||||
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
|
||||
@@ -44,8 +47,11 @@ declare module '@vue/runtime-core' {
|
||||
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
|
||||
CSelect: typeof import('./src/ui/c-select/c-select.vue')['default']
|
||||
'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
|
||||
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
|
||||
'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default']
|
||||
CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default']
|
||||
'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default']
|
||||
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
|
||||
'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default']
|
||||
'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default']
|
||||
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
|
||||
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
|
||||
@@ -68,12 +74,12 @@ declare module '@vue/runtime-core' {
|
||||
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
|
||||
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
|
||||
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
|
||||
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
|
||||
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
|
||||
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
||||
IconMdiCameraOutline: typeof import('~icons/mdi/camera-outline')['default']
|
||||
IconMdiCameraVideoOff: typeof import('~icons/mdi/camera-video-off')['default']
|
||||
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
||||
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||
IconMdiClose: typeof import('~icons/mdi/close')['default']
|
||||
@@ -82,14 +88,11 @@ declare module '@vue/runtime-core' {
|
||||
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||
IconMdiMagnify: typeof import('~icons/mdi/magnify')['default']
|
||||
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
||||
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||
IconMdiRecord: typeof import('~icons/mdi/record')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||
IconMdiSearchRound: typeof import('~icons/mdi/search-round')['default']
|
||||
IconMdiTea: typeof import('~icons/mdi/tea')['default']
|
||||
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||
@@ -135,7 +138,6 @@ declare module '@vue/runtime-core' {
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
@@ -146,13 +148,11 @@ declare module '@vue/runtime-core' {
|
||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTable: typeof import('naive-ui')['NTable']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||
@@ -170,9 +170,11 @@ declare module '@vue/runtime-core' {
|
||||
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
|
||||
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
|
||||
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
|
||||
StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
|
||||
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
|
||||
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
|
||||
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
||||
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
|
||||
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
|
||||
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']
|
||||
@@ -186,6 +188,7 @@ declare module '@vue/runtime-core' {
|
||||
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
|
||||
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
|
||||
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
|
||||
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
|
||||
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
|
||||
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
|
||||
YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
|
||||
|
@@ -1,4 +1,5 @@
|
||||
home:
|
||||
categories:
|
||||
newestTools: Newest tools
|
||||
|
||||
allTheTools: All the tools
|
||||
yourFavoriteTools: Your favorite tools
|
||||
|
43
package.json
43
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "it-tools",
|
||||
"version": "2023.8.16-9bd4ad4",
|
||||
"version": "2023.8.21-6f93cba",
|
||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||
"keywords": [
|
||||
"productivity",
|
||||
@@ -21,11 +21,12 @@
|
||||
},
|
||||
"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",
|
||||
@@ -36,12 +37,12 @@
|
||||
"@it-tools/bip39": "^0.0.4",
|
||||
"@it-tools/oggen": "^1.3.0",
|
||||
"@sindresorhus/slugify": "^2.2.0",
|
||||
"@tiptap/pm": "^2.0.3",
|
||||
"@tiptap/starter-kit": "^2.0.3",
|
||||
"@tiptap/pm": "^2.1.6",
|
||||
"@tiptap/starter-kit": "^2.1.6",
|
||||
"@tiptap/vue-3": "^2.0.3",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vicons/tabler": "^0.12.0",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/head": "^1.0.0",
|
||||
"@vueuse/router": "^10.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -58,12 +59,14 @@
|
||||
"fuse.js": "^6.6.2",
|
||||
"highlight.js": "^11.7.0",
|
||||
"iarna-toml-esm": "^3.0.5",
|
||||
"ibantools": "^4.3.3",
|
||||
"json5": "^2.2.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
"lodash": "^4.17.21",
|
||||
"mathjs": "^11.0.0",
|
||||
"mathjs": "^11.9.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"monaco-editor": "^0.41.0",
|
||||
"naive-ui": "^2.34.3",
|
||||
"netmask": "^2.0.2",
|
||||
"node-forge": "^1.3.1",
|
||||
@@ -72,8 +75,7 @@
|
||||
"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",
|
||||
"unicode-emoji-json": "^0.4.0",
|
||||
"unplugin-auto-import": "^0.16.4",
|
||||
@@ -86,44 +88,43 @@
|
||||
"yaml": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.39.3",
|
||||
"@antfu/eslint-config": "^0.41.0",
|
||||
"@iconify-json/mdi": "^1.1.50",
|
||||
"@intlify/unplugin-vue-i18n": "^0.12.0",
|
||||
"@intlify/unplugin-vue-i18n": "^0.13.0",
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/jsdom": "^21.0.0",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/netmask": "^2.0.0",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node-forge": "^1.3.2",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/randombytes": "^2.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@unocss/eslint-config": "^0.55.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@vitejs/plugin-vue": "^4.3.2",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"@vue/compiler-sfc": "^3.2.47",
|
||||
"@vue/runtime-dom": "^3.3.4",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"c8": "^8.0.0",
|
||||
"consola": "^3.0.2",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint": "^8.47.0",
|
||||
"jsdom": "^22.0.0",
|
||||
"less": "^4.1.3",
|
||||
"prettier": "^2.8.7",
|
||||
"typescript": "~4.9.0",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "~5.2.0",
|
||||
"unocss": "^0.55.0",
|
||||
"unocss-preset-scrollbar": "^0.2.1",
|
||||
"unplugin-icons": "^0.16.1",
|
||||
"unplugin-icons": "^0.17.0",
|
||||
"unplugin-vue-components": "^0.25.0",
|
||||
"vite": "^4.0.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-pwa": "^0.16.0",
|
||||
"vite-plugin-vue-markdown": "^0.23.5",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
|
@@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const isCI = !!process.env.CI;
|
||||
const baseUrl = process.env.BASE_URL || 'http://localhost:5050';
|
||||
const useWebServer = process.env.NO_WEB_SERVER !== 'true';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
@@ -52,13 +53,13 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
|
||||
...(isCI
|
||||
? {}
|
||||
: {
|
||||
webServer: {
|
||||
command: 'npm run preview',
|
||||
url: 'http://127.0.0.1:5050',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
}),
|
||||
...(useWebServer
|
||||
&& {
|
||||
webServer: {
|
||||
command: 'npm run preview',
|
||||
url: 'http://127.0.0.1:5050',
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
2820
pnpm-lock.yaml
generated
2820
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard, useVModel } from '@vueuse/core';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = defineProps<{ value: string }>();
|
||||
const emit = defineEmits(['update:value']);
|
||||
|
||||
const value = useVModel(props, 'value', emit);
|
||||
const tooltipText = ref('Copy to clipboard');
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
|
||||
function onCopyClicked() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied!';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltipText.value = 'Copy to clipboard';
|
||||
}, 2000);
|
||||
}
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,7 +15,7 @@ function onCopyClicked() {
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" size="small" @click="onCopyClicked">
|
||||
<c-button circle variant="text" size="small" @click="copy()">
|
||||
<icon-mdi-content-copy />
|
||||
</c-button>
|
||||
</template>
|
||||
|
@@ -1,26 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
|
||||
const { value } = toRefs(props);
|
||||
|
||||
const initialText = 'Copy to clipboard';
|
||||
const tooltipText = ref(initialText);
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
|
||||
function handleClick() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied!';
|
||||
|
||||
setTimeout(() => (tooltipText.value = initialText), 1000);
|
||||
}
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<span class="value" @click="handleClick">{{ value }}</span>
|
||||
<span class="value" @click="copy()">{{ value }}</span>
|
||||
</template>
|
||||
{{ tooltipText }}
|
||||
</n-tooltip>
|
||||
|
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Copy } from '@vicons/tabler';
|
||||
import { useClipboard, useElementSize } from '@vueuse/core';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import jsonHljs from 'highlight.js/lib/languages/json';
|
||||
import sqlHljs from 'highlight.js/lib/languages/sql';
|
||||
import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
||||
import iniHljs from 'highlight.js/lib/languages/ini';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -33,17 +34,8 @@ hljs.registerLanguage('toml', iniHljs);
|
||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
const tooltipText = ref(copyMessage.value);
|
||||
|
||||
function onCopyClicked() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied !';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltipText.value = copyMessage.value;
|
||||
}, 2000);
|
||||
}
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -61,7 +53,7 @@ function onCopyClicked() {
|
||||
<n-tooltip v-if="value" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="copy-button" :class="[copyPlacement]">
|
||||
<c-button circle important:h-10 important:w-10 @click="onCopyClicked">
|
||||
<c-button circle important:h-10 important:w-10 @click="copy()">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</c-button>
|
||||
</div>
|
||||
@@ -70,7 +62,7 @@ function onCopyClicked() {
|
||||
</n-tooltip>
|
||||
</c-card>
|
||||
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
|
||||
<c-button @click="onCopyClicked">
|
||||
<c-button @click="copy()">
|
||||
{{ tooltipText }}
|
||||
</c-button>
|
||||
</div>
|
||||
|
@@ -1,19 +1,16 @@
|
||||
<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">
|
||||
<c-card class="tool-card">
|
||||
<c-card class="tool-card" shadow>
|
||||
<div flex items-center justify-between>
|
||||
<n-icon class="icon" size="40" :component="tool.icon" />
|
||||
<div flex items-center gap-8px>
|
||||
@@ -32,15 +29,14 @@ const appTheme = useAppTheme();
|
||||
<FavoriteButton :tool="tool" />
|
||||
</div>
|
||||
</div>
|
||||
<n-h3 class="title">
|
||||
<n-ellipsis>{{ tool.name }}</n-ellipsis>
|
||||
<n-h3 class="title" truncate>
|
||||
{{ tool.name }}
|
||||
</n-h3>
|
||||
|
||||
<div class="description">
|
||||
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
|
||||
<div line-clamp-2 style="min-height: 44.78px">
|
||||
{{ tool.description }}
|
||||
<br>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
</c-card>
|
||||
</router-link>
|
||||
@@ -52,16 +48,14 @@ a {
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
transition: border-color ease 0.5s;
|
||||
border-width: 2px !important;
|
||||
color: transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: v-bind('appTheme.primary.colorHover');
|
||||
}
|
||||
position: relative;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
|
||||
.icon {
|
||||
opacity: 0.6;
|
||||
opacity: 0.4;
|
||||
color: v-bind('theme.textColorBase');
|
||||
}
|
||||
|
||||
@@ -74,5 +68,46 @@ a {
|
||||
color: v-bind('theme.textColorBase');
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
--mask-radius: 20em;
|
||||
|
||||
border-radius: 15px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
display: block;
|
||||
height: calc(100% - 4px) ;
|
||||
width: calc(100% - 4px) ;
|
||||
background: #18a05818;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
-webkit-mask: radial-gradient(
|
||||
var(--mask-radius) var(--mask-radius) at 45px 45px,
|
||||
#000 1%,
|
||||
transparent 50%
|
||||
);
|
||||
|
||||
mask: radial-gradient(
|
||||
var(--mask-radius) var(--mask-radius) at 45px 45px,
|
||||
#000 1%,
|
||||
transparent 50%
|
||||
);
|
||||
|
||||
will-change: mask;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
--mask-radius: 50em;
|
||||
border: 2px solid #18a058;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,11 +1,19 @@
|
||||
import { type MaybeRef, get, useClipboard } from '@vueuse/core';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import type { MaybeRefOrGetter } from 'vue';
|
||||
|
||||
export function useCopy({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<string>; text?: string; createToast?: boolean } = {}) {
|
||||
const { copy, copied, ...rest } = useClipboard({
|
||||
source,
|
||||
legacy: true,
|
||||
});
|
||||
|
||||
export function useCopy({ source, text = 'Copied to the clipboard' }: { source?: MaybeRef<unknown>; text?: string } = {}) {
|
||||
const { copy } = useClipboard(source ? { source: computed(() => String(get(source))) } : {});
|
||||
const message = useMessage();
|
||||
|
||||
return {
|
||||
...rest,
|
||||
isJustCopied: copied,
|
||||
async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) {
|
||||
if (source) {
|
||||
await copy();
|
||||
@@ -14,7 +22,9 @@ export function useCopy({ source, text = 'Copied to the clipboard' }: { source?:
|
||||
await copy(content);
|
||||
}
|
||||
|
||||
message.success(notificationMessage ?? text);
|
||||
if (createToast) {
|
||||
message.success(notificationMessage ?? text);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -1,99 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
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>
|
||||
<div class="home-page">
|
||||
<div class="grid-wrapper">
|
||||
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi>
|
||||
<ColoredCard title="You like it-tools?" :icon="Heart">
|
||||
Give us a star on
|
||||
<a
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' GitHub repository"
|
||||
>GitHub</a>
|
||||
or follow us on
|
||||
<a
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' Twitter account"
|
||||
>Twitter</a>! Thank you
|
||||
<n-icon :component="Heart" />
|
||||
</ColoredCard>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div class="home-page" m-auto mt-50px max-w-1800px>
|
||||
<div my-8 />
|
||||
|
||||
<transition name="height">
|
||||
<div v-if="toolStore.favoriteTools.length > 0">
|
||||
<n-h3>Your favorite tools</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
||||
<ToolCard :tool="tool" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-if="toolStore.newTools.length > 0">
|
||||
<n-h3>{{ t('home.categories.newestTools', 'Newest tools') }}</n-h3>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
|
||||
<ToolCard :tool="tool" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div v-if="toolStore.favoriteTools.length > 0">
|
||||
<div mb-2 mt-6 text-lg font-semibold>
|
||||
{{ $t('home.categories.yourFavoriteTools') }}
|
||||
</div>
|
||||
<div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px>
|
||||
<tool-card v-for="tool in toolStore.favoriteTools" :key="tool.name" :tool="tool" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-h3>All the tools</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>
|
||||
<ToolCard :tool="tool" />
|
||||
</transition>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div v-if="toolStore.newTools.length > 0">
|
||||
<div mb-2 mt-6 text-lg font-semibold>
|
||||
{{ $t('home.categories.newestTools') }}
|
||||
</div>
|
||||
<div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px>
|
||||
<tool-card v-for="tool in toolStore.newTools" :key="tool.name" :tool="tool" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mb-2 mt-6 text-lg font-semibold>
|
||||
{{ $t('home.categories.allTheTools') }}
|
||||
</div>
|
||||
<div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px>
|
||||
<tool-card v-for="tool in toolStore.tools" :key="tool.name" :tool="tool" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.home-page {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.n-h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
::v-deep(.n-grid) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.height-enter-active,
|
||||
.height-leave-active {
|
||||
transition: all 0.5s ease-in-out;
|
||||
overflow: hidden;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.height-enter-from,
|
||||
.height-leave-to {
|
||||
max-height: 42px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,6 +1,22 @@
|
||||
import type { App } from 'vue';
|
||||
import type { Plugin } from 'vue';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
||||
import baseMessages from '@intlify/unplugin-vue-i18n/messages';
|
||||
import _ from 'lodash';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' });
|
||||
|
||||
const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => {
|
||||
const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? [];
|
||||
const content = parseYaml(await fileDescriptor());
|
||||
|
||||
return { [locale]: content };
|
||||
}));
|
||||
|
||||
const messages = _.merge(
|
||||
baseMessages,
|
||||
_.merge({}, ...messagesByTools),
|
||||
);
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -8,8 +24,8 @@ const i18n = createI18n({
|
||||
messages,
|
||||
});
|
||||
|
||||
export const i18nPlugin = {
|
||||
install: (app: App) => {
|
||||
export const i18nPlugin: Plugin = {
|
||||
install: (app) => {
|
||||
app.use(i18n);
|
||||
},
|
||||
};
|
||||
|
28
src/shims.d.ts
vendored
28
src/shims.d.ts
vendored
@@ -1,21 +1,35 @@
|
||||
declare module '*.vue' {
|
||||
import type { ComponentOptions, ComponentOptions } from 'vue';
|
||||
import type { ComponentOptions } from 'vue';
|
||||
const Component: ComponentOptions;
|
||||
export default Component;
|
||||
}
|
||||
|
||||
declare module '*.md' {
|
||||
import type { ComponentOptions } from 'vue';
|
||||
const Component: ComponentOptions;
|
||||
export default Component;
|
||||
}
|
||||
|
||||
declare module '~icons/*' {
|
||||
import { FunctionalComponent, SVGAttributes } from 'vue';
|
||||
const component: FunctionalComponent<SVGAttributes>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module 'iarna-toml-esm' {
|
||||
export const parse: (toml: string) => any;
|
||||
export const stringify: (obj: any) => string;
|
||||
}
|
||||
|
||||
declare module 'emojilib' {
|
||||
const lib: Record<string, string[]>;
|
||||
export default lib;
|
||||
}
|
||||
|
||||
declare module 'unicode-emoji-json' {
|
||||
const emoji: Record<string, {
|
||||
name: string;
|
||||
slug: string;
|
||||
group: string;
|
||||
emoji_version: string;
|
||||
unicode_version: string;
|
||||
skin_tone_support: boolean;
|
||||
skin_tone_support_unicode_version: string;
|
||||
}>;
|
||||
|
||||
export default emoji;
|
||||
}
|
@@ -45,7 +45,7 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash
|
||||
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
|
||||
</n-form-item>
|
||||
<n-form-item label="Your hash: " label-placement="left">
|
||||
<c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text />
|
||||
<c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text />
|
||||
</n-form-item>
|
||||
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
||||
<div class="compare-result" :class="{ positive: compareMatch }">
|
||||
|
@@ -18,7 +18,7 @@ function computeVariance({ data }: { data: number[] }) {
|
||||
return computeAverage({ data: squaredDiffs });
|
||||
}
|
||||
|
||||
function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) {
|
||||
function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Plus, Trash } from '@vicons/tabler';
|
||||
import { useClipboard, useStorage } from '@vueuse/core';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
|
||||
import DynamicValues from './dynamic-values.vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const suites = useStorage('benchmark-builder:suites', [
|
||||
{ title: 'Suite 1', data: [5, 10] },
|
||||
@@ -47,7 +48,7 @@ const results = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const { copy } = useCopy({ createToast: false });
|
||||
|
||||
const header = {
|
||||
title: 'Suite',
|
||||
|
@@ -92,7 +92,7 @@ const inputLabelAlignmentConfig = {
|
||||
v-bind="inputLabelAlignmentConfig"
|
||||
/>
|
||||
|
||||
<div divider my-16px />
|
||||
<div my-16px divider />
|
||||
|
||||
<InputCopyable
|
||||
v-for="format in formats"
|
||||
|
@@ -82,7 +82,7 @@ const formats: DateFormat[] = [
|
||||
{
|
||||
name: 'Mongo ObjectID',
|
||||
fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`,
|
||||
toDate: objectId => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
formatMatcher: date => isMongoObjectId(date),
|
||||
},
|
||||
];
|
||||
@@ -146,7 +146,7 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date
|
||||
<c-input-text
|
||||
v-model:value="inputDate"
|
||||
autofocus
|
||||
placeholder="Put you date string here..."
|
||||
placeholder="Put your date string here..."
|
||||
clearable
|
||||
test-id="date-time-converter-input"
|
||||
:validation="validation"
|
||||
|
@@ -29,7 +29,7 @@ const { copy } = useCopy();
|
||||
Unicode: <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span>
|
||||
</div> -->
|
||||
|
||||
<div flex gap-2 font-mono text-xs op-70>
|
||||
<div flex gap-2 text-xs font-mono op-70>
|
||||
<span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })">
|
||||
{{ emojiInfo.codePoints }}
|
||||
</span>
|
||||
|
@@ -5,4 +5,4 @@ export type EmojiInfo = {
|
||||
emoji: string
|
||||
codePoints: string | undefined
|
||||
unicode: string
|
||||
} & typeof emojiUnicodeData['\uD83E\uDD10'];
|
||||
} & typeof emojiUnicodeData[string];
|
||||
|
@@ -2,6 +2,6 @@ export function convertHexToBin(hex: string) {
|
||||
return hex
|
||||
.trim()
|
||||
.split('')
|
||||
.map(byte => parseInt(byte, 16).toString(2).padStart(4, '0'))
|
||||
.map(byte => Number.parseInt(byte, 16).toString(2).padStart(4, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { format } from 'prettier';
|
||||
import htmlParser from 'prettier/parser-html';
|
||||
import htmlParser from 'prettier/plugins/html';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import Editor from './editor/editor.vue';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
|
||||
const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>');
|
||||
|
||||
const formattedHtml = asyncComputed(() => format(html.value, { parser: 'html', plugins: [htmlParser] }), '');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Editor v-model:html="html" />
|
||||
<TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" />
|
||||
<TextareaCopyable :value="formattedHtml" language="html" />
|
||||
</template>
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
|
||||
async function extractIbanInfo({ page }: { page: Page }) {
|
||||
const itemsLines = await page
|
||||
.locator('.c-key-value-list__item').all();
|
||||
|
||||
return await Promise.all(
|
||||
itemsLines.map(async item => [
|
||||
(await item.locator('.c-key-value-list__key').textContent() ?? '').trim(),
|
||||
(await item.locator('.c-key-value-list__value').textContent() ?? '').trim(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('Tool - Iban validator and parser', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/iban-validator-and-parser');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('IBAN validator and parser - IT Tools');
|
||||
});
|
||||
|
||||
test('iban info are extracted from a valid iban', async ({ page }) => {
|
||||
await page.getByTestId('iban-input').fill('DE89370400440532013000');
|
||||
|
||||
const ibanInfo = await extractIbanInfo({ page });
|
||||
|
||||
expect(ibanInfo).toEqual([
|
||||
['Is IBAN valid ?', 'Yes'],
|
||||
['Is IBAN a QR-IBAN ?', 'No'],
|
||||
['Country code', 'DE'],
|
||||
['BBAN', '370400440532013000'],
|
||||
['IBAN friendly format', 'DE89 3704 0044 0532 0130 00'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('invalid iban errors are displayed', async ({ page }) => {
|
||||
await page.getByTestId('iban-input').fill('FR7630006060011234567890189');
|
||||
|
||||
const ibanInfo = await extractIbanInfo({ page });
|
||||
|
||||
expect(ibanInfo).toEqual([
|
||||
['Is IBAN valid ?', 'No'],
|
||||
['IBAN errors', 'Wrong account bank branch checksum Wrong IBAN checksum'],
|
||||
['Is IBAN a QR-IBAN ?', 'No'],
|
||||
['Country code', 'N/A'],
|
||||
['BBAN', 'N/A'],
|
||||
['IBAN friendly format', 'FR76 3000 6060 0112 3456 7890 189'],
|
||||
]);
|
||||
});
|
||||
});
|
@@ -0,0 +1,18 @@
|
||||
import { ValidationErrorsIBAN } from 'ibantools';
|
||||
|
||||
export { getFriendlyErrors };
|
||||
|
||||
const ibanErrorToMessage = {
|
||||
[ValidationErrorsIBAN.NoIBANProvided]: 'No IBAN provided',
|
||||
[ValidationErrorsIBAN.NoIBANCountry]: 'No IBAN country',
|
||||
[ValidationErrorsIBAN.WrongBBANLength]: 'Wrong BBAN length',
|
||||
[ValidationErrorsIBAN.WrongBBANFormat]: 'Wrong BBAN format',
|
||||
[ValidationErrorsIBAN.ChecksumNotNumber]: 'Checksum is not a number',
|
||||
[ValidationErrorsIBAN.WrongIBANChecksum]: 'Wrong IBAN checksum',
|
||||
[ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: 'Wrong account bank branch checksum',
|
||||
[ValidationErrorsIBAN.QRIBANNotAllowed]: 'QR-IBAN not allowed',
|
||||
};
|
||||
|
||||
function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) {
|
||||
return errorCodes.map(errorCode => ibanErrorToMessage[errorCode]).filter(Boolean);
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { extractIBAN, friendlyFormatIBAN, isQRIBAN, validateIBAN } from 'ibantools';
|
||||
import { getFriendlyErrors } from './iban-validator-and-parser.service';
|
||||
import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types';
|
||||
|
||||
const rawIban = ref('');
|
||||
|
||||
const ibanInfo = computed<CKeyValueListItems>(() => {
|
||||
const iban = rawIban.value.toUpperCase().replace(/\s/g, '').replace(/-/g, '');
|
||||
|
||||
if (iban === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { valid: isIbanValid, errorCodes } = validateIBAN(iban);
|
||||
const { countryCode, bban } = extractIBAN(iban);
|
||||
const errors = getFriendlyErrors(errorCodes);
|
||||
|
||||
return [
|
||||
|
||||
{
|
||||
label: 'Is IBAN valid ?',
|
||||
value: isIbanValid,
|
||||
showCopyButton: false,
|
||||
},
|
||||
{
|
||||
label: 'IBAN errors',
|
||||
value: errors.length === 0 ? undefined : errors,
|
||||
hideOnNil: true,
|
||||
showCopyButton: false,
|
||||
},
|
||||
{
|
||||
label: 'Is IBAN a QR-IBAN ?',
|
||||
value: isQRIBAN(iban),
|
||||
showCopyButton: false,
|
||||
},
|
||||
{
|
||||
label: 'Country code',
|
||||
value: countryCode,
|
||||
},
|
||||
{
|
||||
label: 'BBAN',
|
||||
value: bban,
|
||||
},
|
||||
{
|
||||
label: 'IBAN friendly format',
|
||||
value: friendlyFormatIBAN(iban),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const ibanExamples = [
|
||||
'FR7630006000011234567890189',
|
||||
'DE89370400440532013000',
|
||||
'GB29NWBK60161331926819',
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" />
|
||||
|
||||
<c-key-value-list :items="ibanInfo" my-5 data-test-id="iban-info" />
|
||||
|
||||
<c-card title="Valid IBAN examples">
|
||||
<div v-for="iban in ibanExamples" :key="iban">
|
||||
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
12
src/tools/iban-validator-and-parser/index.ts
Normal file
12
src/tools/iban-validator-and-parser/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineTool } from '../tool';
|
||||
import Bank from '~icons/mdi/bank';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'IBAN validator and parser',
|
||||
path: '/iban-validator-and-parser',
|
||||
description: 'Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.',
|
||||
keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'],
|
||||
component: () => import('./iban-validator-and-parser.vue'),
|
||||
icon: Bank,
|
||||
createdAt: new Date('2023-08-26'),
|
||||
});
|
@@ -1,6 +1,9 @@
|
||||
import { tool as base64FileConverter } from './base64-file-converter';
|
||||
import { tool as base64StringConverter } from './base64-string-converter';
|
||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||
import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
|
||||
import { tool as stringObfuscator } from './string-obfuscator';
|
||||
import { tool as textDiff } from './text-diff';
|
||||
import { tool as emojiPicker } from './emoji-picker';
|
||||
import { tool as passwordStrengthAnalyser } from './password-strength-analyser';
|
||||
import { tool as yamlToToml } from './yaml-to-toml';
|
||||
@@ -53,6 +56,7 @@ import { tool as metaTagGenerator } from './meta-tag-generator';
|
||||
import { tool as mimeTypes } from './mime-types';
|
||||
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
|
||||
import { tool as qrCodeGenerator } from './qr-code-generator';
|
||||
import { tool as wifiQrCodeGenerator } from './wifi-qr-code-generator';
|
||||
import { tool as randomPortGenerator } from './random-port-generator';
|
||||
import { tool as romanNumeralConverter } from './roman-numeral-converter';
|
||||
import { tool as sqlPrettify } from './sql-prettify';
|
||||
@@ -114,7 +118,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
},
|
||||
{
|
||||
name: 'Images and videos',
|
||||
components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
||||
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
||||
},
|
||||
{
|
||||
name: 'Development',
|
||||
@@ -145,11 +149,11 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
components: [loremIpsumGenerator, textStatistics, emojiPicker],
|
||||
components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff],
|
||||
},
|
||||
{
|
||||
name: 'Data',
|
||||
components: [phoneParserAndFormatter],
|
||||
components: [phoneParserAndFormatter, ibanValidatorAndParser],
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -23,7 +23,7 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str
|
||||
+ _.chain(ip)
|
||||
.trim()
|
||||
.split('.')
|
||||
.map(part => parseInt(part).toString(16).padStart(2, '0'))
|
||||
.map(part => Number.parseInt(part).toString(16).padStart(2, '0'))
|
||||
.chunk(2)
|
||||
.map(blocks => blocks.join(''))
|
||||
.join(':')
|
||||
|
@@ -13,7 +13,7 @@ function getRangesize(start: string, end: string) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1 + parseInt(end, 2) - parseInt(start, 2);
|
||||
return 1 + Number.parseInt(end, 2) - Number.parseInt(start, 2);
|
||||
}
|
||||
|
||||
function getCidr(start: string, end: string) {
|
||||
@@ -55,8 +55,8 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
|
||||
const cidr = getCidr(start, end);
|
||||
if (cidr != null) {
|
||||
const result: Ipv4RangeExpanderResult = {};
|
||||
result.newEnd = bits2ip(parseInt(cidr.end, 2));
|
||||
result.newStart = bits2ip(parseInt(cidr.start, 2));
|
||||
result.newEnd = bits2ip(Number.parseInt(cidr.end, 2));
|
||||
result.newStart = bits2ip(Number.parseInt(cidr.start, 2));
|
||||
result.newCidr = `${result.newStart}/${cidr.mask}`;
|
||||
result.newSize = getRangesize(cidr.start, cidr.end);
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode';
|
||||
import _ from 'lodash';
|
||||
import { match } from 'ts-pattern';
|
||||
import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants';
|
||||
|
||||
export { decodeJwt };
|
||||
@@ -32,10 +31,15 @@ function parseClaims({ claim, value }: { claim: string; value: unknown }) {
|
||||
}
|
||||
|
||||
function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) {
|
||||
return match(claim)
|
||||
.with('exp', 'nbf', 'iat', () => dateFormatter(value))
|
||||
.with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined))
|
||||
.otherwise(() => undefined);
|
||||
if (['exp', 'nbf', 'iat'].includes(claim)) {
|
||||
return dateFormatter(value);
|
||||
}
|
||||
|
||||
if (claim === 'alg' && _.isString(value)) {
|
||||
return ALGORITHM_DESCRIPTIONS[value];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function dateFormatter(value: unknown) {
|
||||
|
@@ -8,7 +8,7 @@ const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '')
|
||||
const macAddress = ref('20:37:06:12:34:56');
|
||||
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]);
|
||||
|
||||
const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' });
|
||||
const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -15,7 +15,7 @@ export {
|
||||
};
|
||||
|
||||
function hexToBytes(hex: string) {
|
||||
return (hex.match(/.{1,2}/g) ?? []).map(char => parseInt(char, 16));
|
||||
return (hex.match(/.{1,2}/g) ?? []).map(char => Number.parseInt(char, 16));
|
||||
}
|
||||
|
||||
function computeHMACSha1(message: string, key: string) {
|
||||
@@ -32,7 +32,7 @@ function base32toHex(base32: string) {
|
||||
.map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0'))
|
||||
.join('');
|
||||
|
||||
const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
|
||||
const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => Number.parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>();
|
||||
const { copy: copyPrevious, copied: previousCopied } = useClipboard();
|
||||
const { copy: copyCurrent, copied: currentCopied } = useClipboard();
|
||||
const { copy: copyNext, copied: nextCopied } = useClipboard();
|
||||
const { copy: copyPrevious, isJustCopied: previousCopied } = useCopy({ createToast: false });
|
||||
const { copy: copyCurrent, isJustCopied: currentCopied } = useCopy({ createToast: false });
|
||||
const { copy: copyNext, isJustCopied: nextCopied } = useCopy({ createToast: false });
|
||||
|
||||
const { tokens } = toRefs(props);
|
||||
</script>
|
||||
|
@@ -14,6 +14,6 @@ test.describe('Tool - Password strength analyser', () => {
|
||||
|
||||
const crackDuration = await page.getByTestId('crack-duration').textContent();
|
||||
|
||||
expect(crackDuration).toEqual('15,091 milleniums, 3 centurys');
|
||||
expect(crackDuration).toEqual('15,091 millennia, 3 centuries');
|
||||
});
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ export { getPasswordCrackTimeEstimation, getCharsetLength };
|
||||
|
||||
function prettifyExponentialNotation(exponentialNotation: number) {
|
||||
const [base, exponent] = exponentialNotation.toString().split('e');
|
||||
const baseAsNumber = parseFloat(base);
|
||||
const baseAsNumber = Number.parseFloat(base);
|
||||
const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2);
|
||||
return exponent ? `${prettyBase}e${exponent}` : prettyBase;
|
||||
}
|
||||
@@ -19,20 +19,20 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
|
||||
}
|
||||
|
||||
const timeUnits = [
|
||||
{ unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation },
|
||||
{ unit: 'century', secondsInUnit: 3153600000 },
|
||||
{ unit: 'decade', secondsInUnit: 315360000 },
|
||||
{ unit: 'year', secondsInUnit: 31536000 },
|
||||
{ unit: 'month', secondsInUnit: 2592000 },
|
||||
{ unit: 'week', secondsInUnit: 604800 },
|
||||
{ unit: 'day', secondsInUnit: 86400 },
|
||||
{ unit: 'hour', secondsInUnit: 3600 },
|
||||
{ unit: 'minute', secondsInUnit: 60 },
|
||||
{ unit: 'second', secondsInUnit: 1 },
|
||||
{ unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' },
|
||||
{ unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' },
|
||||
{ unit: 'decade', secondsInUnit: 315360000, plural: 'decades' },
|
||||
{ unit: 'year', secondsInUnit: 31536000, plural: 'years' },
|
||||
{ unit: 'month', secondsInUnit: 2592000, plural: 'months' },
|
||||
{ unit: 'week', secondsInUnit: 604800, plural: 'weeks' },
|
||||
{ unit: 'day', secondsInUnit: 86400, plural: 'days' },
|
||||
{ unit: 'hour', secondsInUnit: 3600, plural: 'hours' },
|
||||
{ unit: 'minute', secondsInUnit: 60, plural: 'minutes' },
|
||||
{ unit: 'second', secondsInUnit: 1, plural: 'seconds' },
|
||||
];
|
||||
|
||||
return _.chain(timeUnits)
|
||||
.map(({ unit, secondsInUnit, format = _.identity }) => {
|
||||
.map(({ unit, secondsInUnit, plural, format = _.identity }) => {
|
||||
const quantity = Math.floor(seconds / secondsInUnit);
|
||||
seconds %= secondsInUnit;
|
||||
|
||||
@@ -41,7 +41,7 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
|
||||
}
|
||||
|
||||
const formattedQuantity = format(quantity);
|
||||
return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`;
|
||||
return `${formattedQuantity} ${quantity > 1 ? plural : unit}`;
|
||||
})
|
||||
.compact()
|
||||
.take(2)
|
||||
|
@@ -36,7 +36,7 @@ const validationRoman = useValidation({
|
||||
});
|
||||
|
||||
const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' });
|
||||
const { copy: copyArabic } = useCopy({ source: outputNumeral, text: 'Arabic number copied to the clipboard' });
|
||||
const { copy: copyArabic } = useCopy({ source: () => String(outputNumeral), text: 'Arabic number copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { type FormatFnOptions, format as formatSQL } from 'sql-formatter';
|
||||
import { type FormatOptionsWithLanguage, format as formatSQL } from 'sql-formatter';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const inputElement = ref<HTMLElement>();
|
||||
const styleStore = useStyleStore();
|
||||
const config = reactive<Partial<FormatFnOptions>>({
|
||||
const config = reactive<FormatOptionsWithLanguage>({
|
||||
keywordCase: 'upper',
|
||||
useTabs: false,
|
||||
language: 'sql',
|
||||
|
12
src/tools/string-obfuscator/index.ts
Normal file
12
src/tools/string-obfuscator/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { EyeOff } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'String obfuscator',
|
||||
path: '/string-obfuscator',
|
||||
description: 'Obfuscate a string (like a secret, an IBAN, or a token) to make it shareable and identifiable without revealing its content.',
|
||||
keywords: ['string', 'obfuscator', 'secret', 'token', 'hide', 'obscure', 'mask', 'masking'],
|
||||
component: () => import('./string-obfuscator.vue'),
|
||||
icon: EyeOff,
|
||||
createdAt: new Date('2023-08-16'),
|
||||
});
|
20
src/tools/string-obfuscator/string-obfuscator.model.test.ts
Normal file
20
src/tools/string-obfuscator/string-obfuscator.model.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { obfuscateString } from './string-obfuscator.model';
|
||||
|
||||
describe('string-obfuscator model', () => {
|
||||
describe('obfuscateString', () => {
|
||||
it('the characters in the middle of the string are replaced by the replacement character', () => {
|
||||
expect(obfuscateString('1234567890')).toBe('1234******');
|
||||
expect(obfuscateString('1234567890', { replacementChar: 'x' })).toBe('1234xxxxxx');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 5 })).toBe('12345*****');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 0, keepLast: 5 })).toBe('*****67890');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 5, keepLast: 5 })).toBe('1234567890');
|
||||
expect(obfuscateString('1234567890', { keepFirst: 2, keepLast: 2, replacementChar: 'x' })).toBe('12xxxxxx90');
|
||||
});
|
||||
|
||||
it('by default, the spaces are kept, they can be removed with the keepSpace option', () => {
|
||||
expect(obfuscateString('12345 67890')).toBe('1234* *****');
|
||||
expect(obfuscateString('12345 67890', { keepSpace: false })).toBe('1234*******');
|
||||
});
|
||||
});
|
||||
});
|
35
src/tools/string-obfuscator/string-obfuscator.model.ts
Normal file
35
src/tools/string-obfuscator/string-obfuscator.model.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { get } from '@vueuse/core';
|
||||
import { type MaybeRef, computed } from 'vue';
|
||||
|
||||
export { obfuscateString, useObfuscateString };
|
||||
|
||||
function obfuscateString(
|
||||
str: string,
|
||||
{ replacementChar = '*', keepFirst = 4, keepLast = 0, keepSpace = true }: { replacementChar?: string; keepFirst?: number; keepLast?: number; keepSpace?: boolean } = {}): string {
|
||||
return str
|
||||
.split('')
|
||||
.map((char, index, array) => {
|
||||
if (keepSpace && char === ' ') {
|
||||
return char;
|
||||
}
|
||||
|
||||
return (index < keepFirst || index >= array.length - keepLast) ? char : replacementChar;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function useObfuscateString(
|
||||
str: MaybeRef<string>,
|
||||
config: { replacementChar?: MaybeRef<string>; keepFirst?: MaybeRef<number>; keepLast?: MaybeRef<number>; keepSpace?: MaybeRef<boolean> } = {},
|
||||
|
||||
) {
|
||||
return computed(() => obfuscateString(
|
||||
get(str),
|
||||
{
|
||||
replacementChar: get(config.replacementChar),
|
||||
keepFirst: get(config.keepFirst),
|
||||
keepLast: get(config.keepLast),
|
||||
keepSpace: get(config.keepSpace),
|
||||
},
|
||||
));
|
||||
}
|
47
src/tools/string-obfuscator/string-obfuscator.vue
Normal file
47
src/tools/string-obfuscator/string-obfuscator.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { useObfuscateString } from './string-obfuscator.model';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const str = ref('Lorem ipsum dolor sit amet');
|
||||
const keepFirst = ref(4);
|
||||
const keepLast = ref(4);
|
||||
const keepSpace = ref(true);
|
||||
|
||||
const obfuscatedString = useObfuscateString(str, { keepFirst, keepLast, keepSpace });
|
||||
const { copy } = useCopy({ source: obfuscatedString });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text v-model:value="str" raw-text placeholder="Enter string to obfuscate" label="String to obfuscate:" clearable multiline />
|
||||
|
||||
<div mt-4 flex gap-10px>
|
||||
<div>
|
||||
<div>Keep first:</div>
|
||||
<n-input-number v-model:value="keepFirst" min="0" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>Keep last:</div>
|
||||
<n-input-number v-model:value="keepLast" min="0" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div mb-5px>
|
||||
Keep spaces:
|
||||
</div>
|
||||
<n-switch v-model:value="keepSpace" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<c-card v-if="obfuscatedString" mt-60px max-w-600px flex items-center gap-5px font-mono>
|
||||
<div break-anywhere text-wrap>
|
||||
{{ obfuscatedString }}
|
||||
</div>
|
||||
|
||||
<c-button @click="copy()">
|
||||
<icon-mdi:content-copy />
|
||||
</c-button>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
12
src/tools/text-diff/index.ts
Normal file
12
src/tools/text-diff/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { FileDiff } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Text diff',
|
||||
path: '/text-diff',
|
||||
description: 'Compare two texts and see the differences between them.',
|
||||
keywords: ['text', 'diff', 'compare', 'string', 'text diff', 'code'],
|
||||
component: () => import('./text-diff.vue'),
|
||||
icon: FileDiff,
|
||||
createdAt: new Date('2023-08-16'),
|
||||
});
|
5
src/tools/text-diff/text-diff.vue
Normal file
5
src/tools/text-diff/text-diff.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<c-card w-full important:flex-1 important:pa-0>
|
||||
<c-diff-editor />
|
||||
</c-card>
|
||||
</template>
|
@@ -15,14 +15,12 @@ export function createToken({
|
||||
length?: number
|
||||
alphabet?: string
|
||||
}) {
|
||||
const allAlphabet
|
||||
= alphabet
|
||||
?? [
|
||||
...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''),
|
||||
...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''),
|
||||
...(withNumbers ? '0123456789' : ''),
|
||||
...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''),
|
||||
].join('');
|
||||
const allAlphabet = alphabet ?? [
|
||||
withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '',
|
||||
withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
|
||||
withNumbers ? '0123456789' : '',
|
||||
withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
|
||||
].join(''); ;
|
||||
|
||||
return shuffleString(allAlphabet.repeat(length)).substring(0, length);
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ export const tool = defineTool({
|
||||
name: 'UUIDs v4 generator',
|
||||
path: '/uuid-generator',
|
||||
description:
|
||||
'A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot !).',
|
||||
'A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).',
|
||||
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
|
||||
component: () => import('./uuid-generator.vue'),
|
||||
icon: Fingerprint,
|
||||
|
@@ -34,7 +34,7 @@ const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard'
|
||||
/>
|
||||
|
||||
<div flex justify-center gap-3>
|
||||
<c-button autofocus @click="copy">
|
||||
<c-button autofocus @click="copy()">
|
||||
Copy
|
||||
</c-button>
|
||||
<c-button @click="refreshUUIDs">
|
||||
|
13
src/tools/wifi-qr-code-generator/index.ts
Normal file
13
src/tools/wifi-qr-code-generator/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Qrcode } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'WiFi QR Code generator',
|
||||
path: '/wifi-qrcode-generator',
|
||||
description:
|
||||
'Generate and download QR-codes for quick connections to WiFi networks.',
|
||||
keywords: ['qr', 'code', 'generator', 'square', 'color', 'link', 'low', 'medium', 'quartile', 'high', 'transparent', 'wifi'],
|
||||
component: () => import('./wifi-qr-code-generator.vue'),
|
||||
icon: Qrcode,
|
||||
createdAt: new Date('2023-09-06'),
|
||||
});
|
146
src/tools/wifi-qr-code-generator/useQRCode.ts
Normal file
146
src/tools/wifi-qr-code-generator/useQRCode.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { type MaybeRef, get } from '@vueuse/core';
|
||||
import QRCode, { type QRCodeToDataURLOptions } from 'qrcode';
|
||||
import { isRef, ref, watch } from 'vue';
|
||||
|
||||
export const wifiEncryptions = ['WEP', 'WPA', 'nopass', 'WPA2-EAP'] as const;
|
||||
export type WifiEncryption = typeof wifiEncryptions[number];
|
||||
|
||||
// @see https://en.wikipedia.org/wiki/Extensible_Authentication_Protocol
|
||||
// for a list of available EAP methods. There are a lot (40!) of them.
|
||||
export const EAPMethods = [
|
||||
'MD5',
|
||||
'POTP',
|
||||
'GTC',
|
||||
'TLS',
|
||||
'IKEv2',
|
||||
'SIM',
|
||||
'AKA',
|
||||
'AKA\'',
|
||||
'TTLS',
|
||||
'PWD',
|
||||
'LEAP',
|
||||
'PSK',
|
||||
'FAST',
|
||||
'TEAP',
|
||||
'EKE',
|
||||
'NOOB',
|
||||
'PEAP',
|
||||
] as const;
|
||||
export type EAPMethod = typeof EAPMethods[number];
|
||||
|
||||
export const EAPPhase2Methods = [
|
||||
'None',
|
||||
'MSCHAPV2',
|
||||
] as const;
|
||||
export type EAPPhase2Method = typeof EAPPhase2Methods[number];
|
||||
|
||||
interface IWifiQRCodeOptions {
|
||||
ssid: MaybeRef<string>
|
||||
password: MaybeRef<string>
|
||||
eapMethod: MaybeRef<EAPMethod>
|
||||
isHiddenSSID: MaybeRef<boolean>
|
||||
eapAnonymous: MaybeRef<boolean>
|
||||
eapIdentity: MaybeRef<string>
|
||||
eapPhase2Method: MaybeRef<EAPPhase2Method>
|
||||
color: { foreground: MaybeRef<string>; background: MaybeRef<string> }
|
||||
options?: QRCodeToDataURLOptions
|
||||
}
|
||||
|
||||
interface GetQrCodeTextOptions {
|
||||
ssid: string
|
||||
password: string
|
||||
encryption: WifiEncryption
|
||||
eapMethod: EAPMethod
|
||||
isHiddenSSID: boolean
|
||||
eapAnonymous: boolean
|
||||
eapIdentity: string
|
||||
eapPhase2Method: EAPPhase2Method
|
||||
}
|
||||
|
||||
function escapeString(str: string) {
|
||||
// replaces \, ;, ,, " and : with the same character preceded by a backslash
|
||||
return str.replace(/([\\;,:"])/g, '\\$1');
|
||||
}
|
||||
|
||||
function getQrCodeText(options: GetQrCodeTextOptions): string | null {
|
||||
const { ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method } = options;
|
||||
if (!ssid) {
|
||||
return null;
|
||||
}
|
||||
if (encryption === 'nopass') {
|
||||
return `WIFI:S:${escapeString(ssid)};;`; // type can be omitted in that case, and password is not needed, makes the QR Code smaller
|
||||
}
|
||||
if (encryption !== 'WPA2-EAP' && password) {
|
||||
// EAP has a lot of options, so we'll handle it separately
|
||||
// WPA and WEP are pretty simple though.
|
||||
return `WIFI:S:${escapeString(ssid)};T:${encryption};P:${escapeString(password)};${isHiddenSSID ? 'H:true' : ''};`;
|
||||
}
|
||||
if (encryption === 'WPA2-EAP' && password && eapMethod) {
|
||||
// WPA2-EAP string is a lot more complex, first off, we drop the text if there is no identity, and it's not anonymous.
|
||||
if (!eapIdentity && !eapAnonymous) {
|
||||
return null;
|
||||
}
|
||||
// From reading, I could only find that a phase 2 is required for the PEAP method, I may be wrong though, I didn't read the whole spec.
|
||||
if (eapMethod === 'PEAP' && !eapPhase2Method) {
|
||||
return null;
|
||||
}
|
||||
// The string is built in the following order:
|
||||
// 1. SSID
|
||||
// 2. Authentication type
|
||||
// 3. Password
|
||||
// 4. EAP method
|
||||
// 5. EAP phase 2 method
|
||||
// 6. Identity or anonymous if checked
|
||||
// 7. Hidden SSID if checked
|
||||
const identity = eapAnonymous ? 'A:anon' : `I:${escapeString(eapIdentity)}`;
|
||||
const phase2 = eapPhase2Method !== 'None' ? `PH2:${eapPhase2Method};` : '';
|
||||
return `WIFI:S:${escapeString(ssid)};T:WPA2-EAP;P:${escapeString(password)};E:${eapMethod};${phase2}${identity};${isHiddenSSID ? 'H:true' : ''};`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useWifiQRCode({
|
||||
ssid,
|
||||
password,
|
||||
eapMethod,
|
||||
isHiddenSSID,
|
||||
eapAnonymous,
|
||||
eapIdentity,
|
||||
eapPhase2Method,
|
||||
color: { background, foreground },
|
||||
options,
|
||||
}: IWifiQRCodeOptions) {
|
||||
const qrcode = ref('');
|
||||
const encryption = ref<WifiEncryption>('WPA');
|
||||
|
||||
watch(
|
||||
[ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method, background, foreground].filter(isRef),
|
||||
async () => {
|
||||
// @see https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
|
||||
// This is the full spec, there's quite a bit of logic to generate the string embeddedin the QR code.
|
||||
const text = getQrCodeText({
|
||||
ssid: get(ssid),
|
||||
password: get(password),
|
||||
encryption: get(encryption),
|
||||
eapMethod: get(eapMethod),
|
||||
isHiddenSSID: get(isHiddenSSID),
|
||||
eapAnonymous: get(eapAnonymous),
|
||||
eapIdentity: get(eapIdentity),
|
||||
eapPhase2Method: get(eapPhase2Method),
|
||||
});
|
||||
if (text) {
|
||||
qrcode.value = await QRCode.toDataURL(get(text).trim(), {
|
||||
color: {
|
||||
dark: get(foreground),
|
||||
light: get(background),
|
||||
...options?.color,
|
||||
},
|
||||
errorCorrectionLevel: 'M',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
return { qrcode, encryption };
|
||||
}
|
153
src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
Normal file
153
src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EAPMethods,
|
||||
EAPPhase2Methods,
|
||||
useWifiQRCode,
|
||||
} from './useQRCode';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
|
||||
const foreground = ref('#000000ff');
|
||||
const background = ref('#ffffffff');
|
||||
|
||||
const ssid = ref();
|
||||
const password = ref();
|
||||
const eapMethod = ref();
|
||||
const isHiddenSSID = ref(false);
|
||||
const eapAnonymous = ref(false);
|
||||
const eapIdentity = ref();
|
||||
const eapPhase2Method = ref();
|
||||
|
||||
const { qrcode, encryption } = useWifiQRCode({
|
||||
ssid,
|
||||
password,
|
||||
eapMethod,
|
||||
isHiddenSSID,
|
||||
eapAnonymous,
|
||||
eapIdentity,
|
||||
eapPhase2Method,
|
||||
color: {
|
||||
background,
|
||||
foreground,
|
||||
},
|
||||
options: { width: 1024 },
|
||||
});
|
||||
|
||||
const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<div grid grid-cols-1 gap-12>
|
||||
<div>
|
||||
<c-select
|
||||
v-model:value="encryption"
|
||||
mb-4
|
||||
label="Encryption method"
|
||||
default-value="WPA"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
:options="[
|
||||
{
|
||||
label: 'No password',
|
||||
value: 'nopass',
|
||||
},
|
||||
{
|
||||
label: 'WPA/WPA2',
|
||||
value: 'WPA',
|
||||
},
|
||||
{
|
||||
label: 'WEP',
|
||||
value: 'WEP',
|
||||
},
|
||||
{
|
||||
label: 'WPA2-EAP',
|
||||
value: 'WPA2-EAP',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="mb-6 flex flex-row items-center gap-2">
|
||||
<c-input-text
|
||||
v-model:value="ssid"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
label="SSID:"
|
||||
rows="1"
|
||||
autosize
|
||||
placeholder="Your WiFi SSID..."
|
||||
mb-6
|
||||
/>
|
||||
<n-checkbox v-model:checked="isHiddenSSID">
|
||||
Hidden SSID
|
||||
</n-checkbox>
|
||||
</div>
|
||||
<c-input-text
|
||||
v-if="encryption !== 'nopass'"
|
||||
v-model:value="password"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
label="Password:"
|
||||
rows="1"
|
||||
autosize
|
||||
type="password"
|
||||
placeholder="Your WiFi Password..."
|
||||
mb-6
|
||||
/>
|
||||
<c-select
|
||||
v-if="encryption === 'WPA2-EAP'"
|
||||
v-model:value="eapMethod"
|
||||
label="EAP method"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
:options="EAPMethods.map((method) => ({ label: method, value: method }))"
|
||||
searchable mb-4
|
||||
/>
|
||||
<div v-if="encryption === 'WPA2-EAP'" class="mb-6 flex flex-row items-center gap-2">
|
||||
<c-input-text
|
||||
v-model:value="eapIdentity"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
label="Identity:"
|
||||
rows="1"
|
||||
autosize
|
||||
placeholder="Your EAP Identity..."
|
||||
mb-6
|
||||
/>
|
||||
<n-checkbox v-model:checked="eapAnonymous">
|
||||
Anonymous?
|
||||
</n-checkbox>
|
||||
</div>
|
||||
<c-select
|
||||
v-if="encryption === 'WPA2-EAP'"
|
||||
v-model:value="eapPhase2Method"
|
||||
label="EAP Phase 2 method"
|
||||
label-position="left"
|
||||
label-width="130px"
|
||||
label-align="right"
|
||||
:options="EAPPhase2Methods.map((method) => ({ label: method, value: method }))"
|
||||
searchable mb-4
|
||||
/>
|
||||
<n-form label-width="130" label-placement="left">
|
||||
<n-form-item label="Foreground color:">
|
||||
<n-color-picker v-model:value="foreground" :modes="['hex']" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Background color:">
|
||||
<n-color-picker v-model:value="background" :modes="['hex']" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
<div v-if="qrcode">
|
||||
<div flex flex-col items-center gap-3>
|
||||
<img alt="wifi-qrcode" :src="qrcode" width="200">
|
||||
<c-button @click="download">
|
||||
Download qr-code
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
68
src/ui/c-diff-editor/c-diff-editor.vue
Normal file
68
src/ui/c-diff-editor/c-diff-editor.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const props = withDefaults(defineProps<{ options?: monaco.editor.IDiffEditorOptions }>(), { options: () => ({}) });
|
||||
const { options } = toRefs(props);
|
||||
|
||||
const editorContainer = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneDiffEditor | null = null;
|
||||
|
||||
monaco.editor.defineTheme('it-tools-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
},
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme('it-tools-light', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
},
|
||||
});
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
|
||||
watch(
|
||||
() => styleStore.isDarkTheme,
|
||||
isDarkTheme => monaco.editor.setTheme(isDarkTheme ? 'it-tools-dark' : 'it-tools-light'),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => options.value,
|
||||
options => editor?.updateOptions(options),
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
useResizeObserver(editorContainer, () => {
|
||||
editor?.layout();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!editorContainer.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor = monaco.editor.createDiffEditor(editorContainer.value, {
|
||||
originalEditable: true,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
editor.setModel({
|
||||
original: monaco.editor.createModel('original text', 'txt'),
|
||||
modified: monaco.editor.createModel('modified text', 'txt'),
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="editorContainer" h-600px />
|
||||
</template>
|
27
src/ui/c-key-value-list/c-key-value-list-item.vue
Normal file
27
src/ui/c-key-value-list/c-key-value-list-item.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import _ from 'lodash';
|
||||
import type { CKeyValueListItem } from './c-key-value-list.types';
|
||||
|
||||
const props = defineProps<{ item: CKeyValueListItem }>();
|
||||
const { item } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="_.isArray(item.value)">
|
||||
<div v-for="value in item.value" :key="value">
|
||||
<c-text-copyable :value="value" :show-icon="item.showCopyButton ?? true" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="_.isBoolean(item.value)">
|
||||
<c-text-copyable :value="item.value ? 'true' : 'false'" :displayed-value="item.value ? 'Yes' : 'No'" :show-icon="item.showCopyButton ?? true" />
|
||||
</div>
|
||||
<div v-else-if="_.isNumber(item.value)" font-mono>
|
||||
<c-text-copyable :value="String(item.value)" :show-icon="item.showCopyButton ?? true" />
|
||||
</div>
|
||||
<div v-else-if="_.isNil(item.value) || item.value === ''" op-70>
|
||||
{{ item.placeholder ?? 'N/A' }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<c-text-copyable :value="item.value" :show-icon="item.showCopyButton ?? true" />
|
||||
</div>
|
||||
</template>
|
9
src/ui/c-key-value-list/c-key-value-list.types.ts
Normal file
9
src/ui/c-key-value-list/c-key-value-list.types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface CKeyValueListItem {
|
||||
label: string
|
||||
value: string | string[] | number | boolean | undefined | null
|
||||
hideOnNil?: boolean
|
||||
placeholder?: string
|
||||
showCopyButton?: boolean
|
||||
}
|
||||
|
||||
export type CKeyValueListItems = CKeyValueListItem[];
|
21
src/ui/c-key-value-list/c-key-value-list.vue
Normal file
21
src/ui/c-key-value-list/c-key-value-list.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import _ from 'lodash';
|
||||
import type { CKeyValueListItems } from './c-key-value-list.types';
|
||||
|
||||
const props = withDefaults(defineProps<{ items?: CKeyValueListItems }>(), { items: () => [] });
|
||||
const { items } = toRefs(props);
|
||||
|
||||
const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.value) || !item.hideOnNil));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div my-5>
|
||||
<div v-for="item in formattedItems" :key="item.label" flex gap-2 py-1 class="c-key-value-list__item">
|
||||
<div flex-basis-180px text-right font-bold class="c-key-value-list__key">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
|
||||
<c-key-value-list-item :item="item" class="c-key-value-list__value" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -1,11 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from './c-modal.theme';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{ open?: boolean; centered?: boolean }>(), {
|
||||
open: false,
|
||||
centered: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:open']);
|
||||
|
||||
const isOpen = useVModel(props, 'open', emit, { passive: true });
|
||||
|
||||
const { centered } = toRefs(props);
|
||||
@@ -29,10 +35,6 @@ defineExpose({
|
||||
isOpen,
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const modal = ref();
|
||||
|
||||
|
3
src/ui/c-text-copyable/c-text-copyable.demo.vue
Normal file
3
src/ui/c-text-copyable/c-text-copyable.demo.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<c-text-copyable value="value" displayed-value="displayedValue" />
|
||||
</template>
|
17
src/ui/c-text-copyable/c-text-copyable.vue
Normal file
17
src/ui/c-text-copyable/c-text-copyable.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(defineProps<{ value?: string; displayedValue?: string; showIcon?: boolean }>(), { value: '', displayedValue: undefined, showIcon: true });
|
||||
const { value, displayedValue, showIcon } = toRefs(props);
|
||||
|
||||
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-tooltip :tooltip="isJustCopied ? 'Copied!' : 'Copy to clipboard'" cursor-pointer @click="copy">
|
||||
<span flex items-center gap-2>
|
||||
{{ displayedValue ?? value }}
|
||||
<icon-mdi-content-copy v-if="showIcon" op-40 />
|
||||
</span>
|
||||
</c-tooltip>
|
||||
</template>
|
17
src/ui/c-tooltip/c-tooltip.demo.vue
Normal file
17
src/ui/c-tooltip/c-tooltip.demo.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<c-tooltip>
|
||||
Hover me
|
||||
|
||||
<template #tooltip>
|
||||
Tooltip content
|
||||
</template>
|
||||
</c-tooltip>
|
||||
</div>
|
||||
|
||||
<div mt-5>
|
||||
<c-tooltip tooltip="Tooltip content">
|
||||
Hover me
|
||||
</c-tooltip>
|
||||
</div>
|
||||
</template>
|
30
src/ui/c-tooltip/c-tooltip.vue
Normal file
30
src/ui/c-tooltip/c-tooltip.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{ tooltip?: string }>(), { tooltip: '' });
|
||||
const { tooltip } = toRefs(props);
|
||||
|
||||
const targetRef = ref();
|
||||
const isTargetHovered = useElementHover(targetRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative" inline-block>
|
||||
<div ref="targetRef">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2"
|
||||
:class="{
|
||||
'op-0 scale-0': isTargetHovered === false,
|
||||
'op-100 scale-100': isTargetHovered,
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="isTargetHovered"
|
||||
name="tooltip"
|
||||
>
|
||||
{{ tooltip }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -4,7 +4,7 @@ const clampHex = (value: number) => Math.max(0, Math.min(255, Math.round(value))
|
||||
|
||||
function lighten(color: string, amount: number): string {
|
||||
const alpha = color.length === 9 ? color.slice(7) : '';
|
||||
const num = parseInt(color.slice(1, 7), 16);
|
||||
const num = Number.parseInt(color.slice(1, 7), 16);
|
||||
|
||||
const r = clampHex(((num >> 16) & 255) + amount);
|
||||
const g = clampHex(((num >> 8) & 255) + amount);
|
||||
|
@@ -7,5 +7,5 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
|
||||
}
|
||||
|
@@ -1,14 +1,20 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "**/*.d.ts", "node_modules/vite-plugin-pwa/client.d.ts"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021"],
|
||||
"lib": ["ES2022"],
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "Node",
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["naive-ui/volar", "unplugin-icons/types/vue", "@intlify/unplugin-vue-i18n/messages"]
|
||||
"types": ["naive-ui/volar", "@intlify/unplugin-vue-i18n/messages", "unplugin-icons/types/vue"],
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": ["vite.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
@@ -4,6 +4,6 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
"types": ["node", "jsdom", "unplugin-icons/types/vue"]
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ export default defineConfig({
|
||||
runtimeOnly: true,
|
||||
compositionOnly: true,
|
||||
fullInstall: true,
|
||||
include: [resolve(__dirname, 'locales/**'), resolve(__dirname, 'src/tools/*/locales/**')],
|
||||
include: [resolve(__dirname, 'locales/**')],
|
||||
}),
|
||||
AutoImport({
|
||||
imports: [
|
||||
@@ -106,4 +106,7 @@ export default defineConfig({
|
||||
test: {
|
||||
exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'],
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user