Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
157
oudelaa_dashboard/components/dashboard/post-preview-card.tsx
Normal file
157
oudelaa_dashboard/components/dashboard/post-preview-card.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم