@@ -1,10 +1,20 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
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[]>;
|
||||
}
|
||||
|
||||
export function PlaybackPageIndicator({
|
||||
@@ -12,45 +22,79 @@ export function PlaybackPageIndicator({
|
||||
current,
|
||||
progress,
|
||||
onGoTo,
|
||||
presenceBySegment,
|
||||
}: PlaybackPageIndicatorProps) {
|
||||
if (total === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-0.5 px-1">
|
||||
{Array.from({ length: total }, (_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onGoTo(i);
|
||||
}}
|
||||
className="group relative h-3 flex-1"
|
||||
>
|
||||
{/* Dim track */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 h-1 bg-white/30 transition-all",
|
||||
"group-hover:h-1.5 group-hover:top-0.5",
|
||||
<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">
|
||||
{/* Presence avatars above the track */}
|
||||
{presence && presence.length > 0 && (
|
||||
<SegmentPresenceAvatars presence={presence} />
|
||||
)}
|
||||
/>
|
||||
{/* Fill */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 top-0 h-1 bg-white/90 transition-all",
|
||||
"group-hover:h-1.5 group-hover:top-0.5",
|
||||
)}
|
||||
style={{
|
||||
width:
|
||||
i < current
|
||||
? "100%"
|
||||
: i === current
|
||||
? `${progress * 100}%`
|
||||
: "0%",
|
||||
transition: i === current ? "width 300ms linear" : "none",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user