163 أسطر
6.0 KiB
TypeScript
163 أسطر
6.0 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { FileAudio, FileImage, FileVideo, LinkIcon } from "lucide-react";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { resolveMediaUrl } from "@/lib/media-url";
|
|
import type { ApiPost } from "@/types/api";
|
|
|
|
function pickImageUrl(post: ApiPost) {
|
|
const variant = Array.isArray(post.imageVariants)
|
|
? post.imageVariants[0]
|
|
: post.imageVariants;
|
|
|
|
return (
|
|
post.imageItems?.find((item) => item.variants?.medium || item.url)?.variants?.medium ??
|
|
post.imageItems?.find((item) => item.url)?.url ??
|
|
variant?.medium ??
|
|
variant?.thumbnail ??
|
|
post.imageUrls?.[0] ??
|
|
""
|
|
);
|
|
}
|
|
|
|
function getPostMedia(post: ApiPost) {
|
|
const imageUrl = pickImageUrl(post);
|
|
const thumbnailUrl = post.thumbnailVariants?.medium ?? post.thumbnailVariants?.thumbnail ?? post.thumbnailUrl ?? "";
|
|
const playbackUrl = post.hlsUrl || post.videoUrl || "";
|
|
const audioUrl = post.audioUrl ?? "";
|
|
|
|
return {
|
|
imageUrl: resolveMediaUrl(imageUrl),
|
|
thumbnailUrl: resolveMediaUrl(thumbnailUrl),
|
|
playbackUrl: resolveMediaUrl(playbackUrl),
|
|
audioUrl: resolveMediaUrl(audioUrl),
|
|
};
|
|
}
|
|
|
|
export function getMediaHealth(post: ApiPost) {
|
|
const checks = {
|
|
hasImage: Boolean(pickImageUrl(post)),
|
|
hasVideo: Boolean(post.videoUrl || post.hlsUrl),
|
|
hasAudio: Boolean(post.audioUrl),
|
|
hasThumbnail: Boolean(post.thumbnailUrl || post.thumbnailVariants?.thumbnail || post.thumbnailVariants?.medium),
|
|
hasDuration: typeof post.durationSeconds === "number" && post.durationSeconds > 0,
|
|
hasOptimizedImage:
|
|
Boolean(post.imageItems?.some((item) => item.variants && Object.keys(item.variants).length > 0)) ||
|
|
Boolean(post.imageVariants && Object.keys(post.imageVariants).length > 0),
|
|
hasHls: Boolean(post.hlsUrl),
|
|
};
|
|
|
|
const problems: string[] = [];
|
|
if (post.postType === "image" && !checks.hasImage) problems.push("missing image");
|
|
if (post.postType === "image" && !checks.hasOptimizedImage) problems.push("no image variants");
|
|
if (post.postType === "video" && !checks.hasVideo) problems.push("missing video");
|
|
if (post.postType === "video" && !checks.hasThumbnail) problems.push("missing thumbnail");
|
|
if (post.postType === "video" && !checks.hasHls) problems.push("no HLS");
|
|
if ((post.postType === "video" || post.postType === "audio") && !checks.hasDuration) {
|
|
problems.push("missing duration");
|
|
}
|
|
if (post.postType === "audio" && !checks.hasAudio) problems.push("missing audio");
|
|
|
|
return {
|
|
checks,
|
|
problems,
|
|
score: Math.max(0, 100 - problems.length * 20),
|
|
};
|
|
}
|
|
|
|
export function MediaInspector({ post, compact = false }: { post: ApiPost; compact?: boolean }) {
|
|
const media = getPostMedia(post);
|
|
const health = getMediaHealth(post);
|
|
const badges = [
|
|
post.hlsUrl ? "HLS" : null,
|
|
post.thumbnailUrl ? "thumbnail" : null,
|
|
post.durationSeconds ? `${Math.round(post.durationSeconds)}s` : null,
|
|
post.processingStatus ? post.processingStatus : null,
|
|
].filter(Boolean);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="overflow-hidden rounded-lg border border-border/70 bg-background/30">
|
|
{post.postType === "video" && media.playbackUrl ? (
|
|
<video
|
|
className="aspect-video w-full bg-black object-contain"
|
|
controls
|
|
preload="metadata"
|
|
poster={media.thumbnailUrl || undefined}
|
|
>
|
|
<source src={media.playbackUrl} />
|
|
</video>
|
|
) : post.postType === "audio" && media.audioUrl ? (
|
|
<div className="space-y-4 p-4">
|
|
{media.thumbnailUrl ? (
|
|
<Image
|
|
src={media.thumbnailUrl}
|
|
alt={post.content || "audio thumbnail"}
|
|
width={640}
|
|
height={360}
|
|
unoptimized
|
|
className="aspect-video w-full rounded-md object-cover"
|
|
/>
|
|
) : (
|
|
<div className="flex aspect-video w-full items-center justify-center rounded-md bg-secondary/50">
|
|
<FileAudio className="h-8 w-8 text-primary" />
|
|
</div>
|
|
)}
|
|
<audio className="w-full" controls preload="metadata" src={media.audioUrl} />
|
|
</div>
|
|
) : media.imageUrl ? (
|
|
<Image
|
|
src={media.imageUrl}
|
|
alt={post.imageItems?.[0]?.altText || post.content || "post image"}
|
|
width={720}
|
|
height={720}
|
|
unoptimized
|
|
className={compact ? "aspect-video w-full object-cover" : "max-h-[520px] w-full object-contain"}
|
|
/>
|
|
) : (
|
|
<div className="flex aspect-video w-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
|
{post.postType === "video" ? <FileVideo className="h-5 w-5" /> : <FileImage className="h-5 w-5" />}
|
|
No preview media
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Badge variant={health.problems.length ? "warning" : "success"}>
|
|
media health {health.score}%
|
|
</Badge>
|
|
{badges.map((badge) => (
|
|
<Badge key={badge} variant="muted">
|
|
{badge}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
{health.problems.length ? (
|
|
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
|
|
{health.problems.map((problem) => (
|
|
<span key={problem} className="rounded-md border border-border/70 px-2 py-1">
|
|
{problem}
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
|
|
{!compact ? (
|
|
<div className="grid gap-2 text-xs text-muted-foreground">
|
|
{[media.imageUrl, media.playbackUrl, media.audioUrl, media.thumbnailUrl].filter(Boolean).map((url) => (
|
|
<div key={url} className="flex min-w-0 items-center gap-2 rounded-md border border-border/60 px-2 py-1">
|
|
<LinkIcon className="h-3.5 w-3.5 shrink-0" />
|
|
<span dir="ltr" className="truncate font-mono">
|
|
{url}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|