Public Access
feat: add formatting, storage, and validation utilities
This commit is contained in:
1 parent
e211594429
commit
00640fbc04
4 files changed
+105
No files matched your search
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { formatTime, formatDate } from './formatting'
|
||||
|
||||
describe('formatTime', () => {
|
||||
it('formats seconds', () => {
|
||||
expect(formatTime(65000)).toBe('01:05')
|
||||
})
|
||||
it('formats hours', () => {
|
||||
expect(formatTime(3661000)).toBe('1:01:01')
|
||||
})
|
||||
it('handles 0', () => {
|
||||
expect(formatTime(0)).toBe('00:00')
|
||||
})
|
||||
it('handles negative (clamps to 0)', () => {
|
||||
expect(formatTime(-1000)).toBe('00:00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('returns "just now" for very recent timestamps', () => {
|
||||
expect(formatDate(Date.now() - 5000)).toBe('just now')
|
||||
})
|
||||
it('returns minutes ago for recent timestamps', () => {
|
||||
expect(formatDate(Date.now() - 120_000)).toMatch(/m ago/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
export const formatTime = (ms: number | null | undefined): string => {
|
||||
if (ms == null || ms < 0) ms = 0
|
||||
const totalSec = Math.floor(ms / 1000)
|
||||
const h = Math.floor(totalSec / 3600)
|
||||
const m = Math.floor((totalSec % 3600) / 60)
|
||||
const s = totalSec % 60
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`
|
||||
}
|
||||
|
||||
export const formatDate = (ts: number): string => {
|
||||
const d = new Date(ts)
|
||||
const diff = Date.now() - d.getTime()
|
||||
if (diff < 60_000) return 'just now'
|
||||
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
||||
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
||||
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Exam, ExamState, HistoryEntry } from '../types'
|
||||
|
||||
const STORAGE_KEY = 'brainwave.exam.state'
|
||||
const HISTORY_KEY = 'brainwave.exam.history'
|
||||
|
||||
export interface PersistedExam {
|
||||
exam: Exam
|
||||
state: ExamState
|
||||
}
|
||||
|
||||
export const loadState = (): PersistedExam | null => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null') as PersistedExam | null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const saveState = (data: PersistedExam): void => {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) } catch { /* quota */ }
|
||||
}
|
||||
|
||||
export const clearState = (): void => {
|
||||
try { localStorage.removeItem(STORAGE_KEY) } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export const loadHistory = (): HistoryEntry[] => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(HISTORY_KEY) ?? '[]') as HistoryEntry[]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const appendHistory = (entry: HistoryEntry): void => {
|
||||
try {
|
||||
const hist = loadHistory()
|
||||
hist.unshift(entry)
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(hist.slice(0, 30)))
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export const clearHistory = (): void => {
|
||||
try { localStorage.removeItem(HISTORY_KEY) } catch { /* ignore */ }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Exam } from '../types'
|
||||
|
||||
export const validateExam = (data: unknown): string | null => {
|
||||
if (!data || typeof data !== 'object') return 'Not a JSON object'
|
||||
const d = data as Record<string, unknown>
|
||||
if (!d['title']) return 'Missing "title"'
|
||||
if (!Array.isArray(d['questions']) || d['questions'].length === 0) return 'Missing "questions" array'
|
||||
for (const q of d['questions'] as Record<string, unknown>[]) {
|
||||
if (!q['id']) return 'Question missing id'
|
||||
if (!q['type']) return `Question "${String(q['id'])}" missing type`
|
||||
if (!q['prompt']) return `Question "${String(q['id'])}" missing prompt`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const parseExam = (data: unknown): Exam => data as Exam
|
||||
Reference in new issue
Block a user