fix: split collaboration request controllers

هذا الالتزام موجود في:
boutmoun123
2026-05-31 17:05:58 +03:00
الأصل 49e132909e
التزام 1973b8b904
10 ملفات معدلة مع 402 إضافات و18 حذوفات

عرض الملف

@@ -0,0 +1,162 @@
"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>
);
}