Skip to main content
Kreig Durham.

← Featured case study

Portfolio

Client builds and code samples. Filter by type, or open a project.

Code samples

Vue.js Employee Database
  • Front-End Development
  • JS Frameworks
ACF Flexible Content Template
  • Front-End Development
  • WordPress Development
<?php
/**
* Partial template for ACF flexible content
*
* @package Understrap
*/
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
the_title( '<h1 class="entry-title d-none">', '</h1>' );
// check for ACF flexible content data
if ( have_rows( 'modules' ) ) {
// loop through the select ACF flexible content layouts
while( have_rows( 'modules' ) ) {
the_row();
// dispaly the matching flexible content partial
get_template_part( 'modules/' . get_row_layout() );
// Just add your row templates into a directory named `modules` in the root of your theme
// Each row template shoud have a filename matching the ACF slug
// (ex: three-columns.php for a field with slug `three-columns`)
}
}
<?php
/**
* Template Name: Flexible Content
*
* Template for displaying ACF Flexible Content Modules
*
* @package Understrap
*/
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
get_header();
?>
<div class="wrapper" id="page-wrapper">
<div class="container-fluid" id="content" tabindex="-1">
<div class="row">
<main class="site-main" id="main">
<?php
while ( have_posts() ) {
the_post();
// Get the template part for looping through our flexible content fields
get_template_part( 'loop-templates/content', 'flexible' );
// If comments are open or we have at least one comment, load up the comment template.
if ( comments_open() || get_comments_number() ) {
comments_template();
}
}
?>
</main><!-- #main -->
</div><!-- .row -->
</div><!-- #content -->
</div><!-- #page-wrapper -->
<?php
get_footer();
Portfolio Gallery CSS Grid
  • CSS
  • Front-End Development
CSS Only Masonry Layout (No CSS Grid)
  • CSS
  • Front-End Development
Scrolling Bootstrap Card Carousel
  • Front-End Development
Scrolling Bootstrap Card Carousel - 4 Cards per Slide
  • Front-End Development
Flickity WordPress Carousel
  • Front-End Development
  • WordPress Development
Material Design Bootstrap WordPress Carousel
  • Front-End Development
  • WordPress Development
Production Image Redirector
  • WordPress Development

WordPress plugin that points local and staging uploads image URLs at a production host: attachments, srcset, and images inside post content.

Full repository on GitHub · excerpt: includes/class-url-redirector.php

<?php
/**
* URL redirection functionality for Production Image Redirector.
*
* @package Production_Image_Redirector
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Filters image URLs to point at the configured production site.
*
* @since 1.0.0
*/
class Production_Image_Redirector_URL_Redirector {
/**
* Options array cache for the current request.
*
* @var array|null
* @since 1.1.0
*/
private $settings = null;
/**
* Constructor.
*
* @since 1.0.0
*/
public function __construct() {
add_filter( 'wp_get_attachment_url', array( $this, 'redirect_attachment_url' ), 10, 2 );
add_filter( 'wp_get_attachment_image_src', array( $this, 'redirect_attachment_image_src' ), 10, 4 );
add_filter( 'wp_get_attachment_image_attributes', array( $this, 'redirect_attachment_image_attributes' ), 10, 3 );
add_filter( 'wp_calculate_image_srcset', array( $this, 'redirect_calculated_srcset' ), 10, 5 );
add_filter( 'the_content', array( $this, 'redirect_content_images' ) );
add_filter( 'widget_text', array( $this, 'redirect_content_images' ) );
add_filter( 'widget_block_content', array( $this, 'redirect_content_images' ), 10, 4 );
}
/**
* Redirect attachment URLs.
*
* @param string $url Attachment URL.
* @param int $attachment_id Attachment ID.
* @return string
* @since 1.0.0
*/
public function redirect_attachment_url( $url, $attachment_id ) {
unset( $attachment_id );
if ( ! $this->should_redirect( 'attachment_url' ) ) {
return $url;
}
return $this->redirect_url( $url );
}
/**
* Redirect attachment image src arrays.
*
* @param array|false $image Image data or false.
* @param int $attachment_id Attachment ID.
* @param string $size Requested size.
* @param bool $icon Whether the image is an icon.
* @return array|false
* @since 1.0.0
*/
public function redirect_attachment_image_src( $image, $attachment_id, $size, $icon ) {
unset( $attachment_id, $size, $icon );
if ( ! $this->should_redirect( 'attachment_image_src' ) || ! is_array( $image ) ) {
return $image;
}
$image[0] = $this->redirect_url( $image[0] );
return $image;
}
/**
* Redirect attachment image attributes.
*
* @param array $attr Img attributes.
* @param WP_Post $attachment Attachment post.
* @param string|int[] $size Size.
* @return array
* @since 1.0.0
*/
public function redirect_attachment_image_attributes( $attr, $attachment, $size ) {
unset( $attachment, $size );
if ( ! $this->should_redirect( 'attachment_image_attributes' ) ) {
return $attr;
}
if ( isset( $attr['src'] ) ) {
$attr['src'] = $this->redirect_url( $attr['src'] );
}
if ( isset( $attr['srcset'] ) ) {
$attr['srcset'] = $this->redirect_srcset( $attr['srcset'] );
}
return $attr;
}
/**
* Redirect URLs inside calculated srcset sources.
*
* @param array $sources One source entry per width.
* @param array $size_array Width and height.
* @param string $image_src Calculated image src.
* @param array $image_meta Attachment meta.
* @param int $attachment_id Attachment ID.
* @return array
* @since 1.1.0
*/
public function redirect_calculated_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
unset( $size_array, $image_src, $image_meta, $attachment_id );
if ( ! $this->should_redirect( 'wp_calculate_image_srcset' ) || ! is_array( $sources ) ) {
return $sources;
}
foreach ( $sources as $width => $source ) {
if ( is_array( $source ) && isset( $source['url'] ) ) {
$sources[ $width ]['url'] = $this->redirect_url( $source['url'] );
}
}
return $sources;
}
/**
* Redirect images in HTML content.
*
* @param string $content HTML content.
* @return string
* @since 1.0.0
*/
public function redirect_content_images( $content ) {
if ( ! $this->should_redirect( 'the_content' ) ) {
return $content;
}
$content = preg_replace_callback(
'/<img([^>]+)src=["\']([^"\']+)["\']([^>]*)>/i',
array( $this, 'redirect_img_tag' ),
$content
);
$content = preg_replace_callback(
'/style=["\']([^"\']*background-image:\s*url\([^)]+\)[^"\']*)["\']/i',
array( $this, 'redirect_style_background' ),
$content
);
return $content;
}
/**
* Replace img tag src and srcset with production URLs.
*
* @param array $matches Regex matches.
* @return string
* @since 1.0.0
*/
private function redirect_img_tag( $matches ) {
$before_attrs = $matches[1];
$src = $matches[2];
$after_attrs = $matches[3];
$redirected_src = $this->redirect_url( $src );
$before_attrs = preg_replace_callback(
'/srcset=["\']([^"\']+)["\']/i',
array( $this, 'redirect_srcset_callback' ),
$before_attrs
);
$after_attrs = preg_replace_callback(
'/srcset=["\']([^"\']+)["\']/i',
array( $this, 'redirect_srcset_callback' ),
$after_attrs
);
return '<img' . $before_attrs . 'src="' . esc_attr( $redirected_src ) . '"' . $after_attrs . '>';
}
/**
* Replace a srcset attribute value.
*
* @param array $matches Regex matches.
* @return string
* @since 1.0.0
*/
private function redirect_srcset_callback( $matches ) {
$srcset = $matches[1];
$redirected_srcset = $this->redirect_srcset( $srcset );
return 'srcset="' . esc_attr( $redirected_srcset ) . '"';
}
/**
* Rewrite background-image URLs inside a style attribute.
*
* @param array $matches Regex matches.
* @return string
* @since 1.0.0
*/
private function redirect_style_background( $matches ) {
$style = $matches[1];
$style = preg_replace_callback(
'/url\(([^)]+)\)/i',
array( $this, 'redirect_style_url' ),
$style
);
return 'style="' . esc_attr( $style ) . '"';
}
/**
* Rewrite a single CSS url() value.
*
* @param array $matches Regex matches.
* @return string
* @since 1.0.0
*/
private function redirect_style_url( $matches ) {
$raw = $matches[1];
$url = trim( $raw, " \t\n\r\0\x0B\"'" );
$redirected_url = $this->redirect_url( $url );
return 'url("' . esc_url( $redirected_url ) . '")';
}
/**
* Redirect srcset URLs.
*
* @param string $srcset Raw srcset attribute value.
* @return string
* @since 1.0.0
*/
private function redirect_srcset( $srcset ) {
$srcset_parts = explode( ',', $srcset );
$redirected_parts = array();
foreach ( $srcset_parts as $part ) {
$part = trim( $part );
if ( preg_match( '/^([^\s]+)\s+(.+)$/', $part, $matches ) ) {
$url = $matches[1];
$descriptor = $matches[2];
$redirected_url = $this->redirect_url( $url );
$redirected_parts[] = $redirected_url . ' ' . $descriptor;
} else {
$redirected_parts[] = $this->redirect_url( $part );
}
}
return implode( ', ', $redirected_parts );
}
/**
* Whether the URL looks like an uploads path.
*
* @param string $url URL or path.
* @return bool
* @since 1.0.0
*/
private function is_uploads_url( $url ) {
if ( false === strpos( $url, 'uploads' ) ) {
return false;
}
$normalized_url = str_replace( '\\', '/', $url );
if ( false !== strpos( $normalized_url, '/wp-content/uploads/' ) ) {
return true;
}
if ( 0 === strpos( $normalized_url, 'wp-content/uploads/' ) ) {
return true;
}
if ( defined( 'UPLOADS' ) ) {
$uploads_path = trim( UPLOADS, '/' );
if ( ! empty( $uploads_path ) ) {
$parsed_url = wp_parse_url( $normalized_url );
if ( ! is_array( $parsed_url ) ) {
$parsed_url = array();
}
$path = isset( $parsed_url['path'] ) ? $parsed_url['path'] : $normalized_url;
if ( 0 !== strpos( $normalized_url, 'http' ) ) {
$path = $normalized_url;
}
if ( false !== strpos( $path, '/' . $uploads_path . '/' ) ) {
return true;
}
}
}
return false;
}
/**
* Core redirect: production host + optional basic-auth userinfo for uploads URLs.
*
* @param string $url Original URL.
* @return string
* @since 1.0.0
*/
private function redirect_url( $url ) {
$settings = $this->get_settings();
$original = $url;
$new_url = $url;
$production_url = isset( $settings['production_url'] ) ? $settings['production_url'] : '';
if ( empty( $production_url ) || ! $this->is_uploads_url( $url ) ) {
return $this->filter_redirect_url( $new_url, $original, $settings );
}
$htpasswd_username = isset( $settings['htpasswd_username'] ) ? trim( $settings['htpasswd_username'] ) : '';
$htpasswd_password = isset( $settings['htpasswd_password'] ) ? trim( $settings['htpasswd_password'] ) : '';
$production_url = rtrim( $production_url, '/' );
$parsed_url = wp_parse_url( $production_url );
if ( ! is_array( $parsed_url ) ) {
$parsed_url = array();
}
$scheme = isset( $parsed_url['scheme'] ) ? $parsed_url['scheme'] . '://' : 'https://';
$host = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
$port = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : '';
$path_prefix = isset( $parsed_url['path'] ) ? $parsed_url['path'] : '';
if ( ! empty( $htpasswd_username ) && ! empty( $htpasswd_password ) ) {
$encoded_username = rawurlencode( $htpasswd_username );
$encoded_password = rawurlencode( $htpasswd_password );
$production_base = $scheme . $encoded_username . ':' . $encoded_password . '@' . $host . $port . $path_prefix;
} else {
$production_base = $scheme . $host . $port . $path_prefix;
}
$production_base = rtrim( $production_base, '/' );
$url_parsed = wp_parse_url( $url );
if ( ! is_array( $url_parsed ) ) {
$url_parsed = array();
}
$url_host = isset( $url_parsed['host'] ) ? $url_parsed['host'] : '';
if ( $url_host === $host && ! empty( $host ) ) {
return $this->filter_redirect_url( $original, $original, $settings );
}
if ( 0 !== strpos( $url, 'http' ) ) {
$new_url = $production_base . '/' . ltrim( $url, '/' );
return $this->filter_redirect_url( $new_url, $original, $settings );
}
$site_url = get_site_url();
if ( 0 === strpos( $url, $site_url ) ) {
$local_path = str_replace( $site_url, '', $url );
$new_url = $production_base . $local_path;
return $this->filter_redirect_url( $new_url, $original, $settings );
}
return $this->filter_redirect_url( $new_url, $original, $settings );
}
/**
* Apply the redirect URL filter.
*
* @param string $new_url URL produced by redirect logic.
* @param string $original Original URL before redirect.
* @param array $settings Plugin option array.
* @return string
* @since 1.1.0
*/
private function filter_redirect_url( $new_url, $original, $settings ) {
/**
* Filters the image URL after redirect logic.
*
* @since 1.1.0
* @param string $new_url URL to use (possibly unchanged).
* @param string $original Original URL before the plugin ran.
* @param array $settings Plugin settings option.
*/
return apply_filters( 'production_image_redirector_redirect_url', $new_url, $original, $settings );
}
/**
* Cached plugin settings for the request.
*
* @return array
* @since 1.1.0
*/
private function get_settings() {
if ( null === $this->settings ) {
$this->settings = get_option( PRODUCTION_IMAGE_REDIRECTOR_OPTION_NAME, array() );
}
return $this->settings;
}
/**
* Whether redirection is enabled and allowed for this context.
*
* @param string $context Context key e.g. attachment_url, the_content.
* @return bool
* @since 1.1.0
*/
private function should_redirect( $context = 'default' ) {
$settings = $this->get_settings();
$enabled = isset( $settings['enable_redirect'] ) && $settings['enable_redirect'] && ! empty( $settings['production_url'] );
if ( ! $enabled ) {
return false;
}
/**
* Whether to redirect image URLs for the given context.
*
* @since 1.1.0
* @param bool $allow Whether redirection should proceed before this filter.
* @param string $context Entry point identifier (e.g. attachment_url, the_content).
* @param array $settings Plugin option array.
*/
return (bool) apply_filters( 'production_image_redirector_should_redirect', true, $context, $settings );
}
}
Bookshop Inventory — data provider
  • WordPress Development

Custom WordPress plugin for bookstore inventory (Ingram Data Services). A provider contract and filter hook so you can swap the Ingram integration without rewriting the display layer.

Full repository on GitHub

Interface

<?php
/**
* Data provider contract for inventory lists.
*
* @link https://github.com/KreigD/bookshop-inventory
* @since 1.0.0
*
* @package Bookshop_Inventory
* @subpackage Bookshop_Inventory/includes
*/
/**
* Normalized inventory access. Implement with the `bookshop_inventory_data_provider` filter.
*
* @since 1.0.0
*/
interface Bookshop_Inventory_Data_Provider_Interface {
/**
* Return inventory rows as associative arrays (e.g. sku, quantity, updated_at).
*
* @since 1.0.0
* @param array $query_args Optional caps for paging or provider-specific keys.
* @return array List of rows.
*/
public function fetch_inventory_items( $query_args = array() );
}

Provider resolution

<?php
/**
* Data provider accessor.
*
* @link https://github.com/KreigD/bookshop-inventory
* @since 1.0.0
*
* @package Bookshop_Inventory
* @subpackage Bookshop_Inventory/includes
*/
if ( ! function_exists( 'bookshop_inventory_get_data_provider' ) ) {
/**
* Resolved data provider for sync or display code.
*
* @since 1.0.0
* @return Bookshop_Inventory_Data_Provider_Interface Provider instance.
*/
function bookshop_inventory_get_data_provider() {
$default = new Bookshop_Inventory_Null_Data_Provider();
/**
* Supply a custom Bookshop_Inventory_Data_Provider_Interface instance.
*
* @since 1.0.0
* @param Bookshop_Inventory_Data_Provider_Interface $provider Default null provider.
*/
$provider = apply_filters( 'bookshop_inventory_data_provider', $default );
if ( ! $provider instanceof Bookshop_Inventory_Data_Provider_Interface ) {
return $default;
}
return $provider;
}
}
AJAX archive filter
  • WordPress Development
  • Front-End Development

Filter a post archive without a full page reload: PHP handles the AJAX request and markup; JavaScript drives the UI and fetch calls.

Snippet folder on GitHub

ajax-archive.php

<?php
/**
* An example of how I set up AJAX post filtering and AJAX load-on-scroll functionality in a real-world project
*/
// Setup AJAX for filter posts by category and for load more button
function ajax_filter_posts_scripts() {
// Enqueue script
wp_register_script('afp_script', get_stylesheet_directory_uri() . '/js/ajax-filter-posts.js', false, null, false);
wp_enqueue_script('afp_script');
// You have to localize your JS in order to tie into WordPress's AJAX
global $wp_query;
wp_localize_script( 'afp_script', 'afp_vars', array(
'afp_ajax_url' => admin_url( 'admin-ajax.php' ),
'posts' => json_encode( $wp_query->query_vars ), // everything about your loop is here
)
);
}
add_action('wp_enqueue_scripts', 'ajax_filter_posts_scripts', 100);
// AJAX filter posts by category
function prefix_load_cat_posts () {
// Get the selected category/categories
$cat_id = $_POST['category__in'];
$args = array (
'post_type' => 'post',
'post_status' => 'publish',
'category__in' => $cat_id,
'posts_per_page' => 9,
'orderby' => 'date',
'order' => 'DESC',
);
$filter_posts = new WP_Query($args);
ob_start ();
if ( $filter_posts->have_posts() ) : while ( $filter_posts->have_posts() ) : $filter_posts->the_post(); ?>
<!-- This it the HTML we wish to return after the user selects a category -->
<article class="article-card">
<header class="card-header">
<div class="card-img" style="background: url('<?php echo the_post_thumbnail_url( 'small' ); ?>') no-repeat center center; width: 100%; background-size: cover;"></div>
<a href="<?php the_permalink(); ?>" class="article-title"><?php the_title( "<h4>", "</h4>" ) ?></a>
<p class="article-author">By
<?php the_author(); ?>
</p>
</header>
<div class="card-content">
<p>
<?php echo wp_trim_words(get_post_meta(get_the_ID(), '_yoast_wpseo_metadesc', true), 15, '...'); ?>
</p>
</div>
<footer class="card-footer">
<a href="<?php the_permalink(); ?>" class="btn btn-article" role="button">Read Now</a>
</footer>
</article>
<?php
endwhile; endif;
wp_reset_postdata();
$response = ob_get_contents();
ob_end_clean();
echo $response;
die(1);
}
add_action( 'wp_ajax_nopriv_load-filter', 'prefix_load_cat_posts' );
add_action( 'wp_ajax_load-filter', 'prefix_load_cat_posts' );
// AJAX Load More
function afp_load_more() {
$cat_id = $_POST['category__in'];
if (empty($cat_id)) {
$cat_id = array(3, 4, 28, 35, 353);
}
// I seet the args differently here mostly because of how many AJAX request paramaters I was pulling in.
$args = isset( $_POST['query'] ) ? array_map( 'esc_attr', $_POST['query'] ) : array();
$args['post_type'] = isset( $args['post_type'] ) ? esc_attr( $args['post_type'] ) : 'post';
$args['paged'] = esc_attr( $_POST['page'] );
$args['post_status'] = 'publish';
$args['category__in'] = $cat_id;
$args['order'] = 'DESC';
$args['orderby'] = 'date';
$args['offset'] = esc_attr( $_POST['offset'] );
$args['posts_per_page'] = 9;
ob_start();
$loop = new WP_Query( $args );
if( $loop->have_posts() ): while( $loop->have_posts() ): $loop->the_post();
?>
<article class="article-card">
<header class="card-header">
<div class="card-img" style="background: url('<?php echo the_post_thumbnail_url( 'small' ); ?>') no-repeat center center; width: 100%; background-size: cover;"></div>
<a href="<?php the_permalink(); ?>" class="article-title"><?php the_title( "<h4>", "</h4>" ) ?></a>
<p class="article-author">By
<?php the_author(); ?>
</p>
</header>
<div class="card-content">
<p>
<?php echo wp_trim_words(get_post_meta(get_the_ID(), '_yoast_wpseo_metadesc', true), 15, '...'); ?>
</p>
</div>
<footer class="card-footer">
<a href="<?php the_permalink(); ?>" class="btn btn-article" role="button">Read Now</a>
</footer>
</article>
<?php
endwhile; endif; wp_reset_postdata();
$res = ob_get_contents();
ob_end_clean();
echo $res;
wp_die();
}
add_action( 'wp_ajax_afp_load_more', 'afp_load_more' );
add_action( 'wp_ajax_nopriv_afp_load_more', 'afp_load_more' );

ajax-filter-posts.js

jQuery(document).ready(function ($) {
// Uncheck checkboxes on page load to prevent weirdness
function UncheckAll() {
const w = document.getElementsByTagName('input');
for (var i = 0; i < w.length; i++) {
if (w[i].type == 'checkbox') {
w[i].checked = false;
}
}
}
// AJAX Post Filter scripts
const $checkbox = $("#filter input:checkbox");
let categoryIDs = [];
$checkbox.change((e) => {
let value = Number(e.currentTarget.value);
if ($checkbox.is(':checked')) {
categoryIDs.indexOf(value) === -1 ? (
categoryIDs.push(value)
) : (
categoryIDs = categoryIDs.filter((item) => item !== value)
)
} else {
categoryIDs = [3, 4, 28, 35, 353];
}
categoryIDs.forEach((item) => {
$.ajax({
type: 'POST',
url: afp_vars.afp_ajax_url,
data: {
action: "load-filter",
category__in: categoryIDs
},
success: function (response) {
$(".filter-section").empty().html(response);
return false;
}
})
});
});
// AJAX Load More Posts scripts
const canBeLoaded = true;
const bottomOffset = 1500;
let page = 2;
let postOffset = 9;
let loading = false;
const scrollHandling = {
allow: true,
reallow: function () {
scrollHandling.allow = true;
},
delay: 400
};
$(window).scroll(function () {
if (!loading && scrollHandling.allow) {
scrollHandling.allow = false;
setTimeout(scrollHandling.reallow, scrollHandling.delay);
if ($(document).scrollTop() > ($(document).height() - bottomOffset) && canBeLoaded == true) {
loading = true;
$.ajax({
type: 'POST',
url: afp_vars.afp_ajax_url,
data: {
action: "afp_load_more",
page: page,
query: afp_vars.query,
category__in: categoryIDs,
offset: postOffset
},
success: function (res) {
$(".filter-section").append(res);
page += 1;
postOffset += 9;
loading = false;
}
})
}
}
});
});

Get in touch

Got a project or a role you think might be a good fit? I'd love to hear about it. Reach me at [email protected] or connect via the links below.

Open to remote contract and full-time roles.

Send a message