Merge pull request #5 from TheTechNetwork/staging

Update from source repo
This commit is contained in:
elibrody2 2023-11-14 18:50:15 -05:00 committed by GitHub
commit 5fab29688e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1017 additions and 373 deletions

View file

@ -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

View file

@ -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

12
components.d.ts vendored
View file

@ -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']

View file

@ -26,21 +26,23 @@ 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'

View file

@ -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"

View file

@ -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,6 +66,7 @@
"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",
@ -72,6 +74,7 @@
"netmask": "^2.0.2",
"node-forge": "^1.3.1",
"oui": "^12.0.52",
"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",

547
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View 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 });
});
});
});

View file

@ -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;

View file

@ -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
View file

@ -33,3 +33,9 @@ declare module 'unicode-emoji-json' {
export default emoji;
}
declare module 'pdf-signature-reader' {
const verifySignature: (pdf: ArrayBuffer) => ({signatures: SignatureInfo[]});
export default verifySignature;
}

View file

@ -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()">

View file

@ -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(''),

View file

@ -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>

View file

@ -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>

View file

@ -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',

View file

@ -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>

View 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'),
});

View file

@ -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');
});
});

View file

@ -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
}
}
}

View 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>

View file

@ -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,
});

View file

@ -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...

View file

@ -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,
});

View file

@ -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

View file

@ -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>

View file

@ -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(() => {
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;
});
}) as CButtonSelectOption<T>[];
}
return _.map(rawOptions.value, (value, label) => ({ label, value })) as CButtonSelectOption<T>[];
});
const value = useVModel(props, 'value', emits);

View 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>

View 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>

View file

@ -0,0 +1,3 @@
<template>
<c-file-upload />
</template>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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',
},
});