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> + ) +} +