PageSourceSearch

https://fitnessprofi.lovable.app/assets/ProductCard-BpV52lDD.tsx

js fitnessprofi.lovable.app collected 2026-10-03 03:31:28 UTC 10,168 bytes, 276 lines download raw bytes

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.