Skip to content

Commit 8e9c5c0

Browse files
committed
feat(new tool): PIN Code Generator
FIx CorentinTh#1242
1 parent 318fb6e commit 8e9c5c0

File tree

7 files changed

+184
-5
lines changed

7 files changed

+184
-5
lines changed

components.d.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,9 @@ declare module '@vue/runtime-core' {
129129
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
130130
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
131131
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
132-
NCode: typeof import('naive-ui')['NCode']
133132
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
134133
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
135134
NEllipsis: typeof import('naive-ui')['NEllipsis']
136-
NForm: typeof import('naive-ui')['NForm']
137135
NFormItem: typeof import('naive-ui')['NFormItem']
138136
NH1: typeof import('naive-ui')['NH1']
139137
NH3: typeof import('naive-ui')['NH3']
@@ -142,7 +140,6 @@ declare module '@vue/runtime-core' {
142140
NLayout: typeof import('naive-ui')['NLayout']
143141
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
144142
NMenu: typeof import('naive-ui')['NMenu']
145-
NScrollbar: typeof import('naive-ui')['NScrollbar']
146143
NSlider: typeof import('naive-ui')['NSlider']
147144
NSwitch: typeof import('naive-ui')['NSwitch']
148145
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
@@ -152,6 +149,7 @@ declare module '@vue/runtime-core' {
152149
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
153150
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
154151
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
152+
PinCodeGenerator: typeof import('./src/tools/pin-code-generator/pin-code-generator.vue')['default']
155153
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
156154
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
157155
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']

src/composable/queryParams.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useRouteQuery } from '@vueuse/router';
22
import { computed } from 'vue';
3+
import { useStorage } from '@vueuse/core';
34

4-
export { useQueryParam };
5+
export { useQueryParam, useQueryParamOrStorage };
56

67
const transformers = {
78
number: {
@@ -16,6 +17,12 @@ const transformers = {
1617
fromQuery: (value: string) => value.toLowerCase() === 'true',
1718
toQuery: (value: boolean) => (value ? 'true' : 'false'),
1819
},
20+
object: {
21+
fromQuery: (value: string) => {
22+
return JSON.parse(value);
23+
},
24+
toQuery: (value: object) => JSON.stringify(value),
25+
},
1926
};
2027

2128
function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) {
@@ -33,3 +40,27 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
3340
},
3441
});
3542
}
43+
44+
function useQueryParamOrStorage<T>({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) {
45+
const type = typeof defaultValue;
46+
const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
47+
48+
const storageRef = useStorage(storageName, defaultValue);
49+
const proxyDefaultValue = transformer.toQuery(defaultValue as never);
50+
const proxy = useRouteQuery(name, proxyDefaultValue);
51+
52+
const r = ref(defaultValue);
53+
54+
watch(r,
55+
(value) => {
56+
proxy.value = transformer.toQuery(value as never);
57+
storageRef.value = value as never;
58+
},
59+
{ deep: true });
60+
61+
r.value = (proxy.value && proxy.value !== proxyDefaultValue
62+
? transformer.fromQuery(proxy.value) as unknown as T
63+
: storageRef.value as T) as never;
64+
65+
return r;
66+
}

src/tools/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { tool as base64FileConverter } from './base64-file-converter';
22
import { tool as base64StringConverter } from './base64-string-converter';
33
import { tool as basicAuthGenerator } from './basic-auth-generator';
44
import { tool as emailNormalizer } from './email-normalizer';
5+
import { tool as pinCodeGenerator } from './pin-code-generator';
56

67
import { tool as asciiTextDrawer } from './ascii-text-drawer';
78

@@ -88,7 +89,20 @@ import { tool as yamlViewer } from './yaml-viewer';
8889
export const toolsByCategory: ToolCategory[] = [
8990
{
9091
name: 'Crypto',
91-
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
92+
components: [
93+
tokenGenerator,
94+
pinCodeGenerator,
95+
hashText,
96+
bcrypt,
97+
uuidGenerator,
98+
ulidGenerator,
99+
cypher,
100+
bip39,
101+
hmacGenerator,
102+
rsaKeyPairGenerator,
103+
passwordStrengthAnalyser,
104+
pdfSignatureChecker,
105+
],
92106
},
93107
{
94108
name: 'Converter',

src/tools/pin-code-generator/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FileDigit } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Pin Code Generator',
6+
path: '/pin-code-generator',
7+
description: 'Generate Unique PIN (digits) codes',
8+
keywords: ['pin', 'code', 'digits', 'generator'],
9+
component: () => import('./pin-code-generator.vue'),
10+
icon: FileDigit,
11+
createdAt: new Date('2024-08-15'),
12+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { randomNumber } from './pin-code-generator.service';
3+
4+
describe('pin-code-generator', () => {
5+
describe('randomNumber', () => {
6+
it('should generate random pin code of given length', () => {
7+
expect(randomNumber({
8+
length: 10,
9+
})).toHaveLength(10);
10+
expect(randomNumber({
11+
length: 4,
12+
})).toHaveLength(4);
13+
expect(randomNumber({
14+
length: 25,
15+
})).toHaveLength(25);
16+
});
17+
it('should generate random pin code without duplicate', () => {
18+
const pin = randomNumber({
19+
length: 10,
20+
});
21+
expect(pin).toHaveLength(10);
22+
expect(pin.length).to.eq([...new Set(pin.split(''))].length);
23+
});
24+
25+
it('should generate random pin code with duplicate', () => {
26+
const pin = randomNumber({
27+
length: 12,
28+
});
29+
const uniqueDigits = [...new Set(pin.split(''))];
30+
expect(pin).toHaveLength(12);
31+
expect(pin.length).to.greaterThan(uniqueDigits.length);
32+
expect(uniqueDigits.length).to.lessThanOrEqual(10);
33+
});
34+
});
35+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export function randomNumber({
2+
length,
3+
repeatDigits = false,
4+
}: {
5+
length: number
6+
repeatDigits?: boolean
7+
}) {
8+
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
9+
const result: number[] = [];
10+
const willRepeat = length > 10 || repeatDigits;
11+
12+
while (result.length < length) {
13+
const random = Math.floor(Math.random() * 10); // NOSONAR
14+
const num = arr[random];
15+
if (!willRepeat && result.includes(num)) {
16+
continue;
17+
}
18+
result.push(num);
19+
}
20+
21+
return result.join('');
22+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<script setup lang="ts">
2+
import { randomNumber } from './pin-code-generator.service';
3+
import { useCopy } from '@/composable/copy';
4+
import { useQueryParamOrStorage } from '@/composable/queryParams';
5+
import { computedRefreshable } from '@/composable/computedRefreshable';
6+
7+
const count = useQueryParamOrStorage({ name: 'count', storageName: 'pin-generator:count', defaultValue: 1 });
8+
const length = useQueryParamOrStorage({ name: 'length', storageName: 'pin-generator:length', defaultValue: 6 });
9+
const repeat = useQueryParamOrStorage({ name: 'repeat', storageName: 'pin-generator:repeat', defaultValue: true });
10+
11+
const [pins, refreshPins] = computedRefreshable(() =>
12+
Array.from({ length: count.value },
13+
() => randomNumber({
14+
length: length.value,
15+
repeatDigits: repeat.value,
16+
})).join('\n'),
17+
);
18+
19+
const { copy } = useCopy({ source: pins, text: 'Pin code copied to clipboard!' });
20+
</script>
21+
22+
<template>
23+
<div>
24+
<c-card>
25+
<n-form-item :label="`Number of digits (${length})`" label-placement="left">
26+
<n-slider v-model:value="length" :step="1" :min="1" :max="512" mr-2 />
27+
<n-input-number v-model:value="length" size="small" />
28+
</n-form-item>
29+
30+
<n-form-item label="Allow repeated digits" label-placement="left">
31+
<n-switch v-model:value="repeat" />
32+
</n-form-item>
33+
34+
<n-form-item label="Number of PIN codes to generate" label-placement="left">
35+
<n-input-number v-model:value="count" size="small" />
36+
</n-form-item>
37+
38+
<c-input-text
39+
v-model:value="pins"
40+
multiline
41+
placeholder="PIN codes..."
42+
readonly
43+
rows="3"
44+
autosize
45+
class="pin-code-display"
46+
word-wrap
47+
/>
48+
49+
<div mt-5 flex justify-center gap-3>
50+
<c-button @click="copy()">
51+
Copy
52+
</c-button>
53+
<c-button @click="refreshPins">
54+
Refresh
55+
</c-button>
56+
</div>
57+
</c-card>
58+
</div>
59+
</template>
60+
61+
<style scoped lang="less">
62+
::v-deep(.pin-code-display) {
63+
textarea {
64+
text-align: center;
65+
}
66+
}
67+
</style>

0 commit comments

Comments
 (0)