Jake Vanderwerf
2026-09-14 7935d26cd16a4f8f69a59a44e27a7263b60e54bf
inc/blocks/MenuBlock.php
@@ -1,12 +1,12 @@
<?php
namespace JVBase\blocks;
use JVBase\base\Site;
use JVBase\managers\Cache;
use JVBase\forms\TaxonomySelector;
use JVBase\meta\Form;
use JVBase\meta\Meta;
use JVBase\meta\Render;
use JVBase\registrar\Registrar;
use WP_Block;
use WP_Post;
use WP_Query;
if (!defined('ABSPATH')) {
@@ -26,14 +26,34 @@
    protected string $detailsTitle;
    protected array $details;
   protected array|false $sections = false;
   protected string $base;
    public function __construct()
    {
        $this->cache = Cache::for('menu', WEEK_IN_SECONDS)->connectTo('post', 'menu_item');
        $this->cache = Cache::for('menu', 'post', WEEK_IN_SECONDS);
      if (Site::hasIntegration('square')) {
         $this->base = '_square_';
      } else if (Site::hasIntegration('helcim')) {
         $this->base = '_helcim_';
      }
        add_action('init', [ $this, 'registerBlock' ]);
      add_action('save_post', [$this, 'flushCache'], 10, 2);
      add_action('wp_footer', [$this, 'renderGallery']);
    }
    public function registerBlock()
   public function flushCache(int $postID, WP_Post $post):void
   {
      if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
      if (wp_is_post_revision($postID) || wp_is_post_autosave($postID)) return;
      $postType = $post->post_type;
      if ($postType !== 'product' || !in_array($postType, Registrar::getFoodMenuTypes())) {
         return;
      }
      error_log('Flushing food menu block cache');
      $this->cache->flush();
   }
    public function registerBlock():void
    {
        register_block_type($this->path, [
            'render_callback' => [ $this, 'render' ]
@@ -42,10 +62,12 @@
   protected function buildParams():void
   {
      if (is_post_type_archive(BASE.'menu_item')) {
//    if (is_post_type_archive(BASE.'menu_item') ||
      if (is_post_type_archive('product') ||
         (function_exists('is_shop') && is_shop())) {
         $this->params = ['all'];
      } elseif (is_tax(BASE.'section')) {
         jvbDump(get_queried_object());
//    } elseif (is_tax(BASE.'section')) {
      } elseif (is_tax('product_cat')) {
         $this->params = [];
      } else {
         $this->params = [];
@@ -58,9 +80,9 @@
      if (empty($this->params)) {
         return '';
      }
      $key = $this->cache->generateKey($this->params);
      jvbInlineStyles('forms');
      return $this->cache->remember(
         $key,
         $this->params,
         function() {
            ob_start();
            $this->renderBlock();
@@ -76,6 +98,23 @@
        $this->renderOnThisPage();
    }
   protected function termHasPosts(int $termID):bool
   {
      $query = new WP_Query([
         'post_type' => 'product',
         'post_status'  => 'publish',
         'posts_per_page'  => 1,
         'fields' => 'ids',
         'tax_query' => [
            [
               'taxonomy' => 'product_cat',
               'terms'  => $termID
            ]
         ]
      ]);
      return $query->have_posts();
   }
    protected function renderHeader():void
    {
      $title = 'Our Menu';
@@ -86,11 +125,13 @@
        ?>
        <header id="top">
            <h1><?=$title?></h1>
         <nav class="menu"><ul>
        </header>
      <nav class="menu"><ul>
            <?php
            $sections = $this->getSections();
            foreach ($sections as $section) {
               if (!jvbTermHasPosts($section['id'], BASE.'section')) {
//             if (!jvbTermHasPosts($section['id'], BASE.'section')) {
               if (!$this->termHasPosts($section['id'])) {
                  continue;
               }
               ?>
@@ -99,19 +140,13 @@
            }
            ?>
         </ul></nav>
        </header>
        <?php
    }
   protected function getSections():array
   {
      if (!$this->sections) {
         $options = Meta::forOptions('options');
         $sections = $options->get('menu_section_order');
         if (!is_array($sections)) {
            $sections = [];
         }
         $this->sections = $sections;
         $this->sections = aneb_menu_sections();
      }
      return $this->sections;
@@ -121,13 +156,18 @@
    {
      $sections = $this->getSections();
      foreach ($sections as $section) {
         if($section['name'] === 'Uncategorized') {
            continue;
         }
         $items = new WP_Query([
            'post_type' => BASE.'menu_item',
            'post_type' => 'product',
//          'post_type' => BASE.'menu_item',
            'order' => 'ASC',
            'posts_per_page' => -1,
            'tax_query' => [
               [
                  'taxonomy' => BASE.'section',
                  'taxonomy' => 'product_cat',
//                'taxonomy' => BASE.'section',
                  'terms' => (int) $section['id'],
               ]
            ],
@@ -154,46 +194,269 @@
      }
    }
   protected function renderMenuItem(int $ID, string $slug, string $postType = 'menu_item') {
   /**
    * Renders from WooCommerce product
    * @param int $ID
    * @param string $slug
    * @param string $postType
    * @return void
    */
   protected function renderMenuItem(int $ID, string $slug, string $postType = 'product'):void {
      $meta = Meta::forPost($ID);
      $fields = $meta->getAll();
      $excerpt = get_the_excerpt($ID);
      $isVariable = !empty($fields['variations']??[]);
      $product = wc_get_product($ID);
      $isFeatured = $product->is_featured();
      $isOnSale = $product->is_on_sale();
      $isPurchasable = $product->is_purchasable() && $product->is_in_stock();
      $priceRange = $this->getPrice($fields, $isOnSale, $isPurchasable);
      if ($isVariable) {
         $prices = array_map('floatval', array_column($fields['variations'], 'price'));
         $min = min($prices);
         $max = max($prices);
         $priceRange = $min===$max ? jvbCurrency($min) : jvbCurrency($min) .' - '.jvbCurrency($max);
      }
//    if (in_array($fields['post_title'],  ['Chopped Brisket Sandwich', 'Breakfast Poutine'])) {
//       jvbDump ($fields);
//    }
      $gallery = '';
      if (!empty($fields['gallery_image_ids'])) {
         $gallery = implode(array_map(function($id) use ($fields){
            $id = absint($id);
            $caption = wp_get_attachment_caption($id);
            return sprintf(
               '<figure>%s<figcaption>%s</figcaption></figure>',
               jvbFormatImage($id,'tiny','medium',true,'menu'),
               empty($caption) ? $fields['post_title'] : $caption
            );
         }, $fields['gallery_image_ids']));
      }
      $caption = '';
      if (!empty($fields['image_id'])) {
         $caption = wp_get_attachment_caption((int)$fields['image_id']);
         $caption = empty($caption) ? $fields['post_title'] : $caption;
      }
      $out = sprintf(
   '<div id="%s" class="menu-item%s" data-section="%s" data-label="%s" data-id="%s">
            <div class="tags abs top left row">%s%s</div>
            <div class="header row x-btw">
               <figure>%s<figcaption>%s</figcaption></figure>
               <div class="text row x-btw">
                  <h3>%s</h3>
                  <p class="price">%s</p>
               </div>
            </div>
            <div class="gallery row left%s">%s</div>
            <div class="description">%s</div>
            <div class="info col right">
            %s',
         sanitize_title($fields['post_title']),
         $isVariable ? ' variable' : '',
         $slug,
         $fields['post_title'],
         $ID,
         $isOnSale ? '<span class="sale row left top">'.jvbIcon('tag-chevron').'Sale!</span>' : '',
         $isFeatured ? '<span class="featured row top">'.jvbIcon('star').'We Recommend</span>' : '',
         !empty($fields['image_id']) ? jvbFormatImage((int)$fields['image_id'],'tiny','medium',true,'menu') : '',
         $caption,
         $fields['post_title'],
         $priceRange,
         empty($gallery) ? '' : ' has',
         $gallery,
         $excerpt,
         $this->getSaleText($fields, $isPurchasable)
      );
      if (!empty($fields['variations']??[])) {
         foreach ($fields['variations'] as $index => $row) {
            $v = wc_get_product($row['id']);
            $isOnSale = $v->is_on_sale();
            $isPurchasable = $v->is_purchasable() && $v->is_in_stock();
            $price = $this->getPrice($row, $isOnSale, $isPurchasable);
            $title = str_replace($fields['post_title'].' - ','',$row['post_title']);
            $out .= sprintf(
            '<div class="row right top nowrap">
                  %s
                  <figure>%s<figcaption>%s</figcaption></figure>
                  <div>
                     <h4>%s — %s</h4>
                     %s
                  </div><a %shref="?add-to-cart=%d&variation_id=%d"
   data-quantity="1"
   data-product_id="%d"
   data-variation_id="%d"
   class="btn chip add_to_cart_button ajax_add_to_cart add-to-cart">
   %s
   <span>Add to Cart</span>
</a>%s', //Note: we had 3 %s%s%s before the last </div> last time. This was to add optional ingredients or preparation time duration, not default in WooCommerce
            $isOnSale ? '<span class="abs top right">'.jvbIcon('tag-chevron').'Sale!</span>' : '',
                  !empty($row['image_id']) ? jvbFormatImage($row['image_id'], 'tiny', 'medium') : '',
                  empty($row['post_title']) ? $fields['post_title'] : $row['post_title'],
                  empty($title) ? $fields['post_title'] : $title,
                  $price,
                  empty($row['description']) ? '' : wpautop($row['description']),
                  $isPurchasable ? '' : 'class="disabled" ',
                  $ID,
                  $row['id'],
                  $ID,
                  $row['id'],
                  jvbIcon('plus-square'),
                  $this->getSaleText($row, $isPurchasable)
            );
//          $out .= Form::render(
//             $row['id'],
//             '',
//             [
//                'type'   => 'quantity',
//                'label'  => $row['title'],
//                'min' => 0,
//                'max' => 30,
//                'data'   => [
//                   'price'  => $row['price'],
//                   'id'  => $row['id'],
//                   'sku' => $row['sku'],
//                   'name'   => $row['title'],
//                   'add-to-cart'  => ''
//                ],
//                'add' => 'Add 1 '.$row['title'].' to cart',
//                'remove' => 'Remove 1 '.$row['title']. ' from cart',
//             ]
//          );
            $out .= '</div>';
         }
      } else {
         $isPurchasable = $product->is_purchasable() && $product->is_in_stock();
         $out .= sprintf(
            '<a href="?add-to-cart=%d"
               data-quantity="1"
               data-product_id="%d"
               class="%sbtn chip add_to_cart_button ajax_add_to_cart add-to-cart">
               %s
                  <span>Add to Cart</span>
            </a>',
            $ID,
            $ID,
            $isPurchasable ? '' : 'disabled ',
            jvbIcon('plus-square')
         );
//       $out .= Form::render(
//          $ID,
//          '',
//          [
//             'type'   => 'quantity',
//             'label'  => 'Add to Order',
//             'min' => 0,
//             'max' => 30,
//             'step'   => 1,
//             'data'   => [
//                'price'  => $fields['_price'],
//                'id'  => $ID,
//                'name'   => $fields['post_title'],
//                'step'   => 1,
//                'add-to-cart'  => '',
//             ],
//             'add' => 'Add 1 '.$fields['post_title'].' to cart',
//             'remove' => 'Remove 1 '.$fields['title']. ' from cart',
//          ]
//       );
      }
      $out .= '</div></div>';
      echo $out;
   }
   protected function getPrice(array $item, bool $isOnSale, bool $isPurchasable):string
   {
      $price = jvbCurrency($item['regular_price']);
      $price = $isOnSale ? '<s> '.$price.' </s> '.jvbCurrency($item['sale_price']) : $price;
      return $price;
   }
   protected function getSaleText(array $item, bool $isPurchasable):string
   {
      if (empty($item['date_on_sale_to']) && $isPurchasable) {
         return '';
      }
      $out = sprintf(
         '<span class="sale-until">%sOn sale until <time datetime="%s">%s</time>.</span>',
         jvbIcon('tag-chevron'),
         $item['date_on_sale_to']->format('c'),
         $item['date_on_sale_to']->format('l, F j, Y')
      );
      if (!$isPurchasable) {
         $out .= sprintf(
            '<span class="out-of-stock">%sTemporarily Out of Stock</span>',
            jvbIcon('warning-circle')
         );
      }
      return $out;
   }
   /**
   protected function renderMenuItemOLD(int $ID, string $slug, string $postType = 'menu_item') {
      $meta = Meta::forPost($ID);
      $fields = $meta->getAll();
      $values = $meta->getAll([
         'post_title',
         '_square_catalog_id',
         'price',
         'product_variations',
         $this->base.'item_id',
         $this->base.'price',
         $this->base.'product_variations',
         'max_order',
         'min_order',
         'step'
      ]);
      $priceRange = jvbCurrency($values['price']);
      $variations = $values['product_variations'];
      $priceRange = jvbCurrency($values[$this->base.'price']);
      $variations = $values[$this->base.'product_variations'];
      if(!empty($variations) && $variations[0]['name'] === '') {
      if(!empty($variations) && $variations[0][$this->base.'name'] === '') {
         $variations = [];
      }
      if (!empty($variations)) {
         $prices = [];
         foreach ($variations as $row) {
            if (isset($row['price']) && is_numeric($row['price'])) {
               $prices[] = (float) $row['price'];
            if (isset($row[$this->base.'price']) && is_numeric($row[$this->base.'price'])) {
               $prices[] = (float) $row[$this->base.'price'];
            }
         }
         if (!empty($prices)) {
         if (!empty($prices) && (min($prices) !== max($prices))) {
            $priceRange = jvbCurrency(min($prices)).' - '.jvbCurrency(max($prices));
         } elseif (!empty($prices)) {
            $priceRange = jvbCurrency(max($prices));
         }
      }
      $max = $values['max_order'];
      $min = $values['min_order'];
      $step = $values['step'];
      $max = $values['max_order']??'';
      $min = $values['min_order']??'';
      $step = $values['step']??'';
      $max = ($max === '') ? 50 : $max;
      $min = ($min === '') ? 0 : $min;
      $step = ($step === '') ? 1 : $step;
      ?>
      <div class="menu-item<?= !empty($variations) ? ' variable' : '' ?>" data-section="<?=$slug?>">
      <div class="menu-item<?= !empty($variations) ? ' variable' : '' ?>" data-section="<?=$slug?>" data-label="<?=$values['post_title']?>" data-id="<?=$ID?>">
         <div class="header row x-btw">
            <h3><?= $values['post_title']?></h3>
            <p class="price"><?= $priceRange ?></p>
@@ -201,52 +464,64 @@
         <div class="description">
            <?= Render::renderFrom($meta, 'post_excerpt')?>
         </div>
         <div class="info row right">
         <div class="info col right">
            <?php
            if (empty($variations)) {
               Form::renderFrom($meta,
               echo Form::render(
                  $ID.'|cart_quantity',
                  '',
                  [
                     'type'   => 'number',
                     'type'   => 'quantity',
                     'label'  => 'Add to Order',
                     'min' => $min,
                     'max' => $max,
                     'step'   => $step,
                     'data'   => [
                        'price' => $values['price'],
                        'price' => $values[$this->base.'price'],
                        'id'  => $ID,
                        'square-catalog-id'  => $values['_square_catalog_id'],
                        'catalog-id'   => $values[$this->base.'item_id'],
                        'name'   => $values['post_title'],
                        'step'   => $step,
                        'add-to-cart' => ''
                     ],
                     'add' => 'Add 1 '.$values['post_title'].' to cart',
                     'remove' => 'Remove 1 '.$values['post_title'].' from cart',
                  ],
                  true);
                  ]);
            } else {
               foreach ($variations as $index =>$row) {
                  jvbDump($index, 'index');
                  jvbDump($row, 'row');
                  Form::renderFrom($meta,
                  echo sprintf(
                     '<div class="row right nowrap">
                     <div>
                        <h4>%s — %s</h4>
                        %s%s%s
                     </div>',
                     $row[$this->base.'name'],
                     jvbCurrency($row[$this->base.'price']),
                     empty($row[$this->base.'description']) ? '' : wpautop($row[$this->base.'description']),
                     empty($row[$this->base.'ingredients']) ? '' : wpautop('<b>Ingredients:</b>'.$row[$this->base.'ingredients']),
                     empty($row[$this->base.'preparation_time_duration']) ? '' : wpautop('Ready in <b>'.$row[$this->base.'preparation_time_duration'].'</b>'),
                  );
                  echo Form::render(
                     'quantity-'.$index,
                     '',
                     [
                        'type'   => 'number',
                        'label'  => $row['label'],
                        'type'   => 'quantity',
                        'label'  => $row[$this->base.'name'],
                        'min' => $min,
                        'max' => $max,
                        'step'   => $step,
                        'data'   => [
                           'price' => $row['price'],
                           'id'  => $ID,
                           'square-catalog-id'  => $row['_square_catalog_id'],
                           'name'   => $row['label'],
                           'price' => $row[$this->base.'price'],
                           'id'  => $ID.'-'.$row[$this->base.'sku'],
                           'catalog-id'   => $row[$this->base.'item_id'],
                           'name'   => $row[$this->base.'name'],
                           'add-to-cart' => ''
                        ],
                        'add' => 'Add 1 '.$row['label'].' to cart.',
                        'remove' => 'Remove 1 '.$row['label'].' from cart.',
                     ],
                     true);
                        'add' => 'Add 1 '.$row[$this->base.'name'].' to cart.',
                        'remove' => 'Remove 1 '.$row[$this->base.'name'].' from cart.',
                     ]
                  );
                  echo '</div>';
               }
            }
            ?>
@@ -255,7 +530,8 @@
      </div>
      <?php
   }
   } **/
    protected function renderOnThisPage():void
@@ -265,4 +541,9 @@
        }
        echo jvbOnThisPage(array_keys($this->details));
    }
   public function renderGallery():void
   {
      jvbRenderGallery();
   }
}