Skip to main content

Custom to Widgets Migration

This guide helps you migrate from fully custom UI implementations to pre-built Fanfare widgets. Widgets provide a faster path to integration while still supporting customization.

Benefits of Widgets

BenefitDescription
Less CodeNo need to build state management or UI
Automatic UpdatesBug fixes and improvements without code changes
Consistent UXBattle-tested user experience patterns
AccessibilityBuilt-in accessibility features
i18n SupportMulti-language support out of the box
ThemingCustomizable via CSS variables

Migration Approach

Option 1: Full Widget Replacement

Replace your entire custom UI with a widget:
// Before: Custom implementation
function CustomQueue() {
  const { status, position, enter, leave, isLoading, error } = useQueue("queue_123");

  // 100+ lines of custom UI code...
  return (
    <div className="queue-container">
      <header className="queue-header">...</header>
      <div className="queue-body">...</div>
      <footer className="queue-footer">...</footer>
    </div>
  );
}

// After: Widget
function WidgetQueue() {
  return <fanfare-queue-widget queue-id="queue_123" />;
}

Option 2: Partial Customization with Slots

Keep some custom elements while using the widget structure:
import { QueueWidget } from "@waitify-io/fanfare-sdk-solid";

function PartiallyCustomQueue() {
  return (
    <QueueWidget
      queueId="queue_123"
      slots={{
        header: (props) => (
          <div className="my-brand-header">
            <img src="/logo.svg" alt="Brand" />
            <h2>{props.title}</h2>
          </div>
        ),
        // Use default for other slots
      }}
    />
  );
}

Option 3: Render Props for Full Control

Use render props to maintain full control while leveraging widget state:
<QueueWidget queueId="queue_123">
  {({ status, position, enter, leave, isEntering, isLeaving }) => (
    <YourCompletelyCustomUI status={status} position={position} onEnter={enter} onLeave={leave} />
  )}
</QueueWidget>

Queue Migration

Custom Queue UI

function CustomQueueUI({ queueId }: { queueId: string }) {
  const { isAuthenticated, guest } = useFanfareAuth();
  const { queue, status, position, estimatedWait, admittanceToken, enter, leave, isLoading, error } = useQueue(queueId);

  if (isLoading) {
    return (
      <div className="queue-loading">
        <Spinner />
        <p>Loading queue...</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="queue-error">
        <h3>Error</h3>
        <p>{error.message}</p>
        <button onClick={() => window.location.reload()}>Retry</button>
      </div>
    );
  }

  if (status === "admitted") {
    return (
      <div className="queue-admitted">
        <CheckIcon />
        <h2>You are In!</h2>
        <a href={`/checkout?token=${admittanceToken}`}>Continue to Checkout</a>
      </div>
    );
  }

  return (
    <div className="queue-ui">
      <header className="queue-header">
        <QueueIcon />
        <h2>Virtual Waiting Room</h2>
        <p>Join the queue for exclusive access</p>
      </header>

      {status === "queued" && (
        <div className="queue-position">
          <span className="position-number">{position}</span>
          <span className="position-label">in line</span>
          {estimatedWait && <p>Estimated wait: ~{estimatedWait} min</p>}
        </div>
      )}

      <footer className="queue-actions">
        {status === "queued" ? (
          <button onClick={leave}>Leave Queue</button>
        ) : (
          <button
            onClick={async () => {
              if (!isAuthenticated) await guest();
              await enter();
            }}
          >
            Enter Queue
          </button>
        )}
      </footer>
    </div>
  );
}

Widget Replacement

function WidgetQueueUI({ queueId }: { queueId: string }) {
  const widgetRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const widget = widgetRef.current;
    if (!widget) return;

    const handleAdmitted = (e: CustomEvent<{ token: string }>) => {
      window.location.href = `/checkout?token=${e.detail.token}`;
    };

    widget.addEventListener("fanfare-queue-admitted", handleAdmitted);
    return () => widget.removeEventListener("fanfare-queue-admitted", handleAdmitted);
  }, []);

  return (
    <fanfare-queue-widget
      ref={widgetRef}
      queue-id={queueId}
      show-header="true"
      show-estimated-wait="true"
      show-actions="true"
    />
  );
}

Draw Migration

Custom Draw UI

function CustomDrawUI({ drawId }: { drawId: string }) {
  const { draw, status, result, timeUntilDraw, enter, withdraw, isLoading } = useDraw(drawId);

  // ... 80+ lines of custom UI
}

Widget Replacement

function WidgetDrawUI({ drawId }: { drawId: string }) {
  const widgetRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const widget = widgetRef.current;
    if (!widget) return;

    const handleResult = (e: CustomEvent<{ won: boolean }>) => {
      if (e.detail.won) {
        showConfetti();
      }
    };

    widget.addEventListener("fanfare-draw-result", handleResult);
    return () => widget.removeEventListener("fanfare-draw-result", handleResult);
  }, []);

  return <fanfare-draw-widget ref={widgetRef} draw-id={drawId} />;
}

Auction Migration

Custom Auction UI

function CustomAuctionUI({ auctionId }: { auctionId: string }) {
  const { auction, status, currentBid, myBid, minNextBid, bidHistory, timeRemaining, isWinning, placeBid, isLoading } =
    useAuction(auctionId);

  const [bidAmount, setBidAmount] = useState(minNextBid);

  // ... 150+ lines of custom UI with bid form, history, countdown
}

Widget Replacement

function WidgetAuctionUI({ auctionId }: { auctionId: string }) {
  const widgetRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const widget = widgetRef.current;
    if (!widget) return;

    const handleOutbid = () => {
      new Audio("/sounds/outbid.mp3").play();
    };

    const handleWin = (e: CustomEvent<{ token: string }>) => {
      showConfetti();
      window.location.href = `/checkout?token=${e.detail.token}`;
    };

    widget.addEventListener("fanfare-auction-outbid", handleOutbid);
    widget.addEventListener("fanfare-auction-win", handleWin);

    return () => {
      widget.removeEventListener("fanfare-auction-outbid", handleOutbid);
      widget.removeEventListener("fanfare-auction-win", handleWin);
    };
  }, []);

  return <fanfare-auction-widget ref={widgetRef} auction-id={auctionId} show-bid-history="true" currency-code="USD" />;
}

Experience Journey Migration

Custom Journey Flow

function CustomExperienceFlow({ experienceId }: { experienceId: string }) {
  const { journey, state, status, start } = useExperienceJourney(experienceId);

  // Complex switch statement handling all journey stages
  // 200+ lines of conditional rendering
}

Widget Replacement

function WidgetExperienceFlow({ experienceId }: { experienceId: string }) {
  const widgetRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const widget = widgetRef.current;
    if (!widget) return;

    const handleAdmitted = (e: CustomEvent<{ token: string }>) => {
      window.location.href = `/checkout?token=${e.detail.token}`;
    };

    widget.addEventListener("fanfare-admitted", handleAdmitted);
    return () => widget.removeEventListener("fanfare-admitted", handleAdmitted);
  }, []);

  return (
    <fanfare-experience-widget
      ref={widgetRef}
      experience-id={experienceId}
      auto-start="true"
      checkout-url="/checkout"
    />
  );
}

Styling Comparison

Custom CSS (Before)

.queue-container {
  max-width: 400px;
  margin: 0 auto;
  padding: 24px;
  border-radius: 12px;
  background: white;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.queue-header h2 {
  font-size: 1.5rem;
  color: #1f2937;
}

.queue-button {
  background: #3b82f6;
  color: white;
  padding: 12px 24px;
  border-radius: 8px;
}

/* ... 50+ more rules */

Widget CSS Variables (After)

fanfare-queue-widget {
  --fanfare-primary: #3b82f6;
  --fanfare-background: #ffffff;
  --fanfare-foreground: #1f2937;
  --fanfare-radius: 0.75rem;
  --fanfare-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  max-width: 400px;
  margin: 0 auto;
}

What to Keep Custom

Not everything should be a widget. Keep custom implementations for:
  • Unique brand experiences that differ significantly from standard patterns
  • Complex integrations with other systems (shopping cart, user profiles)
  • Custom animations beyond what CSS variables support
  • A/B testing different UI approaches

Migration Checklist

  • Identify which custom UIs can be replaced with widgets
  • Install @waitify-io/fanfare-sdk-solid
  • Register web components at app startup
  • Replace custom UIs with widgets one at a time
  • Migrate event handling from hooks to widget events
  • Update styling to use CSS variables
  • Test all flows thoroughly
  • Remove unused hook code