ARTICLE AD BOX
Live job list search not working properly (title, tags, description)
I'm building a job listing component in WordPress using a shortcode that displays all jobs (jobpost custom post type). Each job has a title, description, location, tags, and company name.
I added a live search/filter form with:
a keyword input
location input
"remote only" checkbox
"full time only" checkbox
Everything is rendered client-side, and the search is handled by JavaScript only (no AJAX or page reload). The problem is:
🔴 Filtering doesn't work correctly.
Specifically, the search should look through:
job title (.job-title)
full description (.job-full-details-inner)
tags (.job-tags)
company name (.job-company)
But when I type something in the keyword field, no jobs are filtered correctly, even if I enter exact matches.
// Shortcode: [hrc_job_list] // Lista ofert pracy 1:1 jak w mockupie OFERTY_PRACY.HTML, BEZ zielonego banera function hrc_job_list_shortcode() { // pobieramy oferty z Simple Job Board (post type: jobpost) $jobs = new WP_Query(array( 'post_type' => 'jobpost', 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'DESC', )); if ( ! $jobs->have_posts() ) { return '<p>Brak aktualnych ofert pracy.</p>'; } // URL do strony z formularzem — dopasuj jeśli trzeba $apply_base = home_url('/dla-pracownika/'); ob_start(); ?> <style> /* --- KONTENER I STICKY SEARCH --- */ .jobs-container { max-width: 1100px; margin: 0 auto 80px; padding: 0 20px; position: relative; box-sizing: border-box; } .gdpr-disclaimer { font-size: 0.85rem; color: #777; margin: 30px 0 20px; line-height: 1.5; } .sticky-search-panel { position: sticky; top: 80px; z-index: 5; background-color: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 10px 30px -10px rgba(0,0,0,0.15); margin-bottom: 40px; border: 1px solid #eee; } .search-form-grid { display: grid; grid-template-columns: 1fr 1fr auto; gap: 20px; margin-bottom: 20px; } .input-group input { width: 100%; padding: 15px 20px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 1rem; outline: none; transition: border-color 0.3s; box-sizing: border-box; } .input-group input:focus { border-color: #004d26; } .search-btn { background-color: #004d26; color: white; border: none; padding: 0 30px; border-radius: 8px; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background-color 0.3s; height: 100%; } .search-btn:hover { background-color: #006b35; } .filters-row { display: flex; flex-wrap: wrap; gap: 25px; align-items: center; padding-top: 15px; border-top: 1px solid #eee; font-size: 0.95rem; } .filter-checkbox { display: flex; align-items: center; gap: 8px; color: #555; cursor: pointer; } .filter-checkbox input[type="checkbox"] { accent-color: #004d26; width: 16px; height: 16px; cursor: pointer; } /* --- LISTA OGŁOSZEŃ (AKORDEON) --- */ .jobs-list { display: flex; flex-direction: column; gap: 20px; } .job-item { background: white; border-radius: 10px; border: 1px solid #eee; box-shadow: 0 2px 8px rgba(0,0,0,0.03); overflow: hidden; transition: all 0.3s ease; } .job-item.active { border-left: 5px solid #004d26; box-shadow: 0 15px 30px rgba(0,0,0,0.1); } /* Nagłówek karty */ .job-summary { padding: 25px; display: flex; align-items: center; gap: 30px; cursor: pointer; } .job-logo { width: 80px; height: 80px; flex-shrink: 0; background-color: #f4f4f4; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #aaa; overflow: hidden; font-size: 1.2rem; } .job-content { flex-grow: 1; } .job-title { margin: 0 0 8px 0; color: #004d26; font-size: 1.3rem; } .job-company { font-weight: 600; color: #555; margin-bottom: 12px; } .job-details-row { display: flex; gap: 20px; color: #777; font-size: 0.9rem; align-items: center; } .job-tags { display: flex; gap: 10px; flex-wrap: wrap; } .tag { background-color: #e8f5e9; color: #004d26; padding: 4px 10px; border-radius: 50px; font-size: 0.8rem; font-weight: 600; } .job-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 15px; min-width: 140px; } .job-date { font-size: 0.85rem; color: #999; } .apply-btn-outline { text-decoration: none; color: #004d26; border: 2px solid #004d26; padding: 8px 25px; border-radius: 50px; font-weight: 600; transition: all 0.3s; white-space: nowrap; background: white; } .apply-btn-outline:hover { background-color: #004d26; color: white; } /* Szczegóły (akordeon) */ .job-full-details { max-height: 0; overflow: hidden; background-color: #fafafa; border-top: 1px solid #eee; transition: max-height 0.5s ease-out; } .job-full-details-inner { padding: 30px; color: #444; line-height: 1.6; } .details-section { margin-bottom: 25px; } .details-section h4 { color: #004d26; margin: 0 0 10px 0; font-size: 1.1rem; text-transform: uppercase; letter-spacing: 0.5px; } /* RWD */ @media (max-width: 768px) { .sticky-search-panel { top: 70px; padding: 20px; } .search-form-grid { grid-template-columns: 1fr; } .job-summary { flex-direction: column; align-items: flex-start; gap: 15px; } .job-actions { flex-direction: row; width: 100%; justify-content: space-between; align-items: center; margin-top: 10px; } .job-logo { width: 60px; height: 60px; } } </style> <div class="jobs-container"> <p class="gdpr-disclaimer"> Wysyłając aplikację wyrażasz zgodę na przetwarzanie Twoich danych osobowych zgodnie z ustawą o ochronie danych osobowych. Administratorem danych jest HR Consulting Sp. z o.o. Sp. k. z siedzibą w Poznaniu. </p> <div class="sticky-search-panel"> <form id="job-search-form"> <div class="search-form-grid"> <div class="input-group"> <input type="text" id="job-search-keywords" placeholder="Słowa kluczowe..."> </div> <div class="input-group"> <input type="text" id="job-search-location" placeholder="Lokalizacja..."> </div> <button type="submit" class="search-btn">Szukaj oferty</button> </div> <div class="filters-row"> <label class="filter-checkbox"> <input type="checkbox" id="filter-remote"> Tylko praca zdalna </label> <label class="filter-checkbox"> <input type="checkbox" id="filter-fulltime" checked> Pełny etat </label> </div> </form> </div> <div class="jobs-list"> <?php while ( $jobs->have_posts() ) : $jobs->the_post(); $post_id = get_the_ID(); $title = get_the_title(); $content = apply_filters( 'the_content', get_the_content() ); // ACF – nazwa i logo firmy $company = get_field('company_name', $post_id); $logo = get_field('company_logo', $post_id); // fallback gdy puste if ( !$company ) { $company = get_bloginfo('name'); } if ( !$logo ) { // jeśli nie ma logo → użyj inicjałów $logo_text = ''; $words = preg_split('/\s+/', $company); foreach ($words as $w) { $logo_text .= mb_substr($w, 0, 1); if (mb_strlen($logo_text) >= 3) break; } $logo_text = mb_strtoupper($logo_text); } // lokalizacja $location_terms = get_the_terms( $post_id, 'jobpost_location' ); $location = ( ! empty( $location_terms ) && ! is_wp_error( $location_terms ) ) ? $location_terms[0]->name : ''; // typ $type_terms = get_the_terms( $post_id, 'jobpost_job_type' ); $job_type = ( ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ) ? $type_terms[0]->name : ''; // czy zdalna $is_remote = ( stripos( $location, 'zdal' ) !== false || stripos( $job_type, 'zdal' ) !== false ) ? '1' : '0'; // data typu „2 dni temu” $diff = human_time_diff( get_the_time( 'U' ), current_time( 'timestamp' ) ); $date_label = ( $diff === '0 seconds' ) ? 'Dzisiaj' : $diff . ' temu'; // inicjały $logo_text = ''; if ( $company ) { $words = preg_split( '/\s+/', $company ); foreach ( $words as $w ) { $logo_text .= mb_substr( $w, 0, 1 ); if ( mb_strlen( $logo_text ) >= 3 ) break; } $logo_text = mb_strtoupper( $logo_text ); } if ( $logo_text === '' ) { $logo_text = 'HR'; } // link aplikuj $apply_url = add_query_arg( 'temat', rawurlencode( $title ), $apply_base ); $apply_url .= '#contact-section'; ?> <div class="job-item" data-title="<?php echo esc_attr( mb_strtolower( $title ) ); ?>" data-location="<?php echo esc_attr( mb_strtolower( $location ) ); ?>" data-type="<?php echo esc_attr( mb_strtolower( $job_type ) ); ?>" data-remote="<?php echo esc_attr( $is_remote ); ?>"> <div class="job-summary"> <div class="job-logo"><?php echo esc_html( $logo_text ); ?></div> <div class="job-content"> <h3 class="job-title"><?php echo esc_html( $title ); ?></h3> <div class="job-company"><?php echo esc_html( $company ); ?></div> <div class="job-details-row"> <?php if ( $location ) : ?> <span>📍 <?php echo esc_html( $location ); ?></span> <?php endif; ?> <div class="job-tags"> <?php if ( $job_type ) : ?> <span class="tag"><?php echo esc_html( $job_type ); ?></span> <?php endif; ?> <?php if ( $is_remote === '1' ) : ?> <span class="tag">Praca zdalna</span> <?php endif; ?> </div> </div> </div> <div class="job-actions"> <span class="job-date"></span> <a href="#" class="apply-btn-outline open-apply-popup" data-job-title="<?php echo esc_html( $title ); ?>"> Aplikuj </a> </div> </div> <div class="job-full-details"> <div class="job-full-details-inner"> <div class="details-section"> <h4>Praca polega na:</h4> <?php echo $content; ?> </div> </div> </div> </div> <?php endwhile; wp_reset_postdata(); ?> </div> </div> <script> //----------------------------------------- // 🔥 FILTRY I AKORDEON OGŁOSZEŃ //----------------------------------------- document.addEventListener('DOMContentLoaded', function () { const jobItems = document.querySelectorAll('.job-item'); jobItems.forEach(item => { const summary = item.querySelector('.job-summary'); const details = item.querySelector('.job-full-details'); summary.addEventListener('click', function (e) { if (e.target.closest('.apply-btn-outline')) return; const isActive = item.classList.contains('active'); jobItems.forEach(other => { if (other !== item) { other.classList.remove('active'); const otherDetails = other.querySelector('.job-full-details'); if (otherDetails) otherDetails.style.maxHeight = null; } }); if (isActive) { item.classList.remove('active'); details.style.maxHeight = null; } else { item.classList.add('active'); details.style.maxHeight = details.scrollHeight + 'px'; } }); }); //----------------------------------------- // 🔍 FILTROWANIE OGŁOSZEŃ - POPRAWIONE //----------------------------------------- const form = document.getElementById('job-search-form'); const inputKeywords = document.getElementById('job-search-keywords'); const inputLocation = document.getElementById('job-search-location'); const checkboxRemote = document.getElementById('filter-remote'); const checkboxFull = document.getElementById('filter-fulltime'); function filterJobs() { const kw = inputKeywords.value.trim().toLowerCase(); const loc = inputLocation.value.trim().toLowerCase(); const onlyRemote = checkboxRemote.checked; const onlyFull = checkboxFull.checked; jobItems.forEach(item => { // Pobieramy wszystkie teksty do przeszukiwania const jobTitle = item.dataset.title || ''; const jobLocation = item.dataset.location || ''; const jobType = item.dataset.type || ''; // Pobieramy opis z sekcji szczegółów (bez HTML tagów) const detailsSection = item.querySelector('.job-full-details-inner'); const jobDescription = detailsSection ? detailsSection.textContent.toLowerCase() : ''; // Pobieramy tagi const tagsSection = item.querySelector('.job-tags'); const jobTags = tagsSection ? tagsSection.textContent.toLowerCase() : ''; // Pobieramy nazwę firmy const companySection = item.querySelector('.job-company'); const jobCompany = companySection ? companySection.textContent.toLowerCase() : ''; // Łączymy wszystkie teksty do przeszukiwania const searchableText = `${jobTitle} ${jobLocation} ${jobType} ${jobDescription} ${jobTags} ${jobCompany}`.toLowerCase(); let visible = true; // Filtrowanie po słowach kluczowych if (kw && kw.length > 0) { // Sprawdzamy czy słowa kluczowe występują w tekście const keywords = kw.split(' ').filter(word => word.length > 0); const hasAllKeywords = keywords.every(keyword => searchableText.includes(keyword)); if (!hasAllKeywords) { visible = false; } } // Filtrowanie po lokalizacji if (loc && loc.length > 0) { if (!jobLocation.includes(loc)) { visible = false; } } // Filtrowanie - tylko praca zdalna if (onlyRemote && item.dataset.remote !== "1") { visible = false; } // Filtrowanie - tylko pełny etat if (onlyFull && jobType && !jobType.includes("pełny")) { visible = false; } // Pokazujemy/ukrywamy element item.style.display = visible ? "block" : "none"; // Zamykamy szczegóły ukrytych elementów if (!visible && item.classList.contains('active')) { item.classList.remove('active'); const details = item.querySelector('.job-full-details'); if (details) { details.style.maxHeight = null; } } }); // Sprawdzamy czy są widoczne oferty const visibleJobs = Array.from(jobItems).filter(item => item.style.display !== "none"); // Komunikat jeśli brak wyników let noResultsMsg = document.querySelector('.no-results-message'); if (visibleJobs.length === 0) { if (!noResultsMsg) { noResultsMsg = document.createElement('div'); noResultsMsg.className = 'no-results-message'; noResultsMsg.innerHTML = '<p style="text-align: center; color: #777; padding: 40px; font-size: 1.1rem;">Brak ofert spełniających kryteria wyszukiwania.</p>'; document.querySelector('.jobs-list').appendChild(noResultsMsg); } noResultsMsg.style.display = 'block'; } else { if (noResultsMsg) { noResultsMsg.style.display = 'none'; } } } // Funkcja resetująca filtry function resetFilters() { inputKeywords.value = ''; inputLocation.value = ''; checkboxRemote.checked = false; checkboxFull.checked = true; // domyślnie zaznaczone filterJobs(); } // Dodajemy przycisk resetowania function addResetButton() { const filtersRow = document.querySelector('.filters-row'); if (filtersRow && !document.querySelector('.reset-filters-btn')) { const resetBtn = document.createElement('button'); resetBtn.type = 'button'; resetBtn.className = 'reset-filters-btn'; resetBtn.textContent = 'Wyczyść filtry'; resetBtn.style.cssText = ` background: #f5f5f5; border: 1px solid #ddd; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 0.9rem; color: #666; transition: all 0.3s; `; resetBtn.addEventListener('mouseenter', function() { this.style.background = '#e0e0e0'; }); resetBtn.addEventListener('mouseleave', function() { this.style.background = '#f5f5f5'; }); resetBtn.addEventListener('click', resetFilters); filtersRow.appendChild(resetBtn); } } // Inicjalizujemy przycisk resetowania addResetButton(); if (form) { form.addEventListener('submit', e => { e.preventDefault(); filterJobs(); }); inputKeywords.addEventListener('input', filterJobs); inputLocation.addEventListener('input', filterJobs); checkboxRemote.addEventListener('change', filterJobs); checkboxFull.addEventListener('change', filterJobs); } }); //----------------------------------------- // 🔥 POPUP LOGIKA (OTWARCIE/ZAMKNIĘCIE) //----------------------------------------- document.addEventListener('DOMContentLoaded', function () { const popup = document.getElementById('apply-popup'); const popupClose = document.querySelector('.apply-popup-close'); const popupJobTitle = document.getElementById('popup-job-title'); const jobTitleHidden = document.getElementById('job-title-hidden'); document.querySelectorAll('.open-apply-popup').forEach(btn => { btn.addEventListener('click', function (e) { e.preventDefault(); const jobTitle = this.dataset.jobTitle; popupJobTitle.textContent = jobTitle; if (jobTitleHidden) jobTitleHidden.value = jobTitle; popup.style.display = 'flex'; }); }); if (popupClose) { popupClose.addEventListener('click', () => popup.style.display = 'none'); } window.addEventListener('click', e => { if (e.target === popup) popup.style.display = 'none' }); }); //----------------------------------------- // 🔥 WYSYŁKA FORMULARZA (AJAX + VALIDACJA) //----------------------------------------- document.addEventListener("DOMContentLoaded", function () { const form = document.getElementById('job-apply-form'); if (!form) return; const ajaxurl = "<?php echo admin_url('admin-ajax.php'); ?>"; form.addEventListener('submit', async function(e) { e.preventDefault(); const fullname = form.querySelector('[name="fullname"]').value.trim(); const email = form.querySelector('[name="email"]').value.trim(); const phone = form.querySelector('[name="phone"]').value.trim(); if (!fullname || !email || !phone) return alert("⚠ Uzupełnij wszystkie pola!"); if (!email.includes("@") || !email.includes(".")) return alert("⚠ Podaj poprawny e-mail!"); if (phone.length < 6) return alert("⚠ Numer telefonu zbyt krótki!"); let formData = new FormData(form); formData.append('action', 'send_job_application'); formData.append('nonce', '<?php echo wp_create_nonce("job_application_nonce"); ?>'); try { let response = await fetch(ajaxurl, { method: "POST", body: formData }); let result = await response.text(); if (result === "OK") { alert("✔ Aplikacja wysłana! Odezwiemy się."); document.getElementById('apply-popup').style.display = "none"; form.reset(); } else { alert("❌ Błąd serwera — spróbuj ponownie."); } } catch (error) { alert("❌ Błąd połączenia — spróbuj ponownie."); } }); }); </script> <!-- POPUP W KODZIE SHORTCODU --> <div id="apply-popup" class="apply-popup"> <div class="apply-popup-content"> <span class="apply-popup-close">×</span> <h3>Aplikujesz na stanowisko:</h3> <h2 id="popup-job-title"></h2> <form class="apply-form" id="job-apply-form"> <input type="hidden" id="job-title-hidden" name="job_title"> <label>Imię i nazwisko</label> <input type="text" name="fullname" placeholder="Wpisz swoje imię i nazwisko"> <label>Email</label> <input type="email" name="email" placeholder="Wpisz swój email"> <label>Telefon</label> <input type="text" name="phone" placeholder="Wpisz numer telefonu"> <button type="submit">Wyślij aplikację</button> </form> </div> </div> <?php return ob_get_clean(); } add_shortcode('hrc_job_list', 'hrc_job_list_shortcode');