Files
oma-memorial/src/lib/utils.ts
T
denshooter bf070ec47f Fix UI issues, add tests, and patch missing auth on API routes
- Rewrote CandleSection with properly aligned animated flames (fix
  Framer Motion transform-overwrite bug by using left:0 / pixel offsets
  instead of translateX(-50%)); added size prop for inline flames
- Fixed Timeline: CSS dots/line replacing broken SVG estimates, mobile
  padding (pl-10), larger dots, cards grow with content
- Fixed MusicPlayer: actual play/pause (not mute), visibilitychange +
  pagehide handlers so music stops on iOS lock screen / app switch
- Fixed nav: horizontal scroll on mobile (overflow-x-auto whitespace-nowrap)
- Fixed footer: Impressum centered, admin link moved to absolute/hidden
- Removed meine-oma link from TributeSection (page still accessible)
- Added admin auth (isAdmin cookie check) to /api/upload POST,
  /api/contributions/[id] PUT/DELETE, and /api/candles/[id] PUT/DELETE
- Extracted sp(), relativeTime(), formatDate() to src/lib/utils.ts
- Added Vitest with 15 unit tests for utility functions (npm test)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:20:48 +01:00

50 lines
1.7 KiB
TypeScript

/**
* Deterministic pseudo-random number in [0, 1) from id + salt.
* Using a simple LCG hash so values are identical on server and client
* (avoids React hydration mismatches from Math.random()).
*/
export function sp(id: number, salt: number): number {
const n = ((id * 9301 + salt * 49297 + 1) % 233280 + 233280) % 233280
return n / 233280
}
/**
* Human-readable relative time string (German) for a UTC timestamp.
* The `created_at` value from SQLite has no timezone suffix, so we append 'Z'.
*/
export function relativeTime(created_at: string): string {
const now = Date.now()
const created = new Date(created_at.endsWith('Z') ? created_at : created_at + 'Z').getTime()
const diffMs = now - created
const minutes = Math.floor(diffMs / 60000)
const hours = Math.floor(diffMs / 3600000)
const days = Math.floor(diffMs / 86400000)
if (minutes < 1) return 'gerade eben'
if (minutes < 60) return `vor ${minutes} Min.`
if (hours < 24) return `vor ${hours} Std.`
return `vor ${days} ${days === 1 ? 'Tag' : 'Tagen'}`
}
/**
* Formats a German date string from year/month/day components.
*/
export function formatDate(
year: string,
month?: string | null,
day?: string | null
): string {
if (day && month) {
const monthNames = ['Jan', 'Feb', 'März', 'Apr', 'Mai', 'Juni', 'Juli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dez']
const monthName = monthNames[parseInt(month) - 1] || month
return `${day}. ${monthName} ${year}`
} else if (month) {
const monthNames = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
]
const monthName = monthNames[parseInt(month) - 1] || `Monat ${month}`
return `${monthName} ${year}`
}
return year
}