MdMasud

WordPress, Laravel, Flutter

Blogs

  • The Ultimate Guide to WooCommerce Order Status Tweaks: Custom Statuses, Editable Orders, Custom Fields and Order Table Columns

    Managing orders in WooCommerce is flexible, but many developers need extra control beyond the default workflow. This guide brings together several advanced yet practical customisations you can apply to WooCommerce, including adding custom order statuses, setting a default status, making custom statuses editable, adding custom fields to the order details page, and creating custom columns in the order list table.

    This is a complete, step-by-step guide with real working code. All examples use standard WordPress and WooCommerce hooks, and are written for small and medium-sized custom shop workflows.


    1. Adding Custom Order Statuses in WooCommerce

    WooCommerce has built-in statuses such as Pending, Processing, Completed and Cancelled. However, many businesses need more tailored stages, such as Price Request or Customer Visit.

    To add custom statuses, use register_post_status():

    add_action('init', 'add_more_status_to_order');
    
    function add_more_status_to_order()
    {
        register_post_status('wc-price-request', [
            'label'                     => _x('Price request', 'Order status', 'my-domain'),
            'public'                    => true,
            'exclude_from_search'       => false,
            'show_in_admin_all_list'    => true,
            'show_in_admin_status_list' => true,
            'label_count'               => _n_noop('Price request <span class="count">(%s)</span>', 'Price request <span class="count">(%s)</span>', 'my-domain'),
        ]);
    
        register_post_status('wc-quoted', [
            'label'                     => _x('Quoted', 'Order status', 'my-domain'),
            'public'                    => true,
            'exclude_from_search'       => false,
            'show_in_admin_all_list'    => true,
            'show_in_admin_status_list' => true,
            'label_count'               => _n_noop('Quoted <span class="count">(%s)</span>', 'Quoted <span class="count">(%s)</span>', 'my-domain'),
        ]);
    }


    These statuses behave just like normal WooCommerce statuses, including appearing in reports and the admin table if configured.


    2. Displaying Custom Statuses in the WooCommerce Status Dropdown


    After registering new statuses, WooCommerce will not show them in the order dropdown unless you merge them manually.

    add_filter('wc_order_statuses', 'show_new_statuses_in_dropdown', 99, 1);
    
    function show_new_statuses_in_dropdown($statuses)
    {
        return array_merge(
            ['wc-price-request' => __('Price request', 'my-domain')],
            ['wc-pending-pricing' => __('Pending pricing', 'my-domain')],
            ['wc-quoted' => __('Quoted', 'my-domain')],
            ['wc-customer-visit' => __('Customer visit', 'my-domain')],
            $statuses
        );
    }


    This ensures your statuses appear alongside the built-in ones on the order details screen.


    3. Setting a Custom Default Order Status


    By default, new orders in WooCommerce start as Pending Payment. For some businesses, this is not suitable. You can override this behaviour and set your own default status.

    add_action('woocommerce_new_order', 'set_default_order_status', 99, 1);
    
    function set_default_order_status($order_id)
    {
        $order = wc_get_order($order_id);
    
        if ($order && $order->get_status() === 'pending') {
            $order->update_status('price-request', 'Default status.');
        }
    }


    Now, every new order will automatically be marked as Price Request instead of Pending.


    4. Allowing Orders with Custom Statuses to Be Editable

    Many custom statuses cannot be edited by default. WooCommerce only allows editing in a few core statuses.

    Use this filter to allow editing in your custom statuses:

    add_filter('wc_order_is_editable', 'add_statuses_to_order_be_editable',10, 2);
    
    function add_statuses_to_order_be_editable($editable, $order)
    {
        $editable_status = [
            'price-request' => true,
            'pending-pricing' => true,
            'quoted'=> true,
            'customer-visit' => true
        ];
    
        if ( current_user_can("manage_options")) {
            if ($order && $editable_status[$order->get_status()]) {
                return true;
            }
        }
    
        return $editable;
    }


    This allows administrators to make changes to orders using these statuses.


    5. Adding Custom Fields to the WooCommerce Order Details Page


    Sometimes you need to store extra data per order, such as a visit date. You can add a custom field directly to the order details box inside the admin area.


    Display the custom field

    add_action( 'woocommerce_admin_order_data_after_order_details', 'add_custom_field_for_order_details', 10, 1 );
    
    function add_custom_field_for_order_details($order)
    {
        $meta_key = '_customer_visit_date_time';
        $prev_value = get_post_meta($order->get_id(), $meta_key, true);
    
        $field_value_date = '';
        $field_value_hour = '';
        $field_value_minute = '';
    
        if ($prev_value) {
            $field_value_date = date('Y-m-d', strtotime($prev_value));
            $field_value_hour = date('h', strtotime($prev_value));
            $field_value_minute = date('i', strtotime($prev_value));
        }
        ?>
        <p class="form-field form-field-wide wc-order-vist-date">
            <label for="customer_visit_date">Visit Date:</label>
            <input type="text" class="date-picker" placeholder="yyyy-mm-dd" 
                   name="visit_date_date" maxlength="10" value="<?= $field_value_date ?>">
            @
            <input type="number" class="hour" placeholder="h"
                   name="visit_date_hour" min="0" max="23"
                   value="<?= $field_value_hour ?>">
            :
            <input type="number" class="minute" placeholder="m"
                   name="visit_date_minute" min="0" max="59"
                   value="<?= $field_value_minute ?>">
            <br/><span class="description">Select a date and time (24h format)</span>
        </p>
        <?php
    }


    Saving the custom field

    add_action('woocommerce_process_shop_order_meta', 'save_custom_fields_for_order_details');
    
    function save_custom_fields_for_order_details($order_id)
    {
        $order = wc_get_order($order_id);
    
        $visit_date_date = wc_clean($_POST['visit_date_date']);
        $visit_date_hour = wc_clean($_POST['visit_date_hour']);
        $visit_date_minute = wc_clean($_POST['visit_date_minute']);
    
        $visit_date_time = $visit_date_date.' '.$visit_date_hour.':'.$visit_date_minute;
    
        $order->update_meta_data('_customer_visit_date_time', $visit_date_time);
        $order->save();
    }


    This stores the value as standard order meta.


    6. Adding Custom Columns in the WooCommerce Orders List Table


    To make your workflow faster, you may want to see custom information directly in the orders list in the admin dashboard. You can add new columns and populate them with meta values.


    Add a new column

    add_filter('manage_edit-shop_order_columns', 'update_shop_order_columns', 20, 1);
    
    function update_shop_order_columns($columns)
    {
        $new_columns = array();
    
        foreach ($columns as $key => $label) {
            if ('order_date' === $key) {
                $label = __('Order Date', 'my-domain');
            }
    
            $new_columns[$key] = $label;
    
            if ('order_status' === $key) {
                $new_columns['visit_date'] = __('Visit Date', 'my-domain');
            }
        }
    
        return $new_columns;
    }


    Display the value in the column

    add_filter('manage_shop_order_posts_custom_column', 'update_shop_order_columns_content', 20, 2);
    
    function update_shop_order_columns_content($column, $order_post_id)
    {
        if ('visit_date' !== $column) {
            return;
        }
    
        $visit_date = get_post_meta($order_post_id, '_customer_visit_date_time', true);
    
        if (!$visit_date) {
            echo '<span>Add now</span>';
            return;
        }
    
        $timestamp = strtotime($visit_date);
    
        if ($timestamp) {
            echo '<span style="color:#008a00;">'.date('Y-m-d @ H:i', $timestamp).'</span>';
        } else {
            echo '<span style="color:#d63638;">Not valid</span>';
        }
    }


    This displays the date in green when valid, and a warning in red if not.

    This guide brings together a full set of WooCommerce order management enhancements that you can use to build a more tailored workflow:

    • Adding custom order statuses
    • Showing them in the admin dropdown
    • Setting a custom default order status
    • Keeping orders editable in specific statuses
    • Adding custom fields to order details
    • Showing those fields in the order list table

  • How to add and save Custom Fields in the WooCommerce Order Details page


    Sometimes you need to store extra information for an order, such as a customer visit date. WooCommerce allows you to add your own fields to the order edit screen and save that information as order meta.


    Adding a Custom Date and Time Field

    add_action( 'woocommerce_admin_order_data_after_order_details', 'add_custom_field_for_order_details', 10, 1 );
    
    function add_custom_field_for_order_details($order)
    {
        $meta_key = '_customer_visit_date_time';
        $prev_value = get_post_meta($order->get_id(), $meta_key, true);
    
        $field_value_date = '';
        $field_value_hour = '';
        $field_value_minute = '';
    
        if ($prev_value) {
            $field_value_date = date('Y-m-d', strtotime($prev_value));
            $field_value_hour = date('h', strtotime($prev_value));
            $field_value_minute = date('i', strtotime($prev_value));
        }
        ?>
        <p class="form-field form-field-wide wc-order-vist-date">
            <label for="customer_visit_date">Visit Date:</label>
            <input type="text" class="date-picker" placeholder="yyyy-mm-dd"
                   name="visit_date_date" maxlength="10" value="<?= $field_value_date ?>">
            @
            <input type="number" class="hour" placeholder="h" name="visit_date_hour"
                   min="0" max="23" value="<?= $field_value_hour ?>">
            :
            <input type="number" class="minute" placeholder="m" name="visit_date_minute"
                   min="0" max="59" value="<?= $field_value_minute ?>">
            <br/><span class="description">Select a date and time (24h format)</span>
        </p>
        <?php
    }


    Saving the Custom Field

    add_action('woocommerce_process_shop_order_meta', 'save_custom_fields_for_order_details');
    
    function save_custom_fields_for_order_details($order_id)
    {
        $order = wc_get_order($order_id);
    
        $visit_date_date = wc_clean($_POST['visit_date_date']);
        $visit_date_hour = wc_clean($_POST['visit_date_hour']);
        $visit_date_minute = wc_clean($_POST['visit_date_minute']);
    
        $visit_date_time = $visit_date_date.' '.$visit_date_hour.':'.$visit_date_minute;
    
        $order->update_meta_data('_customer_visit_date_time', $visit_date_time);
        $order->save();
    }


    Adding custom fields to the WooCommerce order details screen is straightforward and allows you to store important data directly with each order.

  • How to Allow WooCommerce Orders with Custom Statuses to Stay Editable

    By default custom statuses prevent the order from being edited.
    WooCommerce only allows editing in a few built-in statuses, but you can override this and allow editing for your own workflow.


    Allow Editing in Custom Statuses

    add_filter('wc_order_is_editable', 'add_statuses_to_order_be_editable',10, 2);
    
    function add_statuses_to_order_be_editable($editable, $order)
    {
        $editable_status = [
            'price-request' => true,
            'pending-pricing' => true,
            'quoted'=> true,
            'customer-visit' => true
        ];
    
        if ( current_user_can("manage_options")) {
            if ($order && $editable_status[$order->get_status()]) {
                return true;
            }
        }
        return $editable;
    }


    This allows users with admin rights to edit orders even when they are in one of the custom statuses.


    If your order lifecycle requires edits at different steps, enabling edit access for custom statuses ensures your workflow stays smooth.

  • How to add WooCommerce custom Order Statuses and display them in the dropdown


    WooCommerce will not automatically show your custom statuses in the admin dropdown unless you explicitly add them. Below you can find how to register the statuses and make them appear in the status selection dropdown inside the order details screen.


    Showing Your Statuses in the Admin Dropdown

    add_filter('wc_order_statuses', 'show_new_statuses_in_dropdown', 99, 1);
    
    function show_new_statuses_in_dropdown($statuses)
    {
        return array_merge(
            ['wc-price-request' => __('Price request', 'my-domain')],
            ['wc-pending-pricing' => __('Pending pricing', 'my-domain')],
            ['wc-quoted' => __('Quoted', 'my-domain')],
            ['wc-customer-visit' => __('Customer visit', 'my-domain')],
            $statuses
        );
    }


    This merges your custom statuses with the existing ones in a clean way.


    Once your statuses are registered and added to the dropdown, your WooCommerce order management becomes more flexible and easier to track.

  • How to add a custom Order Status in WooCommerce and set it as the default

    WooCommerce has default order statuses such as Pending Payment, Processing, and Completed.
    However, Some online businesses need their own custom workflow. For example, you may want new orders to start in a “Price Request” status instead of “Pending”.

    In this guide, I explain how to register new order statuses and set one of them as the default when an order is created.


    Registering custom Order Statuses

    WooCommerce uses register_post_status() to add new statuses.
    Below we add four custom statuses to WordPress:

    add_action('init', 'add_more_status_to_order');
    
    function add_more_status_to_order()
    {
        register_post_status('wc-price-request', [
            'label'                     => _x('Price request', 'Order status', 'my-domain'),
            'public'                    => true,
            'exclude_from_search'       => false,
            'show_in_admin_all_list'    => true,
            'show_in_admin_status_list' => true,
            'label_count'               => _n_noop('Price request <span class="count">(%s)</span>', 'Price request <span class="count">(%s)</span>', 'my-domain'),
        ]);
    
        register_post_status('wc-quoted', [
            'label'                     => _x('Quoted', 'Order status', 'my-domain'),
            'public'                    => true,
            'exclude_from_search'       => false,
            'show_in_admin_all_list'    => true,
            'show_in_admin_status_list' => true,
            'label_count'               => _n_noop('Quoted <span class="count">(%s)</span>', 'Quoted <span class="count">(%s)</span>', 'my-domain'),
        ]);
    
        register_post_status('wc-pending-pricing', [
            'label'                     => _x('Pending pricing', 'Order status', 'my-domain'),
            'public'                    => true,
            'exclude_from_search'       => false,
            'show_in_admin_all_list'    => true,
            'show_in_admin_status_list' => true,
            'label_count'               => _n_noop('Pending pricing <span class="count">(%s)</span>', 'Pending pricing <span class="count">(%s)</span>', 'my-domain'),
        ]);
    }


    Setting a Custom Default Order Status

    By default, WooCommerce sets all new orders to “Pending”.
    If you want every new order to start as “Price Request”, use this hook:

    add_action('woocommerce_new_order', 'set_default_order_status', 99, 1);
    
    function set_default_order_status($order_id)
    {
        $order = wc_get_order($order_id);
        if ($order && $order->get_status() === 'pending') {
            $order->update_status('price-request', 'Default status.');
        }
    }


    This helps when your business process is different from a standard retail store.


    By registering custom statuses and setting one as the default, you can build an order workflow that matches the business requirements.

  • How to Group Hierarchical WordPress Taxonomy Terms in a Select Dropdown Without Extra SQL Queries

    How to Group Hierarchical WordPress Taxonomy Terms in a Select Dropdown Without Extra SQL Queries

    If you’re building a custom dropdown menu for a hierarchical taxonomy in WordPress — for example, letting users choose a role or category — you might want to:

    • Show parent terms as group labels (like <optgroup>)
    • Show **child terms as selectable <option>s
    • Include terms that aren’t attached to any posts
    • Avoid running SQL queries inside loops

    Seems simple, right? But it’s trickier than it looks.

    Let me show you the cleanest way to build it — no unnecessary queries, no missing terms, and no over-complicated logic.

    The Issue

    If you use get_terms() and loop through the results, it’s tempting to grab each parent and then call get_terms() again to fetch its children.

    $parents = get_terms([... 'parent' => 0 ...]);
    
    foreach ($parents as $parent) {
        $children = get_terms([... 'parent' => $parent->term_id ...]); // Inefficient
    }
    

    This works — but it creates a new SQL query for every parent term. That’s a classic performance problem, especially on larger sites.

    The Efficient Solution

    Instead, just call get_terms() once — and group the results in PHP.

    Here’s the 10-line approach that avoids all the extra queries:

    $grouped_terms = [];
    $terms = get_terms(['taxonomy' => 'your_taxonomy', 'hide_empty' => false]);
    
    if ($terms) {
        foreach ($terms as $term) {
            if ($term->parent == 0) {
                $grouped_terms[$term->term_id]['name'] = $term->name;
            } else {
                $grouped_terms[$term->parent]['children'][] = $term;
            }
        }
    }
    

    Its better way

    • Only one query to fetch all terms (parents + children)
    • No need to pre-sort the results
    • If a child term’s parent wasn’t processed yet, that’s okay — it’ll still get grouped
    • Even unused terms (not attached to posts) are included thanks to 'hide_empty' => false

    Rendering the Select Dropdown

    Now that you’ve grouped your terms, render the dropdown with <optgroup> for parents and <option> for children:

    echo '<select name="your_field_name">';
    foreach ($grouped_terms as $group) {
        if (empty($group['children'])) continue;
    
        $label = $group['name'] ?? 'Other';
    
        echo '<optgroup label="' . esc_html($label) . '">';
        foreach ($group['children'] as $child) {
            echo '<option value="' . esc_attr($child->term_id) . '">' . esc_html($child->name) . '</option>';
        }
        echo '</optgroup>';
    }
    echo '</select>';
    

    This gives you a clean, user-friendly dropdown grouped by parent terms.

    Use Cases

    • Admin filters in meta boxes
    • Frontend submission forms
    • Custom post type selectors (e.g. “Job Roles”, “Product Categories”)
    • Anywhere you want a taxonomy-based grouped select

    When working with WordPress taxonomies, efficiency matters. Avoiding unnecessary SQL calls can make your plugins or themes faster, lighter, and more scalable.

    This technique helps you build a clean, scalable dropdown for any hierarchical taxonomy — with zero extra queries, no missing terms, and just 10 lines of code.

  • DataTables in WordPress: Use Callback Data to Calculate Totals and Update the Page (Frontend‑Only)

    DataTables in WordPress: Use Callback Data to Calculate Totals and Update the Page (Frontend‑Only)

    When you’re building dashboards in WordPress, you often need to use the data currently loaded into your DataTable—not just display it. This post shows how to sum values from all rows (across pages), split totals by Payment Method (Cash vs Card), and push those numbers into elements elsewhere on the page using a reusable callback function.

    What you’ll achieve

    • Fetch table data via AJAX (already set up in your site).
    • Initialise DataTables in WordPress.
    • Use a separate, reusable function to:
      • Iterate all rows, ignoring pagination.
      • Parse currency like £45.00.
      • Update three on‑page counters: #log_cash, #log_card, #log_total.

    HTML markup

    <div class="table-container">
      <table id="todaysLogs" class="table display" style="width:100%">
        <thead>
          <tr>
            <th>Time</th>
            <th>Driver</th>
            <th>Sold By</th>
            <th>Amount</th>
            <th>Discount</th>
            <th>Payment Method</th>
            <th>Action</th>
          </tr>
        </thead>
      </table>
    </div>
    
    <!-- Totals display anywhere on the page -->
    <div class="totals">
      <div>Cash: <strong id="log_cash">£0.00</strong></div>
      <div>Card: <strong id="log_card">£0.00</strong></div>
      <div>Total: <strong id="log_total">£0.00</strong></div>
    </div>
    

    Reusable totals function + DataTable initialisation

    function updateLogTotals(tableApi) {
        var parseValue = function (val) {
    	if (typeof val === 'string') {
    		return parseFloat(val.replace(/[£,]/g, '')) || 0;
    	}
    	return typeof val === 'number' ? val : 0;
       };
       var totalCash = 0;
       var totalCard = 0;
       tableApi.rows().every(function () {
            var row = this.data();
    	var value  = row[3];
    	var type   = row[5];
    	if (type === 'Cash') {
    		totalCash += parseValue(value);
    	} else {
    		totalCard += parseValue(value);
    	}
       });
    
       $('#log_cash').text('£' + totalCash.toFixed(2));
       $('#log_card').text('£' + totalCard.toFixed(2));
       $('#log_total').text('£' + (totalCash + totalCard).toFixed(2));
    }
    
    var todaysLogsTable = $('#todaysLogs').DataTable({
    	dom: 'Bfrtip',
    	pageLength: 9,
    	buttons: [
    		'copyHtml5',
    		'excelHtml5',
    		'csvHtml5',
    		'pdfHtml5'
    	],
    	columns: [
    		null,
    		null,
    		null,
    		null,
    		null,
    		null,
    		{ orderable: false }
    	],
    	ajax: {
    		url: ajaxurl + '?action=pos&post_action=get_todays_log'
    		},
    		responsive: true,
    		drawCallback: function (settings) {
    			updateLogTotals(this.api());
    		}
    });

    Make it truly reusable

    You can call updateLogTotals(todaysLogsTable) from anywhere—buttons, tabs, or additional DataTables events:

    $('#todaysLogs').on('search.dt order.dt page.dt', function () {
        updateLogTotals(todaysLogsTable);
    });
    

    Only count visible (filtered) rows? Swap:

    tableApi.rows().every(…)

    for:

    tableApi.rows({ filter: 'applied' }).every(…)
    

    Currency parsing

    parseFloat(val.replace(/[£,]/g, ''))
    • If your data can include other currencies or locale formats, either:
      • Normalise in your renderer (e.g. store a hidden numeric value and display £ in the cell), or
      • Expand the regex/logic to handle your formats.

    Troubleshooting

    • Totals don’t change: Ensure your callback runs on drawCallback or bind to search.dt/order.dt/page.dt.
    • NaN totals: Check the column indexes and the currency format in your cells.
    • Wrong values: Confirm your table’s JSON returns the columns in the order you expect.

    Takeaway

    With a small reusable function and DataTables’ API, you can read current table data on the client and push real‑time totals into any element on your WordPress page—no page refreshes, no backend edits required here.

  • How to Disable Weekends, Specific Dates and Use Date Ranges in Gravity Forms

    How to Disable Weekends, Specific Dates and Use Date Ranges in Gravity Forms

    If you have used a Gravity Forms Date field and wanted to block weekends, disable certain dates, or only allow a selected range of dates, you may have found the official documentation a bit limiting.

    Gravity Forms provides examples for each of these individually, but not how to combine them. In this post, I’ll show you how to do just that, with clear examples and a complete working script.

    Step 1: Use the gform_datepicker_options_pre_init filter

    This Gravity Forms filter allows you to customise the jQuery UI Datepicker used by date fields. You need to target your form ID and field ID:

    gform.addFilter('gform_datepicker_options_pre_init', function(optionsObj, formId, fieldId) {
      // Your logic goes here
      return optionsObj;
    });

    Step 2: Define your allowed date ranges

    We’ll define specific ranges during which dates should be selectable.

    var ranges = [
      { start: new Date('09/25/2025'), end: new Date('10/05/2025') },
      { start: new Date('11/10/2025'), end: new Date('11/20/2025') }
    ];

    Step 3: Disable specific dates

    You may want to block specific public holidays or unavailable days. Format these as dd/mm/yyyy.

    var disabledDays = [
      '26/09/2025',
      '29/09/2025',
      '25/12/2025',
      '01/01/2026',
      '05/01/2026'
    ];

    Step 4: Disable weekends

    Gravity Forms uses jQuery UI Datepicker under the hood, which includes a built-in function to block weekends:

    var noWeekend = jQuery.datepicker.noWeekends(date);
    if (!noWeekend[0]) return noWeekend;

    Step 5: Combine all conditions in a single function

    Full Working Example

    Here is the complete code including form and field checks:

    gform.addFilter('gform_datepicker_options_pre_init', function(optionsObj, formId, fieldId) {
      if (formId == 12 && fieldId == 16) {
    
        var ranges = [
          { start: new Date('09/25/2025'), end: new Date('10/05/2025') },
          { start: new Date('11/10/2025'), end: new Date('11/20/2025') }
        ];
    
        var disabledDays = [
          '26/09/2025',
          '29/09/2025',
          '25/12/2025',
          '01/01/2026',
          '05/01/2026'
        ];
    
        optionsObj.minDate = ranges[0].start;
        optionsObj.maxDate = ranges[ranges.length - 1].end;
        optionsObj.firstDay = 1;
    
        optionsObj.beforeShowDay = function(date) {
          var inRange = ranges.some(function(range) {
            return date >= range.start && date <= range.end;
          });
          if (!inRange) return [false];
    
          var noWeekend = jQuery.datepicker.noWeekends(date);
          if (!noWeekend[0]) return noWeekend;
    
          var formatted = ('0' + date.getDate()).slice(-2) + '/' +
                          ('0' + (date.getMonth() + 1)).slice(-2) + '/' +
                          date.getFullYear();
    
          if (disabledDays.indexOf(formatted) !== -1) return [false];
    
          return [true];
        };
      }
    
      return optionsObj;
    });

    Optional: Hide Dates from Other Months

    By default, the datepicker shows days from the previous and next months to fill out the calendar grid. To hide those days for a cleaner look, you can use the following CSS:

    .ui-datepicker td.ui-datepicker-other-month {
      visibility: hidden;
    }

    This hides the content but keeps the grid layout intact. If you prefer to completely remove those cells, you can use display: none, but it may affect layout consistency.

    Wrapping Up

    Gravity Forms makes it easy to add date pickers, but combining rules for weekends, specific dates, and ranges requires a bit of manual work. With the example above, you now have full control over which dates can be selected in your form.

    If you’re working on a more complex booking or survey system, this kind of control can be essential.

  • Custom Cursor Navigation in SwiperJS – What I Built and How I Fixed It

    Today, I was working on a custom slider using SwiperJS, and I wanted to do something a bit more interactive than the usual arrows or dots.

    Instead of showing fixed navigation arrows, I thought it would be more fun to show floating “Next” and “Previous” text that follow the cursor, depending on which side of the slider you’re hovering. Kind of like a tail that hints where the slider will move if you click.

    It turned out to be a really nice effect – but getting it right wasn’t completely straightforward. Here’s how I made it work, and what issues came up along the way.

    What I Was Trying to Do

    • Show “Next” on the right side of the slider
    • Show “Previous” on the left
    • Make the text follow the cursor with a smooth animation (like a trailing effect)
    • Hide the floating text when hovering buttons inside each slide (so it doesn’t get in the way)

    The First Hurdle: Cursor Trail Jumping or Sticking

    At first, I tried updating the cursor-following text using transform: translate(...) inside mousemove, and it seemed okay. But it felt jumpy, and sometimes the text would just snap to the corner or get stuck.

    After a bit of testing, I realised the issue was that I was mixing transform with left/top positioning. That doesn’t play well when you’re trying to animate smoothly using requestAnimationFrame.

    The Fix: Left & Top + Easing Transition

    To get that smooth “trailing” or “wave” motion, I switched to updating left and top with easing. That way, the text glides behind the mouse rather than sticking to it like glue.

    This made a big difference. Now the text follows the cursor in a fluid way – not jerky, not laggy, just smooth.

    floatX += (mouseX - floatX) * 0.1;
    floatY += (mouseY - floatY) * 0.1;

    Next Issue: Hovering Over Buttons

    Each slide had a button in the bottom-right corner (like “Book Now”). But when you hovered over that button, the floating “Next”/”Previous” text would still be there — not ideal for usability or design.

    So I added a simple fix: detect when the user is hovering the button (or the caption area), and hide the floating text during that time.

    $(document).on('mouseenter', '.v-pageHead__caption', function () {
    	hoveringButton = true;
    	$('.floating-nav').css('opacity', 0);
    });
    
    $(document).on('mouseleave', '.v-pageHead__caption', function () {
    	hoveringButton = false;
    });

    Now, when you’re over the button, the cursor tail disappears. Clean and and tidy.

    Keeping It Clean: Only Run If Slider Is Active

    One more thing I wanted to avoid was unnecessary code running on pages where there was only one slide — or no slider at all.

    So I wrapped everything inside a conditional check like this:

    if ($('.v-pageHead__slide', '.v-pageHead__slider').length > 1) {
    	// Only run this whole block if more than 1 slide
    }

    That way, no Swiper is initialised unless needed, and no extra listeners or animations are running in the background.

    Finally, This was a fun little UX tweak — a bit fiddly at times, but the end result feels smooth and adds a nice touch. It’s the kind of detail that makes a site feel that bit more alive.

    If you’re using Swiper and fancy something a bit cooler than the usual arrows, give it a try! And if you want a peek at the code, just give me a shout here.

  • How I Replaced a Static PHP Product Array with a WordPress Custom Post Type + ACF + Database Sync

    How I Replaced a Static PHP Product Array with a WordPress Custom Post Type + ACF + Database Sync

    If you’ve hardcoded product data into a PHP file and later needed it to be editable from the admin panel without breaking your existing database logic — here’s exactly how I handled that.

    The Problem

    We had a static array inside a PHP class:

    $merch = TCA_Pos::MERCH;

    Which populated a merch section like this:

    <div class="merch-product" data-item-id="<?= $item['id'] ?>">
    	<span class="name"><?= $item['name'] ?></span>
    	...
    </div>

    This array was also linked (by ID) to entries in a custom table: wp_pos_order_merch.

    Updating a product required editing PHP and coordinating updates in the database. That had to change.

    The Goal

    • Move product data into WordPress Custom Post Type (merch)
    • Use ACF for cost and options fields
    • Allow admin users to manage everything from the backend
    • Keep existing database entries consistent by syncing old item_id values

    Step-by-Step: What I Did

    1. Created the CPT

    register_post_type('merch', [
    	'label' => 'Merch',
    	'public' => false,
    	'show_ui' => true,
    	'show_in_menu' => true,
    	'supports' => ['title', 'editor', 'thumbnail'],
    	'capability_type' => 'post',
    	'map_meta_cap' => true,
    	'menu_icon' => 'dashicons-cart',
    ]);

    2. Created ACF Fields

    • cost (number)
    • options (repeater of text values)

    3. Wrote an Importer Script

    This CLI script:

    • Deletes all old merch posts
    • Re-creates them from the static array
    • Captures a mapping between the old static id and the new post_id
    • Updates wp_pos_order_merch accordingly
    $wp_ref_ID_array[] = ['db_id' => $item['id'], 'new_id' => $post_id];
    
    foreach ($wp_ref_ID_array as $ref) {
    	$wpdb->update('wp_pos_order_merch', ['item_id' => $ref['new_id']], ['item_id' => $ref['db_id']]);
    }

    4. Replaced Front-End Static Array

    Now, instead of loading TCA_Pos::MERCH, I fetch merch posts dynamically:

    $posts = get_posts(['post_type' => 'merch', 'numberposts' => -1]);
    
    foreach ($posts as $post) {
    	$merch[] = [
    		'id' => $post->ID,
    		'name' => $post->post_title,
    		'cost' => get_field('cost', $post->ID),
    		'options' => array_column(get_field('options', $post->ID) ?: [], 'option'),
    	];
    }

    Why This Works

    • Data is now fully managed via WordPress admin
    • Existing item_id relationships remain valid
    • It’s scalable: new merch, price changes, sizes — no more code edits
    • It’s safe: your legacy table remains usable

    What Brought You Here

    If you’re searching for:

    • “How to migrate static PHP array to WordPress CPT”
    • “Sync CPT with custom database table”
    • “Use ACF to replace static data”
    • “WordPress CLI content importer with ID remapping”

    …this solution gives you a clean, flexible, future-proof way to get it done.

    If you need to update your current site or modernise how your data is managed get in touch, I’d be happy to help.