Gym Flooring Store Demo (Ticket 1542507)

Gym flooring store demo (Ticket 1542507)

This page collects everything that was built and tested for the pre-sale questions in ticket 1542507. Stack used: GeneratePress 3.6.1 (free) as the active theme, GenerateBlocks 2.4.1 (free), SureCart 4.8.0. The store runs in test mode with the Test Processor, so you can complete a checkout without paying anything.

1. Pages to try

  • Gym Flooring Shop: a GenerateBlocks header above the native SureCart Product List. Limited to one collection, with search, sorting and two custom attribute filters (Thickness and Material).
  • Same shop, pre-filtered to 20mm + Rubber (filters narrow the results, see the note on the AND fix below).
  • Rubber Gym Floor Tile: the product page whose layout is built from GenerateBlocks Element and Text blocks wrapping SureCart’s product blocks. It has variants (Thickness x Colour) with their own price, SKU, stock and image, a compare-at price with a Sale badge, a gallery with thumbnails, stock tracking with back-orders allowed on 20mm / Grey, a flooring calculator, a specifications table and a public spec-sheet PDF.
  • EVA Foam Interlocking Mat and PVC Garage Floor Tile: two more products in the collection (using the standard product template) so the filters have something to compare.
  • Specification sheet PDF: a normal media library file linked from the product page, downloadable before purchase.

2. Suggested walk-through

  • Open the Rubber Gym Floor Tile page. Switch Thickness to 20mm (price changes to $29) and Colour to Grey (the gallery jumps to the grey image).
  • In the flooring calculator enter 5 m x 4 m with 10% wastage and click Calculate and set quantity. The SureCart Quantity field changes to 22.
  • Click Add To Cart. The cart shows 22 x Rubber Gym Floor Tile, 20mm / Grey, $638 (with $660 struck through). The 20mm / Grey variant has zero stock and is accepted because back-orders are allowed on the product.
  • Click Checkout, enter any name, email and a UK address. Shipping offers Free delivery on orders over 300 (selected automatically) and Standard delivery (priced by number of tiles) at $29.95 for 10 to 49 tiles. Try 5 tiles instead and only Standard at $14.95 appears; 50 or more tiles hits the $59.95 band.
  • Pay with the Test Processor to see the order and the customer dashboard.

3. Calculator on a landing page instead of the product page

The product-page calculator above writes into SureCart’s own Quantity field, so the normal Add To Cart button, variants and the cart drawer keep working. If the calculator lives on a separate landing page, the documented route is a checkout URL that carries the price, the variant and the quantity. Try it here:

Standalone calculator (sends the quantity to the checkout through the URL)

Reference: Creating custom buy links (line_items[0][price_id], line_items[0][quantity], line_items[0][variant_id]).

4. Code used on this site

Snippet A registers the two attribute taxonomies (Thickness, Material) and the [gym_flooring_calculator] shortcode. The shortcode reads the square metres per tile and the default wastage from the product’s own metadata (tile_area_m2, wastage_percent), so each product can have different values.

<?php
/**
 * TICKET 1542507 - Flooring store demo helpers
 * 1) Two product attribute taxonomies (Thickness, Material) usable in the Product List filters.
 * 2) [gym_flooring_calculator] shortcode: length x width -> tiles needed (with wastage),
 *    written straight into SureCart's own Quantity field on the product page.
 */
if ( ! defined( 'ABSPATH' ) ) { exit; }

add_action( 'init', function () {
	$common = [
		'public'            => true,
		'hierarchical'      => true,
		'show_ui'           => true,
		'show_admin_column' => true,
		'show_in_rest'      => true,
	];
	register_taxonomy( 'sc_thickness', [ 'sc_product' ], array_merge( $common, [
		'labels'    => [ 'name' => 'Thickness', 'singular_name' => 'Thickness', 'menu_name' => 'Thickness', 'add_new_item' => 'Add New Thickness' ],
		'rest_base' => 'sc_thickness',
		'rewrite'   => [ 'slug' => 'thickness' ],
	] ) );
	register_taxonomy( 'sc_material', [ 'sc_product' ], array_merge( $common, [
		'labels'    => [ 'name' => 'Material', 'singular_name' => 'Material', 'menu_name' => 'Material', 'add_new_item' => 'Add New Material' ],
		'rest_base' => 'sc_material',
		'rewrite'   => [ 'slug' => 'material' ],
	] ) );
}, 20 );

add_action( 'init', function () {
	foreach ( [ 'sc_thickness' => [ '15mm', '20mm', '30mm', '40mm' ], 'sc_material' => [ 'Rubber', 'EVA Foam', 'PVC' ] ] as $tax => $terms ) {
		foreach ( $terms as $t ) {
			if ( ! term_exists( $t, $tax ) ) { wp_insert_term( $t, $tax ); }
		}
	}
}, 30 );

add_shortcode( 'gym_flooring_calculator', function ( $atts ) {
	$atts = shortcode_atts( [ 'tile_area' => '', 'wastage' => '' ], $atts );
	$tile_area = (float) $atts['tile_area'];
	$wastage   = $atts['wastage'];
	// Read the per-product values from SureCart product metadata when on a product page.
	if ( function_exists( 'sc_get_product' ) ) {
		$product = sc_get_product();
		if ( $product && ! is_wp_error( $product ) ) {
			if ( ! $tile_area && ! empty( $product->metadata->tile_area_m2 ) ) { $tile_area = (float) $product->metadata->tile_area_m2; }
			if ( '' === $wastage && isset( $product->metadata->wastage_percent ) ) { $wastage = (float) $product->metadata->wastage_percent; }
		}
	}
	if ( ! $tile_area ) { $tile_area = 1; }
	if ( '' === $wastage ) { $wastage = 10; }
	$id = 'gfc-' . wp_unique_id();
	ob_start(); ?>
<div class="gfc" id="<?php echo esc_attr( $id ); ?>" data-tile-area="<?php echo esc_attr( $tile_area ); ?>" style="border:1px solid #d9dde3;border-radius:10px;padding:18px 20px;background:#f7f8fa;margin:8px 0 16px">
	<div style="font-weight:600;font-size:17px;margin-bottom:4px">Flooring calculator</div>
	<div style="font-size:14px;color:#555;margin-bottom:12px">Each tile covers <strong><?php echo esc_html( rtrim( rtrim( number_format( $tile_area, 2, '.', '' ), '0' ), '.' ) ); ?> m&sup2;</strong>. Enter the area and we will set the quantity for you.</div>
	<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;align-items:end">
		<label style="font-size:13px">Length (m)<input class="gfc-l" type="number" min="0" step="0.01" value="5" style="width:100%;margin-top:4px;padding:8px;border:1px solid #cfd4db;border-radius:6px"></label>
		<label style="font-size:13px">Width (m)<input class="gfc-w" type="number" min="0" step="0.01" value="4" style="width:100%;margin-top:4px;padding:8px;border:1px solid #cfd4db;border-radius:6px"></label>
		<label style="font-size:13px">Wastage (%)<input class="gfc-p" type="number" min="0" step="1" value="<?php echo esc_attr( $wastage ); ?>" style="width:100%;margin-top:4px;padding:8px;border:1px solid #cfd4db;border-radius:6px"></label>
	</div>
	<button type="button" class="gfc-go" style="margin-top:12px;padding:10px 16px;border:0;border-radius:6px;background:#111827;color:#fff;font-weight:600;cursor:pointer">Calculate and set quantity</button>
	<div class="gfc-out" style="margin-top:12px;font-size:15px;line-height:1.5"></div>
</div>
<script>
(function(){
	var box=document.getElementById('<?php echo esc_js( $id ); ?>'); if(!box) return;
	var tileArea=parseFloat(box.getAttribute('data-tile-area'))||1;
	box.querySelector('.gfc-go').addEventListener('click',function(){
		var L=parseFloat(box.querySelector('.gfc-l').value)||0, W=parseFloat(box.querySelector('.gfc-w').value)||0, P=parseFloat(box.querySelector('.gfc-p').value)||0;
		var area=L*W, withWaste=area*(1+P/100), tiles=Math.max(1,Math.ceil(withWaste/tileArea));
		var out=box.querySelector('.gfc-out');
		if(!area){ out.innerHTML='Please enter a length and a width.'; return; }
		/* SureCart product page: the form that wraps this product's blocks */
		var form=box.closest('form[data-sc-block-id="product-page"]')||document.querySelector('form[data-sc-block-id="product-page"]');
		var qty=form?form.querySelector('input[data-wp-on--change="callbacks.onQuantityChange"], input.sc-quantity-selector__control, input.wp-block-surecart-product-quantity-input'):null;
		var msg='Area '+area.toFixed(2)+' m&sup2; + '+P+'% wastage = '+withWaste.toFixed(2)+' m&sup2; &rarr; <strong>'+tiles+' tiles</strong>.';
		if(qty){
			qty.value=tiles;
			qty.dispatchEvent(new Event('change',{bubbles:true}));
			msg+=' Quantity has been set to '+tiles+'. Choose your thickness and colour, then click Add To Cart.';
		}else{
			msg+=' (No SureCart quantity field found on this page.)';
		}
		out.innerHTML=msg;
	});
})();
</script>
	<?php
	return ob_get_clean();
} );

Snippet B is needed for GenerateBlocks (or any block that prints its CSS once) inside SureCart’s Product Page block. Without it the GenerateBlocks styles inside the product area are lost, because the Product Page block renders its inner blocks twice and keeps the second pass. This has been reported to the SureCart engineering team.

<?php
/**
 * TICKET 1542507 - FIX: stop SureCart's Product Page block rendering its inner blocks twice.
 *
 * WordPress renders a dynamic block's inner blocks first and hands the result to the block's
 * render callback. SureCart's surecart/product-page view.php ignores that result and renders the
 * inner blocks again itself (the same pattern core/post-template uses). Core avoids the double
 * render by registering post-template with skip_inner_blocks => true; SureCart does not, so any
 * inner block with "print once" side effects (GenerateBlocks CSS, for example) prints in the first,
 * discarded pass and ends up missing from the page.
 */
add_action( 'init', function () {
	$type = \WP_Block_Type_Registry::get_instance()->get_registered( 'surecart/product-page' );
	if ( $type ) {
		$type->skip_inner_blocks = true;
	}
}, 100 );

Snippet C makes the Product List filters narrow the results (AND across facets, OR within one facet). Out of the box SureCart combines the collection restriction and every filter with OR, so a collection-limited list ignores the filters and two different filters show the union. Also reported to engineering.

<?php
/**
 * TICKET 1542507 - FIX 2: make Product List filters NARROW the results (AND between facets).
 *
 * SureCart's Product List block builds one tax_query group with relation => OR
 * (app/src/Models/Blocks/ProductListBlock.php). That means a list limited to a collection
 * plus a Thickness filter returns "in the collection OR has that thickness", and two different
 * filters (Thickness + Material) return the union instead of the intersection. Switching the
 * group to AND gives shop-style faceting: OR within one facet, AND across facets.
 */
add_action( 'pre_get_posts', function ( $query ) {
	if ( is_admin() || 'sc_product' !== $query->get( 'post_type' ) ) {
		return;
	}
	$tax_query = $query->get( 'tax_query' );
	if ( empty( $tax_query ) || ! is_array( $tax_query ) ) {
		return;
	}
	foreach ( $tax_query as $i => $group ) {
		if ( is_array( $group ) && isset( $group['relation'] ) && 'OR' === strtoupper( $group['relation'] ) ) {
			$tax_query[ $i ]['relation'] = 'AND';
		}
	}
	$query->set( 'tax_query', $tax_query );
}, 20 );

5. Layout note for GenerateBlocks users

When the SureCart Product Media (gallery) block sits inside a CSS grid column, give that column min-width: 0 (or use minmax(0, 1fr) columns). Without it the slider inside the gallery reports an enormous intrinsic width and the column stretches off the page. This is standard CSS grid behaviour with any slider, not a SureCart or GenerateBlocks bug.

Scroll to Top