Auctions14 examples
Components / Production
Auctions
Browse auction items and use the built-in bid or buy-now checkout.
Previewlight site theme
This page uses production. Browsing does not place a bid. Submit only if you intend to bid or purchase. Keep this page open during submission.
Loading auction selection…
CodeApp.tsx
import type { LockerverseStyle } from "@lockerverse/react";
import { LockerverseAuction as AuctionView } from "@lockerverse/react/auction";
import { useLockerverseAuction } from "@lockerverse/react/auction-data";
import {
LockerverseAuctionItem,
type LockerverseAuctionPurchaseRecovery,
} from "@lockerverse/react/auction-item";
import {
LockerverseAuctions,
useLockerverseAuctions,
} from "@lockerverse/react/auctions";
import "@lockerverse/react/auctions.css";
import type { LockerverseAuction } from "@lockerverse/sdk/auctions";
import { useEffect, useRef, useState } from "react";
// Native document navigation provides reload, Back/Forward, and open-in-new-tab.
// Keep host query parameters; never put buyer or recovery data in a URL.
function auctionHref(base: string, community: string, auction = "", item = "") {
const url = new URL(base);
url.searchParams.set("community", community);
for (const [key, value] of [
["auction", auction],
["item", item],
] as const) {
if (value) {
url.searchParams.set(key, value);
} else {
url.searchParams.delete(key);
}
}
url.hash = "";
return url.href;
}
function usePendingNavigation(pending: boolean) {
useEffect(() => {
if (!pending) {
return;
}
const warnBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
// Retain the legacy unload prompt fallback for older browsers.
event.returnValue = "";
};
const blockLink = (event: MouseEvent) => {
if (event.target instanceof Element && event.target.closest("a[href]")) {
event.preventDefault();
event.stopImmediatePropagation();
}
};
const blockNavigation = (event: Event) => {
if (event.cancelable) {
event.preventDefault();
}
};
// Browsers without Navigation API still receive the unload warning and link guard.
const navigation =
"navigation" in window && window.navigation instanceof EventTarget
? window.navigation
: null;
window.addEventListener("beforeunload", warnBeforeUnload);
document.addEventListener("click", blockLink, true);
document.addEventListener("auxclick", blockLink, true);
navigation?.addEventListener("navigate", blockNavigation);
return () => {
window.removeEventListener("beforeunload", warnBeforeUnload);
document.removeEventListener("click", blockLink, true);
document.removeEventListener("auxclick", blockLink, true);
navigation?.removeEventListener("navigate", blockNavigation);
};
}, [pending]);
}
type Route = { base: string; community: string; auction: string; item: string };
type ViewProps = {
route: Route;
theme: "light" | "dark";
style?: LockerverseStyle;
};
function CommunityAuctions({ route, theme, style }: ViewProps) {
const auctions = useLockerverseAuctions({ communitySlug: route.community });
return (
<LockerverseAuctions
auctions={auctions}
getAuctionHref={(auction) =>
auctionHref(
route.base,
route.community,
auction.slug || auction.path || auction.id
)
}
getItemHref={(auction, item) =>
auctionHref(
route.base,
route.community,
auction.slug || auction.path || auction.id,
item.slug || item.path || item.id
)
}
theme={theme}
{...(style ? { style } : {})}
/>
);
}
function SingleAuction({
route,
theme,
onPendingChange,
style,
}: ViewProps & { onPendingChange: (pending: boolean) => void }) {
const resource = useLockerverseAuction(
{ communitySlug: route.community },
route.auction
);
const [selection, setSelection] = useState<{
auction: LockerverseAuction;
itemId: string;
} | null>(null);
const [locked, setLocked] = useState(false);
const [recovery, setRecovery] =
useState<LockerverseAuctionPurchaseRecovery | null>(null);
const [uncertainBid, setUncertainBid] = useState(false);
const [message, setMessage] = useState("");
const [showFilters, setShowFilters] = useState(true);
const [showHeader, setShowHeader] = useState(true);
const pending = locked || recovery !== null || uncertainBid;
const pendingState = useRef({
locked: false,
recovery: false,
uncertainBid: false,
});
function reportPending(next: Partial<typeof pendingState.current>) {
Object.assign(pendingState.current, next);
const flags = pendingState.current;
// biome-ignore lint/suspicious/noUnnecessaryConditions: Event callbacks update these flags synchronously.
onPendingChange(flags.locked || flags.recovery || flags.uncertainBid);
}
// biome-ignore lint/suspicious/noUnnecessaryConditions: The hook has no auction during its initial read or initial failure.
const resolvedItem = resource.auction?.items.find(
(item) =>
item.id === route.item ||
item.slug === route.item ||
item.path === route.item
);
useEffect(() => {
if (!selection && resource.auction && resolvedItem) {
setSelection({ auction: resource.auction, itemId: resolvedItem.id });
}
}, [selection, resource.auction, resolvedItem]);
const catalogHref = auctionHref(route.base, route.community, route.auction);
return (
<div>
{resource.loading ? (
<p role="status">
{resource.auction ? "Refreshing auction…" : "Loading auction…"}
</p>
) : null}
{resource.error ? (
<div role="alert">
<p>
{resource.error.status === 404
? "This auction was not found."
: "Auction data could not load. Try again."}
</p>
<button
disabled={resource.loading}
onClick={resource.refresh}
type="button"
>
Try again
</button>
</div>
) : null}
{!route.item && resource.auction ? (
<>
<fieldset>
<legend>Single auction display</legend>
<label>
<input
checked={showFilters}
onChange={(event) => setShowFilters(event.target.checked)}
type="checkbox"
/>{" "}
Show filters
</label>{" "}
<label>
<input
checked={showHeader}
onChange={(event) => setShowHeader(event.target.checked)}
type="checkbox"
/>{" "}
Show header
</label>
</fieldset>
<AuctionView
auction={resource.auction}
getItemHref={(auction, item) =>
auctionHref(
route.base,
route.community,
auction.slug || auction.path || auction.id,
item.slug || item.path || item.id
)
}
onBack={() =>
window.location.assign(auctionHref(route.base, route.community))
}
showFilters={showFilters}
showHeader={showHeader}
theme={theme}
{...(style ? { style } : {})}
/>
</>
) : null}
{route.item && resource.auction && !resolvedItem && !selection ? (
<p role="status">
This item was not found. <a href={catalogHref}>View auction items</a>
</p>
) : null}
{selection ? (
<LockerverseAuctionItem
auction={resource.auction ?? selection.auction}
client={resource.client}
itemId={selection.itemId}
onBack={
pending ? undefined : () => window.location.assign(catalogHref)
}
onBidSubmitted={() => {
setMessage(
"Bid submitted. This does not mean you have won or been charged."
);
resource.refresh();
}}
onInteractionLockChange={(next) => {
setLocked(next);
reportPending({ locked: next });
}}
onPurchaseComplete={() => {
setMessage("Payment complete.");
resource.refresh();
}}
onPurchaseRecoveryChange={(next) => {
setRecovery(next);
reportPending({ recovery: next !== null });
}}
onUncertain={(kind) => {
if (kind === "bid") {
setUncertainBid(true);
}
reportPending({
locked: true,
uncertainBid: kind === "bid" || uncertainBid,
});
setMessage(
kind === "bid"
? "The bid result is unknown. Do not submit another bid. Contact the auction organizer."
: "The payment result is unknown. Keep this page open and use the recovery action."
);
}}
returnToItemHref={auctionHref(
route.base,
route.community,
route.auction,
route.item
)}
theme={theme}
{...(style ? { style } : {})}
/>
) : null}
<p aria-live="polite" className="example-status">
{message}
</p>
{pending ? (
<p className="example-status">
Keep this page open until the submission is resolved. Recovery is held
in memory only; browser shutdown or forced navigation can discard it.
</p>
) : null}
</div>
);
}
export default function AuctionsExample({
theme = "light",
style,
}: {
theme?: "light" | "dark";
style?: LockerverseStyle;
}) {
const [pending, setPending] = useState(false);
const [draftCommunity, setDraftCommunity] = useState("auburn");
const [draftAuction, setDraftAuction] = useState("");
const [route, setRoute] = useState<Route | null>(null);
usePendingNavigation(pending);
useEffect(() => {
const url = new URL(window.location.href);
const community = url.searchParams.get("community")?.trim() || "auburn";
const auction = url.searchParams.get("auction")?.trim() || "";
setDraftCommunity(community);
setDraftAuction(auction);
setRoute({
auction,
base: url.href,
community,
item: url.searchParams.get("item")?.trim() || "",
});
}, []);
return (
<div>
<form
onSubmit={(event) => {
event.preventDefault();
if (route && draftCommunity.trim() && !pending) {
window.location.assign(
auctionHref(
route.base,
draftCommunity.trim(),
draftAuction.trim()
)
);
}
}}
>
<label>
Community slug{" "}
<input
disabled={pending}
onChange={(event) => setDraftCommunity(event.target.value)}
required
value={draftCommunity}
/>
</label>
<label>
Auction slug (optional){" "}
<input
disabled={pending}
onChange={(event) => setDraftAuction(event.target.value)}
value={draftAuction}
/>
</label>
<button disabled={pending || !route} type="submit">
{draftAuction.trim() ? "Load auction" : "Load auctions"}
</button>
</form>
<p className="example-status">
This page uses production. Browsing does not place a bid. Submit only if
you intend to bid or purchase. Keep this page open during submission.
</p>
{route ? null : <p role="status">Loading auction selection…</p>}
{route?.auction ? (
<SingleAuction
key={`${route.community}:${route.auction}:${route.item}`}
onPendingChange={setPending}
route={route}
theme={theme}
{...(style ? { style } : {})}
/>
) : null}
{route && !route.auction ? (
<>
{route.item ? (
<p role="status">
An item link must also include its auction. Choose an auction
below.
</p>
) : null}
<CommunityAuctions
key={route.community}
route={route}
theme={theme}
{...(style ? { style } : {})}
/>
</>
) : null}
</div>
);
}