Full-stack marketplace for buying/selling second-hand items. React 19 + TypeScript + Tailwind CSS v4 frontend with 17 screens, Express + Prisma + Socket.io backend, Stripe payments, JWT auth. Deployed at https://marketplace.173.212.212.157.sslip.io/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
64 lines
2.7 KiB
TypeScript
64 lines
2.7 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { Heart, MapPin } from 'lucide-react';
|
|
import { Badge } from '../ui/Badge';
|
|
import type { Listing } from '../../types';
|
|
import { formatCurrency } from '../../utils/format';
|
|
|
|
interface ListingCardProps {
|
|
listing: Listing;
|
|
}
|
|
|
|
export function ListingCard({ listing }: ListingCardProps) {
|
|
const [isFav, setIsFav] = useState(listing.isFavorited ?? false);
|
|
|
|
const conditionVariant = listing.condition === 'NEW' ? 'success'
|
|
: listing.condition === 'LIKE_NEW' ? 'info'
|
|
: 'default';
|
|
|
|
return (
|
|
<Link to={`/listings/${listing.id}`} className="group block">
|
|
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200">
|
|
{/* Image */}
|
|
<div className="relative aspect-square bg-gray-100">
|
|
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary-50 to-pink-50">
|
|
<span className="text-4xl">
|
|
{listing.category === 'FURNITURE' ? '\uD83E\uDE91' :
|
|
listing.category === 'ELECTRONICS' ? '\uD83C\uDFA7' :
|
|
listing.category === 'CLOTHING' ? '\uD83D\uDC55' :
|
|
listing.category === 'HOME_GARDEN' ? '\u2615' :
|
|
listing.category === 'SPORTS' ? '\uD83D\uDEB4' :
|
|
listing.category === 'BOOKS' ? '\uD83D\uDCDA' :
|
|
listing.category === 'GAMES' ? '\uD83C\uDFAE' : '\uD83D\uDCE6'}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={(e) => { e.preventDefault(); setIsFav(!isFav); }}
|
|
className="absolute top-2 right-2 p-1.5 bg-white/80 backdrop-blur rounded-full hover:bg-white transition-colors cursor-pointer"
|
|
>
|
|
<Heart className={`w-4 h-4 ${isFav ? 'fill-pink-500 text-pink-500' : 'text-gray-400'}`} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-3">
|
|
<h3 className="text-sm font-semibold text-gray-900 truncate group-hover:text-primary-600 transition-colors">
|
|
{listing.title}
|
|
</h3>
|
|
<p className="text-lg font-bold text-primary-600 mt-1">
|
|
{formatCurrency(listing.price)}
|
|
{listing.obo && <span className="text-xs font-normal text-gray-400 ml-1">OBO</span>}
|
|
</p>
|
|
<div className="flex items-center justify-between mt-2">
|
|
<Badge variant={conditionVariant}>{listing.condition.replace('_', ' ')}</Badge>
|
|
<span className="flex items-center gap-1 text-xs text-gray-400">
|
|
<MapPin className="w-3 h-3" />
|
|
{listing.location}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|