107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import type { HumanPresence } from "@/hooks/use-presence-positions";
|
|
|
|
const MAX_VISIBLE_AVATARS = 3;
|
|
|
|
interface PlaybackPageIndicatorProps {
|
|
total: number;
|
|
current: number;
|
|
progress: number;
|
|
onGoTo: (index: number) => void;
|
|
presenceBySegment?: Map<number, HumanPresence[]>;
|
|
/** Render only avatars or only tracks. Omit to render both. */
|
|
layer?: "avatars" | "tracks";
|
|
}
|
|
|
|
export function PlaybackPageIndicator({
|
|
total,
|
|
current,
|
|
progress,
|
|
onGoTo,
|
|
presenceBySegment,
|
|
layer,
|
|
}: PlaybackPageIndicatorProps) {
|
|
if (total === 0) return null;
|
|
|
|
const showAvatars = layer !== "tracks";
|
|
const showTracks = layer !== "avatars";
|
|
|
|
return (
|
|
<div className="flex w-full items-end gap-px leading-none">
|
|
{Array.from({ length: total }, (_, i) => {
|
|
const presence = presenceBySegment?.get(i);
|
|
return (
|
|
<div key={i} className="flex flex-1 flex-col items-stretch">
|
|
{showAvatars && presence && presence.length > 0 && (
|
|
<SegmentPresenceAvatars presence={presence} />
|
|
)}
|
|
{showTracks && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onGoTo(i);
|
|
}}
|
|
className="group relative block h-3 w-full"
|
|
>
|
|
{/* Dim track */}
|
|
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/30 transition-all group-hover:h-1.5" />
|
|
{/* Fill */}
|
|
<div
|
|
className="absolute left-0 bottom-0 h-[3px] bg-white/90 transition-all group-hover:h-1.5"
|
|
style={{
|
|
width:
|
|
i < current
|
|
? "100%"
|
|
: i === current
|
|
? `${progress * 100}%`
|
|
: "0%",
|
|
transition: i === current ? "width 300ms linear" : "none",
|
|
}}
|
|
/>
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SegmentPresenceAvatars({
|
|
presence,
|
|
}: {
|
|
presence: HumanPresence[];
|
|
}) {
|
|
const visible = presence.slice(0, MAX_VISIBLE_AVATARS);
|
|
const overflow = presence.length - MAX_VISIBLE_AVATARS;
|
|
|
|
return (
|
|
<div className="flex items-center justify-center -space-x-1.5 pb-0.5">
|
|
{visible.map((human) => (
|
|
<Tooltip key={human.humanId}>
|
|
<TooltipTrigger asChild>
|
|
<Avatar size="xs" className="ring-1 ring-black/50">
|
|
<AvatarFallback>
|
|
{human.emailPrefix.slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" className="text-xs">
|
|
{human.email}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
))}
|
|
{overflow > 0 && (
|
|
<span className="text-[10px] text-white/70 pl-1">
|
|
+{overflow}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|