Skip to content

Commit 3e537a7

Browse files
author
Bart Coppens
committed
Add tool to generate Belgian national id. Expandable with other countries.
1 parent d505845 commit 3e537a7

14 files changed

Lines changed: 858 additions & 1 deletion

components.d.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,28 @@ declare module '@vue/runtime-core' {
9090
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
9191
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
9292
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
93+
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
9394
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
95+
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
96+
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
97+
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
9498
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
9599
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
96100
IconMdiClose: typeof import('~icons/mdi/close')['default']
97101
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
102+
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
103+
IconMdiDownload: typeof import('~icons/mdi/download')['default']
98104
IconMdiEye: typeof import('~icons/mdi/eye')['default']
99105
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
100106
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
107+
IconMdiPause: typeof import('~icons/mdi/pause')['default']
108+
IconMdiPlay: typeof import('~icons/mdi/play')['default']
109+
IconMdiRecord: typeof import('~icons/mdi/record')['default']
110+
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
101111
IconMdiSearch: typeof import('~icons/mdi/search')['default']
102112
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
103113
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
114+
IconMdiVideo: typeof import('~icons/mdi/video')['default']
104115
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
105116
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
106117
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
@@ -129,20 +140,43 @@ declare module '@vue/runtime-core' {
129140
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
130141
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
131142
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
143+
NAlert: typeof import('naive-ui')['NAlert']
144+
NationalIdGenerator: typeof import('./src/tools/national-id-generator/national-id-generator.vue')['default']
132145
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
146+
NButton: typeof import('naive-ui')['NButton']
133147
NCheckbox: typeof import('naive-ui')['NCheckbox']
148+
NCode: typeof import('naive-ui')['NCode']
134149
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
150+
NColorPicker: typeof import('naive-ui')['NColorPicker']
135151
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
152+
NDatePicker: typeof import('naive-ui')['NDatePicker']
136153
NDivider: typeof import('naive-ui')['NDivider']
154+
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
137155
NEllipsis: typeof import('naive-ui')['NEllipsis']
156+
NForm: typeof import('naive-ui')['NForm']
157+
NFormItem: typeof import('naive-ui')['NFormItem']
158+
NGi: typeof import('naive-ui')['NGi']
159+
NGrid: typeof import('naive-ui')['NGrid']
138160
NH1: typeof import('naive-ui')['NH1']
161+
NH2: typeof import('naive-ui')['NH2']
139162
NH3: typeof import('naive-ui')['NH3']
140163
NIcon: typeof import('naive-ui')['NIcon']
164+
NImage: typeof import('naive-ui')['NImage']
165+
NInputGroup: typeof import('naive-ui')['NInputGroup']
166+
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
167+
NInputNumber: typeof import('naive-ui')['NInputNumber']
141168
NLayout: typeof import('naive-ui')['NLayout']
142169
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
143170
NMenu: typeof import('naive-ui')['NMenu']
171+
NProgress: typeof import('naive-ui')['NProgress']
172+
NScrollbar: typeof import('naive-ui')['NScrollbar']
173+
NSlider: typeof import('naive-ui')['NSlider']
144174
NSpace: typeof import('naive-ui')['NSpace']
175+
NSpin: typeof import('naive-ui')['NSpin']
176+
NStatistic: typeof import('naive-ui')['NStatistic']
177+
NSwitch: typeof import('naive-ui')['NSwitch']
145178
NTable: typeof import('naive-ui')['NTable']
179+
NTag: typeof import('naive-ui')['NTag']
146180
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
147181
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
148182
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']

locales/en.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,7 @@ tools:
392392
text-to-binary:
393393
title: Text to ASCII binary
394394
description: Convert text to its ASCII binary representation and vice-versa.
395+
396+
national-id-generator:
397+
title: National ID generator
398+
description: Generate a national identification number for a specific country.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## Plan: Belgian SSIN Generator — Updates
2+
3+
**What**: Apply the changes in the section below.
4+
5+
---
6+
7+
**Changes**
8+
9+
- UI: The 'date of birth' field is indented slightly more than the other fields, making it shorter. This is probably because of the label being too long. Keep the label and make all input fiels equally wide, aligned to the left edge of the longest label. This is a more consistent and visually balanced layout.
10+
- UI: The 'date of birth' and 'gender' fields both have a default "random" option, but this is shown in different ways: the date picker is has 'default' text greyed out, while the gender select has an explicit "— random —" option. Make these consistent by showing a 'random' greyed-out option in the gender select, and removing the "— random —" option. This makes it clearer that both fields have a random default, and reduces visual clutter in the gender select.
11+
- UI: After generating a number, the result card is shown to the right of the form, which looks a bit disconnected. Move the result card below the form, centered, to create a more cohesive and balanced layout. This also allows more space for the result card, which can be wider and easier to read. Make the result card always visible, but show placeholder text (e.g. "Your generated SSIN will appear here") before the first generation, to indicate where the output will be. This improves the user experience by providing a clear and consistent layout, and guiding the user on where to look for the results.
12+
- UI: In the result card, the copy buttons are shown as buttons next to the result fields. Integrate them as icons inside the input fields, aligned to the right, to save space and create a cleaner look. This also makes it more intuitive that clicking the icon will copy the field value. Use a standard copy icon (e.g. clipboard) for this purpose.
13+
14+
---
15+
16+
**Steps**
17+
18+
### Phase 1 — Consistent field alignment (`id-number-generator.vue`)
19+
1. Remove the built-in `label` prop from all `c-select` usages (Country, Gender) — place labels manually outside the component instead, matching the existing manual rows.
20+
2. Wrap the whole options form in a CSS grid: `display: grid; grid-template-columns: 140px 1fr` — each row contributes one label cell and one input cell, ensuring all inputs start at the same horizontal position regardless of which component renders them.
21+
3. The `n-date-picker` and `n-switch` rows already use manual labels; adjust their label width to match the grid column (140 px).
22+
23+
### Phase 2 — Consistent "random" placeholder for gender (`id-number-generator.vue`)
24+
4. Remove `{ value: '', label: '— Random —' }` from `genderOptions`; the array should only contain `Male` and `Female`.
25+
5. Change `selectedGender` initial value from `''` to `null` (type `string | null`).
26+
6. Add `placeholder="Random"` to the gender `c-select` — the component renders placeholder text greyed out when no option is selected, matching the date picker's greyed-out `"Random"` placeholder text.
27+
7. Update the `generate()` function: treat `selectedGender.value === null` the same as the existing empty-string guard (random fallback in the service).
28+
29+
### Phase 3 — Result card below, always visible (`id-number-generator.vue`)
30+
8. Wrap both `<c-card>` elements in a single `<div flex flex-col gap-4>` container. The tool layout assigns `flex: 0 1 600px` to each *direct* child of `.tool-content`; wrapping in one `div` forces both cards into a single column of that width.
31+
9. Remove `v-if="result"` from the result card so it is always rendered.
32+
10. Inside the result card, show a centred, muted placeholder paragraph (e.g. `"Your generated SSIN will appear here"`) when `result` is `null`, and hide it once a result exists (`v-if="!result"`).
33+
11. Keep the result rows under `v-if="result"` so they only appear after the first generation.
34+
35+
### Phase 4 — Copy icons inside input fields (`id-number-generator.vue`)
36+
12. Replace each `c-input-text` + standalone `c-button "Copy"` pair with a single `c-input-text` that uses its `#suffix` slot:
37+
```html
38+
<template #suffix>
39+
<c-tooltip :tooltip="isJustCopiedFormatted ? 'Copied!' : 'Copy to clipboard'">
40+
<c-button circle variant="text" size="small" @click="copyFormatted()">
41+
<icon-mdi-content-copy />
42+
</c-button>
43+
</c-tooltip>
44+
</template>
45+
```
46+
(Same pattern for the raw SSIN field, using a separate `isJustCopiedRaw`.)
47+
13. Destructure `isJustCopied` from each `useCopy` call and rename to `isJustCopiedFormatted` / `isJustCopiedRaw` for the tooltip text.
48+
14. Reference pattern: `src/components/InputCopyable.vue` — uses `#suffix` slot with `c-tooltip`, `c-button circle variant="text" size="small"`, and `icon-mdi-content-copy`.
49+
50+
---
51+
52+
**Relevant files**
53+
- `src/tools/id-number-generator/id-number-generator.vue` — all changes are here
54+
55+
**Decisions**
56+
- No service or test changes required — purely a UI/layout update.
57+
- Grid column width set to 140 px to comfortably fit "Date of birth" (the longest label).
58+
- `c-tooltip` used for copy icon to provide "Copied!" feedback without a toast, matching `InputCopyable.vue`.
59+

plan-idNumberGenerator.prompt.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
## Plan: Belgian SSIN Generator Tool
2+
3+
**What**: Complete `id-number-generator` to generate valid Belgian SSINs (nationaal registernummer / numéro national), with optional user inputs and "Generate" button, displaying both formatted and raw output.
4+
5+
---
6+
7+
### Belgian SSIN format
8+
`YY.MM.DD-SSS.CC` — 11 digits total:
9+
- `YYMMDD` — date of birth
10+
- `SSS` — serial: **odd** (001–997) for males, **even** (002–998) for females
11+
- `CC` — checksum: `97 − (9-digit-number mod 97)`, left-padded to 2 digits; for births ≥ 2000, prepend `"2"` to the 9 digits before the mod
12+
13+
---
14+
15+
**Steps**
16+
17+
### Phase 1 — Service layer (`id-number-generator.service.ts`)
18+
1. Export `generateBelgianSSIN(opts: { birthDate?: Date; gender?: 'male' | 'female'; fictitious?: boolean })` returning `{ raw, formatted, gender, birthDate }`
19+
- Random date fallback (1900–current year range)
20+
- Random gender fallback
21+
- `fictitious: false` (default) → serial in low range (001–499 odd for male, 002–498 even for female)
22+
- `fictitious: true` → serial in high range (501–997 odd for male, 500–998 even for female); these numbers are never assigned to real persons by the Belgian registry
23+
- Checksum: `97 − (YYMMDDSS % 97)`, prepend `"2"` for year ≥ 2000
24+
2. Export `formatBelgianSSIN(ssin: string): string``YY.MM.DD-SSS.CC`
25+
26+
### Phase 2 — Unit tests (`id-number-generator.service.test.ts`)
27+
3. Validate checksum against ≥ 3 known valid Belgian SSINs (pre-2000 and post-2000)
28+
4. Assert male → odd SSS, female → even SSS
29+
5. Test `formatBelgianSSIN` output shape
30+
31+
### Phase 3 — Vue component (`id-number-generator.vue`)
32+
6. Keep existing country `c-select`
33+
7. Add optional `n-date-picker` for date of birth (blank = random)
34+
8. Add optional gender `c-select` (Male / Female / — random —)
35+
9. Add `c-checkbox` or toggle "Fictitious (non-existing) number" — when checked, passes `fictitious: true` to the service (uses high serial range)
36+
10. `c-button` “Generate” triggers `generateBelgianSSIN`
37+
11. Result card (hidden until first generate) shows:
38+
- Formatted SSIN (`XX.XX.XX-XXX.XX`) + copy button
39+
- Unformatted raw 11-digit SSIN + copy button
40+
- Uses `useCopy` composable from `@/composable/copy`
41+
42+
### Phase 4 — Tool registration (`index.ts`)
43+
12. Set `description: 'Generate a valid Belgian national identification number (NISS/INSZ)'`
44+
45+
### Phase 5 — E2E (`id-number-generator.e2e.spec.ts`)
46+
13. Add a test: clicking Generate produces visible SSIN output
47+
14. Add a test: enabling "Fictitious" and clicking Generate produces a valid SSIN with serial ≥ 500
48+
49+
---
50+
51+
**Relevant files**
52+
- `src/tools/id-number-generator/id-number-generator.vue` — full rewrite
53+
- `src/tools/id-number-generator/id-number-generator.service.ts` — new implementation
54+
- `src/tools/id-number-generator/id-number-generator.service.test.ts` — unit tests
55+
- `src/tools/id-number-generator/id-number-generator.e2e.spec.ts` — E2E update
56+
- `src/tools/id-number-generator/index.ts` — description update
57+
58+
**Reference patterns**
59+
- `src/tools/bcrypt/bcrypt.vue``c-card`, `c-input-text` (readonly), `c-button`, `useCopy`
60+
- `useCopy({ source: ref })` from `@/composable/copy`
61+
62+
**Decisions**
63+
- All inputs optional; missing values are randomised
64+
- Generation triggered by button click only
65+
- Country dropdown kept for future country additions
66+
- Default serial range (low) avoids realistic collisions with real numbers
67+
- Fictitious mode uses the high serial range (500–999), which the Belgian registry reserves and never assigns to real persons

src/tools/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
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';
4+
import { tool as nationalIdGenerator } from './national-id-generator';
45
import { tool as emailNormalizer } from './email-normalizer';
56

67
import { tool as asciiTextDrawer } from './ascii-text-drawer';
@@ -188,7 +189,11 @@ export const toolsByCategory: ToolCategory[] = [
188189
},
189190
{
190191
name: 'Data',
191-
components: [phoneParserAndFormatter, ibanValidatorAndParser],
192+
components: [
193+
phoneParserAndFormatter,
194+
ibanValidatorAndParser,
195+
nationalIdGenerator,
196+
],
192197
},
193198
];
194199

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Id } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'National ID generator',
6+
path: '/national-id-generator',
7+
description: 'Generate a valid national identification number',
8+
keywords: ['national', 'id', 'number', 'generator'],
9+
component: () => import('./national-id-generator.vue'),
10+
icon: Id,
11+
createdAt: new Date('2026-05-19'),
12+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.describe('Tool - National ID generator', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/national-id-generator');
6+
});
7+
8+
test('Has correct title', async ({ page }) => {
9+
await expect(page).toHaveTitle('NationalID generator - IT Tools');
10+
});
11+
12+
test('Clicking Generate produces a visible formatted SSIN', async ({ page }) => {
13+
await page.getByRole('button', { name: 'Generate' }).click();
14+
// Formatted SSIN matches pattern XX.XX.XX-XXX.XX
15+
const formattedInput = page.locator('input[readonly]').first();
16+
await expect(formattedInput).toHaveValue(/^\d{2}\.\d{2}\.\d{2}-\d{3}\.\d{2}$/);
17+
});
18+
19+
test('Enabling Fictitious and clicking Generate produces a SSIN with serial >= 900', async ({ page }) => {
20+
// Enable fictitious toggle
21+
await page.locator('.n-switch').click();
22+
// Select female so serial >= 900
23+
await page.getByText('— Random —').click();
24+
await page.getByText('Female').click();
25+
26+
await page.getByRole('button', { name: 'Generate' }).click();
27+
28+
const rawInput = page.locator('input[readonly]').nth(1);
29+
const raw = await rawInput.inputValue();
30+
const serial = Number(raw.slice(6, 9));
31+
expect(serial).toBeGreaterThanOrEqual(900);
32+
});
33+
});

0 commit comments

Comments
 (0)