feat(frontend): add types + format helpers with tdd

This commit is contained in:
devilreef 2026-04-30 09:29:52 +06:00
parent 3b8400b2ed
commit a1e24c6a81
Signed by: devilreef
SSH key fingerprint: SHA256:UZisRr4iuXx+IhkbZnR655L2RWAT6o2rgbGv5F/6m3Y
5 changed files with 150 additions and 0 deletions

View file

@ -0,0 +1,28 @@
export function fmtMs(seconds: number): string {
const s = Math.max(0, Math.floor(seconds));
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${String(r).padStart(2, '0')}`;
}
export function fmtRelative(d: Date, now: Date = new Date()): string {
const ms = now.getTime() - d.getTime();
if (ms < 5_000) return 'now';
const s = Math.floor(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h`;
const days = Math.floor(h / 24);
return `${days}d`;
}
export function fmtJpDate(d: Date): string {
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}`;
}
export function fmtJpTime(d: Date): string {
const p = (n: number) => String(n).padStart(2, '0');
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}

41
frontend/src/lib/types.ts Normal file
View file

@ -0,0 +1,41 @@
export interface Station {
id: string;
name: string;
description: string;
color: string;
tags: string[];
mounts: { mp3: string; opus: string };
cover: string | null;
}
export interface NowPlaying {
station: string;
artist: string;
title: string;
album: string;
filename: string;
duration: string;
started_at: string;
}
export interface HistoryEntry {
title: string;
artist: string;
album: string;
filename: string;
started_at: string;
}
export interface IcecastSourceJson {
listenurl: string;
server_name?: string;
server_type?: string;
listeners?: number;
}
export interface IcecastStatusJson {
icestats: {
host?: string;
source?: IcecastSourceJson | IcecastSourceJson[];
};
}

View file

@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import { fmtMs, fmtRelative, fmtJpDate, fmtJpTime } from '@lib/format';
describe('fmtMs', () => {
it('formats zero as 0:00', () => {
expect(fmtMs(0)).toBe('0:00');
});
it('pads seconds to two digits', () => {
expect(fmtMs(7)).toBe('0:07');
});
it('formats 215 as 3:35', () => {
expect(fmtMs(215)).toBe('3:35');
});
it('handles minutes >= 10 correctly', () => {
expect(fmtMs(605)).toBe('10:05');
});
it('clamps negative input to 0:00', () => {
expect(fmtMs(-3)).toBe('0:00');
});
it('floors fractional seconds', () => {
expect(fmtMs(7.9)).toBe('0:07');
});
});
describe('fmtRelative', () => {
const now = new Date('2026-04-30T12:00:00Z');
it('formats <60s as Xs', () => {
expect(fmtRelative(new Date('2026-04-30T11:59:30Z'), now)).toBe('30s');
});
it('formats <1h as Xm', () => {
expect(fmtRelative(new Date('2026-04-30T11:55:00Z'), now)).toBe('5m');
});
it('formats <24h as Xh', () => {
expect(fmtRelative(new Date('2026-04-30T09:00:00Z'), now)).toBe('3h');
});
it('formats >=24h as Xd', () => {
expect(fmtRelative(new Date('2026-04-28T12:00:00Z'), now)).toBe('2d');
});
it('returns "now" for the current moment', () => {
expect(fmtRelative(new Date('2026-04-30T12:00:00Z'), now)).toBe('now');
});
it('returns "now" for a future timestamp (clock skew)', () => {
expect(fmtRelative(new Date('2026-04-30T12:00:05Z'), now)).toBe('now');
});
});
describe('fmtJpDate', () => {
it('formats with full kanji', () => {
// build the date in local time so the test isn't tz-sensitive
const d = new Date(2026, 3, 30); // month is 0-indexed → April
expect(fmtJpDate(d)).toBe('2026年4月30日');
});
});
describe('fmtJpTime', () => {
it('formats HH:MM:SS', () => {
const d = new Date();
d.setHours(7); d.setMinutes(3); d.setSeconds(9);
expect(fmtJpTime(d)).toBe('07:03:09');
});
});

12
frontend/vite.config.ts Normal file
View file

@ -0,0 +1,12 @@
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
resolve: {
alias: {
'@lib': resolve(__dirname, 'src/lib'),
'@components': resolve(__dirname, 'src/components'),
'@styles': resolve(__dirname, 'src/styles'),
},
},
});

View file

@ -1,4 +1,5 @@
import { defineConfig } from 'vitest/config';
import { resolve } from 'node:path';
export default defineConfig({
test: {
@ -6,4 +7,11 @@ export default defineConfig({
include: ['src/tests/**/*.test.ts', 'src/tests/**/*.test.tsx'],
globals: false,
},
resolve: {
alias: {
'@lib': resolve(__dirname, 'src/lib'),
'@components': resolve(__dirname, 'src/components'),
'@styles': resolve(__dirname, 'src/styles'),
},
},
});