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>
75 lines
3.3 KiB
TypeScript
75 lines
3.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { GradientButton } from '../components/ui/GradientButton';
|
|
import { Button } from '../components/ui/Button';
|
|
import { Badge } from '../components/ui/Badge';
|
|
import { Avatar } from '../components/ui/Avatar';
|
|
import { mockOffers } from '../utils/mockData';
|
|
import { formatCurrency, formatDate } from '../utils/format';
|
|
|
|
export function MyOffersPage() {
|
|
const [sortBy] = useState('newest');
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">My Offers</h1>
|
|
<p className="text-sm text-gray-500 mt-1">Review and manage incoming offers on your items</p>
|
|
</div>
|
|
<select value={sortBy} className="px-3 py-2 rounded-xl border border-gray-200 text-sm bg-white focus:outline-none">
|
|
<option value="newest">Sort: Most Recent</option>
|
|
<option value="price_high">Highest Offer</option>
|
|
<option value="price_low">Lowest Offer</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{mockOffers.map(offer => {
|
|
const savings = offer.listing.price - offer.amount;
|
|
const statusVariant = offer.status === 'ACCEPTED' ? 'success' : offer.status === 'DECLINED' ? 'error' : offer.status === 'COUNTERED' ? 'warning' : 'info';
|
|
|
|
return (
|
|
<div key={offer.id} className="bg-white rounded-2xl border border-gray-100 p-4 flex items-center gap-4">
|
|
{/* Item thumbnail */}
|
|
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-primary-50 to-pink-50 flex items-center justify-center flex-shrink-0">
|
|
<span className="text-2xl">
|
|
{offer.listing.category === 'FURNITURE' ? '\uD83E\uDE91' : offer.listing.category === 'ELECTRONICS' ? '\uD83C\uDFA7' : '\uD83D\uDCE6'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-gray-900 truncate">{offer.listing.title}</h3>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<Avatar name={offer.buyer.fullName} size="sm" />
|
|
<span className="text-xs text-gray-500">{offer.buyer.fullName}</span>
|
|
<span className="text-xs text-gray-400">{formatDate(offer.createdAt)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Prices */}
|
|
<div className="text-right flex-shrink-0">
|
|
<p className="text-xs text-gray-400 line-through">{formatCurrency(offer.listing.price)}</p>
|
|
<p className="text-lg font-bold text-primary-600">{formatCurrency(offer.amount)}</p>
|
|
<Badge variant="error" size="sm">-{formatCurrency(savings)}</Badge>
|
|
</div>
|
|
|
|
{/* Status / Actions */}
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{offer.status === 'PENDING' ? (
|
|
<>
|
|
<Button variant="secondary" size="sm">Accept</Button>
|
|
<GradientButton size="sm">Counteroffer</GradientButton>
|
|
</>
|
|
) : (
|
|
<Badge variant={statusVariant} size="md">{offer.status}</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|