sushilideaclan01's picture
.
d415d18
"use client";
import React, { useState, useEffect, memo } from "react";
import Link from "next/link";
import Image from "next/image";
import { Card, CardContent } from "@/components/ui/Card";
import { formatRelativeDate, formatNiche, getImageUrlFallback, isUnoptimizedImageUrl } from "@/lib/utils/formatters";
import { CheckSquare, Square } from "lucide-react";
import type { AdCreativeDB } from "@/types/api";
interface AdCardProps {
ad: AdCreativeDB;
isSelected?: boolean;
onSelect?: (adId: string) => void;
hasAnySelection?: boolean;
}
// Reusable gradient badge component
const GradientBadge: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<span className="inline-flex items-center text-xs font-bold px-3 py-1.5 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-full shadow-sm whitespace-nowrap">
{children}
</span>
);
export const AdCard: React.FC<AdCardProps> = memo(({
ad,
isSelected = false,
onSelect,
hasAnySelection = false,
}) => {
const { primary, fallback, initialUrl } = getImageUrlFallback(ad.image_url, ad.image_filename, ad.r2_url);
const [imageSrc, setImageSrc] = useState<string | null>(initialUrl);
const [imageError, setImageError] = useState(false);
useEffect(() => {
setImageSrc(initialUrl);
setImageError(false);
}, [ad.id, ad.image_url, ad.image_filename, ad.r2_url, initialUrl]);
const handleImageError = () => {
// Try fallback if primary failed
if (!imageError && fallback && imageSrc === primary) {
setImageSrc(fallback);
setImageError(true);
} else {
// Both failed, show placeholder
setImageSrc(null);
}
};
return (
<Card
variant={isSelected ? "glass" : "elevated"}
className={`cursor-pointer transition-all duration-300 group h-full flex flex-col ${
isSelected ? "ring-4 ring-blue-500 ring-opacity-50 scale-105" : "hover:scale-[1.02]"
}`}
onClick={() => onSelect?.(ad.id)}
>
<Link href={`/gallery/${ad.id}`} onClick={(e) => e.stopPropagation()} className="flex flex-col h-full">
<CardContent className="p-0 flex flex-col flex-1">
{(imageSrc || ad.image_filename || ad.image_url) && (
<div className="aspect-video bg-gradient-to-br from-gray-100 to-gray-200 rounded-t-2xl overflow-hidden relative flex items-center justify-center flex-shrink-0">
{imageSrc ? (
<Image
src={imageSrc}
alt={ad.headline}
fill
className="object-contain group-hover:scale-105 transition-transform duration-500"
onError={handleImageError}
loading="lazy"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
unoptimized={isUnoptimizedImageUrl(imageSrc)}
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<div className="text-center text-gray-400">
<svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-xs">Image unavailable</p>
</div>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{/* Selection checkbox - show on hover when nothing selected, always show when something is selected */}
<div className="absolute top-3 left-3 z-10">
<button
className={`flex items-center justify-center w-6 h-6 rounded-md border-2 transition-all duration-200 ${
isSelected
? "bg-blue-500 border-blue-500 text-white shadow-lg"
: hasAnySelection
? "bg-white/90 backdrop-blur-sm border-gray-300 hover:border-blue-400 hover:bg-white shadow-md"
: "bg-white/90 backdrop-blur-sm border-gray-300 hover:border-blue-400 hover:bg-white shadow-md opacity-0 group-hover:opacity-100"
}`}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onSelect?.(ad.id);
}}
>
{isSelected ? (
<CheckSquare className="h-4 w-4" />
) : (
<Square className="h-4 w-4 text-gray-400" />
)}
</button>
</div>
</div>
)}
<div className="p-5 flex-1 flex flex-col">
{/* Header with badge and timestamp - properly aligned on same line */}
<div className="flex items-center justify-between gap-2 mb-3">
<GradientBadge>{formatNiche(ad.niche)}</GradientBadge>
<span className="text-xs text-gray-500 font-medium flex-shrink-0">
{formatRelativeDate(ad.created_at)}
</span>
</div>
{/* Headline - improved typography */}
<h3 className="font-bold text-lg text-gray-900 line-clamp-2 mb-3 group-hover:text-blue-600 transition-colors leading-tight">
{ad.headline}
</h3>
{/* Description section - improved spacing and hierarchy */}
<div className="mt-auto space-y-1.5">
{ad.title && (
<p className="text-sm text-gray-700 line-clamp-1 font-medium">
{ad.title}
</p>
)}
{ad.psychological_angle && (
<p className="text-xs text-gray-500 line-clamp-1 italic">
{ad.psychological_angle}
</p>
)}
</div>
</div>
</CardContent>
</Link>
</Card>
);
}, (prevProps, nextProps) => {
return (
prevProps.ad.id === nextProps.ad.id &&
prevProps.isSelected === nextProps.isSelected &&
prevProps.hasAnySelection === nextProps.hasAnySelection &&
prevProps.ad.image_url === nextProps.ad.image_url &&
prevProps.ad.image_filename === nextProps.ad.image_filename &&
prevProps.ad.r2_url === nextProps.ad.r2_url
);
});
AdCard.displayName = "AdCard";