1import { Heart, MapPin, Calendar, Eye, Star, User } from "lucide-react"; 2import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; 3import { Badge } from '@/components/ui/badge'; 4import { Button } from '@/components/ui/button'; 5import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 6import { SellerRatingDisplay } from './SellerRatingDisplay'; 7import LikeButton from './social/LikeButton'; 8import { memo } from 'react'; 9import { getOptimizedImageUrl, getResponsiveSrcSet } from '@/lib/imageOptimization'; 10 11// Legacy interface for backward compatibility 12interface LegacyProductCardProps { 13 images?: string[]; 14 title: string; 15 price: string; 16 location: string; 17 condition: string; 18 featured?: boolean; 19} 20 21// New interface for listings 22interface Listing { 23 id: string; 24 title: string; 25 description: string; 26 price: number; 27 status: string; 28 created_at: string; 29 category: string; 30 size: string; 31 condition?: string; 32 location?: string; 33 display_location?: string; 34 images?: string[]; 35 quantity: number; 36 seller_display_name?: string; 37 seller_avatar_url?: string; 38 seller_rating_average?: number; 39 seller_rating_count?: number; 40 is_featured?: boolean; 41 featured_until?: string; 42} 43 44interface NewProductCardProps { 45 listing: Listing; 46 onClick?: () => void; 47 showOwnerActions?: boolean; 48} 49 50type ProductCardProps = LegacyProductCardProps | NewProductCardProps; 51 52const isLegacyProps = (props: ProductCardProps): props is LegacyProductCardProps => { 53 return 'price' in props && typeof props.price === 'string'; 54}; 55 56const ProductCard = (props: ProductCardProps) => { 57 if (isLegacyProps(props)) { 58 // Legacy component for FeaturedProducts 59 const { images = [], title, price, location, condition, featured } = props; 60 const displayImage = images.length > 0 ? images[0] : '/placeholder.svg'; 61 const optimizedSrc = getOptimizedImageUrl(displayImage, { width: 240, quality: 75 }); 62 const responsiveSrcSet = getResponsiveSrcSet(displayImage, [180, 240, 320]); 63 64 return ( 65 <Card className="overflow-hidden hover:shadow-[var(--card-shadow-hover)] transition-all duration-300 group cursor-pointer"> 66 <div className="relative"> 67 <img 68 src={optimizedSrc} 69 alt={title} 70 className="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300" 71 loading="lazy" 72 decoding="async" 73 width={280} 74 height={192} 75 style={{ 76 aspectRatio: '280/192', 77 maxWidth: '400px', 78 height: 'auto' 79 }} 80 sizes="(max-width: 640px) 240px, (max-width: 768px) 280px, (max-width: 1024px) 280px, 240px" 81 srcSet={responsiveSrcSet} 82 /> 83 {featured && ( 84 <Badge className="absolute top-2 left-2 bg-accent hover:bg-accent"> 85 Featured 86 </Badge> 87 )} 88 <Button 89 variant="ghost" 90 size="icon" 91 className="absolute top-2 right-2 bg-card/80 hover:bg-card" 92 > 93 <Heart className="w-4 h-4" /> 94 </Button> 95 </div> 96 97 <CardContent className="p-4"> 98 <h3 className="font-semibold text-foreground mb-2 line-clamp-2 group-hover:text-primary transition-colors"> 99 {title} 100 </h3> 101 102 <div className="flex items-center justify-between mb-2"> 103 <span className="text-2xl font-bold text-primary">{price}</span> 104 <Badge variant="secondary">{condition}</Badge> 105 </div> 106 107 <div className="flex items-center text-muted-foreground text-sm"> 108 <MapPin className="w-4 h-4 mr-1" /> 109 {location} 110 </div> 111 </CardContent> 112 </Card> 113 ); 114 } else { 115 // New component for listings 116 const { listing, onClick, showOwnerActions = false } = props; 117 const optimizedSrc = listing.images?.[0] ? getOptimizedImageUrl(listing.images[0], { width: 240, quality: 75 }) : ''; 118 const responsiveSrcSet = listing.images?.[0] ? getResponsiveSrcSet(listing.images[0], [180, 240, 320]) : ''; 119 120 // Check if featured and still active 121 const isFeatured = listing.is_featured && 122 (!listing.featured_until || new Date(listing.featured_until) > new Date()); 123 124 // Get status badge 125 const getStatusBadge = () => { 126 switch (listing.status) { 127 case 'sold':
128 return <Badge variant="destructive">Verkauft</Badge>; 129 case 'reserved': 130 return <Badge variant="secondary">Reserviert</Badge>; 131 case 'inactive': 132 return <Badge variant="outline">Inaktiv</Badge>; 133 default: 134 return null; 135 } 136 }; 137 138 return ( 139 <Card className={`cursor-pointer hover:shadow-lg transition-all ${isFeatured ? 'ring-2 ring-accent shadow-xl' : ''}`} onClick={onClick}> 140 <div className="aspect-square overflow-hidden rounded-t-lg relative"> 141 {isFeatured && ( 142 <Badge className="absolute top-2 left-2 z-10 bg-accent text-accent-foreground"> 143 â Featured 144 </Badge> 145 )} 146 {getStatusBadge() && ( 147 <div className="absolute top-2 right-2 z-10"> 148 {getStatusBadge()} 149 </div> 150 )} 151 {listing.images && listing.images.length > 0 ? ( 152 <> 153 <img 154 src={optimizedSrc} 155 alt={listing.title} 156 className="w-full h-full object-cover" 157 loading="lazy" 158 decoding="async" 159 width={280} 160 height={280} 161 style={{ 162 aspectRatio: '1/1', 163 maxWidth: '400px', 164 height: 'auto' 165 }} 166 sizes="(max-width: 640px) 240px, (max-width: 768px) 280px, (max-width: 1024px) 280px, 240px" 167 srcSet={responsiveSrcSet} 168 /> 169 {listing.images.length > 1 && ( 170 <Badge className="absolute bottom-2 right-2 bg-background/80"> 171 +{listing.images.length - 1} Bilder 172 </Badge> 173 )} 174 </> 175 ) : ( 176 <div className="w-full h-full bg-muted flex items-center justify-center"> 177 <span className="text-muted-foreground">Kein Bild</span> 178 </div> 179 )} 180 </div> 181 182 <CardHeader className="pb-2"> 183 <div className="flex justify-between items-start"> 184 <CardTitle className="text-lg leading-tight">{listing.title}</CardTitle> 185 {listing.condition && ( 186 <Badge variant="outline" className="ml-2"> 187 {listing.condition} 188 </Badge> 189 )} 190 </div> 191 <CardDescription className="text-xl font-bold text-primary"> 192 â¬{listing.price.toFixed(2)} 193 </CardDescription> 194 </CardHeader> 195 196 <CardContent className="pt-0"> 197 <p className="text-sm text-muted-foreground mb-3 line-clamp-2"> 198 {listing.description} 199 </p> 200 201 {/* Seller Info */} 202 {listing.seller_display_name && ( 203 <div className="flex items-center justify-between mb-3"> 204 <div className="flex items-center gap-2"> 205 <Avatar className="h-8 w-8"> 206 <AvatarImage src={listing.seller_avatar_url} /> 207 <AvatarFallback className="text-xs"> 208 {listing.seller_display_name?.charAt(0) || 'U'} 209 </AvatarFallback> 210 </Avatar> 211 <div className="flex flex-col"> 212 <span className="text-sm font-medium"> 213 {listing.seller_display_name} 214 </span> 215 {listing.seller_rating_average && listing.seller_rating_average > 0 && ( 216 <div className="flex items-center gap-1"> 217 <Star className="h-3 w-3 fill-yellow-400 text-yellow-400" /> 218 <span className="text-xs text-muted-foreground"> 219 {listing.seller_rating_average.toFixed(1)} 220 </span> 221 </div> 222 )} 223 </div> 224 </div> 225 226 <LikeButton listingId={listing.id} /> 227 </div> 228 )} 229 230 <div className="space-y-2"> 231 <div className="flex items-center justify-between text-xs text-muted-foreground"> 232 <div className="flex items-center"> 233 <Badge variant="secondary" className="mr-2"> 234 {listing.category} 235 </Badge>
236 <span>{listing.size}</span> 237 </div> 238 <Badge variant="outline" className="text-xs"> 239 {listing.quantity} Stück 240 </Badge> 241 </div> 242 243 {(listing.display_location || listing.location) && ( 244 <div className="flex items-center text-xs text-muted-foreground"> 245 <MapPin className="w-3 h-3 mr-1" /> 246 <span>{listing.display_location || listing.location}</span> 247 </div> 248 )} 249 250 <div className="flex items-center justify-between text-xs text-muted-foreground"> 251 <div className="flex items-center"> 252 <Calendar className="w-3 h-3 mr-1" /> 253 <span>{new Date(listing.created_at).toLocaleDateString('de-DE')}</span> 254 </div> 255 {!showOwnerActions && ( 256 <Button 257 variant="outline" 258 size="sm" 259 onClick={(e) => { 260 e.stopPropagation(); 261 if (onClick) onClick(); 262 }} 263 > 264 <Eye className="w-3 h-3 mr-1" /> 265 Ansehen 266 </Button> 267 )} 268 </div> 269 </div> 270 </CardContent> 271 </Card> 272 ); 273 } 274}; 275 276export default memo(ProductCard);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.