@@ -1,22 +1,26 @@
import { useCallback , useEffect , useRef , useState } from "react" ;
import { useNavigate , useParams } from "react-router-dom" ;
import { Link , useNavigate, useParams } from "react-router-dom" ;
import {
ArrowLeft , Lock , Maximize , Minimize , P aus e, Play ,
RotateCcw , RotateCw , Volume1 , Volume2 , VolumeX ,
ArrowLeft , CalendarDays , Check , Clock , Download , G aug e, Keyboard , Link2 ,
Lock , Maximize , Minimize , MonitorPlay , Pause , Pencil , PictureInPicture2 ,
Play , RotateCcw , RotateCw , Volume1 , Volume2 , VolumeX ,
} from "lucide-react" ;
import clsx from "clsx" ;
import { AuthError } from "../utils/api/client.ts" ;
import Card from "../components/ui/Card.tsx" ;
import Button from "../components/ui/Button.tsx" ;
import Badge from "../components/ui/Badge.tsx" ;
import EmptyState from "../components/ui/EmptyState.tsx" ;
import Spinner from "../components/ui/Spinner.tsx" ;
import Menu , { MenuItem } from "../components/ui/Menu.tsx" ;
import { dateToTimeAgo , formatTime , stringToDate } from "../utils/utils.ts" ;
// Both VoDs and clips share the same on-screen shape.
type MediaItem = {
title : string ,
description? : string ,
duration? : number ,
createdAt : string ,
} ;
@@ -28,20 +32,61 @@ type MediaPlayerProps = {
getMediaUrl : ( id : string ) = > string ,
/** Fetches the media metadata for the given id. */
fetchDetails : ( id : string ) = > Promise < MediaItem | null > ,
/** Where the "Edit details" action goes. */
editPath : ( id : string ) = > string ,
} ;
const ControlButton = ( { label , onClick , children } : { label : string ; onClick : ( ) = > void ; children : React.ReactNode } ) = > (
const SPEEDS = [ 0.5 , 0.75 , 1 , 1.25 , 1.5 , 2 ] ;
const SHORTCUTS : [ string , string ] [ ] = [
[ "Space / K" , "Play or pause" ] ,
[ "← / →" , "Skip 5 seconds" ] ,
[ "J / L" , "Skip 10 seconds" ] ,
[ "↑ / ↓" , "Volume" ] ,
[ "0 – 9" , "Jump to 0– 90%" ] ,
[ "M" , "Mute" ] ,
[ "F" , "Fullscreen" ] ,
[ "P" , "Picture in picture" ] ,
] ;
/** Turns a frame height into the label people actually use for it. */
const qualityLabel = ( height : number ) = > {
if ( height >= 2160 ) return "4K" ;
if ( height >= 1440 ) return "1440p" ;
if ( height >= 1080 ) return "1080p" ;
if ( height >= 720 ) return "720p" ;
if ( height >= 480 ) return "480p" ;
return ` ${ height } p ` ;
} ;
const Stat = ( { icon : Icon , label , value } : { icon : typeof Clock ; label : string ; value : string } ) = > (
< div className = "flex items-center gap-2" >
< Icon size = { 14 } className = "shrink-0 text-subtle" / >
< span className = "text-[11px] font-semibold uppercase tracking-[0.08em] text-subtle" > { label } < / span >
< span className = "font-mono text-[12.5px] tabular-nums text-fg" > { value } < / span >
< / div >
) ;
const ControlButton = ( { label , onClick , active = false , children } : {
label : string ;
onClick : ( ) = > void ;
active? : boolean ;
children : React.ReactNode ;
} ) = > (
< button
aria-label = { label }
title = { label }
onClick = { onClick }
className = "flex items-center justify-center rounded-lg p-1.5 text-white/90 transition-colors duration-150 hover:bg-white/15 hover:text-white"
className = { clsx (
"flex items-center justify-center rounded-lg p-1.5 transition-colors duration-150 hover:bg-white/15 hover:text-white" ,
active ? "text-accent" : "text-white/90" ,
) }
>
{ children }
< / button >
) ;
const MediaPlayer = ( { noun , getMediaUrl , fetchDetails } : MediaPlayerProps ) = > {
const MediaPlayer = ( { noun , getMediaUrl , fetchDetails , editPath } : MediaPlayerProps ) = > {
const { id } = useParams ( ) ;
const navigate = useNavigate ( ) ;
@@ -64,6 +109,8 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
const [ duration , setDuration ] = useState ( 0 ) ;
const [ bufferedPct , setBufferedPct ] = useState ( 0 ) ;
const [ isWaiting , setIsWaiting ] = useState ( false ) ;
const [ speed , setSpeed ] = useState ( 1 ) ;
const [ dimensions , setDimensions ] = useState < { width : number ; height : number } | null > ( null ) ;
// Volume
const [ volume , setVolume ] = useState ( 1 ) ;
@@ -71,8 +118,13 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
// UI
const [ isFullscreen , setIsFullscreen ] = useState ( false ) ;
const [ isPip , setIsPip ] = useState ( false ) ;
const [ controlsVisible , setControlsVisible ] = useState ( true ) ;
const [ isDragging , setIsDragging ] = useState ( false ) ;
const [ hoverTime , setHoverTime ] = useState < number | null > ( null ) ;
const [ showRemaining , setShowRemaining ] = useState ( false ) ;
const [ copied , setCopied ] = useState ( false ) ;
const [ shortcutsOpen , setShortcutsOpen ] = useState ( false ) ;
// ── Data loading ────────────────────────────────────────────────────────
// The video stream itself is loaded by the <video> element from `mediaUrl`,
@@ -109,7 +161,7 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
scheduleHide ( ) ;
} , [ scheduleHide ] ) ;
// ── Fullscreen ───────────────────── ─────────────────────────────────────
// ── Fullscreen & picture-in-picture ─────────────────────────────────────
useEffect ( ( ) = > {
const onChange = ( ) = > setIsFullscreen ( ! ! document . fullscreenElement ) ;
document . addEventListener ( 'fullscreenchange' , onChange ) ;
@@ -119,7 +171,33 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
const toggleFullscreen = useCallback ( ( ) = > {
if ( ! containerRef . current ) return ;
if ( document . fullscreenElement ) document . exitFullscreen ( ) ;
else containerRef . current . requestFullscreen ( ) ;
else containerRef . current . requestFullscreen ( ) . catch ( ( ) = > { } ) ;
} , [ ] ) ;
// React has no JSX props for these, so subscribe directly.
useEffect ( ( ) = > {
const video = videoRef . current ;
if ( ! video ) return ;
const onEnter = ( ) = > setIsPip ( true ) ;
const onLeave = ( ) = > setIsPip ( false ) ;
video . addEventListener ( "enterpictureinpicture" , onEnter ) ;
video . addEventListener ( "leavepictureinpicture" , onLeave ) ;
return ( ) = > {
video . removeEventListener ( "enterpictureinpicture" , onEnter ) ;
video . removeEventListener ( "leavepictureinpicture" , onLeave ) ;
} ;
} , [ ] ) ;
const togglePip = useCallback ( ( ) = > {
const video = videoRef . current ;
if ( ! video || ! document . pictureInPictureEnabled ) return ;
if ( document . pictureInPictureElement ) document . exitPictureInPicture ( ) . catch ( ( ) = > { } ) ;
else video . requestPictureInPicture ( ) . catch ( ( ) = > { } ) ;
} , [ ] ) ;
const changeSpeed = useCallback ( ( rate : number ) = > {
if ( videoRef . current ) videoRef . current . playbackRate = rate ;
setSpeed ( rate ) ;
} , [ ] ) ;
const skip = useCallback ( ( seconds : number ) = > {
@@ -129,6 +207,14 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
showControls ( ) ;
} , [ showControls ] ) ;
const nudgeVolume = useCallback ( ( delta : number ) = > {
const video = videoRef . current ;
if ( ! video ) return ;
video . volume = Math . min ( 1 , Math . max ( 0 , video . volume + delta ) ) ;
video . muted = video . volume === 0 ;
showControls ( ) ;
} , [ showControls ] ) ;
// ── Keyboard shortcuts ──────────────────────────────────────────────────
useEffect ( ( ) = > {
const onKey = ( e : KeyboardEvent ) = > {
@@ -136,6 +222,14 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
const video = videoRef . current ;
if ( ! video ) return ;
// Digits jump to that tenth of the timeline.
if ( /^Digit[0-9]$/ . test ( e . code ) && video . duration ) {
e . preventDefault ( ) ;
video . currentTime = ( Number ( e . code . slice ( 5 ) ) / 10 ) * video . duration ;
showControls ( ) ;
return ;
}
switch ( e . code ) {
case 'Space' : case 'KeyK' :
e . preventDefault ( ) ;
@@ -143,37 +237,40 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
else video . pause ( ) ;
showControls ( ) ;
break ;
case 'ArrowLeft' :
e . preventDefault ( ) ;
skip ( - 5 ) ;
break ;
case 'ArrowRight' :
e . preventDefault ( ) ;
skip ( 5 ) ;
break ;
case 'ArrowLeft' : e . preventDefault ( ) ; skip ( - 5 ) ; break ;
case 'ArrowRight' : e . preventDefault ( ) ; skip ( 5 ) ; break ;
case 'KeyJ' : e . preventDefault ( ) ; skip ( - 10 ) ; break ;
case 'KeyL' : e . preventDefault ( ) ; skip ( 10 ) ; break ;
case 'ArrowUp' : e . preventDefault ( ) ; nudgeVolume ( 0.1 ) ; break ;
case 'ArrowDown' : e . preventDefault ( ) ; nudgeVolume ( - 0.1 ) ; break ;
case 'KeyM' :
video . muted = ! video . muted ;
setIsMuted ( video . muted ) ;
break ;
case 'KeyF' :
toggleFullscreen ( ) ;
showControls ( ) ;
break ;
case 'KeyF' : toggleFullscreen ( ) ; break ;
case 'KeyP' : togglePip ( ) ; break ;
}
} ;
document . addEventListener ( 'keydown' , onKey ) ;
return ( ) = > document . removeEventListener ( 'keydown' , onKey ) ;
} , [ showControls , toggleFullscreen , skip ] ) ;
} , [ showControls , toggleFullscreen , togglePip , skip , nudgeVolume ] ) ;
// ── Progress bar seeking ────────────────────────────────────────────────
const seekTo = useCallback ( ( clientX : number ) = > {
const timeAtClientX = useCallback ( ( clientX : number ) = > {
const rect = progressRef . current ? . getBoundingClientRect ( ) ;
const video = videoRef . current ;
if ( ! rect || ! video || ! duration ) return ;
if ( ! rect || ! duration ) return null ;
const ratio = Math . max ( 0 , Math . min ( 1 , ( clientX - rect . left ) / rect . width ) ) ;
video . currentTime = ratio * duration ;
setCurrentTime ( ratio * duration ) ;
return ratio * duration ;
} , [ duration ] ) ;
const seekTo = useCallback ( ( clientX : number ) = > {
const time = timeAtClientX ( clientX ) ;
const video = videoRef . current ;
if ( time === null || ! video ) return ;
video . currentTime = time ;
setCurrentTime ( time ) ;
} , [ timeAtClientX ] ) ;
useEffect ( ( ) = > {
if ( ! isDragging ) return ;
const onMove = ( e : MouseEvent ) = > seekTo ( e . clientX ) ;
@@ -198,8 +295,6 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
const handleVolumeChange = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
const v = parseFloat ( e . target . value ) ;
setVolume ( v ) ;
setIsMuted ( v === 0 ) ;
if ( videoRef . current ) {
videoRef . current . volume = v ;
videoRef . current . muted = v === 0 ;
@@ -210,7 +305,6 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
const video = videoRef . current ;
if ( ! video ) return ;
video . muted = ! video . muted ;
setIsMuted ( video . muted ) ;
} ;
const togglePlay = ( ) = > {
@@ -221,9 +315,17 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
showControls ( ) ;
} ;
const handleCopyLink = ( ) = > {
navigator . clipboard . writeText ( window . location . href ) ;
setCopied ( true ) ;
setTimeout ( ( ) = > setCopied ( false ) , 2000 ) ;
} ;
const progressPct = duration > 0 ? ( currentTime / duration ) * 100 : 0 ;
const hoverPct = hoverTime !== null && duration > 0 ? ( hoverTime / duration ) * 100 : 0 ;
const effectiveVolume = isMuted ? 0 : volume ;
const VolumeIcon = effectiveVolume === 0 ? VolumeX : effectiveVolume < 0.5 ? Volume1 : Volume2 ;
const totalDuration = duration || item ? . duration || 0 ;
if ( notAuthenticated ) {
return (
@@ -243,7 +345,7 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
}
return (
< div className = "mx-auto flex h-full w-full max-w-6xl flex-col gap-4 px-4 py -5 sm:px-6" >
< div className = "mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 pb-12 pt -5 sm:px-6" >
< Button
variant = "ghost"
size = "sm"
@@ -257,10 +359,11 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
{ /* Player */ }
< div
ref = { containerRef }
className = "relative min-h-0 flex-1 select-none overflow-hidden rounded-2xl border border-line bg-black shadow-raised"
className = "player-shell relative aspect-video max-h-[68vh] w-full select-none overflow-hidden rounded-2xl border border-line bg-black shadow-raised"
onMouseMove = { showControls }
onMouseLeave = { ( ) = > isPlaying && setControlsVisible ( false ) }
onClick = { togglePlay }
onDoubleClick = { toggleFullscreen }
style = { { cursor : controlsVisible ? 'default' : 'none' } }
>
< video
@@ -278,9 +381,19 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
} }
onTimeUpdate = { handleTimeUpdate }
onLoadedMetadata = { ( ) = > {
if ( videoRef . current ) setDuration ( videoRef . current . duration ) ;
const video = videoRef . current ;
if ( ! video ) return ;
setDuration ( video . duration ) ;
setDimensions ( { width : video.videoWidth , height : video.videoHeight } ) ;
setMediaLoaded ( true ) ;
} }
onVolumeChange = { ( ) = > {
const video = videoRef . current ;
if ( ! video ) return ;
setVolume ( video . volume ) ;
setIsMuted ( video . muted ) ;
} }
onRateChange = { ( ) = > videoRef . current && setSpeed ( videoRef . current . playbackRate ) }
onWaiting = { ( ) = > setIsWaiting ( true ) }
onCanPlay = { ( ) = > setIsWaiting ( false ) }
onError = { ( e ) = > setError ( e . currentTarget . error ? . message || "An error occurred." ) }
@@ -293,15 +406,22 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
< / div >
) }
{ /* Big c entre play badge while paused */ }
{ /* C entre play badge while paused */ }
{ ! isPlaying && mediaLoaded && (
< div className = "pointer-events-none absolute inset-0 flex items-center justify-center" >
< span className = "flex h-16 w-16 items-center justify-center rounded-full bg-black/45 text-white backdrop-blur-sm" >
< span className = "flex h-16 w-16 animate-fade items-center justify-center rounded-full bg-black/45 text-white backdrop-blur-sm" >
< Play size = { 26 } fill = "currentColor" className = "ml-1" / >
< / span >
< / div >
) }
{ /* Speed badge, so a non-default rate is never a surprise */ }
{ speed !== 1 && (
< div className = "pointer-events-none absolute right-3 top-3 rounded-lg bg-black/60 px-2 py-1 font-mono text-[12px] font-medium text-white backdrop-blur-sm" >
{ speed } ×
< / div >
) }
{ /* Controls overlay — pointer-events-none so video clicks pass through */ }
< div
className = { clsx (
@@ -315,19 +435,37 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
< div
className = "pointer-events-auto relative px-4 pb-4 pt-10"
onClick = { ( e ) = > e . stopPropagation ( ) }
// Never fade the bar out from under a cursor that is using it.
onMouseEnter = { ( ) = > {
if ( hideTimerRef . current ) clearTimeout ( hideTimerRef . current ) ;
setControlsVisible ( true ) ;
} }
onMouseLeave = { ( ) = > isPlaying && scheduleHide ( ) }
>
{ /* Scrub bar */ }
< div
ref = { progressRef }
className = "group/prog relative mb-3 h-1 cursor-pointer rounded-full bg-white/25 transition-[height] duration-150 hover:h-1.5"
onMouseDown = { ( e ) = > { setIsDragging ( true ) ; seekTo ( e . clientX ) ; } }
onMouseMove = { ( e ) = > setHoverTime ( timeAtClientX ( e . clientX ) ) }
onMouseLeave = { ( ) = > setHoverTime ( null ) }
>
< div className = "absolute inset-y-0 left-0 rounded-full bg-white/35" style = { { width : ` ${ bufferedPct } % ` } } / >
< div className = "absolute inset-y-0 left-0 rounded-full bg-accent" style = { { width : ` ${ progressPct } % ` } } / >
< div
className = "absolute top-1/2 h-3.5 w-3.5 -translate-y-1/2 rounded-full bg-accent shadow opacity-0 transition-opacity group-hover/prog:opacity-100"
className = "absolute top-1/2 h-3.5 w-3.5 -translate-y-1/2 rounded-full bg-accent opacity-0 shadow transition-opacity group-hover/prog:opacity-100"
style = { { left : ` calc( ${ progressPct } % - 7px) ` } }
/ >
{ /* Hovered timestamp */ }
{ hoverTime !== null && (
< div
className = "pointer-events-none absolute bottom-5 -translate-x-1/2 rounded-md bg-black/85 px-1.5 py-0.5 font-mono text-[11px] tabular-nums text-white shadow"
style = { { left : ` ${ hoverPct } % ` } }
>
{ formatTime ( hoverTime ) }
< / div >
) }
< / div >
{ /* Buttons row */ }
@@ -361,12 +499,56 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
< / div >
< / div >
< span className = "ml-2 font-mono text-[12px] tabular-nums text-white/85" >
{ formatTime ( currentTime ) } / { formatTime ( duration ) }
< / span >
< button
onClick = { ( ) = > setShowRemaining ( ( v ) = > ! v ) }
title = { showRemaining ? "Show elapsed time" : "Show remaining time" }
className = "ml-2 font-mono text-[12px] tabular-nums text-white/85 transition-colors hover:text-white"
>
{ showRemaining
? ` - ${ formatTime ( Math . max ( 0 , duration - currentTime ) ) } `
: formatTime ( currentTime ) }
< span className = "text-white/50" > / { formatTime ( duration ) } < / span >
< / button >
< div className = "flex-1" / >
< Menu
placement = "top"
align = "right"
menuClassName = "min-w-32"
trigger = { ( ) = > (
< span
title = "Playback speed"
className = "flex items-center gap-1 rounded-lg px-2 py-1.5 text-[12px] font-medium text-white/90 transition-colors hover:bg-white/15 hover:text-white"
>
< Gauge size = { 16 } / >
{ speed } ×
< / span >
) }
>
{ ( close ) = > (
< >
{ SPEEDS . map ( ( rate ) = > (
< MenuItem
key = { rate }
onClick = { ( ) = > { changeSpeed ( rate ) ; close ( ) ; } }
icon = { rate === speed
? < Check size = { 14 } className = "text-accent" / >
: < span className = "w-3.5" / > }
>
{ rate === 1 ? "Normal" : ` ${ rate } × ` }
< / MenuItem >
) ) }
< / >
) }
< / Menu >
{ document . pictureInPictureEnabled && (
< ControlButton label = "Picture in picture" onClick = { togglePip } active = { isPip } >
< PictureInPicture2 size = { 17 } / >
< / ControlButton >
) }
< ControlButton
label = { isFullscreen ? "Exit fullscreen" : "Fullscreen" }
onClick = { toggleFullscreen }
@@ -379,25 +561,128 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => {
< / div >
{ error && (
< p className = "shrink-0 rounded-xl border border-danger/25 bg-danger/8 px-4 py-2.5 text-[13px] text-danger" >
< p className = "rounded-xl border border-danger/25 bg-danger/8 px-4 py-2.5 text-[13px] text-danger" >
{ error }
< / p >
) }
{ ! mediaLoaded && ! error && (
< p className = "flex shrink-0 items-center gap-2 text-[13px] text-muted" >
< p className = "flex items-center gap-2 text-[13px] text-muted" >
< Spinner size = { 13 } / >
Loading { noun . toLowerCase ( ) } …
< / p >
) }
< Card className = "shrink-0 p-5" >
< h1 className = "font-serif text-xl leading-tight text-fg" > { item ? . title || "Untitled" } < / h1 >
< p className = "mt-1 text-[13px] text-subtle" > { timeAgo } < / p >
{ item ? . description && (
< p className = "mt-3 whitespace-pre-line border-t border-line pt-3 text-sm leading-relaxed text-muted " >
{ item . description }
< / p >
{ /* Details */ }
< Card className = "overflow-hidden" >
< div className = "p-5 sm:p-6" >
< div className = "flex flex-wrap items-start justify-between gap-4" >
< div className = "min-w-0 " >
< Badge tone = "accent" > { noun } < / Badge >
< h1 className = "mt-2.5 font-serif text-2xl leading-tight text-fg" >
{ item ? . title || "Untitled" }
< / h1 >
< p className = "mt-1.5 text-[13px] text-subtle" >
{ timeAgo }
{ item ? . createdAt && (
< >
{ " · " }
{ stringToDate ( item . createdAt ) . toLocaleString ( undefined , {
dateStyle : "medium" ,
timeStyle : "short" ,
} ) }
< / >
) }
< / p >
< / div >
< div className = "flex shrink-0 items-center gap-2" >
< Button
variant = "secondary"
size = "sm"
icon = { copied ? < Check size = { 14 } / > : < Link2 size = { 14 } / > }
onClick = { handleCopyLink }
>
{ copied ? "Copied" : "Copy link" }
< / Button >
{ mediaUrl && (
< a href = { mediaUrl } download = { ` ${ item ? . title || noun } .mp4 ` } >
< Button variant = "secondary" size = "sm" icon = { < Download size = { 14 } / > } >
Download
< / Button >
< / a >
) }
{ id && (
< Link to = { editPath ( id ) } >
< Button variant = "secondary" size = "sm" icon = { < Pencil size = { 14 } / > } >
Edit
< / Button >
< / Link >
) }
< / div >
< / div >
{ item ? . description ? (
< p className = "mt-5 whitespace-pre-line border-t border-line pt-5 text-sm leading-relaxed text-muted" >
{ item . description }
< / p >
) : (
< p className = "mt-5 border-t border-line pt-5 text-sm italic text-subtle" >
No description yet .
< / p >
) }
< / div >
{ /* Technical strip */ }
< div className = "flex flex-wrap items-center gap-x-6 gap-y-2.5 border-t border-line bg-sunken px-5 py-3.5 sm:px-6" >
< Stat icon = { Clock } label = "Length" value = { formatTime ( totalDuration ) } / >
{ dimensions && dimensions . height > 0 && (
< Stat
icon = { MonitorPlay }
label = "Video"
value = { ` ${ dimensions . width } × ${ dimensions . height } · ${ qualityLabel ( dimensions . height ) } ` }
/ >
) }
< Stat icon = { Gauge } label = "Speed" value = { ` ${ speed } × ` } / >
{ item ? . createdAt && (
< Stat
icon = { CalendarDays }
label = "Added"
value = { stringToDate ( item . createdAt ) . toLocaleDateString ( undefined , {
year : "numeric" , month : "short" , day : "numeric" ,
} ) }
/ >
) }
{ mediaLoaded && bufferedPct > 0 && bufferedPct < 99.5 && (
< Stat icon = { Download } label = "Buffered" value = { ` ${ Math . round ( bufferedPct ) } % ` } / >
) }
< / div >
< / Card >
{ /* Shortcuts */ }
< Card className = "overflow-hidden" >
< button
onClick = { ( ) = > setShortcutsOpen ( ( v ) = > ! v ) }
className = "flex w-full items-center gap-2.5 px-5 py-3.5 text-left transition-colors hover:bg-surface-hover sm:px-6"
>
< Keyboard size = { 16 } className = "text-subtle" / >
< span className = "text-[13px] font-medium text-fg" > Keyboard shortcuts < / span >
< span className = "ml-auto text-[12px] text-subtle" > { shortcutsOpen ? "Hide" : "Show" } < / span >
< / button >
{ shortcutsOpen && (
< div className = "grid animate-rise grid-cols-1 gap-x-8 gap-y-2 border-t border-line px-5 py-4 sm:grid-cols-2 sm:px-6 lg:grid-cols-4" >
{ SHORTCUTS . map ( ( [ keys , action ] ) = > (
< div key = { keys } className = "flex items-center justify-between gap-3" >
< span className = "text-[13px] text-muted" > { action } < / span >
< kbd className = "shrink-0 rounded-md border border-line bg-sunken px-2 py-0.5 font-mono text-[11px] text-fg" >
{ keys }
< / kbd >
< / div >
) ) }
< / div >
) }
< / Card >
< / div >