bf070ec47f
- 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>
50 lines
1.7 KiB
TypeScript
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
|
|
}
|