bradycandell.com BradyCandell.com
  • —
  • Career Timeline
  • Case Study

Complex Requirements

Simple,
Measurable Results

Making WordPress do what you need, not what it wants.

rocket_launch

Fast Delivery

code_blocks

Clean Code

menu_book
  View the Case Study
timeline
  Career Timeline & Resume

Featured Projects

Shared Vision

Clear communication helps align diverse perspectives. Whether sketching strategies or mapping next steps, I value using visuals and teamwork to build momentum.

Insight in Action

Turning raw data into meaningful direction through open discussion and analysis.

Ideas in Motion

Innovation doesn’t wait for the boardroom. Sharing ideas on the move keeps projects agile, conversations fresh, and solutions practical.

Focused Execution

Where planning meets precision — collaborating hands-on to ensure accuracy and results.

Interactive Card Stack

The client needed a clear, engaging way to present information, and the UX designer proposed an interactive card stack. I implemented the design by solving challenges with z-index, transitions, and transforms, delivering a dynamic, layered experience that makes content easy and fun to explore.

Code Review

<article id="cardMachine" role="region" aria-label="Card carousel">
    <button id="btnBack" aria-label="Previous card" type="button">
        <svg viewBox="0 0 25 21" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
        <path d="M2 10.75L23 10.75M2 10.75L11 2M2 10.75L11 19.5" stroke="#003865" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path>
        </svg>
    </button>
    
    <section id="cardHolder">
        <article class="actualCard cardNumber1" style="z-index: 10000; background-color: rgb(0, 43, 73); border-color: rgb(0, 43, 73); transform: translate(0px, -6.5px) rotate(0deg) scale(1); opacity: 1; pointer-events: auto;">
            <div class="imageHolder">
                <img decoding="async" 
                     src="/wp-content/uploads/2025/09/people-in-smart-casual-wear.jpg" 
                     class="cardImage"
                     alt="Professional team members collaborating, representing shared vision">
            </div>
            <h3 class="cardHeadline" style="color: #FFFFFF;">Shared Vision</h3>
            <p class="cardTextArea" style="color: #FFFFFF;">Clear communication helps align diverse perspectives. Whether sketching strategies or mapping next steps, I value using visuals and teamwork to build momentum.</p>
        </article>
    </section>
    
    <button id="btnForward" aria-label="Next card" type="button">
        <svg viewBox="0 0 25 21" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
        <path d="M23 10.75L2 10.75M23 10.75L14 19.5M23 10.75L14 2" stroke="#003865" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path>
        </svg>
    </button>
</article>
<script>
// JavaScript for handling the flip card and code area switching
document.addEventListener('DOMContentLoaded', function() {
    // Handle flip toggle functionality
    const flipButtons = document.querySelectorAll('.flipToggle[data-flip-target="#<?php echo esc_js($block_id); ?>"]');
    const flipBox = document.querySelector('#<?php echo esc_js($block_id); ?>');
    
    flipButtons.forEach(button => {
        button.addEventListener('click', function(e) {
            e.preventDefault();
            if (flipBox) {
                flipBox.classList.toggle('flipped');
            }
        });
    });
    
    // Handle code area button clicks
    const codeButtons = document.querySelectorAll('#<?php echo esc_js($block_id); ?> .btnBackFlipBox');
    codeButtons.forEach(button => {
        button.addEventListener('click', function(e) {
            e.preventDefault();
            
            // Hide all code areas in this block
            const allCodeAreas = document.querySelectorAll('#<?php echo esc_js($block_id); ?> .codeFlipBox');
            allCodeAreas.forEach(area => {
                area.style.display = 'none';
            });
            
            // Reset all button states
            codeButtons.forEach(btn => {
                btn.setAttribute('aria-expanded', 'false');
                btn.classList.remove('active');
            });
            
            // Show target code area
            const targetId = this.getAttribute('data-target');
            const targetArea = document.getElementById(targetId);
            if (targetArea) {
                targetArea.style.display = 'block';
                this.setAttribute('aria-expanded', 'true');
                this.classList.add('active');
            }
        });
    });
    
    // Show first code area by default if buttons exist
    if (codeButtons.length > 0) {
        codeButtons[0].click();
    }
});
</script>
<style>
section#cardStackBox {
    display: flex;
    max-width: 1300px;
    margin: 0 auto;
    padding: 106px 0 0 50px;
    gap: 80px;
}
#btnForward svg,
#btnBack svg {
    width: 25px;
    height: 21px;
}
article#cardMachine {
    display: flex;
    width: 517px;
    height: 415px;
    align-items: center;
}

section#cardHolder {
    position: relative;
    height: 437px;
    width: 476px;
    left: 45px;
}

.actualCard {
    box-sizing: content-box;
    position: absolute;
    height: 376px;
    width: 310px;
    padding: 16px;
    border: 1px solid #FFFFFF;
    border-radius: 20px 0;
    transition: transform 0.4s ease-in-out, z-index 0s 0.4s;
    box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
}

.imageHolder {
    width: 310px;
    height: 192px;
}

.cardImage {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    border-radius: 20px 0;
}

.cardNumber1 {
    transform: translate(0, -6.5px) rotate(0deg) scale(1);
}

.cardNumber2 {
    transform: translate(25px, -5px) rotate(2deg) scale(1);
}

.cardNumber3 {
    transform: translate(51px, 3px) rotate(1.65deg) scale(1);
}

.cardNumber4 {
    transform: translate(74px, 12px) rotate(2deg) scale(1);
}

.cardNumberHidden {
    transform: translate(74px, 12px) rotate(2deg) scale(1);
    opacity: 0;
    pointer-events: none;
}
aside.cardInfo {
    max-width: 600px;
    padding-top: 52px;
}
h2.cardInfo_headline {
    font: 700 48px / 56px Karla, sans-serif;
}
div.cardInfo_paragraph p {
    font: 500 16px / 20px Karla, sans-serif;
}
h3.cardHeadline {
    font: 700 24px / 24px Triront, serif;
    height: 42px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 8px 0;
    text-transform: uppercase;
    letter-spacing: normal;
}
p.cardTextArea {
    font: 500 18px / 19px Karla, sans-serif;
    width: 305px;
}
a#btnBack {
    position: relative;
    z-index: 999999;
}
#btnForward {
    position: relative;
    z-index: 99999;
}
/* Works for both <button> and <a> controls */
#btnForward.is-disabled,
#btnBack.is-disabled,
#btnForward[disabled],
#btnBack[disabled] {
  opacity: 0.4;
  pointer-events: none; /* stops mouse/touch on anchors */
  cursor: not-allowed;
}
.cm-fit {
  width: 345px;
  height: 292px;
  position: relative;
  top: 15px;
}
.cm-fit #cardMachine {
  width: 517px;
  height: 415px;
  transform-origin: top left;
  transform: scale(0.6673) !important;
}
</style>
View on GitHub

Course Catalog

Previously, grade levels had individual static course pages with no interactivity. The client wanted a single catalog with filters and search that editors could update easily. I built a custom options page for courses and a plugin to handle AJAX filtering, allowing users to refine results instantly without page reloads.  View the catalog here.

Course Catalog Code

// Function to extract and normalize grade level from custom taxonomies
if (!function_exists('extract_grade_level')) {
    function extract_grade_level($custom_taxonomies) {
        if (!$custom_taxonomies || !is_array($custom_taxonomies)) return [999, 'Unknown'];
        
        foreach ($custom_taxonomies as $taxonomy_group) {
            if (!is_array($taxonomy_group)) continue;
            
            $taxonomy_text = strtolower($taxonomy_group['taxonomy_text'] ?? '');
            
            // Look for grade-related taxonomy (flexible keywords)
            if (strpos($taxonomy_text, 'grade') !== false || strpos($taxonomy_text, 'level') !== false) {
                $term_name = $taxonomy_group['term_name'] ?? '';
                
                // Check sub-terms first (more specific grades)
                if (!empty($taxonomy_group['sub_terms']) && is_array($taxonomy_group['sub_terms'])) {
                    foreach ($taxonomy_group['sub_terms'] as $sub_term) {
                        if (!is_array($sub_term)) continue;
                        $sub_name = $sub_term['sub_term_name'] ?? '';
                        $sort_value = normalize_grade_for_sorting($sub_name);
                        if ($sort_value !== 999) {
                            return [$sort_value, $sub_name];
                        }
                    }
                }
                
                // If no specific sub-term, use the main term
                $sort_value = normalize_grade_for_sorting($term_name);
                return [$sort_value, $term_name];
            }
        }
        
        return [999, 'Unknown']; // Default for unknown
    }
}

// Function to normalize any grade string into a sortable number
if (!function_exists('normalize_grade_for_sorting')) {
    function normalize_grade_for_sorting($grade_string) {
        $grade_lower = strtolower(trim($grade_string));
        
        // Handle kindergarten variations
        if (strpos($grade_lower, 'k') !== false || strpos($grade_lower, 'kindergarten') !== false) {
            return 0;
        }
        
        // Extract numbers from grade strings (1st, 2nd, 3rd, etc.)
        if (preg_match('/(\d+)/', $grade_lower, $matches)) {
            return (int)$matches[1];
        }
        
        // Handle specific school level terms
        if (strpos($grade_lower, 'elementary school') !== false) return 50;
        if (strpos($grade_lower, 'middle school') !== false) return 100;
        if (strpos($grade_lower, 'high school') !== false) return 200;
        
        // If we can't determine, return high number so it sorts to end
        return 999;
    }
}

// Custom sorting function: Grade Level > Subject > Course Name
usort($courses, function($a, $b) {
    // 1. Sort by Grade Level (numeric)
    if ($a['grade_level_sort'] !== $b['grade_level_sort']) {
        return $a['grade_level_sort'] <=> $b['grade_level_sort'];
    }
    
    // 2. Sort by Subject with sub-terms (alphabetical)
    if ($a['subject'] !== $b['subject']) {
        return strcmp($a['subject'], $b['subject']);
    }
    
    // 3. Sort by Course Name (alphabetical)
    return strcmp($a['title'], $b['title']);
});
// Advanced Hierarchical Filtering with Smart AND/OR Logic
function filterAndPaginateCourses() {
    const filtersByTaxonomy = {};
    
    // Organize filters by taxonomy groups (grades, subjects, etc.)
    $('.course-catalog-sidebar input[type="checkbox"]:checked').each(function() {
        const $checkbox = $(this);
        const taxonomyGroup = $checkbox.attr('name');
        const isSubTerm = $checkbox.closest('.sub-term-list').length > 0;
        
        if (!filtersByTaxonomy[taxonomyGroup]) {
            filtersByTaxonomy[taxonomyGroup] = { mainTerms: [], subTerms: [] };
        }
        
        // Hierarchical term handling - sub-terms override main terms
        if (isSubTerm) {
            filtersByTaxonomy[taxonomyGroup].subTerms.push('subterm-' + sanitizeClass($checkbox.val()));
        } else {
            filtersByTaxonomy[taxonomyGroup].mainTerms.push('term-' + sanitizeClass($checkbox.val()));
        }
    });

    $('.course-card').each(function() {
        const $card = $(this);
        let matchesFilters = true;
        
        // AND logic between taxonomy groups, OR within each group
        for (const [taxonomyGroup, filters] of Object.entries(filtersByTaxonomy)) {
            let matchesThisTaxonomy = false;
            
            // Hierarchical priority: check sub-terms first, then main terms
            if (filters.subTerms.length > 0) {
                matchesThisTaxonomy = filters.subTerms.some(filterClass => $card.hasClass(filterClass));
            } else if (filters.mainTerms.length > 0) {
                matchesThisTaxonomy = filters.mainTerms.some(filterClass => $card.hasClass(filterClass));
            }
            
            if (!matchesThisTaxonomy) {
                matchesFilters = false;
                break;
            }
        }
        
        $card.toggleClass('visible-course', matchesFilters);
    });
    
    paginateVisibleCourses();
}
/* Mobile-First Responsive Sidebar with State Management */
.course-catalog-sidebar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: white;
    z-index: 1000;
    transform: translateX(-100%);
    display: flex;
    flex-direction: column;
}

.course-catalog-sidebar.active {
    transform: translateX(0);
    width: 268px;
    left: auto;
}

/* Prevent body scroll when sidebar is open */
body.sidebar-open {
    overflow: hidden;
    position: fixed;
    width: 100%;
}

/* Desktop: Convert to static sidebar */
@media (min-width: 1178px) {
    .course-catalog-sidebar {
        position: unset !important; 
        transform: none !important;
        width: 164px;
        background: transparent !important;
    }
}

/* Accordion with CSS-only animation */
.filter-group h3:after {
    content: '';
    width: 31px;
    height: 31px;
    background-image: url('data:image/svg+xml...');
    transition: transform 0.3s ease;
}

.filter-group.open h3:after {
    transform: rotate(180deg);
}
// COURSE CATALOG BLOCKS
array(
    'name'              => 'course-catalog-sidebar',
    'title'             => __('Course Catalog Sidebar'),
    'description'       => __('Filter sidebar for the course catalog'),
    'render_template'   => 'acf-blocks/course-catalog-sidebar.php',
    'category'          => 'laurel-springs-blocks',
    'icon'              => 'filter',
    'keywords'          => array('course', 'filter', 'sidebar'),
    'supports'          => array('align' => false ),
    'mode'              => 'edit',
),
array(
    'name'              => 'course-catalog-loop',
    'title'             => __('Course Catalog Loop'),
    'description'       => __('Displays filtered list of course posts'),
    'render_template'   => 'acf-blocks/course-catalog-loop.php',  // ← Direct path
    'category'          => 'laurel-springs-blocks',
    'icon'              => 'welcome-learn-more',
    'keywords'          => array( 'courses', 'catalog', 'loop' ),
    'mode'              => 'preview',
    'supports'          => array( 'anchor' => true ),
),
View on GitHub

Deeper insights: Case Studies

A view of the desktop version of the Course Catalog

The Moody's Analytics' Course Catalog

At Moody’s, I led the redesign of their course catalog from discovery through design and implementation. I analyzed site analytics and user behavior to uncover pain points like high bounce rates, siloed catalogs, and poor mobile usability.

I then prototyped solutions that emphasized unified navigation, responsive filters, and a more intuitive browsing experience. Finally, I built the catalog in Sitecore—writing the necessary front-end and CMS code to support dynamic filtering and enabling content creators to manage courses with ease.

Over time, I produced several iterations of the catalog, continuously refining the design and functionality based on data and feedback.

View the Full Case Study

Copyright © 2025 · Genesis Child – DDC on Genesis Framework · WordPress · Log in