mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-21 15:26:15 -04:00
feat(new tool): ULID generator (#623)
This commit is contained in:
parent
557b30426f
commit
5c4d775e2d
11 changed files with 174 additions and 1 deletions
3
components.d.ts
vendored
3
components.d.ts
vendored
|
@ -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']
|
||||||
|
|
|
@ -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
8
pnpm-lock.yaml
generated
|
@ -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:
|
||||||
|
|
|
@ -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',
|
||||||
|
|
12
src/tools/ulid-generator/index.ts
Normal file
12
src/tools/ulid-generator/index.ts
Normal 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'),
|
||||||
|
});
|
23
src/tools/ulid-generator/ulid-generator.e2e.spec.ts
Normal file
23
src/tools/ulid-generator/ulid-generator.e2e.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
46
src/tools/ulid-generator/ulid-generator.vue
Normal file
46
src/tools/ulid-generator/ulid-generator.vue
Normal 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>
|
14
src/ui/c-buttons-select/c-buttons-select.demo.vue
Normal file
14
src/ui/c-buttons-select/c-buttons-select.demo.vue
Normal 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>
|
5
src/ui/c-buttons-select/c-buttons-select.types.ts
Normal file
5
src/ui/c-buttons-select/c-buttons-select.types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import type { CSelectOption } from '../c-select/c-select.types';
|
||||||
|
|
||||||
|
export type CButtonSelectOption<T> = CSelectOption<T> & {
|
||||||
|
tooltip?: string
|
||||||
|
};
|
59
src/ui/c-buttons-select/c-buttons-select.vue
Normal file
59
src/ui/c-buttons-select/c-buttons-select.vue
Normal 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>
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue