Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions app/marketplace/listings/[id]/_components/AuctionRoomClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Timer, Users, TrendingUp, Award, Zap, Clock, Gavel, AlertCircle } from 'lucide-react'
import { Timer, Zap, Gavel, AlertCircle } from 'lucide-react'
import { formatCurrency, formatTimeLeft, getInitials } from '@/lib/utils'
import { useToast } from '@/hooks/use-toast'
import type { BidDTO, AuctionDTO } from '@/lib/marketplace/types'
Expand All @@ -31,16 +31,25 @@ interface AuctionData {
}

function getAuctionData(data: AuctionData | AuctionDTO | null): AuctionData {
if (!data) return null as unknown as AuctionData
if (!data) {
return { id: '', title: '', basePrice: 0, endDate: '', auctionStatus: '', _count: { bids: 0 }, bids: [] }
}
if ('listing' in data) {
const a = data as AuctionDTO
return {
id: a.listing.id, title: a.listing.title, description: a.listing.description,
basePrice: a.listing.basePrice, bidIncrement: a.listing.bidIncrement,
endDate: a.listing.endDate, auctionStatus: a.listing.auctionStatus,
autoExtendMinutes: a.listing.autoExtendMinutes, currentLeaderId: a.listing.currentLeaderId,
highestBid: a.listing.highestBid, winningBid: a.listing.winningBid,
_count: { bids: a.stats.totalBids }, bids: a.listing.bids,
id: a.listing?.id || '',
title: a.listing?.title || '',
description: a.listing?.description,
basePrice: a.listing?.basePrice || 0,
bidIncrement: a.listing?.bidIncrement,
endDate: a.listing?.endDate || '',
auctionStatus: a.listing?.auctionStatus || '',
autoExtendMinutes: a.listing?.autoExtendMinutes,
currentLeaderId: a.listing?.currentLeaderId,
highestBid: a.listing?.highestBid,
winningBid: a.listing?.winningBid,
_count: { bids: a.stats?.totalBids ?? 0 },
bids: a.listing?.bids || [],
}
}
return data as AuctionData
Expand All @@ -53,7 +62,6 @@ interface AuctionRoomClientProps {

export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientProps) {
const [bidAmount, setBidAmount] = useState('')
const [isAutoBid, setIsAutoBid] = useState(false)
const { toast } = useToast()
const { auction, bids, loading, placeBid, timeRemaining, isLive, error } = useAuction(listingId)

Expand All @@ -66,19 +74,19 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
if (!bidAmount) return
const amount = parseFloat(bidAmount)
if (amount < minNextBid) {
toast({ title: 'Bid too low', description: "Minimum bid is " + formatCurrency(minNextBid), variant: 'destructive' })
toast({ title: 'Bid too low', description: 'Minimum bid is ' + formatCurrency(minNextBid), variant: 'destructive' })
return
}
try {
await placeBid(amount, isAutoBid)
await placeBid(amount, false)
setBidAmount('')
toast({ title: 'Bid placed!', description: 'Your bid has been registered.' })
} catch (err) {
toast({ title: 'Failed to place bid', description: err instanceof Error ? err.message : 'Please try again', variant: 'destructive' })
}
}

if (!currentAuction) {
if (!currentAuction || !currentAuction.id) {
return <div className="flex items-center justify-center min-h-[400px]"><p className="text-muted-foreground">Auction not found</p></div>
}

Expand All @@ -101,7 +109,7 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
</div>
<div className="space-y-4">
<div className="flex gap-2">
<Input type="number" placeholder={"Min " + formatCurrency(minNextBid)} value={bidAmount} onChange={(e) => setBidAmount(e.target.value)} min={minNextBid} disabled={!isLive} className="flex-1" />
<Input type="number" placeholder={'Min ' + formatCurrency(minNextBid)} value={bidAmount} onChange={(e) => setBidAmount(e.target.value)} min={minNextBid} disabled={!isLive} className="flex-1" />
<Button onClick={handlePlaceBid} disabled={!isLive || loading} size="lg" className="min-w-[120px]">{loading ? 'Placing...' : 'Place Bid'}</Button>
</div>
</div>
Expand All @@ -114,7 +122,7 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
<div className="flex items-center gap-3">
{index === 0 && <Badge className="bg-yellow-500">Leader</Badge>}
<Avatar className="h-8 w-8"><AvatarImage src={bid.farmer?.imageUrl || ''} /><AvatarFallback>{getInitials(bid.farmer?.name || '?')}</AvatarFallback></Avatar>
<div><p className="font-medium">{bid.farmer?.name || 'Anonymous'}</p><p className="text-xs text-muted-foreground">{new Date(bid.createdAt).toLocaleString()}</p></div>
<div><p className="font-medium">{bid.farmer?.name || 'Anonymous'}</p><p className="text-xs text-muted-foreground">{new Date(bid.createdAt).toISOString().replace("T", " ").substring(0, 19)}</p></div>
</div>
<div className="text-right"><p className="font-bold text-lg">{formatCurrency(bid.amount)}</p></div>
</div>
Expand All @@ -129,7 +137,7 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
<dl className="space-y-3">
<div className="flex justify-between"><dt className="text-muted-foreground">Starting Price</dt><dd className="font-medium">{formatCurrency(currentAuction.basePrice)}</dd></div>
<div className="flex justify-between"><dt className="text-muted-foreground">Bid Increment</dt><dd className="font-medium">{formatCurrency(currentAuction.bidIncrement || 1000)}</dd></div>
<div className="flex justify-between"><dt className="text-muted-foreground">Ends At</dt><dd className="font-medium">{new Date(currentAuction.endDate).toLocaleString()}</dd></div>
<div className="flex justify-between"><dt className="text-muted-foreground">Ends At</dt><dd className="font-medium">{new Date(currentAuction.endDate).toISOString().replace("T", " ").substring(0, 19)}</dd></div>
</dl>
</Card>
</div>
Expand Down
11 changes: 6 additions & 5 deletions hooks/useAuction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@ export function useAuction(listingId: string): UseAuctionReturn {

const fetchAuctionData = useCallback(async () => {
try {
const response = await fetch("/api/marketplace/" + listingId);
const response = await fetch("/api/marketplace/" + listingId + "/auction");
if (!response.ok) throw new Error("Failed to fetch auction");
const result = await response.json();
const data = result.data || result;
const auctionData = data.listing || data;
setAuction(data);
setBids(data.bids || []);
const endTime = new Date(data.endDate).getTime();
setBids(auctionData.bids || []);
const endTime = new Date(auctionData.endDate).getTime();
setTimeRemaining(Math.max(0, endTime - Date.now()));
setError(null);
} catch (err) {
Expand Down Expand Up @@ -137,8 +138,8 @@ export function useAuction(listingId: string): UseAuctionReturn {
if (amount < minBid) throw new Error("Bid must be at least ?" + minBid.toLocaleString());

try {
const response = await fetch("/api/marketplace/" + listingId, {
method: "PUT",
const response = await fetch("/api/marketplace/" + listingId + "/bids", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ farmerId, amount, isAutoBid }),
});
Expand Down
46 changes: 18 additions & 28 deletions hooks/usePusher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// hooks/usePusher.ts
import { useEffect } from 'react'
import { pusherClient } from '@/lib/pusher/client'
import { getPusherClient } from '@/lib/pusher/client'

// Define types that match your API response
export interface NewBidEvent {
Expand All @@ -18,43 +18,33 @@ export interface AuctionExtendedEvent {
newEndTime?: string
}

export interface AuctionEndedEvent {
winningBidId: string
winningAmount: number
winnerId: string
message: string
}

// Type-safe callback types
type NewBidCallback = (data: NewBidEvent) => void
type AuctionExtendedCallback = (data: AuctionExtendedEvent) => void
type AuctionEndedCallback = (data: AuctionEndedEvent) => void

interface PusherCallbacks {
onNewBid?: NewBidCallback
onAuctionExtended?: AuctionExtendedCallback
onAuctionEnded?: AuctionEndedCallback
onNewBid?: (data: NewBidEvent) => void
onAuctionExtended?: (data: AuctionExtendedEvent) => void
}

export function usePusher(channelName: string, callbacks: PusherCallbacks) {
export function usePusher(channel: string, callbacks: PusherCallbacks) {
useEffect(() => {
const channel = pusherClient.subscribe(channelName)
const client = getPusherClient()
if (!client) return

const ch = client.subscribe(channel)

if (callbacks.onNewBid) {
channel.bind('new-bid', callbacks.onNewBid)
ch.bind('new-bid', callbacks.onNewBid)
}

if (callbacks.onAuctionExtended) {
channel.bind('auction-extended', callbacks.onAuctionExtended)
}

if (callbacks.onAuctionEnded) {
channel.bind('auction-ended', callbacks.onAuctionEnded)
ch.bind('auction-extended', callbacks.onAuctionExtended)
}

return () => {
channel.unbind_all()
pusherClient.unsubscribe(channelName)
if (callbacks.onNewBid) {
ch.unbind('new-bid', callbacks.onNewBid)
}
if (callbacks.onAuctionExtended) {
ch.unbind('auction-extended', callbacks.onAuctionExtended)
}
client.unsubscribe(channel)
}
}, [channelName, callbacks.onNewBid, callbacks.onAuctionExtended, callbacks.onAuctionEnded])
}, [channel, callbacks.onNewBid, callbacks.onAuctionExtended])
}
Loading
Loading