Step 2
Choose Your Fabric
Select a fabric to update your cushion quote.
Search Fabrics
Loading fabrics…
(() => {
const root = document.getElementById( 'oc-home-configurator' );
if ( !root || root.dataset.initialised === '1' ) { return; }
root.dataset.initialised = '1';
/* ========================================================= WORDPRESS / PLUGIN CONNECTION
These endpoints will be supplied by our Outdoor Cushions Builder plugin. ========================================================== */
const config = {
fabricEndpoint: '/wp-json/outdoor-cushions/v1/fabrics',
quoteEndpoint: '/wp-json/outdoor-cushions/v1/quote',
addToCartEndpoint: '/wp-json/outdoor-cushions/v1/add-to-cart',
currency: 'ZAR'
};
/* ========================================================= ELEMENTS ========================================================== */
const form = root.querySelector( '#oc-cushion-form' );
const lengthInput = root.querySelector( '#oc-length' );
const widthInput = root.querySelector( '#oc-width' );
const thicknessInput = root.querySelector( '#oc-thickness' );
const quantityInput = root.querySelector( '#oc-quantity' );
const fabricIdInput = root.querySelector( '#oc-fabric-id' );
const fabricNameInput = root.querySelector( '#oc-fabric-name' );
const selectedFabricBox = root.querySelector( '#oc-selected-fabric' );
const selectedFabricName = root.querySelector( '#oc-selected-fabric-name' );
const unitPriceEl = root.querySelector( '#oc-unit-price' );
const totalPriceEl = root.querySelector( '#oc-total-price' );
const totalLabelEl = root.querySelector( '#oc-total-label' );
const quoteMessageEl = root.querySelector( '#oc-quote-message' );
const orderButton = root.querySelector( '#oc-order-button' );
const orderButtonText = root.querySelector( '#oc-order-button-text' );
const colourFilter = root.querySelector( '#oc-filter-colour' );
const patternFilter = root.querySelector( '#oc-filter-pattern' );
const searchFilter = root.querySelector( '#oc-filter-search' );
const fabricGrid = root.querySelector( '#oc-fabric-grid' );
const fabricStatus = root.querySelector( '#oc-fabric-status' );
const loadMoreButton = root.querySelector( '#oc-load-more' );
/* ========================================================= STATE ========================================================== */
let selectedFabric = null;
let quoteController = null;
let fabricsController = null;
let quoteTimer = null;
let fabricTimer = null;
let currentPage = 1;
let hasMoreFabrics = false;
/* ========================================================= MONEY FORMAT ========================================================== */
const money = value =>
new Intl.NumberFormat( 'en-ZA', { style: 'currency', currency: config.currency || 'ZAR',
minimumFractionDigits: 2 } ).format( Number(value || 0) );
/* ========================================================= NUMBER VALIDATION ========================================================== */
const positiveNumber = input => {
const value = Number(input.value);
return ( Number.isFinite(value) && value > 0 ) ? value : 0; };
/* ========================================================= CURRENT CUSHION CONFIGURATION ========================================================== */
const currentPayload = () => {
const finish = form.querySelector( 'input[name="finish_type"]:checked' )?.value || 'plain';
const deepButtons = root.querySelector( '#oc-deep-buttons' ).checked;
return {
cushion_type: 'box',
unit: 'cm',
length_cm: positiveNumber( lengthInput ),
width_cm: positiveNumber( widthInput ),
thickness_cm: positiveNumber( thicknessInput ),
quantity: Math.max( 1, Math.floor( positiveNumber( quantityInput ) || 1 ) ),
inner_type: form.querySelector( 'input[name="inner_type"]:checked' )?.value || 'solid_foam',
finish_type: finish,
piping_type: finish === 'piping' ? 'self' : 'none',
deep_buttons: deepButtons ? 'yes' : 'none',
zip_position: deepButtons ? 'none' : 'long_side',
fabric_id: selectedFabric ? Number( selectedFabric.id ) : 0 }; };
/* ========================================================= VALIDATION ========================================================== */
const validForQuote = payload => (
payload.length_cm > 0 &&
payload.width_cm > 0 &&
payload.thickness_cm > 0 &&
payload.quantity >= 1 &&
payload.fabric_id > 0
);
/* ========================================================= CTA TEXT ========================================================== */
const updateCta = () => {
const p = currentPayload();
const qtyWord = p.quantity === 1 ? 'Cushion' : 'Cushions';
if ( p.length_cm && p.width_cm && p.thickness_cm ) {
orderButtonText.textContent = `Order your ${p.quantity} × ${p.length_cm} × ${p.width_cm} × ${p.thickness_cm} cm ${qtyWord}`;
} else {
orderButtonText.textContent = `Order your custom ${qtyWord.toLowerCase()}`; }
totalLabelEl.textContent = `Total for ${p.quantity} ${p.quantity === 1 ? 'Cushion' : 'Cushions'}`; };
/* ========================================================= RESET QUOTE ========================================================== */
const resetQuote = message => {
unitPriceEl.textContent = '—';
totalPriceEl.textContent = '—';
quoteMessageEl.textContent = message;
orderButton.disabled = true; };
/* ========================================================= LIVE QUOTE ========================================================== */
const scheduleQuote = () => {
updateCta();
clearTimeout( quoteTimer );
quoteTimer = setTimeout( requestQuote, 260 ); };
async function requestQuote() {
const payload = currentPayload();
if (!validForQuote(payload)) {
resetQuote( 'Enter your dimensions and choose a fabric to calculate your quote.' );
return; }
/* Oxford pricing has not yet been defined */
if ( payload.finish_type === 'oxford' ) {
resetQuote( 'The 1 cm Oxford finish is not yet available for online ordering.' );
return; }
if ( !config.quoteEndpoint ) {
resetQuote( 'Quote calculator is not connected.' );
return; }
if (quoteController) {
quoteController.abort(); }
quoteController = new AbortController();
quoteMessageEl.textContent = 'Calculating your custom quote…';
orderButton.disabled = true;
try {
const response = await fetch( config.quoteEndpoint, { method: 'POST',
credentials: 'same-origin',
signal: quoteController.signal,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( payload ) } );
const data = await response.json();
if ( !response.ok || data.success === false ) {
throw new Error( data.message || 'We could not calculate this cushion.' ); }
const unit = Number( data.individual_cost ?? data.unit_cost ?? 0 );
const total = Number( data.total_cost ?? ( unit * payload.quantity ) );
if ( !Number.isFinite(unit) || !Number.isFinite(total) || unit <= 0 || total <= 0 ) { throw new Error( 'The calculator returned an invalid price.' ); } unitPriceEl.textContent = money(unit); totalPriceEl.textContent = money(total); quoteMessageEl.textContent = selectedFabric ? `Quoted in ${selectedFabric.name}.` : 'Quote calculated.'; orderButton.disabled = false; } catch (error) { if ( error.name === 'AbortError' ) { return; } resetQuote( error.message || 'We could not calculate your quote.' ); } } /* ========================================================= FABRIC CARDS ========================================================== */ function renderFabrics( items, append = false ) { if (!append) { fabricGrid.innerHTML = ''; } items.forEach( fabric => {
const button = document.createElement( 'button' );
button.type = 'button';
button.className = 'oc-fabric-card';
button.dataset.fabricId = fabric.id;
button.setAttribute( 'aria-pressed',
selectedFabric && String( selectedFabric.id ) === String( fabric.id ) ? 'true' : 'false' );
const imageWrap = document.createElement( 'span' );
imageWrap.className = 'oc-fabric-card__image';
if (fabric.image) {
const image = document.createElement( 'img' );
image.src = fabric.image;
image.alt = fabric.name ? `${fabric.name} fabric` : 'Fabric';
image.loading = 'lazy';
imageWrap.appendChild( image ); }
const body = document.createElement( 'span' );
body.className = 'oc-fabric-card__body';
const name = document.createElement( 'span' );
name.className = 'oc-fabric-card__name';
name.textContent = fabric.name || 'Fabric';
const meta = document.createElement( 'span' );
meta.className = 'oc-fabric-card__meta';
const colour = document.createElement( 'span' );
colour.textContent = fabric.colour || '';
const pattern = document.createElement( 'span' );
pattern.textContent = fabric.pattern || '';
meta.append( colour, pattern );
body.append( name, meta );
button.append( imageWrap, body );
button.addEventListener( 'click', () => {
selectedFabric = fabric;
fabricIdInput.value = fabric.id;
fabricNameInput.value = fabric.name || '';
selectedFabricName.textContent = fabric.name || 'Selected Fabric';
selectedFabricBox.hidden = false;
fabricGrid .querySelectorAll( '.oc-fabric-card' ) .forEach( card => {
card.setAttribute( 'aria-pressed', card === button ? 'true' : 'false' ); } );
scheduleQuote(); } );
fabricGrid.appendChild( button );
} ); }
/* ========================================================= COLOUR FILTER OPTIONS ========================================================== */
function populateColours( colours ) {
if ( !Array.isArray(colours) || colourFilter.options.length > 1 ) { return; }
colours .filter(Boolean) .sort( (a, b) => a.localeCompare(b) ) .forEach( colour => {
const option = document.createElement( 'option' );
option.value = colour;
option.textContent = colour;
colourFilter.appendChild( option );
} ); }
/* ========================================================= LOAD FABRICS FROM WORDPRESS ========================================================== */
async function loadFabrics({ append = false } = {}) {
if ( !config.fabricEndpoint ) {
fabricStatus.textContent = 'Fabric catalogue is not connected yet.';
return; }
if ( fabricsController ) {
fabricsController.abort(); }
fabricsController = new AbortController();
if (!append) {
currentPage = 1; }
fabricStatus.textContent = append ? 'Loading more fabrics…' : 'Loading fabrics…';
try {
const url = new URL( config.fabricEndpoint, window.location.origin );
url.searchParams.set( 'page', currentPage );
url.searchParams.set( 'per_page', '24' );
if ( colourFilter.value ) {
url.searchParams.set( 'colour', colourFilter.value ); }
if ( patternFilter.value ) {
url.searchParams.set( 'pattern', patternFilter.value ); }
if ( searchFilter .value .trim() ) {
url.searchParams.set( 'search', searchFilter .value .trim() ); }
const response = await fetch( url.toString(), { credentials: 'same-origin',
signal: fabricsController.signal } );
const data = await response.json();
if (!response.ok) {
throw new Error( data.message || 'Unable to load fabrics.' ); }
const items = Array.isArray( data.items ) ? data.items : ( Array.isArray(data) ? data : [] );
renderFabrics( items, append );
populateColours( data.colours || [] );
hasMoreFabrics = Boolean( data.has_more );
loadMoreButton.hidden = !hasMoreFabrics;
fabricStatus.textContent = items.length || append ? '' : 'No fabrics match those filters.';
} catch (error) {
if ( error.name === 'AbortError' ) { return; }
fabricStatus.textContent = error.message || 'Unable to load fabrics.'; } }
/* ========================================================= FILTER DEBOUNCE ========================================================== */
function scheduleFabricReload() {
clearTimeout( fabricTimer );
fabricTimer = setTimeout( () => loadFabrics({ append: false }), 260 ); }
/* ========================================================= EVENT LISTENERS ========================================================== */
[ lengthInput, widthInput, thicknessInput, quantityInput ].forEach( input => input.addEventListener( 'input', scheduleQuote ) );
form .querySelectorAll( 'input[name="inner_type"], input[name="finish_type"], #oc-deep-buttons' ) .forEach( input => input.addEventListener( 'change', scheduleQuote ) );
colourFilter.addEventListener( 'change', scheduleFabricReload );
patternFilter.addEventListener( 'change', scheduleFabricReload );
searchFilter.addEventListener( 'input', scheduleFabricReload );
loadMoreButton.addEventListener( 'click', () => {
if ( !hasMoreFabrics ) { return; }
currentPage += 1;
loadFabrics({ append: true }); } );
/* ========================================================= ADD CUSTOM CUSHION TO CART ========================================================== */
form.addEventListener( 'submit', async event => {
event.preventDefault();
const payload = currentPayload();
if ( !validForQuote(payload) || orderButton.disabled ) { return; }
if ( !config.addToCartEndpoint ) {
quoteMessageEl.textContent = 'The order system still needs to be connected to WooCommerce.';
return; }
orderButton.disabled = true;
quoteMessageEl.textContent = 'Adding your custom cushion to the basket…';
try {
const response = await fetch( config.addToCartEndpoint, { method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( payload ) } );
const data = await response.json();
if ( !response.ok || data.success === false ) {
throw new Error( data.message || 'Unable to add this cushion to the basket.' ); }
if ( data.cart_url ) {
window.location.assign( data.cart_url );
return; }
quoteMessageEl.textContent = 'Your custom cushion has been added to the basket.';
orderButton.disabled = false;
} catch (error) {
quoteMessageEl.textContent = error.message || 'Unable to add this cushion to the basket.';
orderButton.disabled = false; } } );
/* ========================================================= START PAGE ========================================================== */
updateCta();
loadFabrics();
})();
