81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
|
|
/**
|
|||
|
|
* Real-time state hook for the silent auction catalog.
|
|||
|
|
* Subscribes to silent_bid_accepted, silent_outbid, silent_window_closing,
|
|||
|
|
* silent_window_extended, silent_item_closed.
|
|||
|
|
*/
|
|||
|
|
import { useState, useEffect, useCallback } from "react";
|
|||
|
|
import { getSocket } from "../lib/socket.js";
|
|||
|
|
import { useOfflineBids } from "./useOfflineBids.js";
|
|||
|
|
import { useAuthStore } from "../store/auth.js";
|
|||
|
|
import type { AuctionItem } from "@storybid/shared";
|
|||
|
|
|
|||
|
|
export function useSilentAuction(eventId: string) {
|
|||
|
|
const [items, setItems] = useState<AuctionItem[]>([]);
|
|||
|
|
const [outbidItemIds, setOutbidItemIds] = useState<Set<string>>(new Set());
|
|||
|
|
const bidderId = useAuthStore((s) => s.bidder?.id);
|
|||
|
|
const { queueBid, getDeviceId } = useOfflineBids();
|
|||
|
|
|
|||
|
|
let clientSeq = 0;
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const socket = getSocket();
|
|||
|
|
socket.emit("join_event", eventId);
|
|||
|
|
|
|||
|
|
socket.on("silent_bid_accepted", ({ item }) => {
|
|||
|
|
setItems((prev) =>
|
|||
|
|
prev.map((i) => (i.id === item.id ? item : i)),
|
|||
|
|
);
|
|||
|
|
// Clear outbid flag if we just won
|
|||
|
|
if (item.currentHighBidderId === bidderId) {
|
|||
|
|
setOutbidItemIds((prev) => {
|
|||
|
|
const next = new Set(prev);
|
|||
|
|
next.delete(item.id);
|
|||
|
|
return next;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
socket.on("silent_outbid", ({ itemId }) => {
|
|||
|
|
setOutbidItemIds((prev) => new Set([...prev, itemId]));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
socket.on("silent_item_closed", ({ itemId }) => {
|
|||
|
|
setItems((prev) =>
|
|||
|
|
prev.map((i) => (i.id === itemId ? { ...i, state: "closed" } : i)),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
socket.emit("leave_event", eventId);
|
|||
|
|
socket.off("silent_bid_accepted");
|
|||
|
|
socket.off("silent_outbid");
|
|||
|
|
socket.off("silent_item_closed");
|
|||
|
|
};
|
|||
|
|
}, [eventId, bidderId]);
|
|||
|
|
|
|||
|
|
const placeSilentBid = useCallback(
|
|||
|
|
async (itemId: string, amount: number) => {
|
|||
|
|
if (!bidderId) return;
|
|||
|
|
const socket = getSocket();
|
|||
|
|
const deviceId = getDeviceId();
|
|||
|
|
const seq = ++clientSeq;
|
|||
|
|
|
|||
|
|
if (socket.connected) {
|
|||
|
|
socket.emit("place_silent_bid", {
|
|||
|
|
itemId,
|
|||
|
|
amount,
|
|||
|
|
deviceId,
|
|||
|
|
clientSeq: seq,
|
|||
|
|
clientCreatedAt: new Date().toISOString(),
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// Offline – write to IndexedDB outbox
|
|||
|
|
await queueBid(itemId, bidderId, amount);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
[bidderId, getDeviceId, queueBid],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return { items, setItems, outbidItemIds, placeSilentBid };
|
|||
|
|
}
|