Skip to content
Lockerverse SDK
Esc
↑↓navigate↵open⌘Jpreview
Auctions14 examples

Components / Production

Auctions

Browse auction items and use the built-in bid or buy-now checkout.

All examples
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>
  );
}