Skip to content

Commit 8273181

Browse files
committed
improvements
1 parent fad6234 commit 8273181

14 files changed

Lines changed: 807 additions & 5 deletions

src/app/shared/services/tool-registry.service.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ export class ToolRegistryService {
137137
route: '/tools/crypto/uuid',
138138
description: 'Generate UUIDv4 identifiers.',
139139
keywords: ['uuid', 'guid', 'v4']
140+
},
141+
{
142+
id: 'sortable-ids',
143+
label: 'UUIDv7 / ULID',
144+
route: '/tools/crypto/sortable-ids',
145+
description: 'Generate sortable identifiers.',
146+
keywords: ['uuid', 'ulid', 'v7', 'sortable']
140147
}
141148
]
142149
},
@@ -157,6 +164,20 @@ export class ToolRegistryService {
157164
route: '/tools/data/csv',
158165
description: 'Convert CSV and JSON arrays.',
159166
keywords: ['csv', 'json', 'convert']
167+
},
168+
{
169+
id: 'cron',
170+
label: 'Cron Next Run',
171+
route: '/tools/data/cron',
172+
description: 'Calculate upcoming cron runs.',
173+
keywords: ['cron', 'schedule', 'time']
174+
},
175+
{
176+
id: 'timezone',
177+
label: 'Timezone Converter',
178+
route: '/tools/data/timezone',
179+
description: 'Convert time between zones.',
180+
keywords: ['timezone', 'time', 'convert']
160181
}
161182
]
162183
}

src/app/tools/crypto/crypto-tools.module.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,27 @@ import { UrlToolComponent } from './url/url-tool.component';
66
import { JwtToolComponent } from './jwt/jwt-tool.component';
77
import { HashToolComponent } from './hash/hash-tool.component';
88
import { UuidToolComponent } from './uuid/uuid-tool.component';
9+
import { SortableIdsComponent } from './sortable-ids/sortable-ids.component';
910

1011
const routes: Routes = [
1112
{ path: '', pathMatch: 'full', redirectTo: 'base64' },
1213
{ path: 'base64', component: Base64ToolComponent, data: { animation: 'base64' } },
1314
{ path: 'url', component: UrlToolComponent, data: { animation: 'url' } },
1415
{ path: 'jwt', component: JwtToolComponent, data: { animation: 'jwt' } },
1516
{ path: 'hash', component: HashToolComponent, data: { animation: 'hash' } },
16-
{ path: 'uuid', component: UuidToolComponent, data: { animation: 'uuid' } }
17+
{ path: 'uuid', component: UuidToolComponent, data: { animation: 'uuid' } },
18+
{ path: 'sortable-ids', component: SortableIdsComponent, data: { animation: 'sortable-ids' } }
1719
];
1820

1921
@NgModule({
20-
declarations: [Base64ToolComponent, UrlToolComponent, JwtToolComponent, HashToolComponent, UuidToolComponent],
22+
declarations: [
23+
Base64ToolComponent,
24+
UrlToolComponent,
25+
JwtToolComponent,
26+
HashToolComponent,
27+
UuidToolComponent,
28+
SortableIdsComponent
29+
],
2130
imports: [SharedModule, RouterModule.forChild(routes)]
2231
})
2332
export class CryptoToolsModule {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.count-field {
2+
width: 110px;
3+
}
4+
5+
.error {
6+
color: #f97316;
7+
font-size: 12px;
8+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<div class="tool-page">
2+
<div class="tool-header">
3+
<div>
4+
<div class="tool-title">UUIDv7 / ULID Generator</div>
5+
<div class="tool-subtitle">Generate sortable identifiers for logs, databases, and event streams.</div>
6+
</div>
7+
<div class="tool-actions">
8+
<mat-form-field appearance="outline">
9+
<mat-label>Type</mat-label>
10+
<mat-select [(ngModel)]="kind">
11+
<mat-option value="uuidv7">UUID v7</mat-option>
12+
<mat-option value="ulid">ULID</mat-option>
13+
</mat-select>
14+
</mat-form-field>
15+
<mat-form-field appearance="outline" class="count-field">
16+
<mat-label>Count</mat-label>
17+
<input matInput type="number" min="1" max="50" [(ngModel)]="count">
18+
</mat-form-field>
19+
<button mat-raised-button color="primary" type="button" (click)="generate()">
20+
<mat-icon>auto_awesome</mat-icon>
21+
Generate
22+
</button>
23+
</div>
24+
</div>
25+
26+
<div class="tool-grid">
27+
<div class="tool-panel glass-card">
28+
<div class="chip"><span class="material-icons">numbers</span>Output</div>
29+
<app-result-panel
30+
[value]="output"
31+
[language]="'text'"
32+
filename="sortable-ids.txt"
33+
emptyHint="Generate identifiers to see output."></app-result-panel>
34+
<div class="error" *ngIf="error">{{ error }}</div>
35+
</div>
36+
</div>
37+
</div>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Component, OnDestroy, OnInit } from '@angular/core';
2+
import { ToastService } from '../../../shared/services/toast.service';
3+
import { ToolStateService } from '../../../shared/services/tool-state.service';
4+
import { ActiveToolService } from '../../../shared/services/active-tool.service';
5+
6+
const ULID_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
7+
8+
type SortableIdType = 'uuidv7' | 'ulid';
9+
10+
@Component({
11+
selector: 'app-sortable-ids',
12+
standalone: false,
13+
templateUrl: './sortable-ids.component.html',
14+
styleUrl: './sortable-ids.component.css'
15+
})
16+
export class SortableIdsComponent implements OnInit, OnDestroy {
17+
kind: SortableIdType = 'uuidv7';
18+
count = 5;
19+
output = '';
20+
error = '';
21+
private readonly runHandler = () => this.generate();
22+
23+
constructor(
24+
private toast: ToastService,
25+
private toolState: ToolStateService,
26+
private activeTool: ActiveToolService
27+
) {}
28+
29+
ngOnInit(): void {
30+
this.kind = this.toolState.get('sortable-ids.kind', 'uuidv7');
31+
this.count = this.toolState.get('sortable-ids.count', 5);
32+
this.activeTool.register(this.runHandler);
33+
}
34+
35+
ngOnDestroy(): void {
36+
this.activeTool.clear(this.runHandler);
37+
}
38+
39+
generate(): void {
40+
this.error = '';
41+
try {
42+
const total = Math.max(1, Math.min(50, Number(this.count) || 1));
43+
const ids = Array.from({ length: total }, () => this.kind === 'ulid' ? createUlid() : createUuidV7());
44+
this.output = ids.join('\n');
45+
this.toolState.set('sortable-ids.kind', this.kind);
46+
this.toolState.set('sortable-ids.count', total);
47+
this.toast.success('Identifiers generated.');
48+
} catch (error) {
49+
this.error = error instanceof Error ? error.message : 'Unable to generate identifiers.';
50+
this.toast.error('Unable to generate identifiers.');
51+
}
52+
}
53+
}
54+
55+
function createUuidV7(): string {
56+
const bytes = new Uint8Array(16);
57+
const now = Date.now();
58+
bytes[0] = (now >>> 40) & 0xff;
59+
bytes[1] = (now >>> 32) & 0xff;
60+
bytes[2] = (now >>> 24) & 0xff;
61+
bytes[3] = (now >>> 16) & 0xff;
62+
bytes[4] = (now >>> 8) & 0xff;
63+
bytes[5] = now & 0xff;
64+
65+
const random = crypto.getRandomValues(new Uint8Array(10));
66+
bytes[6] = (random[0] & 0x0f) | 0x70;
67+
bytes[7] = random[1];
68+
bytes[8] = (random[2] & 0x3f) | 0x80;
69+
bytes[9] = random[3];
70+
bytes[10] = random[4];
71+
bytes[11] = random[5];
72+
bytes[12] = random[6];
73+
bytes[13] = random[7];
74+
bytes[14] = random[8];
75+
bytes[15] = random[9];
76+
77+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
78+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
79+
}
80+
81+
function createUlid(): string {
82+
const time = encodeUlidTime(Date.now());
83+
const randomness = encodeUlidRandomness();
84+
return `${time}${randomness}`;
85+
}
86+
87+
function encodeUlidTime(ms: number): string {
88+
let value = BigInt(ms);
89+
let output = '';
90+
for (let i = 0; i < 10; i++) {
91+
const mod = Number(value % 32n);
92+
output = ULID_ALPHABET[mod] + output;
93+
value = value / 32n;
94+
}
95+
return output;
96+
}
97+
98+
function encodeUlidRandomness(): string {
99+
const randomBytes = crypto.getRandomValues(new Uint8Array(10));
100+
let value = 0n;
101+
for (const byte of randomBytes) {
102+
value = (value << 8n) | BigInt(byte);
103+
}
104+
let output = '';
105+
for (let i = 0; i < 16; i++) {
106+
const mod = Number(value % 32n);
107+
output = ULID_ALPHABET[mod] + output;
108+
value = value / 32n;
109+
}
110+
return output;
111+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.count-field {
2+
width: 110px;
3+
}
4+
5+
.row {
6+
display: flex;
7+
gap: 12px;
8+
align-items: center;
9+
}
10+
11+
.results {
12+
display: flex;
13+
flex-direction: column;
14+
gap: 8px;
15+
}
16+
17+
.result {
18+
padding: 10px 12px;
19+
border-radius: 12px;
20+
background: rgba(47, 215, 255, 0.12);
21+
font-size: 13px;
22+
}
23+
24+
.error {
25+
color: #f97316;
26+
font-size: 12px;
27+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<div class="tool-page">
2+
<div class="tool-header">
3+
<div>
4+
<div class="tool-title">Cron Next Run</div>
5+
<div class="tool-subtitle">Compute upcoming runs for a 5-field cron expression (min hour dom mon dow).</div>
6+
</div>
7+
<div class="tool-actions">
8+
<mat-form-field appearance="outline" class="count-field">
9+
<mat-label>Count</mat-label>
10+
<input matInput type="number" min="1" max="10" [(ngModel)]="count">
11+
</mat-form-field>
12+
<button mat-raised-button color="primary" type="button" (click)="run()">
13+
<mat-icon>schedule</mat-icon>
14+
Calculate
15+
</button>
16+
</div>
17+
</div>
18+
19+
<div class="tool-grid">
20+
<div class="tool-panel glass-card">
21+
<div class="chip"><span class="material-icons">edit</span>Expression</div>
22+
<mat-form-field appearance="outline">
23+
<mat-label>Cron</mat-label>
24+
<input matInput [(ngModel)]="expression" placeholder="*/5 * * * *">
25+
</mat-form-field>
26+
27+
<div class="row">
28+
<mat-form-field appearance="outline">
29+
<mat-label>Base date/time</mat-label>
30+
<input matInput type="datetime-local" [(ngModel)]="baseDateInput">
31+
</mat-form-field>
32+
<mat-slide-toggle [(ngModel)]="useUtc">Use UTC</mat-slide-toggle>
33+
</div>
34+
35+
<div class="hint muted">If both day-of-month and day-of-week are set, this tool matches either.</div>
36+
<div class="error" *ngIf="error">{{ error }}</div>
37+
</div>
38+
39+
<div class="tool-panel glass-card">
40+
<div class="chip"><span class="material-icons">event</span>Next Runs</div>
41+
<div class="results" *ngIf="results.length; else empty">
42+
<div class="result" *ngFor="let item of results">{{ item }}</div>
43+
</div>
44+
<ng-template #empty>
45+
<div class="muted">Calculate to see the next run times.</div>
46+
</ng-template>
47+
</div>
48+
</div>
49+
</div>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Component, OnDestroy, OnInit } from '@angular/core';
2+
import { ToastService } from '../../../shared/services/toast.service';
3+
import { ToolStateService } from '../../../shared/services/tool-state.service';
4+
import { ActiveToolService } from '../../../shared/services/active-tool.service';
5+
import { computeNextCronRuns, formatCronRun, parseDateTimeLocal } from '../utils/cron';
6+
7+
@Component({
8+
selector: 'app-cron-tool',
9+
standalone: false,
10+
templateUrl: './cron-tool.component.html',
11+
styleUrl: './cron-tool.component.css'
12+
})
13+
export class CronToolComponent implements OnInit, OnDestroy {
14+
expression = '';
15+
baseDateInput = '';
16+
count = 5;
17+
useUtc = false;
18+
results: string[] = [];
19+
error = '';
20+
private readonly runHandler = () => this.run();
21+
22+
constructor(
23+
private toast: ToastService,
24+
private toolState: ToolStateService,
25+
private activeTool: ActiveToolService
26+
) {}
27+
28+
ngOnInit(): void {
29+
this.expression = this.toolState.get('cron.expression', '*/5 * * * *');
30+
this.baseDateInput = this.toolState.get('cron.baseDate', '');
31+
this.count = this.toolState.get('cron.count', 5);
32+
this.useUtc = this.toolState.get('cron.useUtc', false);
33+
this.activeTool.register(this.runHandler);
34+
}
35+
36+
ngOnDestroy(): void {
37+
this.activeTool.clear(this.runHandler);
38+
}
39+
40+
run(): void {
41+
this.error = '';
42+
this.results = [];
43+
try {
44+
const total = Math.max(1, Math.min(10, Number(this.count) || 1));
45+
const baseDate = parseDateTimeLocal(this.baseDateInput, this.useUtc);
46+
const runs = computeNextCronRuns(this.expression, baseDate, total, this.useUtc);
47+
this.results = runs.map(date => formatCronRun(date, this.useUtc));
48+
this.toolState.set('cron.expression', this.expression);
49+
this.toolState.set('cron.baseDate', this.baseDateInput);
50+
this.toolState.set('cron.count', total);
51+
this.toolState.set('cron.useUtc', this.useUtc);
52+
this.toast.success('Cron schedule calculated.');
53+
} catch (error) {
54+
this.error = error instanceof Error ? error.message : 'Invalid cron expression.';
55+
this.toast.error(this.error);
56+
}
57+
}
58+
}

src/app/tools/data/data-tools.module.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ import { RouterModule, Routes } from '@angular/router';
33
import { SharedModule } from '../../shared/shared.module';
44
import { TimestampToolComponent } from './timestamp/timestamp-tool.component';
55
import { CsvToolComponent } from './csv/csv-tool.component';
6+
import { CronToolComponent } from './cron/cron-tool.component';
7+
import { TimezoneToolComponent } from './timezone/timezone-tool.component';
68

79
const routes: Routes = [
810
{ path: '', pathMatch: 'full', redirectTo: 'timestamp' },
911
{ path: 'timestamp', component: TimestampToolComponent, data: { animation: 'timestamp' } },
10-
{ path: 'csv', component: CsvToolComponent, data: { animation: 'csv' } }
12+
{ path: 'csv', component: CsvToolComponent, data: { animation: 'csv' } },
13+
{ path: 'cron', component: CronToolComponent, data: { animation: 'cron' } },
14+
{ path: 'timezone', component: TimezoneToolComponent, data: { animation: 'timezone' } }
1115
];
1216

1317
@NgModule({
14-
declarations: [TimestampToolComponent, CsvToolComponent],
15-
imports: [SharedModule, RouterModule.forChild(routes)]
18+
declarations: [TimestampToolComponent, CsvToolComponent, CronToolComponent],
19+
imports: [SharedModule, TimezoneToolComponent, RouterModule.forChild(routes)]
1620
})
1721
export class DataToolsModule {}

0 commit comments

Comments
 (0)