Skip to content

Next.js Integration: Complete Example

A Next.js component for the Wedding Band Builder. Uses next/dynamic to avoid SSR issues since the viewer requires browser APIs.

Back to Script Tag guide

Component (Client-Only)

jsx
// components/WeddingBandBuilder.jsx
'use client';

import { useEffect, useRef, useState } from 'react';

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,
  style = {},
}) {
  const containerRef = useRef(null);
  const [loading, setLoading] = useState(true);

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

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

      setLoading(false);

      if (theme) api.setTheme(theme);

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

      onReady?.(api);
    }

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

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

      new window.ijewelViewer.Viewer(containerRef.current, {
        name: 'Wedding Band Builder',
        version: 'v5',
        basePath,
        plugins: {
          WeddingBandBuilder: { manifestUrl, showUI },
        },
      }, {
        showCard: false,
        showSwitchNode: false,
        showUiButtons: showUI,
        showConfigurator: false,
        showZoomButtons: showUI,
        enableZoom: true,
      });
    }

    init();

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

  return (
    <div style={{ position: 'relative', width: '100%', height: '100%', ...style }}>
      <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
      {loading && (
        <div style={{
          position: 'absolute', inset: 0,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          background: '#f5f5f5', color: '#888', fontSize: 14,
        }}>
          Loading 3D viewer...
        </div>
      )}
    </div>
  );
}

Page (App Router)

jsx
// app/configurator/page.jsx
'use client';

import { useState } from 'react';
import WeddingBandBuilder from '@/components/WeddingBandBuilder';

export default function ConfiguratorPage() {
  const [price, setPrice] = useState(null);
  const [api, setApi] = useState(null);

  return (
    <main>
      <h1 style={{ padding: '16px 24px', margin: 0 }}>Design Your Wedding Band</h1>

      <div style={{ width: '100%', height: '70vh' }}>
        <WeddingBandBuilder
          basePath="https://your-cdn.com/wbb-assets/"
          theme="modern-minimal"
          onReady={setApi}
          onPriceChange={(p) => setPrice(p)}
        />
      </div>

      <div style={{ padding: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span style={{ fontSize: 28, fontWeight: 600 }}>
          {price ? `$${price.totalUsd.toFixed(2)}` : 'Loading...'}
        </span>
        <button
          onClick={async () => {
            if (!api) return;
            const config = api.exportConfig();
            // POST to your Next.js API route
            await fetch('/api/cart', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify(config),
            });
          }}
          style={{
            padding: '12px 32px', fontSize: 16, fontWeight: 600,
            background: '#333', color: '#fff', border: 'none',
            borderRadius: 8, cursor: 'pointer',
          }}
        >
          Add to Cart
        </button>
      </div>
    </main>
  );
}

Pages Router (Legacy)

If using the Pages Router, use next/dynamic to disable SSR:

jsx
// pages/configurator.jsx
import dynamic from 'next/dynamic';

const WeddingBandBuilder = dynamic(
  () => import('@/components/WeddingBandBuilder'),
  { ssr: false, loading: () => <div>Loading...</div> }
);

export default function ConfiguratorPage() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <WeddingBandBuilder showUI={true} />
    </div>
  );
}

API Route

javascript
// app/api/cart/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
  const config = await request.json();

  // Save to your database, Stripe, or order system
  console.log('Ring config:', config);

  return NextResponse.json({ success: true });
}