Phase 2 and Demo

This commit is contained in:
2026-05-02 20:14:15 -05:00
parent d909cb7c30
commit 056bd27f89
36 changed files with 3867 additions and 299 deletions
@@ -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,
};
}