Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
test:
timeout-minutes: 10
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
shard: [1/3, 2/3, 3/3]
Expand Down
34 changes: 34 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,28 @@ declare module '@vue/runtime-core' {
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
IconMdiClose: typeof import('~icons/mdi/close')['default']
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
IconMdiDownload: typeof import('~icons/mdi/download')['default']
IconMdiEye: typeof import('~icons/mdi/eye')['default']
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
IconMdiPause: typeof import('~icons/mdi/pause')['default']
IconMdiPlay: typeof import('~icons/mdi/play')['default']
IconMdiRecord: typeof import('~icons/mdi/record')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiSearch: typeof import('~icons/mdi/search')['default']
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
IconMdiVideo: typeof import('~icons/mdi/video')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
Expand Down Expand Up @@ -129,20 +140,43 @@ declare module '@vue/runtime-core' {
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NationalIdGenerator: typeof import('./src/tools/national-id-generator/national-id-generator.vue')['default']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NButton: typeof import('naive-ui')['NButton']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDatePicker: typeof import('naive-ui')['NDatePicker']
NDivider: typeof import('naive-ui')['NDivider']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
NH2: typeof import('naive-ui')['NH2']
NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NProgress: typeof import('naive-ui')['NProgress']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch']
NTable: typeof import('naive-ui')['NTable']
NTag: typeof import('naive-ui')['NTag']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
Expand Down
4 changes: 4 additions & 0 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,7 @@ tools:
text-to-binary:
title: Text to ASCII binary
description: Convert text to its ASCII binary representation and vice-versa.

national-id-generator:
title: National ID generator
description: Generate a national identification number for a specific country.
59 changes: 59 additions & 0 deletions plan-idNumberGenerator-v2.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Plan: Belgian SSIN Generator — Updates

**What**: Apply the changes in the section below.

---

**Changes**

- 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.
- 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.
- 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.
- 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.

---

**Steps**

### Phase 1 — Consistent field alignment (`id-number-generator.vue`)
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.
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.
3. The `n-date-picker` and `n-switch` rows already use manual labels; adjust their label width to match the grid column (140 px).

### Phase 2 — Consistent "random" placeholder for gender (`id-number-generator.vue`)
4. Remove `{ value: '', label: '— Random —' }` from `genderOptions`; the array should only contain `Male` and `Female`.
5. Change `selectedGender` initial value from `''` to `null` (type `string | null`).
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.
7. Update the `generate()` function: treat `selectedGender.value === null` the same as the existing empty-string guard (random fallback in the service).

### Phase 3 — Result card below, always visible (`id-number-generator.vue`)
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.
9. Remove `v-if="result"` from the result card so it is always rendered.
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"`).
11. Keep the result rows under `v-if="result"` so they only appear after the first generation.

### Phase 4 — Copy icons inside input fields (`id-number-generator.vue`)
12. Replace each `c-input-text` + standalone `c-button "Copy"` pair with a single `c-input-text` that uses its `#suffix` slot:
```html
<template #suffix>
<c-tooltip :tooltip="isJustCopiedFormatted ? 'Copied!' : 'Copy to clipboard'">
<c-button circle variant="text" size="small" @click="copyFormatted()">
<icon-mdi-content-copy />
</c-button>
</c-tooltip>
</template>
```
(Same pattern for the raw SSIN field, using a separate `isJustCopiedRaw`.)
13. Destructure `isJustCopied` from each `useCopy` call and rename to `isJustCopiedFormatted` / `isJustCopiedRaw` for the tooltip text.
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`.

---

**Relevant files**
- `src/tools/id-number-generator/id-number-generator.vue` — all changes are here

**Decisions**
- No service or test changes required — purely a UI/layout update.
- Grid column width set to 140 px to comfortably fit "Date of birth" (the longest label).
- `c-tooltip` used for copy icon to provide "Copied!" feedback without a toast, matching `InputCopyable.vue`.

67 changes: 67 additions & 0 deletions plan-idNumberGenerator.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
## Plan: Belgian SSIN Generator Tool

**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.

---

### Belgian SSIN format
`YY.MM.DD-SSS.CC` — 11 digits total:
- `YYMMDD` — date of birth
- `SSS` — serial: **odd** (001–997) for males, **even** (002–998) for females
- `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

---

**Steps**

### Phase 1 — Service layer (`id-number-generator.service.ts`)
1. Export `generateBelgianSSIN(opts: { birthDate?: Date; gender?: 'male' | 'female'; fictitious?: boolean })` returning `{ raw, formatted, gender, birthDate }`
- Random date fallback (1900–current year range)
- Random gender fallback
- `fictitious: false` (default) → serial in low range (001–499 odd for male, 002–498 even for female)
- `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
- Checksum: `97 − (YYMMDDSS % 97)`, prepend `"2"` for year ≥ 2000
2. Export `formatBelgianSSIN(ssin: string): string` → `YY.MM.DD-SSS.CC`

### Phase 2 — Unit tests (`id-number-generator.service.test.ts`)
3. Validate checksum against ≥ 3 known valid Belgian SSINs (pre-2000 and post-2000)
4. Assert male → odd SSS, female → even SSS
5. Test `formatBelgianSSIN` output shape

### Phase 3 — Vue component (`id-number-generator.vue`)
6. Keep existing country `c-select`
7. Add optional `n-date-picker` for date of birth (blank = random)
8. Add optional gender `c-select` (Male / Female / — random —)
9. Add `c-checkbox` or toggle "Fictitious (non-existing) number" — when checked, passes `fictitious: true` to the service (uses high serial range)
10. `c-button` “Generate” triggers `generateBelgianSSIN`
11. Result card (hidden until first generate) shows:
- Formatted SSIN (`XX.XX.XX-XXX.XX`) + copy button
- Unformatted raw 11-digit SSIN + copy button
- Uses `useCopy` composable from `@/composable/copy`

### Phase 4 — Tool registration (`index.ts`)
12. Set `description: 'Generate a valid Belgian national identification number (NISS/INSZ)'`

### Phase 5 — E2E (`id-number-generator.e2e.spec.ts`)
13. Add a test: clicking Generate produces visible SSIN output
14. Add a test: enabling "Fictitious" and clicking Generate produces a valid SSIN with serial ≥ 500

---

**Relevant files**
- `src/tools/id-number-generator/id-number-generator.vue` — full rewrite
- `src/tools/id-number-generator/id-number-generator.service.ts` — new implementation
- `src/tools/id-number-generator/id-number-generator.service.test.ts` — unit tests
- `src/tools/id-number-generator/id-number-generator.e2e.spec.ts` — E2E update
- `src/tools/id-number-generator/index.ts` — description update

**Reference patterns**
- `src/tools/bcrypt/bcrypt.vue` — `c-card`, `c-input-text` (readonly), `c-button`, `useCopy`
- `useCopy({ source: ref })` from `@/composable/copy`

**Decisions**
- All inputs optional; missing values are randomised
- Generation triggered by button click only
- Country dropdown kept for future country additions
- Default serial range (low) avoids realistic collisions with real numbers
- Fictitious mode uses the high serial range (500–999), which the Belgian registry reserves and never assigns to real persons
7 changes: 6 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -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 nationalIdGenerator } from './national-id-generator';
import { tool as emailNormalizer } from './email-normalizer';

import { tool as asciiTextDrawer } from './ascii-text-drawer';
Expand Down Expand Up @@ -188,7 +189,11 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Data',
components: [phoneParserAndFormatter, ibanValidatorAndParser],
components: [
phoneParserAndFormatter,
ibanValidatorAndParser,
nationalIdGenerator,
],
},
];

Expand Down
12 changes: 12 additions & 0 deletions src/tools/national-id-generator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Id } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'National ID generator',
path: '/national-id-generator',
description: 'Generate a valid national identification number',
keywords: ['national', 'id', 'number', 'generator'],
component: () => import('./national-id-generator.vue'),
icon: Id,
createdAt: new Date('2026-05-19'),
});
33 changes: 33 additions & 0 deletions src/tools/national-id-generator/national-id-generator.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, test } from '@playwright/test';

test.describe('Tool - National ID generator', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/national-id-generator');
});

test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('NationalID generator - IT Tools');
});

test('Clicking Generate produces a visible formatted SSIN', async ({ page }) => {
await page.getByRole('button', { name: 'Generate' }).click();
// Formatted SSIN matches pattern XX.XX.XX-XXX.XX
const formattedInput = page.locator('input[readonly]').first();
await expect(formattedInput).toHaveValue(/^\d{2}\.\d{2}\.\d{2}-\d{3}\.\d{2}$/);
});

test('Enabling Fictitious and clicking Generate produces a SSIN with serial >= 900', async ({ page }) => {
// Enable fictitious toggle
await page.locator('.n-switch').click();
// Select female so serial >= 900
await page.getByText('— Random —').click();
await page.getByText('Female').click();

await page.getByRole('button', { name: 'Generate' }).click();

const rawInput = page.locator('input[readonly]').nth(1);
const raw = await rawInput.inputValue();
const serial = Number(raw.slice(6, 9));
expect(serial).toBeGreaterThanOrEqual(900);
});
});
Loading
Loading