Skip to content

React Integration: Complete Example

A React component that loads the Wedding Band Builder with full API access.

Back to Script Tag guide

Component

jsx
// WeddingBandBuilder.jsx
import { useEffect, useRef, useState } from 'react';

// Load the mini-viewer script once
const SCRIPT_URL = 'https://releases.ijewel3d.com/libs/mini-viewer/latest/bundle.iife.js';
let scriptLoaded = false;

function loadScript() {
  if (scriptLoaded) return Promise.resolve();
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = SCRIPT_URL;
    script.onload = () => { scriptLoaded = true; resolve(); };
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

export default function WeddingBandBuilder({
  manifestUrl = 'wedding-band-project.json',
  basePath = 'https://your-cdn.com/wbb-assets/',
  showUI = true,
  theme = null,
  onReady = null,
  onPriceChange = null,
}) {
  const containerRef = useRef(null);
  const apiRef = useRef(null);
  const [price, setPrice] = useState(null);

  useEffect(() => {
    let disposed = false;

    async function init() {
      await loadScript();
      if (disposed || !containerRef.current) return;

      const project = {
        name: 'Wedding Band Builder',
        version: 'v5',
        basePath,
        plugins: {
          WeddingBandBuilder: { manifestUrl, showUI },
        },
      };

      new window.ijewelViewer.Viewer(containerRef.current, project, {
        showCard: false,
        showSwitchNode: false,
        showUiButtons: showUI,
        showConfigurator: false,
        showZoomButtons: showUI,
        enableZoom: true,
      });
    }

    function handleReady(e) {
      const viewer = e.detail.viewer;
      const api = viewer.getPluginByType('WeddingBandBuilder')?.controller;
      if (!api) return;

      apiRef.current = api;

      if (theme) api.setTheme(theme);

      api.events.on('price:updated', (data) => {
        if (data.pricing) {
          setPrice(data.pricing.totalUsd);
          onPriceChange?.(data.pricing);
        }
      });

      onReady?.(api);
    }

    window.addEventListener('ijewel-viewer-ready', handleReady);
    init();

    return () => {
      disposed = true;
      window.removeEventListener('ijewel-viewer-ready', handleReady);
    };
  }, [manifestUrl, basePath, showUI]);

  return (
    <div style={{ width: '100%', height: '100%' }}>
      <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
    </div>
  );
}

Usage

jsx
// App.jsx
import WeddingBandBuilder from './WeddingBandBuilder';

function App() {
  const handleReady = (api) => {
    console.log('WBB ready, available profiles:', api.getAvailableProfiles());
  };

  const handlePrice = (pricing) => {
    console.log(`Total: $${pricing.totalUsd.toFixed(2)}`);
  };

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <WeddingBandBuilder
        basePath="https://your-cdn.com/wbb-assets/"
        showUI={true}
        theme="luxury-gold"
        onReady={handleReady}
        onPriceChange={handlePrice}
      />
    </div>
  );
}

Headless with Custom Controls

jsx
import { useState, useCallback } from 'react';
import WeddingBandBuilder from './WeddingBandBuilder';

function CustomConfigurator() {
  const [api, setApi] = useState(null);
  const [price, setPrice] = useState(null);
  const [activeProfile, setActiveProfile] = useState(0);

  const handleReady = useCallback((wbbApi) => {
    setApi(wbbApi);
  }, []);

  const profiles = api?.getAvailableProfiles() || [];
  const metals = api?.getAvailableMetals() || [];

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <div style={{ flex: 1 }}>
        <WeddingBandBuilder
          showUI={false}
          onReady={handleReady}
          onPriceChange={(p) => setPrice(p.totalUsd)}
        />
      </div>

      <div style={{ padding: 16, background: '#fff', borderTop: '1px solid #eee' }}>
        <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
          {profiles.map((p) => (
            <button
              key={p.index}
              onClick={async () => {
                await api.setProfile(p.index);
                setActiveProfile(p.index);
              }}
              style={{
                padding: '8px 16px',
                border: '1px solid #ddd',
                borderRadius: 6,
                background: activeProfile === p.index ? '#333' : '#fff',
                color: activeProfile === p.index ? '#fff' : '#333',
                cursor: 'pointer',
              }}
            >
              {p.name}
            </button>
          ))}
        </div>

        <div style={{ display: 'flex', gap: 8 }}>
          {metals.map((m) => (
            <button
              key={m.id}
              onClick={() => api.setMaterial(1, m.id, 'Polished')}
              style={{
                padding: '8px 16px',
                border: '1px solid #ddd',
                borderRadius: 6,
                background: '#fff',
                cursor: 'pointer',
              }}
            >
              {m.name}
            </button>
          ))}
        </div>

        {price && (
          <div style={{ marginTop: 16, fontSize: 24, fontWeight: 600 }}>
            ${price.toFixed(2)}
          </div>
        )}
      </div>
    </div>
  );
}