refactor: centralize format utilities in lib/formatting.ts

formatViewers, formatCount, formatDuration, formatDate, formatWatchedAt,
formatTimestamp, and formatViewersFull were duplicated across four files.
Divergence was silent — fixing a rounding edge case or locale string in
one copy left the others stale.
This commit is contained in:
Andre
2026-05-14 12:13:08 +02:00
parent 086bda9b9c
commit 0e38f930e7
5 changed files with 63 additions and 78 deletions
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react'
import type { FollowedChannelInfo } from '../types/t4sd'
import LanguageBadge from './LanguageBadge'
import { formatViewers } from '../lib/formatting'
interface Props {
channel: FollowedChannelInfo
@@ -9,12 +10,6 @@ interface Props {
onSelect: () => void
}
function formatViewers(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.`
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
return String(n)
}
export default function FocusableCard({ channel, focused, onFocus, onSelect }: Props): JSX.Element {
const ref = useRef<HTMLButtonElement>(null)
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { GamepadHintItem } from './GamepadPrompt'
import { ClapperboardIcon, EyeIcon, GamepadIcon, PauseIcon, PlayIcon, QualityIcon, StopIcon } from './Icons'
import { formatCount, formatTimestamp } from '../lib/formatting'
interface PlaybackOverlayProps {
playState: 'playing' | 'paused'
@@ -28,20 +29,6 @@ interface PlaybackOverlayProps {
onOpenChapters?: () => void
}
function formatCount(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(n)
}
/** Formats seconds as HH:MM:SS */
function fmt(s: number): string {
const total = Math.max(0, Math.floor(s))
const h = Math.floor(total / 3600)
const m = Math.floor((total % 3600) / 60)
const sec = total % 60
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
}
const HIDE_DELAY_MS = 5_000
const DOUBLE_TAP_MS = 300
@@ -333,7 +320,7 @@ export function PlaybackOverlay({
)}
</div>
<div className="playback-overlay__progress-row">
<span className="playback-overlay__time">{fmt(displayPosition)}</span>
<span className="playback-overlay__time">{formatTimestamp(displayPosition)}</span>
<div
ref={seekBarRef}
@@ -355,7 +342,7 @@ export function PlaybackOverlay({
</div>
</div>
<span className="playback-overlay__time">{fmt(durationSeconds)}</span>
<span className="playback-overlay__time">{formatTimestamp(durationSeconds)}</span>
</div>
</>
) : null}
+50
View File
@@ -0,0 +1,50 @@
export function formatViewers(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.`
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
return String(n)
}
export function formatViewersFull(n: number): string {
return n.toLocaleString('de-DE')
}
export function formatCount(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(n)
}
export function formatDuration(s: number): string {
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
return `${h}:${String(m).padStart(2, '0')}`
}
export function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
export function formatWatchedAt(ms: number): string {
const diff = Date.now() - ms
const mins = Math.floor(diff / 60_000)
const hours = Math.floor(diff / 3_600_000)
const days = Math.floor(diff / 86_400_000)
if (mins < 2) return 'Gerade eben'
if (mins < 60) return `Vor ${mins} Min.`
if (hours < 24) return `Vor ${hours} Std.`
if (days === 1) return 'Gestern'
if (days < 7) return `Vor ${days} Tagen`
return new Date(ms).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
}
export function formatTimestamp(s: number): string {
const total = Math.max(0, Math.floor(s))
const h = Math.floor(total / 3600)
const m = Math.floor((total % 3600) / 60)
const sec = total % 60
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
}
+1 -10
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { GamepadHintItem } from '../components/GamepadPrompt'
import LanguageBadge from '../components/LanguageBadge'
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
import { formatViewers, formatViewersFull } from '../lib/formatting'
interface Props {
hasFocus: boolean
@@ -17,16 +18,6 @@ type FocusRegion = 'shelf' | 'grid'
const STREAM_W = 220
const STREAM_GAP = 14
function formatViewers(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.`
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
return String(n)
}
function formatViewersFull(n: number): string {
return n.toLocaleString('de-DE')
}
export default function BrowseScreen({
hasFocus,
onRequestSidebar,
+8 -46
View File
@@ -5,6 +5,14 @@ import { PlaybackOverlay } from '../components/PlaybackOverlay'
import { VideoPlayer } from '../components/VideoPlayer'
import type { FollowedChannelInfo, VodChapter, VodInfo, VodProgress } from '../types/t4sd'
import { usePlaybackSession } from '../hooks/usePlaybackSession'
import {
formatCount,
formatDate,
formatDuration,
formatTimestamp,
formatViewers,
formatWatchedAt
} from '../lib/formatting'
interface Props {
channel: FollowedChannelInfo
@@ -16,56 +24,10 @@ type FocusRegion = 'hero' | 'shelf' | 'chapters'
const CARD_W = 260
const CARD_GAP = 16
function formatViewers(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.`
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
return String(n)
}
function formatCount(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(n)
}
function formatDuration(s: number): string {
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
return `${h}:${String(m).padStart(2, '0')}`
}
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
function formatWatchedAt(ms: number): string {
const diff = Date.now() - ms
const mins = Math.floor(diff / 60_000)
const hours = Math.floor(diff / 3_600_000)
const days = Math.floor(diff / 86_400_000)
if (mins < 2) return 'Gerade eben'
if (mins < 60) return `Vor ${mins} Min.`
if (hours < 24) return `Vor ${hours} Std.`
if (days === 1) return 'Gestern'
if (days < 7) return `Vor ${days} Tagen`
return new Date(ms).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
}
function resolveThumbnail(url: string | undefined, w = 1280, h = 720): string | undefined {
return url?.replace('{width}', String(w)).replace('{height}', String(h))
}
function formatTimestamp(s: number): string {
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
const sec = s % 60
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
}
export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
const [vods, setVods] = useState<VodInfo[]>([])
const [vodsLoading, setVodsLoading] = useState(true)