localgreenchain/components/marketplace/OfferForm.tsx
Claude b3c2af51bf
Implement marketplace foundation (Agent 9)
Add comprehensive plant trading marketplace with:
- Prisma schema with marketplace models (Listing, Offer, SellerProfile, WishlistItem)
- Service layer for listings, offers, search, and matching
- API endpoints for CRUD operations, search, and recommendations
- Marketplace pages: home, listing detail, create, my-listings, my-offers
- Reusable UI components: ListingCard, ListingGrid, OfferForm, SearchFilters, etc.

Features:
- Browse and search listings by category, price, tags
- Create and manage listings (draft, active, sold, cancelled)
- Make and manage offers on listings
- Seller and buyer views with statistics
- Featured and recommended listings
- In-memory store (ready for database migration via Agent 2)
2025-11-23 03:58:08 +00:00

144 lines
4.3 KiB
TypeScript

import { useState } from 'react';
interface OfferFormProps {
listingId: string;
askingPrice: number;
currency?: string;
onSubmit: (amount: number, message: string) => Promise<void>;
onCancel?: () => void;
}
export function OfferForm({
listingId,
askingPrice,
currency = 'USD',
onSubmit,
onCancel,
}: OfferFormProps) {
const [amount, setAmount] = useState(askingPrice.toString());
const [message, setMessage] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
const offerAmount = parseFloat(amount);
if (isNaN(offerAmount) || offerAmount <= 0) {
setError('Please enter a valid offer amount');
return;
}
setSubmitting(true);
try {
await onSubmit(offerAmount, message);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to submit offer');
} finally {
setSubmitting(false);
}
};
const suggestedOffers = [
{ label: 'Full Price', value: askingPrice },
{ label: '10% Off', value: Math.round(askingPrice * 0.9 * 100) / 100 },
{ label: '15% Off', value: Math.round(askingPrice * 0.85 * 100) / 100 },
];
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
{/* Suggested Offers */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Quick Select
</label>
<div className="flex gap-2">
{suggestedOffers.map((suggestion) => (
<button
key={suggestion.label}
type="button"
onClick={() => setAmount(suggestion.value.toString())}
className={`px-3 py-2 rounded-lg text-sm transition ${
parseFloat(amount) === suggestion.value
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{suggestion.label}
<br />
<span className="font-semibold">${suggestion.value.toFixed(2)}</span>
</button>
))}
</div>
</div>
{/* Custom Amount */}
<div>
<label htmlFor="offerAmount" className="block text-sm font-medium text-gray-700 mb-1">
Your Offer ({currency})
</label>
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500">$</span>
<input
type="number"
id="offerAmount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
required
min="0.01"
step="0.01"
className="w-full pl-8 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<p className="mt-1 text-sm text-gray-500">
Asking price: ${askingPrice.toFixed(2)}
</p>
</div>
{/* Message */}
<div>
<label htmlFor="offerMessage" className="block text-sm font-medium text-gray-700 mb-1">
Message to Seller (optional)
</label>
<textarea
id="offerMessage"
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={3}
placeholder="Introduce yourself or ask a question..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
{/* Actions */}
<div className="flex gap-3 pt-2">
{onCancel && (
<button
type="button"
onClick={onCancel}
className="flex-1 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
>
Cancel
</button>
)}
<button
type="submit"
disabled={submitting}
className="flex-1 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition disabled:opacity-50"
>
{submitting ? 'Submitting...' : 'Submit Offer'}
</button>
</div>
</form>
);
}
export default OfferForm;