jbilcke-hf's picture
jbilcke-hf HF staff
working on some new features
3d4392e
raw
history blame
7.1 kB
"use client"
import { useEffect, useRef, useState } from "react"
import Link from "next/link"
import { RiCheckboxCircleFill } from "react-icons/ri"
import { cn } from "@/lib/utils/cn"
import { MediaDisplayLayout, MediaInfo } from "@/types/general"
import { formatDuration } from "@/lib/formatters/formatDuration"
import { formatTimeAgo } from "@/lib/formatters/formatTimeAgo"
import { isCertifiedUser } from "@/app/certification"
import { transparentImage } from "@/lib/utils/transparentImage"
import { DefaultAvatar } from "../default-avatar"
import { formatLargeNumber } from "@/lib/formatters/formatLargeNumber"
export function VideoCard({
media,
className = "",
layout = "grid",
onSelect,
selected,
index
}: {
media: MediaInfo
className?: string
layout?: MediaDisplayLayout
onSelect?: (media: MediaInfo) => void
selected?: boolean
index: number
}) {
const ref = useRef<HTMLVideoElement>(null)
const [duration, setDuration] = useState(0)
const [channelThumbnail, setChannelThumbnail] = useState(media.channel.thumbnail)
const [mediaThumbnail, setMediaThumbnail] = useState(
`https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${media.id}.webp`
)
const [mediaThumbnailReady, setMediaThumbnailReady] = useState(false)
const [shouldLoadMedia, setShouldLoadMedia] = useState(false)
const isCompact = layout === "vertical"
const handlePointerEnter = () => {
// ref.current?.load()
ref.current?.play()
}
const handlePointerLeave = () => {
ref.current?.pause()
// ref.current?.load()
}
const handleLoad = () => {
if (ref.current?.readyState) {
setDuration(ref.current.duration)
}
}
const handleClick = () => {
onSelect?.(media)
}
const handleBadChannelThumbnail = () => {
try {
if (channelThumbnail) {
setChannelThumbnail("")
}
} catch (err) {
}
}
useEffect(() => {
setTimeout(() => {
setShouldLoadMedia(true)
}, index * 1500)
}, [index])
// uncomment this as a temporary hack when developping, to avoid making too many fetches and downloads
return null;
return (
<Link href={`${process.env.NEXT_PUBLIC_DOMAIN}/watch?v=${media.id}`}>
<div
className={cn(
`w-full flex`,
isCompact
? `space-x-2`
: `flex-col space-y-3`,
`bg-line-900`,
`cursor-pointer`,
className,
)}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
// onClick={handleClick}
>
{/* VIDEO BLOCK */}
<div
className={cn(
`flex flex-col items-center justify-center`,
isCompact ? `` : ``
)}
>
<div className={cn(
`relative w-full`,
`aspect-video`,
// `aspect-video rounded-xl overflow-hidden`,
isCompact ? `w-42 h-24` : ``
)}>
{mediaThumbnailReady && shouldLoadMedia
? <video
// mute the video
muted
// prevent iOS from attempting to open the video in full screen, which is annoying
playsInline
ref={ref}
src={media.assetUrl}
className={cn(
`w-full h-full`,
`object-cover`,
`rounded-xl overflow-hidden aspect-video`,
duration > 0 ? `opacity-100`: 'opacity-0',
`transition-all duration-500`,
)}
onLoadedMetadata={handleLoad}
/> : null}
<img
src={mediaThumbnail}
className={cn(
`absolute`,
`object-cover`,
`rounded-xl overflow-hidden aspect-video`,
mediaThumbnailReady ? `opacity-100`: 'opacity-0',
`hover:opacity-0 w-full h-full top-0 z-30`,
//`pointer-events-none`,
`transition-all duration-500 hover:delay-300 ease-in-out`,
)}
onMouseEnter={() => {
setShouldLoadMedia(true)
}}
onLoad={() => {
setMediaThumbnailReady(true)
}}
onError={() => {
setMediaThumbnail(transparentImage)
setMediaThumbnailReady(false)
}}
/>
</div>
<div className={cn(
// `aspect-video`,
`z-40`,
`w-full flex flex-row items-end justify-end`
)}>
<div className={cn(
`-mt-8`,
`mr-0`,
)}
>
<div className={cn(
`mb-[5px]`,
`mr-[5px]`,
`flex flex-col items-center justify-center text-center`,
`bg-neutral-900 rounded`,
`text-2xs font-semibold px-[3px] py-[1px]`,
isFinite(duration) && !isNaN(duration) && duration > 0 ? 'opacity-100' : 'opacity-0'
)}
>{formatDuration(duration)}</div>
</div>
</div>
</div>
{/* TEXT BLOCK */}
<div className={cn(
`flex flex-row`,
`flex-none`,
isCompact ? `w-40 lg:w-44 xl:w-51` : `space-x-4`,
)}>
{
isCompact ? null
: channelThumbnail ? <div className="flex flex-col">
<div className="flex w-9 rounded-full overflow-hidden">
<img
src={channelThumbnail}
onError={handleBadChannelThumbnail}
/>
</div>
</div>
: <DefaultAvatar
username={media.channel.datasetUser}
bgColor="#fde047"
textColor="#1c1917"
width={36}
roundShape
/>}
<div className={cn(
`flex flex-col`,
isCompact ? `` : `flex-grow`
)}>
<h3 className={cn(
`text-zinc-100 font-medium mb-0 line-clamp-2`,
isCompact ? `text-sm mb-1.5` : `text-base`
)}>{media.label}</h3>
<div className={cn(
`flex flex-row items-center`,
`text-neutral-400 font-normal space-x-1`,
isCompact ? `text-xs` : `text-sm`
)}>
<div>{media.channel.label}</div>
{isCertifiedUser(media.channel.datasetUser) ? <div><RiCheckboxCircleFill className="" /></div> : null}
</div>
<div className={cn(
`flex flex-row`,
`text-neutral-400 font-normal`,
isCompact ? `text-xs` : `text-sm`,
`space-x-1`
)}>
<div>{formatLargeNumber(media.numberOfViews)} views</div>
<div className="font-semibold scale-125">·</div>
<div>{formatTimeAgo(media.updatedAt)}</div>
</div>
</div>
</div>
</div>
</Link>
)
}