feat(new tool): ULID generator (#623)

This commit is contained in:
Corentin THOMASSET 2023-09-12 00:57:42 +02:00 committed by GitHub
parent 557b30426f
commit 5c4d775e2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 174 additions and 1 deletions

3
components.d.ts vendored
View file

@ -25,6 +25,8 @@ declare module '@vue/runtime-core' {
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default'] CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
CButton: typeof import('./src/ui/c-button/c-button.vue')['default'] CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default'] 'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
CButtonsSelect: typeof import('./src/ui/c-buttons-select/c-buttons-select.vue')['default']
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
CCard: typeof import('./src/ui/c-card/c-card.vue')['default'] CCard: 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']
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']
@ -183,6 +185,7 @@ declare module '@vue/runtime-core' {
TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default'] TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default']
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default'] 'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
ToolCard: typeof import('./src/components/ToolCard.vue')['default'] ToolCard: typeof import('./src/components/ToolCard.vue')['default']
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default'] UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default'] UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default'] UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']

View file

@ -77,6 +77,7 @@
"randombytes": "^2.1.0", "randombytes": "^2.1.0",
"sql-formatter": "^13.0.0", "sql-formatter": "^13.0.0",
"ua-parser-js": "^1.0.35", "ua-parser-js": "^1.0.35",
"ulid": "^2.3.0",
"unicode-emoji-json": "^0.4.0", "unicode-emoji-json": "^0.4.0",
"unplugin-auto-import": "^0.16.4", "unplugin-auto-import": "^0.16.4",
"uuid": "^9.0.0", "uuid": "^9.0.0",

8
pnpm-lock.yaml generated
View file

@ -134,6 +134,9 @@ dependencies:
ua-parser-js: ua-parser-js:
specifier: ^1.0.35 specifier: ^1.0.35
version: 1.0.35 version: 1.0.35
ulid:
specifier: ^2.3.0
version: 2.3.0
unicode-emoji-json: unicode-emoji-json:
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0 version: 0.4.0
@ -8246,6 +8249,11 @@ packages:
/ufo@1.1.2: /ufo@1.1.2:
resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
/ulid@2.3.0:
resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
hasBin: true
dev: false
/unbox-primitive@1.0.2: /unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies: dependencies:

View file

@ -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 ulidGenerator } from './ulid-generator';
import { tool as ibanValidatorAndParser } from './iban-validator-and-parser'; import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
import { tool as stringObfuscator } from './string-obfuscator'; import { tool as stringObfuscator } from './string-obfuscator';
import { tool as textDiff } from './text-diff'; import { tool as textDiff } from './text-diff';
@ -74,7 +75,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, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser], components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
}, },
{ {
name: 'Converter', name: 'Converter',

View file

@ -0,0 +1,12 @@
import { SortDescendingNumbers } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'ULID generator',
path: '/ulid-generator',
description: 'Generate random Universally Unique Lexicographically Sortable Identifier (ULID).',
keywords: ['ulid', 'generator', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
component: () => import('./ulid-generator.vue'),
icon: SortDescendingNumbers,
createdAt: new Date('2023-09-11'),
});

View file

@ -0,0 +1,23 @@
import { expect, test } from '@playwright/test';
const ULID_REGEX = /[0-9A-Z]{26}/;
test.describe('Tool - ULID generator', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/ulid-generator');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('ULID generator - IT Tools');
});
test('the refresh button generates a new ulid', async ({ page }) => {
const ulid = await page.getByTestId('ulids').textContent();
expect(ulid?.trim()).toMatch(ULID_REGEX);
await page.getByTestId('refresh').click();
const newUlid = await page.getByTestId('ulids').textContent();
expect(ulid?.trim()).not.toBe(newUlid?.trim());
expect(newUlid?.trim()).toMatch(ULID_REGEX);
});
});

View file

@ -0,0 +1,46 @@
<script setup lang="ts">
import { ulid } from 'ulid';
import _ from 'lodash';
import { computedRefreshable } from '@/composable/computedRefreshable';
import { useCopy } from '@/composable/copy';
const amount = useStorage('ulid-generator-amount', 1);
const formats = [{ label: 'Raw', value: 'raw' }, { label: 'JSON', value: 'json' }] as const;
const format = useStorage<typeof formats[number]['value']>('ulid-generator-format', formats[0].value);
const [ulids, refreshUlids] = computedRefreshable(() => {
const ids = _.times(amount.value, () => ulid());
if (format.value === 'json') {
return JSON.stringify(ids, null, 2);
}
return ids.join('\n');
});
const { copy } = useCopy({ source: ulids, text: 'ULIDs copied to the clipboard' });
</script>
<template>
<div flex flex-col justify-center gap-2>
<div flex items-center>
<label w-75px> Quantity:</label>
<n-input-number v-model:value="amount" min="1" max="100" flex-1 />
</div>
<c-buttons-select v-model:value="format" :options="formats" label="Format: " label-width="75px" />
<c-card mt-5 flex data-test-id="ulids">
<pre m-0 m-x-auto>{{ ulids }}</pre>
</c-card>
<div flex justify-center gap-2>
<c-button data-test-id="refresh" @click="refreshUlids()">
Refresh
</c-button>
<c-button @click="copy()">
Copy
</c-button>
</div>
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
const optionsA = [
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b', tooltip: 'This is a tooltip' },
{ label: 'Option C', value: '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 />
</template>

View file

@ -0,0 +1,5 @@
import type { CSelectOption } from '../c-select/c-select.types';
export type CButtonSelectOption<T> = CSelectOption<T> & {
tooltip?: string
};

View file

@ -0,0 +1,59 @@
<script setup lang="ts" generic="T extends unknown">
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[]
value?: T
size?: 'small' | 'medium' | 'large'
} & CLabelProps >(),
{
options: () => [],
value: undefined,
labelPosition: 'left',
size: 'medium',
},
);
const emits = defineEmits(['update:value']);
const { options: rawOptions, size } = toRefs(props);
const options = computed(() => {
return rawOptions.value.map((option: string | CButtonSelectOption<T>) => {
if (typeof option === 'string') {
return { label: option, value: option };
}
return option;
});
});
const value = useVModel(props, 'value', emits);
function selectOption(option: CButtonSelectOption<T>) {
// @ts-expect-error vue template generic is a bit flacky thanks to withDefaults
value.value = option.value;
}
</script>
<template>
<c-label v-bind="props">
<div class="flex gap-2">
<c-tooltip
v-for="option in options" :key="option.value"
:tooltip="option.tooltip"
>
<c-button
:test-id="option.value"
:size="size"
:type="option.value === value ? 'primary' : 'default'"
@click="selectOption(option)"
>
{{ option.label }}
</c-button>
</c-tooltip>
</div>
</c-label>
</template>

View file

@ -13,6 +13,7 @@ const isTargetHovered = useElementHover(targetRef);
</div> </div>
<div <div
v-if="tooltip || $slots.tooltip"
class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2" class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2"
:class="{ :class="{
'op-0 scale-0': isTargetHovered === false, 'op-0 scale-0': isTargetHovered === false,