From 1bcffe619c70ab2e2f8974a8dbc5c2ef384b5d0f Mon Sep 17 00:00:00 2001
From: Aroy-Art <Aroy-Art@pm.me>
Date: Sat, 22 Mar 2025 21:08:51 +0100
Subject: [PATCH] Add: PostCard component

---
 frontend/src/components/partials/PostCard.tsx | 174 ++++++++++++++++++
 1 file changed, 174 insertions(+)
 create mode 100644 frontend/src/components/partials/PostCard.tsx

diff --git a/frontend/src/components/partials/PostCard.tsx b/frontend/src/components/partials/PostCard.tsx
new file mode 100644
index 0000000..15d57cd
--- /dev/null
+++ b/frontend/src/components/partials/PostCard.tsx
@@ -0,0 +1,174 @@
+import { Link } from "react-router-dom"
+import { Card } from "@/components/ui/card"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+import { File, FileText, Play, ImagePlay } from "lucide-react"
+import { relativeTime, getFirstGrapheme } from "@/lib/utils"
+
+import { getFileUrl } from "@/services/api"
+
+interface PostCardProps {
+    post_id: string
+    title: string
+    description: string
+    creator: {
+        [key: string]: string
+    }
+    date: {
+        [key: string]: string
+    }
+    media: Array<{
+        [key: string]: string
+    }>
+    media_count: number
+    source_site: {
+        [key: string]: string
+    }
+}
+
+export function PostCard({ post_id, title, description, creator, date, media, media_count, source_site }: PostCardProps) {
+    const renderMedia = () => {
+        const mediaCount = media.length;
+
+        const baseImageClass = "object-cover w-full h-full rounded-md";
+
+        if (mediaCount === 0) {
+            return (
+                <div className="relative h-40 md:h-48 xl:h-[14rem] w-full overflow-hidden rounded-md flex items-center justify-center">
+                    <p className="text-center">No attached files</p>
+                </div>
+            );
+        }
+
+        if (mediaCount === 1) {
+            return (
+                <div className="relative h-40 md:h-48 xl:h-[14rem] w-full overflow-hidden rounded-md">
+                    <img src={getFileUrl(media[0].hash, { thumbnail: "sm" }) || "/placeholder.svg"} alt={`${title} - media 1`} className={baseImageClass} />
+                    {renderMediaBadge(media[0])}
+                </div>
+            );
+        }
+
+        if (mediaCount === 2) {
+            return (
+                <div className="grid grid-cols-2 gap-0.5 h-40 md:h-48 xl:h-[14rem]">
+                    {media.map((item, index) => (
+                        <div key={index} className="relative overflow-hidden rounded-md">
+                            <img src={getFileUrl(item.hash, { thumbnail: "sm" }) || "/placeholder.svg"} alt={`${title} - media ${index + 1}`} className={baseImageClass} />
+                            {renderMediaBadge(item)}
+                        </div>
+                    ))}
+                </div>
+            );
+        }
+
+        if (mediaCount === 3) {
+            return (
+                <div className="grid grid-cols-2 gap-0.5 h-40 md:h-48 xl:h-[14rem]">
+                    <div className="relative row-span-2 overflow-hidden rounded-md">
+                        <img src={getFileUrl(media[0].hash, { thumbnail: "sm" }) || "/placeholder.svg"} alt={`${title} - media 1`} className={baseImageClass} />
+                        {renderMediaBadge(media[0])}
+                    </div>
+                    <div className="relative overflow-hidden rounded-md">
+                        <img src={getFileUrl(media[1].hash, { thumbnail: "sm" }) || "/placeholder.svg"} alt={`${title} - media 2`} className={baseImageClass} />
+                        {renderMediaBadge(media[1])}
+                    </div>
+                    <div className="relative overflow-hidden rounded-md">
+                        <img src={getFileUrl(media[2].hash, { thumbnail: "sm" }) || "/placeholder.svg"} alt={`${title} - media 3`} className={baseImageClass} />
+                        {renderMediaBadge(media[2])}
+                    </div>
+                </div>
+            );
+        }
+
+        return (
+            <div className="grid grid-cols-2 gap-0.5 h-40 md:h-48 xl:h-[14rem]">
+                {media.slice(0, 4).map((item, index) => (
+                    <div key={index} className="relative overflow-hidden rounded-md">
+                        <img src={getFileUrl(item.hash, { thumbnail: "sm" }) || "/placeholder.svg"} alt={`${title} - media ${index + 1}`} className={baseImageClass} />
+                        {renderMediaBadge(item)}
+                        {index === 3 && mediaCount > 4 && (
+                            <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
+                                <span className="text-white text-sm font-bold">+{mediaCount - 4}</span>
+                            </div>
+                        )}
+                    </div>
+                ))}
+            </div>
+        );
+    };
+
+    const renderMediaBadge = (item: { type: 'image' | 'video' | 'gif' | 'pdf', src: string }) => {
+        if (item.type === 'video') {
+            return (
+                <div className="absolute top-1 left-1 bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded-md flex items-center">
+                    <Play className="w-3 h-3 mr-1" />
+                    Video
+                </div>
+            );
+        } else if (item.type === 'gif') {
+            return (
+                <div className="absolute top-1 left-1 bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded-md flex items-center">
+                    <ImagePlay className="w-3 h-3 mr-1" />
+                    GIF
+                </div>
+            );
+        } else if (item.type === 'pdf') {
+            return (
+                <div className="absolute top-1 left-1 bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded-md flex items-center">
+                    <FileText className="w-3 h-3 mr-1" />
+                    Doc
+                </div>
+            );
+        };
+        return null;
+    };
+
+    const renderSourceSiteIcon = () => {
+        if (source_site.slug === 'twitter') {
+            return (
+                <div className="absolute top-1 right-1 bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded-xl flex items-center">
+                    <img src="/images/sites/twitter_logo-128.png" alt="Twitter Icon" className="w-3 h-3 md:w-4 md:h-4 m-0.5" />
+                </div>
+            )
+        } else if (source_site.slug === 'furaffinity') {
+            return (
+                <div className="absolute top-1 right-1 bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded-xl flex items-center">
+                    <img src="/images/sites/fa_logo-128.png" alt="Furaffinity Icon" className="w-3 h-3 md:w-4 md:h-4 m-0.5" />
+                </div>
+            )
+        }
+    }
+
+    return (
+        <Link to={`/post/${source_site.slug}/${creator.slug}/${post_id}`}>
+            <Card className="w-44 md:w-56 lg:w-64 xl:w-80 h-full overflow-hidden cursor-pointer">
+                <div className="relative">
+                    {renderMedia()}
+                    {renderSourceSiteIcon()}
+                    <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black to-transparent p-2">
+                        <h2 className="text-sm text-white line-clamp-1">
+                            {title || (description.length > 28 ? `${description.substring(0, 28)}...` : description)}
+                        </h2>
+                        <div className="flex mt-1 flex-col">
+                            <div className="flex items-center space-x-1">
+                                <Avatar className="w-6 h-6">
+                                    {creator.avatar && <AvatarImage src={getFileUrl(creator.avatar, { thumbnail: "sx" })} />}
+                                    <AvatarFallback>{getFirstGrapheme(creator.name)}</AvatarFallback>
+                                </Avatar>
+                                <div className=" text-sm font-semibold text-gray-300 overflow-hidden text-nowrap">{creator.name}</div>
+                            </div>
+                            <div className="pt-1 flex items-center justify-between text-xs text-gray-300">
+                                <div className="">{relativeTime(date.created)}</div>
+                                <div className="flex items-center">
+                                    <File className="w-3 h-3 mr-1" />
+                                    <div>{media_count}</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </Card>
+        </Link>
+    )
+}
+