aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/tools/index.ts3
-rw-r--r--src/tools/ulid-generator/index.ts12
-rw-r--r--src/tools/ulid-generator/ulid-generator.e2e.spec.ts23
-rw-r--r--src/tools/ulid-generator/ulid-generator.vue46
-rw-r--r--src/ui/c-buttons-select/c-buttons-select.demo.vue14
-rw-r--r--src/ui/c-buttons-select/c-buttons-select.types.ts5
-rw-r--r--src/ui/c-buttons-select/c-buttons-select.vue59
-rw-r--r--src/ui/c-tooltip/c-tooltip.vue1
8 files changed, 162 insertions, 1 deletions
diff --git a/src/tools/index.ts b/src/tools/index.ts
index aa38074..cc5f42e 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -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 ulidGenerator } from './ulid-generator';
import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
import { tool as stringObfuscator } from './string-obfuscator';
import { tool as textDiff } from './text-diff';
@@ -74,7 +75,7 @@ import { tool as xmlFormatter } from './xml-formatter';
export const toolsByCategory: ToolCategory[] = [
{
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',
diff --git a/src/tools/ulid-generator/index.ts b/src/tools/ulid-generator/index.ts
new file mode 100644
index 0000000..6a5408d
--- /dev/null
+++ b/src/tools/ulid-generator/index.ts
@@ -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'),
+});
diff --git a/src/tools/ulid-generator/ulid-generator.e2e.spec.ts b/src/tools/ulid-generator/ulid-generator.e2e.spec.ts
new file mode 100644
index 0000000..3447337
--- /dev/null
+++ b/src/tools/ulid-generator/ulid-generator.e2e.spec.ts
@@ -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);
+ });
+});
diff --git a/src/tools/ulid-generator/ulid-generator.vue b/src/tools/ulid-generator/ulid-generator.vue
new file mode 100644
index 0000000..06e695e
--- /dev/null
+++ b/src/tools/ulid-generator/ulid-generator.vue
@@ -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>
diff --git a/src/ui/c-buttons-select/c-buttons-select.demo.vue b/src/ui/c-buttons-select/c-buttons-select.demo.vue
new file mode 100644
index 0000000..dea1528
--- /dev/null
+++ b/src/ui/c-buttons-select/c-buttons-select.demo.vue
@@ -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>
diff --git a/src/ui/c-buttons-select/c-buttons-select.types.ts b/src/ui/c-buttons-select/c-buttons-select.types.ts
new file mode 100644
index 0000000..ccb110d
--- /dev/null
+++ b/src/ui/c-buttons-select/c-buttons-select.types.ts
@@ -0,0 +1,5 @@
+import type { CSelectOption } from '../c-select/c-select.types';
+
+export type CButtonSelectOption<T> = CSelectOption<T> & {
+ tooltip?: string
+};
diff --git a/src/ui/c-buttons-select/c-buttons-select.vue b/src/ui/c-buttons-select/c-buttons-select.vue
new file mode 100644
index 0000000..38fff66
--- /dev/null
+++ b/src/ui/c-buttons-select/c-buttons-select.vue
@@ -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>
diff --git a/src/ui/c-tooltip/c-tooltip.vue b/src/ui/c-tooltip/c-tooltip.vue
index 24c586b..095315f 100644
--- a/src/ui/c-tooltip/c-tooltip.vue
+++ b/src/ui/c-tooltip/c-tooltip.vue
@@ -13,6 +13,7 @@ const isTargetHovered = useElementHover(targetRef);
</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="{
'op-0 scale-0': isTargetHovered === false,