feat(new tool): ULID generator (#623)
This commit is contained in:
		
							parent
							
								
									557b30426f
								
							
						
					
					
						commit
						5c4d775e2d
					
				
							
								
								
									
										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…
	
		Reference in New Issue
	
	Block a user