mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-05 22:07:10 -04:00
Merge pull request #5 from TheTechNetwork/staging
Update from source repo
This commit is contained in:
commit
5fab29688e
38 changed files with 1017 additions and 373 deletions
2
.github/workflows/docker-nightly-release.yml
vendored
2
.github/workflows/docker-nightly-release.yml
vendored
|
@ -80,7 +80,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
corentinth/it-tools:nightly
|
corentinth/it-tools:nightly
|
||||||
|
|
2
.github/workflows/releases.yml
vendored
2
.github/workflows/releases.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
corentinth/it-tools:latest
|
corentinth/it-tools:latest
|
||||||
|
|
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']
|
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
|
||||||
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||||
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
||||||
|
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']
|
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']
|
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
||||||
Chronometer: typeof import('./src/tools/chronometer/chronometer.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: 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']
|
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
|
||||||
CLink: typeof import('./src/ui/c-link/c-link.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']
|
'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: typeof import('./src/ui/c-modal/c-modal.vue')['default']
|
||||||
'CModal.demo': typeof import('./src/ui/c-modal/c-modal.demo.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']
|
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
||||||
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
|
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
|
||||||
ColoredCard: typeof import('./src/components/ColoredCard.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:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||||
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['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']
|
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||||
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
||||||
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['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']
|
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||||
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||||
|
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
|
||||||
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||||
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||||
|
@ -165,6 +175,8 @@ declare module '@vue/runtime-core' {
|
||||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||||
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||||
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.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']
|
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']
|
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']
|
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
||||||
|
|
|
@ -26,21 +26,23 @@ home:
|
||||||
lightMode: 'Light mode'
|
lightMode: 'Light mode'
|
||||||
mode: 'Toggle dark/light mode'
|
mode: 'Toggle dark/light mode'
|
||||||
about:
|
about:
|
||||||
h1: 'About IT-Tools'
|
content: >
|
||||||
h1p1: 'This wonderful website, made with ❤ by'
|
# About IT-Tools
|
||||||
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'
|
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!
|
||||||
h1p4: 'sponsoring me'
|
|
||||||
h2: Technologies
|
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).
|
||||||
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.'
|
## Technologies
|
||||||
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'
|
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.
|
||||||
h3p2: 'issues section'
|
|
||||||
h3p3: 'in the GitHub repository.'
|
## Found a bug? A tool is missing?
|
||||||
h3p4: "And if you found a bug, or something doesn''t work as expected, please file a bug report in the"
|
|
||||||
h3p5: 'issues section'
|
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.
|
||||||
h3p6: '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:
|
404:
|
||||||
notFound: '404 Not Found'
|
notFound: '404 Not Found'
|
||||||
sorry: 'Sorry, this page does not seem to exist'
|
sorry: 'Sorry, this page does not seem to exist'
|
||||||
|
|
|
@ -25,6 +25,22 @@ home:
|
||||||
darkMode: 'Mode sombre'
|
darkMode: 'Mode sombre'
|
||||||
lightMode: 'Mode clair'
|
lightMode: 'Mode clair'
|
||||||
mode: 'Basculer le mode sombre/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:
|
404:
|
||||||
notFound: '404 Not Found'
|
notFound: '404 Not Found'
|
||||||
sorry: "Désolé, cette page n'existe pas"
|
sorry: "Désolé, cette page n'existe pas"
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
"cronstrue": "^2.26.0",
|
"cronstrue": "^2.26.0",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"dompurify": "^3.0.6",
|
||||||
"emojilib": "^3.0.10",
|
"emojilib": "^3.0.10",
|
||||||
"figue": "^1.2.0",
|
"figue": "^1.2.0",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"libphonenumber-js": "^1.10.28",
|
"libphonenumber-js": "^1.10.28",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"marked": "^10.0.0",
|
||||||
"mathjs": "^11.9.1",
|
"mathjs": "^11.9.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"monaco-editor": "^0.43.0",
|
"monaco-editor": "^0.43.0",
|
||||||
|
@ -72,6 +74,7 @@
|
||||||
"netmask": "^2.0.2",
|
"netmask": "^2.0.2",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"oui": "^12.0.52",
|
"oui": "^12.0.52",
|
||||||
|
"pdf-signature-reader": "^1.4.2",
|
||||||
"pinia": "^2.0.34",
|
"pinia": "^2.0.34",
|
||||||
"plausible-tracker": "^0.3.8",
|
"plausible-tracker": "^0.3.8",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
|
@ -97,6 +100,7 @@
|
||||||
"@tsconfig/node18": "^18.2.0",
|
"@tsconfig/node18": "^18.2.0",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/jsdom": "^21.0.0",
|
"@types/jsdom": "^21.0.0",
|
||||||
"@types/lodash": "^4.14.192",
|
"@types/lodash": "^4.14.192",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
|
@ -106,7 +110,7 @@
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"@unocss/eslint-config": "^0.55.0",
|
"@unocss/eslint-config": "^0.57.0",
|
||||||
"@vitejs/plugin-vue": "^4.3.2",
|
"@vitejs/plugin-vue": "^4.3.2",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||||
"@vue/compiler-sfc": "^3.2.47",
|
"@vue/compiler-sfc": "^3.2.47",
|
||||||
|
@ -120,7 +124,7 @@
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "~5.2.0",
|
"typescript": "~5.2.0",
|
||||||
"unocss": "^0.55.0",
|
"unocss": "^0.57.0",
|
||||||
"unocss-preset-scrollbar": "^0.2.1",
|
"unocss-preset-scrollbar": "^0.2.1",
|
||||||
"unplugin-icons": "^0.17.0",
|
"unplugin-icons": "^0.17.0",
|
||||||
"unplugin-vue-components": "^0.25.0",
|
"unplugin-vue-components": "^0.25.0",
|
||||||
|
|
547
pnpm-lock.yaml
generated
547
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
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 { extension as getExtensionFromMime } from 'mime-types';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
function getFileExtensionFromBase64({
|
export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
|
||||||
base64String,
|
|
||||||
|
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',
|
defaultExtension = 'txt',
|
||||||
}: {
|
}: {
|
||||||
base64String: string
|
mimeType: string | undefined
|
||||||
defaultExtension?: string
|
defaultExtension?: string
|
||||||
}) {
|
}) {
|
||||||
const hasMimeType = base64String.match(/data:(.*?);base64/i);
|
if (mimeType) {
|
||||||
|
return getExtensionFromMime(mimeType) ?? defaultExtension;
|
||||||
if (hasMimeType) {
|
|
||||||
return getExtensionFromMime(hasMimeType[1]) || defaultExtension;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultExtension;
|
return defaultExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
|
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
|
||||||
return {
|
return {
|
||||||
download() {
|
download() {
|
||||||
const base64String = source.value;
|
if (source.value === '') {
|
||||||
|
|
||||||
if (base64String === '') {
|
|
||||||
throw new Error('Base64 string is empty');
|
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');
|
const a = document.createElement('a');
|
||||||
a.href = base64String;
|
a.href = base64String;
|
||||||
|
|
|
@ -1,63 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
|
||||||
|
|
||||||
useHead({ title: 'About - IT Tools' });
|
useHead({ title: 'About - IT Tools' });
|
||||||
const { tracker } = useTracker();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div mx-auto mt-50px max-w-600px>
|
<c-markdown :markdown="$t('about.content')" 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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
6
src/shims.d.ts
vendored
6
src/shims.d.ts
vendored
|
@ -33,3 +33,9 @@ declare module 'unicode-emoji-json' {
|
||||||
|
|
||||||
export default emoji;
|
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">
|
<script setup lang="ts">
|
||||||
import { Upload } from '@vicons/tabler';
|
|
||||||
import { useBase64 } from '@vueuse/core';
|
import { useBase64 } from '@vueuse/core';
|
||||||
import type { UploadFileInfo } from 'naive-ui';
|
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||||
|
@ -33,14 +31,12 @@ function downloadFile() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileList = ref();
|
|
||||||
const fileInput = ref() as Ref<File>;
|
const fileInput = ref() as Ref<File>;
|
||||||
const { base64: fileBase64 } = useBase64(fileInput);
|
const { base64: fileBase64 } = useBase64(fileInput);
|
||||||
const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' });
|
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) {
|
if (file) {
|
||||||
fileList.value = [];
|
|
||||||
fileInput.value = file;
|
fileInput.value = file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,18 +61,8 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
|
||||||
</c-card>
|
</c-card>
|
||||||
|
|
||||||
<c-card title="File to base64">
|
<c-card title="File to base64">
|
||||||
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
|
<c-file-upload title="Drag and drop a file here, or click to select a file" @file-upload="onUpload" />
|
||||||
<n-upload-dragger>
|
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" my-2 />
|
||||||
<div mb-2>
|
|
||||||
<n-icon size="35" :depth="3" :component="Upload" />
|
|
||||||
</div>
|
|
||||||
<div op-60>
|
|
||||||
Click or drag a file to this area to upload
|
|
||||||
</div>
|
|
||||||
</n-upload-dragger>
|
|
||||||
</n-upload>
|
|
||||||
|
|
||||||
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 />
|
|
||||||
|
|
||||||
<div flex justify-center>
|
<div flex justify-center>
|
||||||
<c-button @click="copyFileBase64()">
|
<c-button @click="copyFileBase64()">
|
||||||
|
|
|
@ -23,11 +23,11 @@ const input = ref('lorem ipsum dolor sit amet');
|
||||||
const formats = computed(() => [
|
const formats = computed(() => [
|
||||||
{
|
{
|
||||||
label: 'Lowercase:',
|
label: 'Lowercase:',
|
||||||
value: noCase(input.value, baseConfig).toLocaleLowerCase(),
|
value: input.value.toLocaleLowerCase(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Uppercase:',
|
label: 'Uppercase:',
|
||||||
value: noCase(input.value, baseConfig).toLocaleUpperCase(),
|
value: input.value.toLocaleUpperCase(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Camelcase:',
|
label: 'Camelcase:',
|
||||||
|
@ -75,7 +75,7 @@ const formats = computed(() => [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Mockingcase:',
|
label: 'Mockingcase:',
|
||||||
value: noCase(input.value, baseConfig)
|
value: input.value
|
||||||
.split('')
|
.split('')
|
||||||
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
|
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
|
||||||
.join(''),
|
.join(''),
|
||||||
|
|
|
@ -26,8 +26,8 @@ const endAt = computed(() =>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div text-justify op-70>
|
<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
|
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 and 10 minutes to wash them all.
|
hours to wash them all.
|
||||||
</div>
|
</div>
|
||||||
<n-divider />
|
<n-divider />
|
||||||
<div flex gap-2>
|
<div flex gap-2>
|
||||||
|
|
|
@ -60,9 +60,11 @@ const ibanExamples = [
|
||||||
<div>
|
<div>
|
||||||
<c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" />
|
<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">
|
<div v-for="iban in ibanExamples" :key="iban">
|
||||||
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||||
|
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
||||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||||
import { tool as textToBinary } from './text-to-binary';
|
import { tool as textToBinary } from './text-to-binary';
|
||||||
|
@ -78,7 +79,7 @@ import { tool as xmlFormatter } from './xml-formatter';
|
||||||
export const toolsByCategory: ToolCategory[] = [
|
export const toolsByCategory: ToolCategory[] = [
|
||||||
{
|
{
|
||||||
name: 'Crypto',
|
name: 'Crypto',
|
||||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
|
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Converter',
|
name: 'Converter',
|
||||||
|
|
|
@ -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'),
|
name: translate('tools.token-generator.title'),
|
||||||
path: '/token-generator',
|
path: '/token-generator',
|
||||||
description: translate('tools.token-generator.description'),
|
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'),
|
component: () => import('./token-generator.tool.vue'),
|
||||||
icon: ArrowsShuffle,
|
icon: ArrowsShuffle,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
tools:
|
tools:
|
||||||
token-generator:
|
token-generator:
|
||||||
title: Générateur de token
|
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...)
|
uppercase: Majuscules (ABC...)
|
||||||
lowercase: Minuscules (abc...)
|
lowercase: Minuscules (abc...)
|
||||||
numbers: Chiffres (123...)
|
numbers: Chiffres (123...)
|
||||||
symbols: Symboles (!-;...)
|
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';
|
import { defineTool } from '../tool';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'UUIDs v4 generator',
|
name: 'UUIDs generator',
|
||||||
path: '/uuid-generator',
|
path: '/uuid-generator',
|
||||||
description:
|
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'],
|
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique', 'v1', 'v3', 'v5', 'nil'],
|
||||||
component: () => import('./uuid-generator.vue'),
|
component: () => import('./uuid-generator.vue'),
|
||||||
icon: Fingerprint,
|
icon: Fingerprint,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,22 +1,94 @@
|
||||||
<script setup lang="ts">
|
<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 { useCopy } from '@/composable/copy';
|
||||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
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 count = useStorage('uuid-generator:quantity', 1);
|
||||||
|
const v35Args = ref({ namespace: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', name: '' });
|
||||||
|
|
||||||
const [uuids, refreshUUIDs] = computedRefreshable(() =>
|
const validUuidRules = [
|
||||||
Array.from({ length: count.value }, () => generateUUID()).join('\n'),
|
{
|
||||||
);
|
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' });
|
const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div flex items-center justify-center gap-3>
|
<c-buttons-select v-model:value="version" :options="versions" label="UUID version" label-width="100px" mb-2 />
|
||||||
Quantity :
|
|
||||||
<n-input-number v-model:value="count" :min="1" :max="50" placeholder="UUID quantity" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<c-input-text
|
<c-input-text
|
||||||
|
|
|
@ -4,11 +4,18 @@ const optionsA = [
|
||||||
{ label: 'Option B', value: 'b', tooltip: 'This is a tooltip' },
|
{ label: 'Option B', value: 'b', tooltip: 'This is a tooltip' },
|
||||||
{ label: 'Option C', value: 'c' },
|
{ label: 'Option C', value: 'c' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const optionB = {
|
||||||
|
'Option A': 'a',
|
||||||
|
'Option B': 'b',
|
||||||
|
'Option C': 'c',
|
||||||
|
};
|
||||||
|
|
||||||
const valueA = ref('a');
|
const valueA = ref('a');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " />
|
<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="optionsA" label="Label: " label-position="left" mt-2 />
|
<c-buttons-select v-model:value="valueA" :options="optionB" label="Options object: " />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts" generic="T extends unknown">
|
<script setup lang="ts" generic="T extends unknown">
|
||||||
|
import _ from 'lodash';
|
||||||
import type { CLabelProps } from '../c-label/c-label.types';
|
import type { CLabelProps } from '../c-label/c-label.types';
|
||||||
import type { CButtonSelectOption } from './c-buttons-select.types';
|
import type { CButtonSelectOption } from './c-buttons-select.types';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
options?: CButtonSelectOption<T>[] | string[]
|
options?: CButtonSelectOption<T>[] | string[] | Record<string, T>
|
||||||
value?: T
|
value?: T
|
||||||
size?: 'small' | 'medium' | 'large'
|
size?: 'small' | 'medium' | 'large'
|
||||||
} & CLabelProps >(),
|
} & CLabelProps >(),
|
||||||
|
@ -20,14 +21,18 @@ const emits = defineEmits(['update:value']);
|
||||||
|
|
||||||
const { options: rawOptions, size } = toRefs(props);
|
const { options: rawOptions, size } = toRefs(props);
|
||||||
|
|
||||||
const options = computed(() => {
|
const options = computed<CButtonSelectOption<T>[]>(() => {
|
||||||
|
if (_.isArray(rawOptions.value)) {
|
||||||
return rawOptions.value.map((option: string | CButtonSelectOption<T>) => {
|
return rawOptions.value.map((option: string | CButtonSelectOption<T>) => {
|
||||||
if (typeof option === 'string') {
|
if (typeof option === 'string') {
|
||||||
return { label: option, value: option };
|
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);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div my-5>
|
<div flex flex-col gap-2>
|
||||||
<div v-for="item in formattedItems" :key="item.label" flex gap-2 py-1 class="c-key-value-list__item">
|
<div v-for="item in formattedItems" :key="item.label" class="c-key-value-list__item">
|
||||||
<div flex-basis-180px text-right font-bold class="c-key-value-list__key">
|
<div class="c-key-value-list__key" text-13px lh-normal>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<div class="relative overflow-x-auto rounded">
|
<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">
|
<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>
|
<tr>
|
||||||
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
|
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
|
||||||
{{ header.label }}
|
{{ header.label }}
|
||||||
|
@ -54,7 +54,7 @@ const headers = computed(() => {
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<td v-for="header in headers" :key="header.key" class="px-6 py-4">
|
<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] }}
|
{{ row[header.key] }}
|
||||||
</slot>
|
</slot>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -15,11 +15,13 @@ export default defineConfig({
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#1ea54c',
|
primary: '#1ea54c',
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868',
|
'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868',
|
||||||
'divider': 'h-1px bg-current op-10',
|
'divider': 'h-1px bg-current op-10',
|
||||||
'bg-surface': 'bg-#ffffff dark:bg-#232323',
|
'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