CubeCom Pro Docs
Developers

Build a custom storefront

Complete Next.js / React storefront integration consuming 3D state and cart API.

A complete guide for integrating CubeCom Pro 3D canvas rendering and cart handoff in a modern React / Next.js application.

Full React Component Implementation

'use client';

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

export function ConfiguratorPage({ productId }: { productId: string }) {
  const [selections, setSelections] = useState<Record<string, string>>({
    frame: 'walnut',
    fabric: 'beige',
    legs: 'brass',
  });
  const [resolution, setResolution] = useState<any>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    async function updateConfiguration() {
      setLoading(true);
      const res = await fetch('/api/resolve', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          productId,
          selectionsJson: JSON.stringify(selections),
        }),
      });
      const data = await res.json();
      setResolution(data.data.resolveConfiguration);
      setLoading(false);
    }
    updateConfiguration();
  }, [productId, selections]);

  const handleSelect = (choiceKey: string, valueKey: string) => {
    setSelections((prev) => ({ ...prev, [choiceKey]: valueKey }));
  };

  const handleAddToCart = async () => {
    if (!resolution?.valid || !resolution?.commerce?.variantReference) return;
    await fetch('/api/cart/add', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        variantId: resolution.commerce.variantReference,
        sku: resolution.commerce.sku,
      }),
    });
    alert('Item added to cart!');
  };

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-8 p-8">
      {/* 3D Viewport Column */}
      <div className="border rounded-xl bg-slate-900 h-[500px] flex items-center justify-center text-white">
        {loading ? <p>Resolving 3D Scene...</p> : <p>3D Canvas ({resolution?.threeD?.activeObjectAssetRevisionIds?.length || 0} active meshes)</p>}
      </div>

      {/* Option Selector Column */}
      <div className="space-y-6">
        <h1 className="text-2xl font-bold">Custom Lounge Chair</h1>

        {resolution && !resolution.valid && (
          <div className="p-4 bg-red-50 text-red-700 border border-red-200 rounded-lg">
            {resolution.violations.join(', ')}
          </div>
        )}

        <div>
          <h3 className="font-semibold mb-2">Wood Finish</h3>
          <div className="flex gap-2">
            {['walnut', 'oak'].map((v) => (
              <button
                key={v}
                onClick={() => handleSelect('frame', v)}
                className={`px-4 py-2 border rounded ${selections.frame === v ? 'border-indigo-600 bg-indigo-50 font-bold' : ''}`}
              >
                {v}
              </button>
            ))}
          </div>
        </div>

        <button
          onClick={handleAddToCart}
          disabled={!resolution?.valid || loading}
          className="w-full py-3 bg-indigo-600 text-white font-semibold rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
        >
          {loading ? 'Updating...' : resolution?.valid ? 'Add to Cart' : 'Invalid Combination'}
        </button>
      </div>
    </div>
  );
}

On this page