Files
llink/js/src/features/particles/particle-preview.tsx
T
2026-03-30 12:53:35 -07:00

175 lines
5.2 KiB
TypeScript

import { useEffect, useState } from "react";
import type { Particle } from "@/api/types";
import { apiClient } from "@/api/client";
import { Skeleton } from "@/components/ui/skeleton";
import {
Video,
Mic,
ScrollText,
BookOpen,
FileIcon,
FolderIcon,
} from "lucide-react";
export function ParticlePreview({ particle }: { particle: Particle }) {
switch (particle.type) {
case "text":
return <TextPreview particle={particle} />;
case "media":
return <MediaPreview particle={particle} />;
case "quest":
return <QuestPreview particle={particle} />;
case "paper":
return <PaperPreview particle={particle} />;
case "file":
return <FilePreview particle={particle} />;
case "folder":
return <FolderPreview particle={particle} />;
default:
return <EmptyPreview />;
}
}
function TextPreview({ particle }: { particle: Extract<Particle, { type: "text" }> }) {
const truncated =
particle.properties.content.length > 30
? particle.properties.content.slice(0, 30) + "..."
: particle.properties.content;
return (
<div className="flex h-full w-full items-center justify-center p-4">
<p className="line-clamp-4 text-center text-xl leading-relaxed">
{truncated}
</p>
</div>
);
}
function MediaPreview({ particle }: { particle: Extract<Particle, { type: "media" }> }) {
const { mime_type, duration_ms } = particle.properties;
const isVideo = mime_type.startsWith("video");
const durationSec = Math.round(duration_ms / 1000);
const durationLabel = `${Math.floor(durationSec / 60)}:${String(durationSec % 60).padStart(2, "0")}`;
if (isVideo) {
return <VideoThumbnail particleId={particle.properties.object_id} duration={durationLabel} />;
}
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-black/90">
<Mic className="h-8 w-8 text-white/60" />
<span className="font-mono text-xs text-white/50">{durationLabel}</span>
</div>
);
}
function VideoThumbnail({
particleId,
duration,
}: {
particleId: string;
duration: string;
}) {
const [url, setUrl] = useState<string | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
let cancelled = false;
apiClient
.getParticleDownloadUrl(particleId)
.then((downloadUrl) => {
if (!cancelled) setUrl(downloadUrl);
})
.catch(() => {
if (!cancelled) setError(true);
});
return () => {
cancelled = true;
};
}, [particleId]);
if (error) {
return (
<div className="flex h-full w-full items-center justify-center bg-black/80">
<Video className="h-8 w-8 text-white/40" />
</div>
);
}
if (!url) {
return <Skeleton className="h-full w-full rounded-none" />;
}
return (
<div className="relative h-full w-full bg-black">
<video
src={`${url}#t=2`}
preload="metadata"
muted
playsInline
className="h-full w-full object-cover"
/>
<span className="absolute right-1.5 bottom-1.5 rounded bg-black/70 px-1.5 py-0.5 font-mono text-[10px] text-white/80">
{duration}
</span>
</div>
);
}
function QuestPreview({ particle }: { particle: Extract<Particle, { type: "quest" }> }) {
const { title, status } = particle.properties;
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-amber-500/10 p-4">
<ScrollText className="h-6 w-6 text-amber-600/70 dark:text-amber-400/70" />
<p className="line-clamp-2 text-center text-sm font-medium">
{title}
</p>
{status && (
<span className="text-muted-foreground text-[10px] uppercase tracking-wide">
{status}
</span>
)}
</div>
);
}
function PaperPreview({ particle }: { particle: Extract<Particle, { type: "paper" }> }) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-blue-500/10 p-4">
<BookOpen className="h-6 w-6 text-blue-600/70 dark:text-blue-400/70" />
<p className="line-clamp-2 text-center text-sm font-medium">
{particle.properties.title}
</p>
</div>
);
}
function FilePreview({ particle }: { particle: Extract<Particle, { type: "file" }> }) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-emerald-500/10 p-4">
<FileIcon className="h-6 w-6 text-emerald-600/70 dark:text-emerald-400/70" />
<p className="text-muted-foreground line-clamp-1 text-center text-xs">
{particle.properties.filename}
</p>
</div>
);
}
function FolderPreview({ particle }: { particle: Extract<Particle, { type: "folder" }> }) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-orange-500/10 p-4">
<FolderIcon className="h-6 w-6 text-orange-600/70 dark:text-orange-400/70" />
<p className="text-muted-foreground line-clamp-1 text-center text-xs">
{particle.properties.name}
</p>
</div>
);
}
function EmptyPreview() {
return (
<div className="flex h-full w-full items-center justify-center">
<p className="text-muted-foreground text-xs italic">No messages yet</p>
</div>
);
}