Files
flxn-app/src/components/glitch-avatar.tsx
2025-10-04 18:41:46 -05:00

180 lines
4.2 KiB
TypeScript

import { useState, useEffect, useRef } from "react";
import { Paper, Box } from "@mantine/core";
import {
Avatar as MantineAvatar,
AvatarProps as MantineAvatarProps,
} from "@mantine/core";
interface GlitchAvatarProps
extends Omit<MantineAvatarProps, "radius" | "color" | "size"> {
name: string;
src?: string;
glitchSrc?: string;
size?: number;
radius?: string | number;
withBorder?: boolean;
contain?: boolean;
children?: React.ReactNode;
px?: string | number;
}
const GlitchAvatar = ({
name,
src,
glitchSrc,
size = 35,
radius = "100%",
withBorder = true,
contain = false,
children,
px,
...props
}: GlitchAvatarProps) => {
const [showGlitch, setShowGlitch] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (!glitchSrc) return;
const scheduleNextGlitch = () => {
const delay = Math.random() * 10000 + 5000;
return setTimeout(() => {
setShowGlitch(true);
setIsPlaying(true);
setTimeout(() => {
setShowGlitch(false);
setIsPlaying(false);
scheduleNextGlitch();
}, 4000);
}, delay);
};
const timeoutId = scheduleNextGlitch();
return () => clearTimeout(timeoutId);
}, [glitchSrc]);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const handleEnded = () => {
setShowGlitch(false);
setIsPlaying(false);
};
video.addEventListener("ended", handleEnded);
return () => video.removeEventListener("ended", handleEnded);
}, []);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
video.load();
}, [glitchSrc]);
useEffect(() => {
const video = videoRef.current;
if (!video || !showGlitch || !isPlaying) return;
video.currentTime = 0;
video.play().catch((err) => {
console.error("Failed to play glitch", err);
});
}, [showGlitch, isPlaying]);
return (
<Box
style={{
padding: "8px",
borderRadius:
typeof radius === "number"
? `${radius + 8}px`
: "calc(var(--mantine-radius-md) + 8px)",
position: "relative",
}}
>
<Box
style={{
opacity: showGlitch ? 0 : 1,
transition: "opacity 0.05s ease-in-out",
}}
>
<Paper
py={size / 12.5}
px={size / 20}
bg="var(--mantine-color-default-border)"
radius={radius}
withBorder={false}
style={{
cursor: "default",
}}
>
<MantineAvatar
alt={name}
key={name}
name={name}
color="initials"
size={size}
radius={radius}
w={size}
styles={{
image: {
objectFit: contain ? "contain" : "cover",
},
}}
src={src}
{...props}
>
{children}
</MantineAvatar>
</Paper>
</Box>
{glitchSrc && (
<Box
style={{
position: "absolute",
top: "8px",
left: "8px",
opacity: showGlitch ? 1 : 0,
visibility: showGlitch ? "visible" : "hidden",
transition: "opacity 0.05s ease-in-out",
pointerEvents: "none",
}}
>
<Paper
py={size / 12.5}
px={size / 20}
bg="var(--mantine-color-default-border)"
radius={radius}
withBorder={false}
style={{
overflow: "hidden",
}}
>
<video
ref={videoRef}
src={glitchSrc}
style={{
width: `${size}px`,
height: `${size}px`,
objectFit: contain ? "contain" : "cover",
borderRadius: typeof radius === "number" ? `${radius}px` : radius,
display: "block",
}}
muted
playsInline
preload="auto"
/>
</Paper>
</Box>
)}
</Box>
);
};
export default GlitchAvatar;