mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-08 07:11:03 -04:00
Merge branch 'CorentinTh:main' into main
This commit is contained in:
commit
5034b3c846
44 changed files with 1111 additions and 518 deletions
2
.github/workflows/docker-nightly-release.yml
vendored
2
.github/workflows/docker-nightly-release.yml
vendored
|
@ -80,7 +80,7 @@ jobs:
|
|||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
corentinth/it-tools:nightly
|
||||
|
|
2
.github/workflows/releases.yml
vendored
2
.github/workflows/releases.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
|||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
corentinth/it-tools:latest
|
||||
|
|
|
@ -100,7 +100,7 @@ pnpm lint
|
|||
To create a new tool, there is a script that generate the boilerplate of the new tool, simply run:
|
||||
|
||||
```sh
|
||||
pnpm run script:create-new-tool my-tool-name
|
||||
pnpm run script:create:tool my-tool-name
|
||||
```
|
||||
|
||||
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool.
|
||||
|
|
12
components.d.ts
vendored
12
components.d.ts
vendored
|
@ -29,7 +29,11 @@ declare module '@vue/runtime-core' {
|
|||
'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']
|
||||
CCollapse: typeof import('./src/ui/c-collapse/c-collapse.vue')['default']
|
||||
'CCollapse.demo': typeof import('./src/ui/c-collapse/c-collapse.demo.vue')['default']
|
||||
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
||||
CFileUpload: typeof import('./src/ui/c-file-upload/c-file-upload.vue')['default']
|
||||
'CFileUpload.demo': typeof import('./src/ui/c-file-upload/c-file-upload.demo.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']
|
||||
|
@ -39,8 +43,12 @@ declare module '@vue/runtime-core' {
|
|||
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']
|
||||
CMarkdown: typeof import('./src/ui/c-markdown/c-markdown.vue')['default']
|
||||
'CMarkdown.demo': typeof import('./src/ui/c-markdown/c-markdown.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']
|
||||
CModalValue: typeof import('./src/ui/c-modal-value/c-modal-value.vue')['default']
|
||||
'CModalValue.demo': typeof import('./src/ui/c-modal-value/c-modal-value.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']
|
||||
|
@ -83,6 +91,7 @@ declare module '@vue/runtime-core' {
|
|||
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
|
||||
IconMdiArrowRight: typeof import('~icons/mdi/arrow-right')['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']
|
||||
|
@ -100,6 +109,7 @@ declare module '@vue/runtime-core' {
|
|||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['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']
|
||||
|
@ -165,6 +175,8 @@ declare module '@vue/runtime-core' {
|
|||
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']
|
||||
PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default']
|
||||
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.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']
|
||||
|
|
|
@ -7,6 +7,7 @@ home:
|
|||
toggleMenu: 'Toggle menu'
|
||||
home: Home
|
||||
uiLib: 'UI Lib'
|
||||
support: 'Support IT Tools development'
|
||||
buyMeACoffee: 'Buy me a coffee'
|
||||
follow:
|
||||
title: 'You like it-tools?'
|
||||
|
@ -26,26 +27,31 @@ home:
|
|||
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.'
|
||||
content: >
|
||||
# About IT-Tools
|
||||
|
||||
This wonderful website, made with ❤ by [Corentin Thomasset](https://github.com/CorentinTh) , 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!
|
||||
|
||||
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 [sponsoring me](https://www.buymeacoffee.com/cthmsst).
|
||||
|
||||
## Technologies
|
||||
|
||||
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 [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) file of the repository.
|
||||
|
||||
## Found a bug? A tool is missing?
|
||||
|
||||
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 [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) in the GitHub repository.
|
||||
|
||||
And if you found a bug, or something doesn't work as expected, please file a bug report in the [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) 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'
|
||||
favoriteButton:
|
||||
remove: 'Remove from favorites'
|
||||
add: 'Add to favorites'
|
||||
toolCard:
|
||||
new: New
|
||||
search:
|
||||
|
|
|
@ -25,6 +25,22 @@ home:
|
|||
darkMode: 'Mode sombre'
|
||||
lightMode: 'Mode clair'
|
||||
mode: 'Basculer le mode sombre/clair'
|
||||
about:
|
||||
content: >
|
||||
# À propos de IT-Tools
|
||||
|
||||
Ce merveilleux site, fait avec ❤ par [Corentin Thomasset](https://github.com/CorentinTh), regroupe des outils utiles pour les développeurs et les personnes travaillant dans l'informatique. Si vous le trouvez utile, n'hésitez pas à le partager et n'oubliez pas de le mettre dans vos favoris !
|
||||
|
||||
IT Tools est open-source (sous licence MIT) et gratuit, et le restera toujours, mais cela me coûte de l'argent pour l'héberger et renouveler le nom de domaine. Si vous voulez soutenir mon travail, et m'encourager à ajouter plus d'outils, n'hésitez pas à me [soutenir](https://www.buymeacoffee.com/cthmsst).
|
||||
|
||||
## Technologies
|
||||
|
||||
IT Tools est fait en Vue.js (Vue 3) avec la bibliothèque de composants Naive UI et est hébergé et déployé en continu par Vercel. Des bibliothèques open-source tierces sont utilisées dans certains outils, vous pouvez trouver la liste complète dans le fichier [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) du dépôt.
|
||||
|
||||
## Vous avez trouvé un bug ? Un outil manque ?
|
||||
|
||||
Si vous avez besoin d'un outil qui n'est pas encore présent ici, et que vous pensez qu'il peut être utile, vous êtes invité à soumettre une demande de fonctionnalité dans la [section issue](https://github.com/CorentinTh/it-tools/issues/new/choose) du dépôt GitHub.
|
||||
|
||||
404:
|
||||
notFound: '404 Not Found'
|
||||
sorry: "Désolé, cette page n'existe pas"
|
||||
|
|
71
locales/zh.yml
Normal file
71
locales/zh.yml
Normal file
|
@ -0,0 +1,71 @@
|
|||
home:
|
||||
categories:
|
||||
newestTools: '最新工具'
|
||||
favoriteTools: '我的收藏'
|
||||
allTools: '全部工具'
|
||||
subtitle: '助力开发人员和 IT 工作者'
|
||||
toggleMenu: '切换菜单'
|
||||
home: '主页'
|
||||
uiLib: 'UI 库'
|
||||
support: '支持 IT 工具开发'
|
||||
buyMeACoffee: '赞助'
|
||||
follow:
|
||||
title: '关注我们'
|
||||
p1: '给我们 Star'
|
||||
githubRepository: 'GitHub 仓库'
|
||||
p2: '关注我们的'
|
||||
twitterAccount: 'Twitter'
|
||||
thankYou: '感谢您的支持!'
|
||||
nav:
|
||||
github: 'GitHub 仓库'
|
||||
githubRepository: 'GitHub 仓库'
|
||||
twitter: 'Twitter 账号'
|
||||
twitterAccount: 'Twitter 账号'
|
||||
about: '关于 IT-Tools'
|
||||
aboutLabel: '关于'
|
||||
darkMode: '深色模式'
|
||||
lightMode: '浅色模式'
|
||||
mode: '颜色模式'
|
||||
about:
|
||||
content: >
|
||||
# 关于 IT-Tools
|
||||
|
||||
IT-Tools 由 [Corentin Thomasset](https://github.com/CorentinTh) 用 ❤ 开发,汇集了对开发人员和 IT 从业者有用的工具。如果对您有帮助,请将其分享给您的朋友,并且添加到收藏夹中!
|
||||
|
||||
IT-Tools 永久免费且开源(MIT 许可证),但需要资金用于托管和续订域名。如果您想支持我的工作,并鼓励我添加更多工具,请考虑通过 [赞助我](https://www.buymeacoffee.com/cthmsst) 进行支持。
|
||||
|
||||
## 技术
|
||||
|
||||
IT-Tools 采用 Vue.js(Vue 3)和 Naive UI 组件库开发,并由 Vercel 托管和持续部署。某些工具使用了第三方开源库,您可以在仓库的 [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) 文件中找到完整的列表。
|
||||
|
||||
## 发现了 Bug?缺少工具?
|
||||
|
||||
如果目前这里没有您需要的工具,并且您认为它可能有用,欢迎在 GitHub 仓库的 [issues](https://github.com/CorentinTh/it-tools/issues/new/choose) 中提交新增功能的请求。
|
||||
|
||||
如果您发现了 Bug,或者某些功能未能按预期工作,请在 GitHub 仓库的 [issues](https://github.com/CorentinTh/it-tools/issues/new/choose) 中提交错误报告。
|
||||
|
||||
404:
|
||||
notFound: '404 页面不存在'
|
||||
sorry: '抱歉,该页面似乎不存在'
|
||||
maybe: '也许缓存出现了一些问题,试试强制刷新页面?'
|
||||
backHome: '返回主页'
|
||||
favoriteButton:
|
||||
remove: '取消收藏'
|
||||
add: '加入收藏'
|
||||
toolCard:
|
||||
new: '新'
|
||||
search:
|
||||
label: '搜索'
|
||||
tools:
|
||||
categories:
|
||||
favorite-tools: '我的收藏'
|
||||
crypto: '加密'
|
||||
converter: '转换器'
|
||||
web: Web
|
||||
images and videos: '图片和视频'
|
||||
development: '开发'
|
||||
network: '网络'
|
||||
math: '数学'
|
||||
measurement: '测量'
|
||||
text: '文本'
|
||||
data: '数据'
|
10
package.json
10
package.json
|
@ -55,6 +55,7 @@
|
|||
"cronstrue": "^2.26.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"dompurify": "^3.0.6",
|
||||
"emojilib": "^3.0.10",
|
||||
"figue": "^1.2.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
|
@ -65,13 +66,15 @@
|
|||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^10.0.0",
|
||||
"mathjs": "^11.9.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"monaco-editor": "^0.43.0",
|
||||
"naive-ui": "^2.35.0",
|
||||
"netmask": "^2.0.2",
|
||||
"node-forge": "^1.3.1",
|
||||
"oui": "^12.0.52",
|
||||
"oui-data": "^1.0.10",
|
||||
"pdf-signature-reader": "^1.4.2",
|
||||
"pinia": "^2.0.34",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"qrcode": "^1.5.1",
|
||||
|
@ -97,6 +100,7 @@
|
|||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jsdom": "^21.0.0",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
|
@ -106,7 +110,7 @@
|
|||
"@types/qrcode": "^1.5.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@unocss/eslint-config": "^0.55.0",
|
||||
"@unocss/eslint-config": "^0.57.0",
|
||||
"@vitejs/plugin-vue": "^4.3.2",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"@vue/compiler-sfc": "^3.2.47",
|
||||
|
@ -120,7 +124,7 @@
|
|||
"less": "^4.1.3",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "~5.2.0",
|
||||
"unocss": "^0.55.0",
|
||||
"unocss": "^0.57.0",
|
||||
"unocss-preset-scrollbar": "^0.2.1",
|
||||
"unplugin-icons": "^0.17.0",
|
||||
"unplugin-vue-components": "^0.25.0",
|
||||
|
|
691
pnpm-lock.yaml
generated
691
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -24,7 +24,7 @@ function toggleFavorite(event: MouseEvent) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<c-tooltip :tooltip="isFavorite ? 'Remove from favorites' : 'Add to favorites' ">
|
||||
<c-tooltip :tooltip="isFavorite ? $t('favoriteButton.remove') : $t('favoriteButton.add') ">
|
||||
<c-button
|
||||
variant="text"
|
||||
circle
|
||||
|
|
32
src/composable/downloadBase64.test.ts
Normal file
32
src/composable/downloadBase64.test.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { getMimeTypeFromBase64 } from './downloadBase64';
|
||||
|
||||
describe('downloadBase64', () => {
|
||||
describe('getMimeTypeFromBase64', () => {
|
||||
it('when the base64 string has a data URI, it returns the mime type', () => {
|
||||
expect(getMimeTypeFromBase64({ base64String: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' });
|
||||
expect(getMimeTypeFromBase64({ base64String: 'data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/jpg' });
|
||||
});
|
||||
|
||||
it('when the base64 string has no data URI, it try to infer the mime type from the signature', () => {
|
||||
// https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
|
||||
// PNG
|
||||
expect(getMimeTypeFromBase64({ base64String: 'iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' });
|
||||
|
||||
// GIF
|
||||
expect(getMimeTypeFromBase64({ base64String: 'R0lGODdh' })).to.deep.equal({ mimeType: 'image/gif' });
|
||||
expect(getMimeTypeFromBase64({ base64String: 'R0lGODlh' })).to.deep.equal({ mimeType: 'image/gif' });
|
||||
|
||||
// JPG
|
||||
expect(getMimeTypeFromBase64({ base64String: '/9j/' })).to.deep.equal({ mimeType: 'image/jpg' });
|
||||
|
||||
// PDF
|
||||
expect(getMimeTypeFromBase64({ base64String: 'JVBERi0' })).to.deep.equal({ mimeType: 'application/pdf' });
|
||||
});
|
||||
|
||||
it('when the base64 string has no data URI and no signature, it returns an undefined mimeType', () => {
|
||||
expect(getMimeTypeFromBase64({ base64String: 'JVBERi' })).to.deep.equal({ mimeType: undefined });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,32 +1,60 @@
|
|||
import { extension as getExtensionFromMime } from 'mime-types';
|
||||
import type { Ref } from 'vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
function getFileExtensionFromBase64({
|
||||
base64String,
|
||||
export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
|
||||
|
||||
const commonMimeTypesSignatures = {
|
||||
'JVBERi0': 'application/pdf',
|
||||
'R0lGODdh': 'image/gif',
|
||||
'R0lGODlh': 'image/gif',
|
||||
'iVBORw0KGgo': 'image/png',
|
||||
'/9j/': 'image/jpg',
|
||||
};
|
||||
|
||||
function getMimeTypeFromBase64({ base64String }: { base64String: string }) {
|
||||
const [,mimeTypeFromBase64] = base64String.match(/data:(.*?);base64/i) ?? [];
|
||||
|
||||
if (mimeTypeFromBase64) {
|
||||
return { mimeType: mimeTypeFromBase64 };
|
||||
}
|
||||
|
||||
const inferredMimeType = _.find(commonMimeTypesSignatures, (_mimeType, signature) => base64String.startsWith(signature));
|
||||
|
||||
if (inferredMimeType) {
|
||||
return { mimeType: inferredMimeType };
|
||||
}
|
||||
|
||||
return { mimeType: undefined };
|
||||
}
|
||||
|
||||
function getFileExtensionFromMimeType({
|
||||
mimeType,
|
||||
defaultExtension = 'txt',
|
||||
}: {
|
||||
base64String: string
|
||||
mimeType: string | undefined
|
||||
defaultExtension?: string
|
||||
}) {
|
||||
const hasMimeType = base64String.match(/data:(.*?);base64/i);
|
||||
|
||||
if (hasMimeType) {
|
||||
return getExtensionFromMime(hasMimeType[1]) || defaultExtension;
|
||||
if (mimeType) {
|
||||
return getExtensionFromMime(mimeType) ?? defaultExtension;
|
||||
}
|
||||
|
||||
return defaultExtension;
|
||||
}
|
||||
|
||||
export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
|
||||
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
|
||||
return {
|
||||
download() {
|
||||
const base64String = source.value;
|
||||
|
||||
if (base64String === '') {
|
||||
if (source.value === '') {
|
||||
throw new Error('Base64 string is empty');
|
||||
}
|
||||
|
||||
const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`;
|
||||
const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
|
||||
const base64String = mimeType
|
||||
? source.value
|
||||
: `data:text/plain;base64,${source.value}`;
|
||||
|
||||
const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = base64String;
|
||||
|
|
|
@ -100,13 +100,13 @@ const tools = computed<ToolCategory[]>(() => [
|
|||
<NIcon size="25" :component="Menu2" />
|
||||
</c-button>
|
||||
|
||||
<c-tooltip tooltip="Home" position="bottom">
|
||||
<c-tooltip :tooltip="$t('home.home')" position="bottom">
|
||||
<c-button to="/" circle variant="text" :aria-label="$t('home.home')">
|
||||
<NIcon size="25" :component="Home2" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<c-tooltip tooltip="UI Lib" position="bottom">
|
||||
<c-tooltip :tooltip="$t('home.uiLib')" 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>
|
||||
|
@ -120,7 +120,7 @@ const tools = computed<ToolCategory[]>(() => [
|
|||
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
||||
</div>
|
||||
|
||||
<c-tooltip position="bottom" tooltip="Support IT Tools development">
|
||||
<c-tooltip position="bottom" :tooltip="$t('home.support')">
|
||||
<c-button
|
||||
round
|
||||
href="https://www.buymeacoffee.com/cthmsst"
|
||||
|
|
|
@ -1,63 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { useHead } from '@vueuse/head';
|
||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||
|
||||
useHead({ title: 'About - IT Tools' });
|
||||
const { tracker } = useTracker();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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' })"
|
||||
>
|
||||
{{ $t('about.h1p4') }}
|
||||
</c-link>.
|
||||
</p>
|
||||
|
||||
<h2>{{ $t('about.h2') }}</h2>
|
||||
<p text-justify>
|
||||
{{ $t('about.h2p1') }}
|
||||
<c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank">
|
||||
package.json
|
||||
</c-link>
|
||||
{{ $t('about.h2p2') }}
|
||||
</p>
|
||||
|
||||
<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"
|
||||
>
|
||||
{{ $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"
|
||||
>
|
||||
{{ $t('about.h3p5') }}
|
||||
</c-link>
|
||||
{{ $t('about.h3p6') }}
|
||||
</p>
|
||||
</div>
|
||||
<c-markdown :markdown="$t('about.content')" mx-auto mt-50px max-w-600px />
|
||||
</template>
|
||||
|
|
6
src/shims.d.ts
vendored
6
src/shims.d.ts
vendored
|
@ -32,4 +32,10 @@ declare module 'unicode-emoji-json' {
|
|||
}>;
|
||||
|
||||
export default emoji;
|
||||
}
|
||||
|
||||
declare module 'pdf-signature-reader' {
|
||||
const verifySignature: (pdf: ArrayBuffer) => ({signatures: SignatureInfo[]});
|
||||
|
||||
export default verifySignature;
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
<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';
|
||||
|
@ -33,14 +31,12 @@ function downloadFile() {
|
|||
}
|
||||
}
|
||||
|
||||
const fileList = ref();
|
||||
const fileInput = ref() as Ref<File>;
|
||||
const { base64: fileBase64 } = useBase64(fileInput);
|
||||
const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' });
|
||||
|
||||
async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
|
||||
async function onUpload(file: File) {
|
||||
if (file) {
|
||||
fileList.value = [];
|
||||
fileInput.value = file;
|
||||
}
|
||||
}
|
||||
|
@ -65,18 +61,8 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
|
|||
</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 />
|
||||
<c-file-upload title="Drag and drop a file here, or click to select a file" @file-upload="onUpload" />
|
||||
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" my-2 />
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button @click="copyFileBase64()">
|
||||
|
|
|
@ -23,11 +23,11 @@ const input = ref('lorem ipsum dolor sit amet');
|
|||
const formats = computed(() => [
|
||||
{
|
||||
label: 'Lowercase:',
|
||||
value: noCase(input.value, baseConfig).toLocaleLowerCase(),
|
||||
value: input.value.toLocaleLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Uppercase:',
|
||||
value: noCase(input.value, baseConfig).toLocaleUpperCase(),
|
||||
value: input.value.toLocaleUpperCase(),
|
||||
},
|
||||
{
|
||||
label: 'Camelcase:',
|
||||
|
@ -75,7 +75,7 @@ const formats = computed(() => [
|
|||
},
|
||||
{
|
||||
label: 'Mockingcase:',
|
||||
value: noCase(input.value, baseConfig)
|
||||
value: input.value
|
||||
.split('')
|
||||
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
|
||||
.join(''),
|
||||
|
|
|
@ -26,8 +26,8 @@ const endAt = computed(() =>
|
|||
<template>
|
||||
<div>
|
||||
<div text-justify op-70>
|
||||
With a concrete example, if you wash 3 plates in 5 minutes and you have 500 plates to wash, it will take you 5
|
||||
hours and 10 minutes to wash them all.
|
||||
With a concrete example, if you wash 5 plates in 3 minutes and you have 500 plates to wash, it will take you 5
|
||||
hours to wash them all.
|
||||
</div>
|
||||
<n-divider />
|
||||
<div flex gap-2>
|
||||
|
|
|
@ -60,9 +60,11 @@ const ibanExamples = [
|
|||
<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 v-if="ibanInfo.length > 0" mt-5>
|
||||
<c-key-value-list :items="ibanInfo" data-test-id="iban-info" />
|
||||
</c-card>
|
||||
|
||||
<c-card title="Valid IBAN examples">
|
||||
<c-card title="Valid IBAN examples" mt-5>
|
||||
<div v-for="iban in ibanExamples" :key="iban">
|
||||
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 pdfSignatureChecker } from './pdf-signature-checker';
|
||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||
import { tool as textToBinary } from './text-to-binary';
|
||||
|
@ -78,7 +79,7 @@ import { tool as xmlFormatter } from './xml-formatter';
|
|||
export const toolsByCategory: ToolCategory[] = [
|
||||
{
|
||||
name: 'Crypto',
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
|
||||
},
|
||||
{
|
||||
name: 'Converter',
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import db from 'oui/oui.json';
|
||||
import db from 'oui-data';
|
||||
import { macAddressValidationRules } from '@/utils/macAddress';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6);
|
||||
|
||||
const macAddress = ref('20:37:06:12:34:56');
|
||||
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]);
|
||||
const details = computed<string | undefined>(() => (db as Record<string, string>)[getVendorValue(macAddress.value)]);
|
||||
|
||||
const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info copied to the clipboard' });
|
||||
</script>
|
||||
|
|
4
src/tools/mac-address-lookup/oui.d.ts
vendored
4
src/tools/mac-address-lookup/oui.d.ts
vendored
|
@ -1,4 +0,0 @@
|
|||
declare module 'oui/oui.json' {
|
||||
const db: Record<string, string>;
|
||||
export default db;
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
<script setup lang="ts">
|
||||
import type { SignatureInfo } from '../pdf-signature-checker.types';
|
||||
|
||||
const props = defineProps<{ signature: SignatureInfo }>();
|
||||
const { signature } = toRefs(props);
|
||||
|
||||
const tableHeaders = {
|
||||
validityPeriod: 'Validity period',
|
||||
issuedBy: 'Issued by',
|
||||
issuedTo: 'Issued to',
|
||||
pemCertificate: 'PEM certificate',
|
||||
};
|
||||
|
||||
const certs = computed(() => signature.value.meta.certs.map((certificate, index) => ({
|
||||
...certificate,
|
||||
validityPeriod: {
|
||||
notBefore: new Date(certificate.validityPeriod.notBefore).toLocaleString(),
|
||||
notAfter: new Date(certificate.validityPeriod.notAfter).toLocaleString(),
|
||||
},
|
||||
certificateName: `Certificate ${index + 1}`,
|
||||
})),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col gap-2>
|
||||
<c-table :data="certs" :headers="tableHeaders">
|
||||
<template #validityPeriod="{ value }">
|
||||
<c-key-value-list
|
||||
:items="[{
|
||||
label: 'Not before',
|
||||
value: value.notBefore,
|
||||
}, {
|
||||
label: 'Not after',
|
||||
value: value.notAfter,
|
||||
}]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #issuedBy="{ value }">
|
||||
<c-key-value-list
|
||||
:items="[{
|
||||
label: 'Common name',
|
||||
value: value.commonName,
|
||||
}, {
|
||||
label: 'Organization name',
|
||||
value: value.organizationName,
|
||||
}, {
|
||||
label: 'Country name',
|
||||
value: value.countryName,
|
||||
}, {
|
||||
label: 'Locality name',
|
||||
value: value.localityName,
|
||||
}, {
|
||||
label: 'Organizational unit name',
|
||||
value: value.organizationalUnitName,
|
||||
}, {
|
||||
label: 'State or province name',
|
||||
value: value.stateOrProvinceName,
|
||||
}]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #issuedTo="{ value }">
|
||||
<c-key-value-list
|
||||
:items="[{
|
||||
label: 'Common name',
|
||||
value: value.commonName,
|
||||
}, {
|
||||
label: 'Organization name',
|
||||
value: value.organizationName,
|
||||
}, {
|
||||
label: 'Country name',
|
||||
value: value.countryName,
|
||||
}, {
|
||||
label: 'Locality name',
|
||||
value: value.localityName,
|
||||
}, {
|
||||
label: 'Organizational unit name',
|
||||
value: value.organizationalUnitName,
|
||||
}, {
|
||||
label: 'State or province name',
|
||||
value: value.stateOrProvinceName,
|
||||
}]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #pemCertificate="{ value }">
|
||||
<c-modal-value :value="value" label="View PEM cert">
|
||||
<template #value>
|
||||
<div break-all text-xs>
|
||||
{{ value }}
|
||||
</div>
|
||||
</template>
|
||||
</c-modal-value>
|
||||
</template>
|
||||
</c-table>
|
||||
</div>
|
||||
</template>
|
12
src/tools/pdf-signature-checker/index.ts
Normal file
12
src/tools/pdf-signature-checker/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { defineTool } from '../tool';
|
||||
import FileCertIcon from '~icons/mdi/file-certificate-outline';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'PDF signature checker',
|
||||
path: '/pdf-signature-checker',
|
||||
description: 'Verify the signatures of a PDF file. A signed PDF file contains one or more signatures that may be used to determine whether the contents of the file have been altered since the file was signed.',
|
||||
keywords: ['pdf', 'signature', 'checker', 'verify', 'validate', 'sign'],
|
||||
component: () => import('./pdf-signature-checker.vue'),
|
||||
icon: FileCertIcon,
|
||||
createdAt: new Date('2023-12-09'),
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - Pdf signature checker', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/pdf-signature-checker');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('PDF signature checker - IT Tools');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
export interface SignatureInfo {
|
||||
verified: boolean
|
||||
authenticity: boolean
|
||||
integrity: boolean
|
||||
expired: boolean
|
||||
meta: {
|
||||
certs: {
|
||||
clientCertificate?: boolean
|
||||
issuedBy: {
|
||||
commonName: string
|
||||
organizationalUnitName?: string
|
||||
organizationName: string
|
||||
countryName?: string
|
||||
localityName?: string
|
||||
stateOrProvinceName?: string
|
||||
}
|
||||
issuedTo: {
|
||||
commonName: string
|
||||
serialNumber?: string
|
||||
organizationalUnitName?: string
|
||||
organizationName: string
|
||||
countryName?: string
|
||||
localityName?: string
|
||||
stateOrProvinceName?: string
|
||||
}
|
||||
validityPeriod: {
|
||||
notBefore: string
|
||||
notAfter: string
|
||||
}
|
||||
pemCertificate: string
|
||||
}[]
|
||||
signatureMeta: {
|
||||
reason: string
|
||||
contactInfo: string | null
|
||||
location: string
|
||||
name: string | null
|
||||
}
|
||||
}
|
||||
}
|
59
src/tools/pdf-signature-checker/pdf-signature-checker.vue
Normal file
59
src/tools/pdf-signature-checker/pdf-signature-checker.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import verifyPDF from 'pdf-signature-reader';
|
||||
import type { SignatureInfo } from './pdf-signature-checker.types';
|
||||
import { formatBytes } from '@/utils/convert';
|
||||
|
||||
const signatures = ref<SignatureInfo[]>([]);
|
||||
const status = ref<'idle' | 'parsed' | 'error' | 'loading'>('idle');
|
||||
const file = ref<File | null>(null);
|
||||
|
||||
async function onVerifyClicked(uploadedFile: File) {
|
||||
file.value = uploadedFile;
|
||||
const fileBuffer = await uploadedFile.arrayBuffer();
|
||||
|
||||
status.value = 'loading';
|
||||
try {
|
||||
const { signatures: parsedSignatures } = verifyPDF(fileBuffer);
|
||||
signatures.value = parsedSignatures;
|
||||
status.value = 'parsed';
|
||||
}
|
||||
catch (e) {
|
||||
signatures.value = [];
|
||||
status.value = 'error';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<div mx-auto max-w-600px>
|
||||
<c-file-upload title="Drag and drop a PDF file here, or click to select a file" accept=".pdf" @file-upload="onVerifyClicked" />
|
||||
|
||||
<c-card v-if="file" mt-4 flex gap-2>
|
||||
<div font-bold>
|
||||
{{ file.name }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ formatBytes(file.size) }}
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<div v-if="status === 'error'">
|
||||
<c-alert mt-4>
|
||||
No signatures found in the provided file.
|
||||
</c-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="status === 'parsed' && signatures.length" style="flex: 0 0 100%" mt-5 flex flex-col gap-4>
|
||||
<div v-for="(signature, index) of signatures" :key="index">
|
||||
<div mb-2 font-bold>
|
||||
Signature {{ index + 1 }} certificates :
|
||||
</div>
|
||||
|
||||
<pdf-signature-details :signature="signature" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -6,7 +6,7 @@ export const tool = defineTool({
|
|||
name: translate('tools.token-generator.title'),
|
||||
path: '/token-generator',
|
||||
description: translate('tools.token-generator.description'),
|
||||
keywords: ['token', 'random', 'string', 'alphanumeric', 'symbols', 'number', 'letters', 'lowercase', 'uppercase'],
|
||||
keywords: ['token', 'random', 'string', 'alphanumeric', 'symbols', 'number', 'letters', 'lowercase', 'uppercase', 'password'],
|
||||
component: () => import('./token-generator.tool.vue'),
|
||||
icon: ArrowsShuffle,
|
||||
});
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
tools:
|
||||
token-generator:
|
||||
title: Générateur de token
|
||||
description: Génère une chaîne aléatoire avec les caractères que vous voulez, lettres majuscules ou minuscules, chiffres et/ou symboles.
|
||||
|
||||
description: >-
|
||||
Génère une chaîne aléatoire avec les caractères que vous voulez, lettres
|
||||
majuscules ou minuscules, chiffres et/ou symboles.
|
||||
uppercase: Majuscules (ABC...)
|
||||
lowercase: Minuscules (abc...)
|
||||
numbers: Chiffres (123...)
|
||||
symbols: Symboles (!-;...)
|
||||
button:
|
||||
copy: Copier
|
||||
refresh: Rafraichir
|
||||
copied: Le token a été copié
|
||||
length: Longueur
|
||||
tokenPlaceholder: Le token...
|
||||
|
|
|
@ -2,11 +2,11 @@ import { Fingerprint } from '@vicons/tabler';
|
|||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'UUIDs v4 generator',
|
||||
name: 'UUIDs 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!).',
|
||||
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
|
||||
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique', 'v1', 'v3', 'v5', 'nil'],
|
||||
component: () => import('./uuid-generator.vue'),
|
||||
icon: Fingerprint,
|
||||
});
|
||||
|
|
|
@ -1,22 +1,94 @@
|
|||
<script setup lang="ts">
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { v1 as generateUuidV1, v3 as generateUuidV3, v4 as generateUuidV4, v5 as generateUuidV5, NIL as nilUuid } from 'uuid';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
|
||||
const versions = ['NIL', 'v1', 'v3', 'v4', 'v5'] as const;
|
||||
|
||||
const version = useStorage<typeof versions[number]>('uuid-generator:version', 'v4');
|
||||
const count = useStorage('uuid-generator:quantity', 1);
|
||||
const v35Args = ref({ namespace: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', name: '' });
|
||||
|
||||
const [uuids, refreshUUIDs] = computedRefreshable(() =>
|
||||
Array.from({ length: count.value }, () => generateUUID()).join('\n'),
|
||||
);
|
||||
const validUuidRules = [
|
||||
{
|
||||
message: 'Invalid UUID',
|
||||
validator: (value: string) => {
|
||||
if (value === nilUuid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(value.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const generators = {
|
||||
NIL: () => nilUuid,
|
||||
v1: (index: number) => generateUuidV1({
|
||||
clockseq: index,
|
||||
msecs: Date.now(),
|
||||
nsecs: Math.floor(Math.random() * 10000),
|
||||
node: Array.from({ length: 6 }, () => Math.floor(Math.random() * 256)),
|
||||
}),
|
||||
v3: () => generateUuidV3(v35Args.value.name, v35Args.value.namespace),
|
||||
v4: () => generateUuidV4(),
|
||||
v5: () => generateUuidV5(v35Args.value.name, v35Args.value.namespace),
|
||||
};
|
||||
|
||||
const [uuids, refreshUUIDs] = computedRefreshable(() => withDefaultOnError(() =>
|
||||
Array.from({ length: count.value }, (_ignored, index) => {
|
||||
const generator = generators[version.value] ?? generators.NIL;
|
||||
return generator(index);
|
||||
}).join('\n'), ''));
|
||||
|
||||
const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div flex items-center justify-center gap-3>
|
||||
Quantity :
|
||||
<n-input-number v-model:value="count" :min="1" :max="50" placeholder="UUID quantity" />
|
||||
<c-buttons-select v-model:value="version" :options="versions" label="UUID version" label-width="100px" mb-2 />
|
||||
|
||||
<div mb-2 flex items-center>
|
||||
<span w-100px>Quantity </span>
|
||||
<n-input-number v-model:value="count" flex-1 :min="1" :max="50" placeholder="UUID quantity" />
|
||||
</div>
|
||||
|
||||
<div v-if="version === 'v3' || version === 'v5'">
|
||||
<div>
|
||||
<c-buttons-select
|
||||
v-model:value="v35Args.namespace"
|
||||
:options="{
|
||||
DNS: '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
|
||||
URL: '6ba7b811-9dad-11d1-80b4-00c04fd430c8',
|
||||
OID: '6ba7b812-9dad-11d1-80b4-00c04fd430c8',
|
||||
X500: '6ba7b814-9dad-11d1-80b4-00c04fd430c8',
|
||||
}"
|
||||
label="Namespace"
|
||||
label-width="100px"
|
||||
mb-2
|
||||
/>
|
||||
</div>
|
||||
<div flex-1>
|
||||
<c-input-text
|
||||
v-model:value="v35Args.namespace"
|
||||
placeholder="Namespace"
|
||||
label-width="100px"
|
||||
label-position="left"
|
||||
label=" "
|
||||
:validation-rules="validUuidRules"
|
||||
mb-2
|
||||
/>
|
||||
</div>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="v35Args.name"
|
||||
placeholder="Name"
|
||||
label="Name"
|
||||
label-width="100px"
|
||||
label-position="left"
|
||||
mb-2
|
||||
/>
|
||||
</div>
|
||||
|
||||
<c-input-text
|
||||
|
|
|
@ -4,11 +4,18 @@ const optionsA = [
|
|||
{ label: 'Option B', value: 'b', tooltip: 'This is a tooltip' },
|
||||
{ label: 'Option C', value: 'c' },
|
||||
];
|
||||
|
||||
const optionB = {
|
||||
'Option A': 'a',
|
||||
'Option B': 'b',
|
||||
'Option C': 'c',
|
||||
};
|
||||
|
||||
const valueA = ref('a');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " />
|
||||
<c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " label-position="left" mt-2 />
|
||||
<c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " label-position="left" mt-2 />
|
||||
<c-buttons-select v-model:value="valueA" :options="optionB" label="Options object: " />
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script setup lang="ts" generic="T extends unknown">
|
||||
import _ from 'lodash';
|
||||
import type { CLabelProps } from '../c-label/c-label.types';
|
||||
import type { CButtonSelectOption } from './c-buttons-select.types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
options?: CButtonSelectOption<T>[] | string[]
|
||||
options?: CButtonSelectOption<T>[] | string[] | Record<string, T>
|
||||
value?: T
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
} & CLabelProps >(),
|
||||
|
@ -20,14 +21,18 @@ const emits = defineEmits(['update:value']);
|
|||
|
||||
const { options: rawOptions, size } = toRefs(props);
|
||||
|
||||
const options = computed(() => {
|
||||
return rawOptions.value.map((option: string | CButtonSelectOption<T>) => {
|
||||
if (typeof option === 'string') {
|
||||
return { label: option, value: option };
|
||||
}
|
||||
const options = computed<CButtonSelectOption<T>[]>(() => {
|
||||
if (_.isArray(rawOptions.value)) {
|
||||
return rawOptions.value.map((option: string | CButtonSelectOption<T>) => {
|
||||
if (typeof option === 'string') {
|
||||
return { label: option, value: option };
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
return option;
|
||||
}) as CButtonSelectOption<T>[];
|
||||
}
|
||||
|
||||
return _.map(rawOptions.value, (value, label) => ({ label, value })) as CButtonSelectOption<T>[];
|
||||
});
|
||||
|
||||
const value = useVModel(props, 'value', emits);
|
||||
|
|
5
src/ui/c-collapse/c-collapse.demo.vue
Normal file
5
src/ui/c-collapse/c-collapse.demo.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<c-collapse title="Collapse title">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquet iaculis class cubilia metus per nullam gravida ad venenatis. Id elementum elementum enim orci elementum justo facilisi habitant consequat. Justo eget ligula purus laoreet penatibus eros quisque fusce sociis. In eget amet sagittis dignissim eleifend proin lacinia potenti tellus. Interdum vulputate condimentum molestie pulvinar praesent accumsan quisque venenatis imperdiet.
|
||||
</c-collapse>
|
||||
</template>
|
25
src/ui/c-collapse/c-collapse.vue
Normal file
25
src/ui/c-collapse/c-collapse.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{ title?: string }>(), { title: '' });
|
||||
const { title } = toRefs(props);
|
||||
|
||||
const isCollapsed = ref(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div flex cursor-pointer items-center @click="isCollapsed = !isCollapsed">
|
||||
<icon-mdi-triangle-down :class="{ 'transform-rotate--90': isCollapsed }" op-50 transition />
|
||||
|
||||
<slot name="title">
|
||||
<span class="ml-2" font-bold>{{ title }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="!isCollapsed"
|
||||
mt-2
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
3
src/ui/c-file-upload/c-file-upload.demo.vue
Normal file
3
src/ui/c-file-upload/c-file-upload.demo.vue
Normal file
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<c-file-upload />
|
||||
</template>
|
95
src/ui/c-file-upload/c-file-upload.vue
Normal file
95
src/ui/c-file-upload/c-file-upload.vue
Normal file
|
@ -0,0 +1,95 @@
|
|||
<script lang="ts" setup>
|
||||
import _ from 'lodash';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
multiple?: boolean
|
||||
accept?: string
|
||||
title?: string
|
||||
}>(), {
|
||||
multiple: false,
|
||||
accept: undefined,
|
||||
title: 'Drag and drop files here, or click to select files',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'filesUpload', files: File[]): void
|
||||
(event: 'fileUpload', file: File): void
|
||||
}>();
|
||||
|
||||
const { multiple } = toRefs(props);
|
||||
|
||||
const isOverDropZone = ref(false);
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
function triggerFileInput() {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
|
||||
function handleFileInput(event: Event) {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
|
||||
handleUpload(files);
|
||||
}
|
||||
|
||||
function handleDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const files = event.dataTransfer?.files;
|
||||
|
||||
handleUpload(files);
|
||||
}
|
||||
|
||||
function handleUpload(files: FileList | null | undefined) {
|
||||
if (_.isNil(files) || _.isEmpty(files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (multiple.value) {
|
||||
emit('filesUpload', Array.from(files));
|
||||
return;
|
||||
}
|
||||
|
||||
emit('fileUpload', files[0]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col cursor-pointer items-center justify-center border-2px border-gray-300 border-opacity-50 rounded-lg border-dashed p-8 transition-colors"
|
||||
:class="{
|
||||
'border-primary border-opacity-100': isOverDropZone,
|
||||
}"
|
||||
@click="triggerFileInput"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent
|
||||
@dragenter="isOverDropZone = true"
|
||||
@dragleave="isOverDropZone = false"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
@change="handleFileInput"
|
||||
>
|
||||
<slot>
|
||||
<span op-70>
|
||||
{{ title }}
|
||||
</span>
|
||||
|
||||
<!-- separator -->
|
||||
<div my-4 w-full flex items-center justify-center op-70>
|
||||
<div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
|
||||
<div class="mx-2 text-gray-400">
|
||||
or
|
||||
</div>
|
||||
<div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
|
||||
</div>
|
||||
|
||||
<c-button>
|
||||
Browse files
|
||||
</c-button>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
|
@ -9,13 +9,13 @@ const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.v
|
|||
</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">
|
||||
<div flex flex-col gap-2>
|
||||
<div v-for="item in formattedItems" :key="item.label" class="c-key-value-list__item">
|
||||
<div class="c-key-value-list__key" text-13px lh-normal>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
|
||||
<c-key-value-list-item :item="item" class="c-key-value-list__value" />
|
||||
<c-key-value-list-item :item="item" class="c-key-value-list__value" font-bold lh-normal />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
18
src/ui/c-markdown/c-markdown.demo.vue
Normal file
18
src/ui/c-markdown/c-markdown.demo.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
const md = `
|
||||
# IT Tools
|
||||
|
||||
## About
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nisl quis
|
||||
mollis blandit, nunc nisl aliquam nunc, vitae aliquam nisl nunc vitae nisl.
|
||||
|
||||
- Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
- Sed euismod, nisl quis mollis blandit, nunc nisl aliquam nunc, vitae aliquam nisl nunc vitae nisl.
|
||||
|
||||
[it-tools](https://it-tools.tech)
|
||||
`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-markdown :markdown="md" />
|
||||
</template>
|
21
src/ui/c-markdown/c-markdown.vue
Normal file
21
src/ui/c-markdown/c-markdown.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { marked } from 'marked';
|
||||
import DomPurify from 'dompurify';
|
||||
|
||||
const props = withDefaults(defineProps<{ markdown?: string }>(), { markdown: '' });
|
||||
const { markdown } = toRefs(props);
|
||||
|
||||
marked.use({
|
||||
renderer: {
|
||||
link(href, title, text) {
|
||||
return `<a class="text-primary transition decoration-none hover:underline" href="${href}" target="_blank" rel="noopener">${text}</a>`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const html = computed(() => DomPurify.sanitize(marked(markdown.value), { ADD_ATTR: ['target'] }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-html="html" />
|
||||
</template>
|
21
src/ui/c-modal-value/c-modal-value.demo.vue
Normal file
21
src/ui/c-modal-value/c-modal-value.demo.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div flex gap-2>
|
||||
<c-modal-value value="lorem ipsum" label="test" />
|
||||
<c-modal-value>
|
||||
<template #label="{ toggleModal }">
|
||||
<c-button class="text-left" size="small" @click="toggleModal">
|
||||
Bonjour
|
||||
</c-button>
|
||||
</template>
|
||||
|
||||
<template #value>
|
||||
<pre>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
||||
Molestias, quisquam vitae saepe dolores quas debitis ab r
|
||||
ecusandae suscipit ex dignissimos minus quam repellat sunt.
|
||||
Molestiae culpa blanditiis totam sapiente dignissimos.
|
||||
</pre>
|
||||
</template>
|
||||
</c-modal-value>
|
||||
</div>
|
||||
</template>
|
31
src/ui/c-modal-value/c-modal-value.vue
Normal file
31
src/ui/c-modal-value/c-modal-value.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts" setup>
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(defineProps<{ value: string; label?: string; copyable?: boolean }>(), { label: undefined, copyable: true });
|
||||
const { value, label } = toRefs(props);
|
||||
|
||||
const { copy, isJustCopied } = useCopy({ source: value });
|
||||
|
||||
const isModalOpen = ref(false);
|
||||
const toggleModal = useToggle(isModalOpen);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot name="label" :value="value" :toggle-modal="toggleModal" :is-modal-open="isModalOpen">
|
||||
<c-button class="text-left" @click="isModalOpen = true">
|
||||
{{ label }}
|
||||
</c-button>
|
||||
</slot>
|
||||
|
||||
<c-modal v-model:open="isModalOpen">
|
||||
<slot name="value" :value="value" :toggle-modal="toggleModal" :is-modal-open="isModalOpen">
|
||||
{{ value }}
|
||||
</slot>
|
||||
|
||||
<div mt-4 flex justify-center>
|
||||
<c-button class="w-full" @click="copy">
|
||||
{{ isJustCopied ? 'Copied!' : 'Copy' }}
|
||||
</c-button>
|
||||
</div>
|
||||
</c-modal>
|
||||
</template>
|
|
@ -39,7 +39,7 @@ const headers = computed(() => {
|
|||
<template>
|
||||
<div class="relative overflow-x-auto rounded">
|
||||
<table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description">
|
||||
<thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400">
|
||||
<thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5">
|
||||
<tr>
|
||||
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
|
||||
{{ header.label }}
|
||||
|
@ -54,7 +54,7 @@ const headers = computed(() => {
|
|||
}"
|
||||
>
|
||||
<td v-for="header in headers" :key="header.key" class="px-6 py-4">
|
||||
<slot :name="header" :row="row" :headers="headers" :value="row[header.key]">
|
||||
<slot :name="header.key" :row="row" :headers="headers" :value="row[header.key]">
|
||||
{{ row[header.key] }}
|
||||
</slot>
|
||||
</td>
|
||||
|
|
|
@ -15,11 +15,13 @@ export default defineConfig({
|
|||
theme: {
|
||||
colors: {
|
||||
primary: '#1ea54c',
|
||||
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868',
|
||||
'divider': 'h-1px bg-current op-10',
|
||||
'bg-surface': 'bg-#ffffff dark:bg-#232323',
|
||||
'bg-background': 'bg-#f1f5f9 dark:bg-#1c1c1c',
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue