الملفات
back_end_oudelaa/oudelaa_dashboard/components/dashboard/post-preview-card.tsx
boutmoun123 8863f61d00
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
Add Oudelaa dashboard API integration
2026-05-25 20:36:52 +03:00

158 أسطر
6.5 KiB
TypeScript

"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import { AudioLines, Images, PlayCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { formatDateTime } from "@/lib/format";
import { getPostAuthor, getPostPreviewMedia, getUserLabel } from "@/lib/post-utils";
import type { ApiPost } from "@/types/api";
function formatDuration(durationSeconds?: number | null) {
if (typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds) || durationSeconds <= 0) {
return null;
}
const minutes = Math.floor(durationSeconds / 60);
const seconds = Math.floor(durationSeconds % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
export function PostPreviewCard({ post }: { post: ApiPost }) {
const author = getPostAuthor(post);
const media = getPostPreviewMedia(post);
const likes = post.engagement?.likesCount ?? post.likesCount ?? 0;
const comments = post.engagement?.commentsCount ?? post.commentsCount ?? 0;
const shares = post.engagement?.shareCount ?? post.shareCount ?? 0;
const waveformPeaks = Array.isArray(post.waveformPeaks) && post.waveformPeaks.length ? post.waveformPeaks : [18, 32, 24, 44, 28, 40, 22, 36, 26, 30];
const durationLabel = formatDuration(post.durationSeconds);
const [imageFailed, setImageFailed] = useState(false);
const [sourceFailed, setSourceFailed] = useState(false);
useEffect(() => {
setImageFailed(false);
setSourceFailed(false);
}, [media.url, media.sourceUrl]);
const showAudioPreview = media.kind === "audio" && !!media.sourceUrl && !sourceFailed;
const showVideoPreview = media.kind === "video" && !!media.sourceUrl && !sourceFailed && (!media.url || imageFailed);
const showImagePreview = !!media.url && !imageFailed;
const showMediaShell = media.kind !== "text";
const showUnavailable = !showImagePreview && !showVideoPreview && !showAudioPreview;
return (
<Card className="overflow-hidden border-border/70">
<CardContent className="p-0">
{showMediaShell ? (
<div className="relative aspect-[16/9] border-b border-border/70 bg-secondary/30">
{showImagePreview ? (
<Image
src={media.url}
alt={post.content || post.postType || "post"}
fill
unoptimized
loading="lazy"
onError={() => setImageFailed(true)}
className="object-cover"
/>
) : null}
{showVideoPreview ? (
<video
src={media.sourceUrl}
preload="metadata"
muted
playsInline
onError={() => setSourceFailed(true)}
className="h-full w-full object-cover"
/>
) : null}
{showAudioPreview ? (
<div className="absolute inset-0 flex flex-col justify-between bg-gradient-to-br from-secondary/70 via-background/60 to-secondary/80 p-5">
<div className="flex items-center justify-between gap-3 text-sm text-foreground">
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/70 px-3 py-1">
<AudioLines className="h-4 w-4" />
<span>Audio preview</span>
</div>
{durationLabel ? <span className="text-xs text-muted-foreground">{durationLabel}</span> : null}
</div>
<div className="flex h-24 items-end justify-center gap-1.5 px-2">
{waveformPeaks.slice(0, 48).map((peak, index) => {
const safePeak = Number.isFinite(peak) ? Math.max(8, Math.min(100, peak)) : 20;
return (
<span
key={`${post._id}-peak-${index}`}
className="w-1.5 rounded-full bg-primary/80"
style={{ height: `${safePeak}%` }}
/>
);
})}
</div>
<audio
src={media.sourceUrl}
preload="metadata"
controls
onError={() => setSourceFailed(true)}
className="w-full"
/>
</div>
) : null}
{showUnavailable ? (
<div className="absolute inset-0 flex items-center justify-center px-4 text-center text-sm text-muted-foreground">
Media preview unavailable
</div>
) : null}
<div className="absolute left-3 top-3 flex items-center gap-2">
<Badge variant="warning">{post.postType ?? "post"}</Badge>
{media.kind === "image" && media.count > 1 ? (
<Badge variant="muted">
<Images className="mr-1 h-3 w-3" />
{media.count}
</Badge>
) : null}
</div>
<div className="absolute bottom-3 right-3 rounded-full border border-border/60 bg-background/85 p-2 text-foreground">
{media.kind === "audio" ? <AudioLines className="h-4 w-4" /> : <PlayCircle className="h-4 w-4" />}
</div>
</div>
) : null}
<div className="space-y-3 p-4">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="muted">{post.visibility ?? "public"}</Badge>
<Badge
variant={
post.moderationStatus === "hidden"
? "danger"
: post.moderationStatus === "flagged"
? "warning"
: "success"
}
>
{post.moderationStatus ?? "active"}
</Badge>
</div>
<div>
<div className="text-sm font-semibold text-foreground">{getUserLabel(author)}</div>
<div className="mt-1 text-xs text-muted-foreground">{formatDateTime(post.createdAt)}</div>
</div>
<p className="line-clamp-4 text-sm leading-6 text-foreground">
{post.content?.trim() || "Media post without caption."}
</p>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<span>{likes} likes</span>
<span>{comments} comments</span>
<span>{shares} shares</span>
</div>
</div>
</CardContent>
</Card>
);
}