Phase 2 and Demo
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Auctioneer console hook.
|
||||
*
|
||||
* Manages the full live auction control flow:
|
||||
* - Subscribes to all live-auction server events
|
||||
* - Emits auctioneer_* socket events for each control action
|
||||
* - Keeps a local snapshot of the current item, called amount, and bid stream
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getSocket } from "../lib/socket.js";
|
||||
import { api } from "../lib/api.js";
|
||||
import type { AuctionItem, Bid, ItemState } from "@storybid/shared";
|
||||
|
||||
export interface AuctioneerState {
|
||||
items: AuctionItem[]; // all lots for the auction
|
||||
currentItem: AuctionItem | null;
|
||||
currentBid: number | null;
|
||||
calledAmount: number | null;
|
||||
state: ItemState | null;
|
||||
recentBids: Bid[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function useAuctioneerControls(eventId: string, auctionId: string) {
|
||||
const [state, setState] = useState<AuctioneerState>({
|
||||
items: [],
|
||||
currentItem: null,
|
||||
currentBid: null,
|
||||
calledAmount: null,
|
||||
state: null,
|
||||
recentBids: [],
|
||||
loading: true,
|
||||
});
|
||||
|
||||
// Load item list on mount
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<AuctionItem[]>(`/api/items?auctionId=${auctionId}`)
|
||||
.then((items) => setState((s) => ({ ...s, items, loading: false })))
|
||||
.catch(() => setState((s) => ({ ...s, loading: false })));
|
||||
}, [auctionId]);
|
||||
|
||||
// Real-time subscriptions
|
||||
useEffect(() => {
|
||||
const socket = getSocket();
|
||||
socket.emit("join_event", eventId);
|
||||
|
||||
socket.on("item_activated", ({ item }) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
currentItem: item,
|
||||
currentBid: item.currentHighBid,
|
||||
calledAmount: item.openingBid,
|
||||
state: item.state,
|
||||
recentBids: [],
|
||||
// Keep item list in sync
|
||||
items: s.items.map((i) => (i.id === item.id ? item : i)),
|
||||
}));
|
||||
});
|
||||
|
||||
socket.on("next_live_bid", ({ amount }) => {
|
||||
setState((s) => ({ ...s, calledAmount: amount }));
|
||||
});
|
||||
|
||||
socket.on("live_bid_accepted", ({ bid, item }) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
currentItem: item,
|
||||
currentBid: item.currentHighBid,
|
||||
state: item.state,
|
||||
recentBids: [bid, ...s.recentBids].slice(0, 20),
|
||||
items: s.items.map((i) => (i.id === item.id ? item : i)),
|
||||
}));
|
||||
});
|
||||
|
||||
socket.on("item_state_changed", ({ itemId, state: newState }) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
state: s.currentItem?.id === itemId ? newState : s.state,
|
||||
items: s.items.map((i) => (i.id === itemId ? { ...i, state: newState } : i)),
|
||||
}));
|
||||
});
|
||||
|
||||
socket.on("item_sold", ({ itemId, amount }) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
state: s.currentItem?.id === itemId ? "sold" : s.state,
|
||||
currentBid: s.currentItem?.id === itemId ? amount : s.currentBid,
|
||||
items: s.items.map((i) =>
|
||||
i.id === itemId ? { ...i, state: "sold", currentHighBid: amount } : i,
|
||||
),
|
||||
}));
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.emit("leave_event", eventId);
|
||||
socket.off("item_activated");
|
||||
socket.off("next_live_bid");
|
||||
socket.off("live_bid_accepted");
|
||||
socket.off("item_state_changed");
|
||||
socket.off("item_sold");
|
||||
};
|
||||
}, [eventId]);
|
||||
|
||||
// ── Controls ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const activateItem = useCallback((itemId: string) => {
|
||||
getSocket().emit("auctioneer_activate_item", itemId);
|
||||
}, []);
|
||||
|
||||
const callNextBid = useCallback((itemId: string, amount: number) => {
|
||||
getSocket().emit("auctioneer_call_next_bid", { itemId, amount });
|
||||
setState((s) => ({ ...s, calledAmount: amount }));
|
||||
}, []);
|
||||
|
||||
const acceptBid = useCallback((itemId: string, bidderId: string, amount: number) => {
|
||||
getSocket().emit("auctioneer_accept_bid", { itemId, bidderId, amount });
|
||||
}, []);
|
||||
|
||||
const goingOnce = useCallback((itemId: string) => {
|
||||
getSocket().emit("auctioneer_going_once", itemId);
|
||||
}, []);
|
||||
|
||||
const goingTwice = useCallback((itemId: string) => {
|
||||
getSocket().emit("auctioneer_going_twice", itemId);
|
||||
}, []);
|
||||
|
||||
const sold = useCallback((itemId: string) => {
|
||||
getSocket().emit("auctioneer_sold", itemId);
|
||||
}, []);
|
||||
|
||||
const pass = useCallback((itemId: string) => {
|
||||
getSocket().emit("auctioneer_pass", itemId);
|
||||
}, []);
|
||||
|
||||
/** Suggest next bid = current high bid + increment (or opening bid if no bids yet) */
|
||||
const suggestNextBid = useCallback((): number => {
|
||||
const item = state.currentItem;
|
||||
if (!item) return 0;
|
||||
return item.currentHighBid != null
|
||||
? item.currentHighBid + item.bidIncrement
|
||||
: item.openingBid;
|
||||
}, [state.currentItem]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
activateItem,
|
||||
callNextBid,
|
||||
acceptBid,
|
||||
goingOnce,
|
||||
goingTwice,
|
||||
sold,
|
||||
pass,
|
||||
suggestNextBid,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user