10 Commits

Author SHA1 Message Date
Keith Solomon
785266e455 chore: Update changelog for version 1.6.1; fix filter removal bug and enhance template customization instructions #release
Some checks failed
Add release / Create release (push) Has been cancelled
2025-03-29 15:42:49 -05:00
Keith Solomon
699c06654c refactor: Enhance resource filter functionality and improve UI elements in filter forms 2025-03-29 15:35:42 -05:00
Keith Solomon
758b464121 fix: Update GitHubUpdater path to account for Windows-based paths 2025-03-29 13:18:47 -05:00
Keith Solomon
c297c27ba2 release: Bump version to 1.6.0; implement full admin configurability and automate release builds #release
Some checks failed
Add release / Create release (push) Has been cancelled
2025-03-29 12:18:54 -05:00
Keith Solomon
bf70df2513 refactor: Update README for latest release link and enhance filter display with taxonomy names #release
Some checks failed
Add release / Create release (push) Has been cancelled
2025-03-29 12:14:34 -05:00
Keith Solomon
71b8d4261f refactor: Update resource filter to use global posts total and set query count 2025-03-27 15:58:31 -05:00
Keith Solomon
733d5c3fec feature: Add search term filter display in filter summary 2025-03-27 14:41:25 -05:00
Keith Solomon
d5c8686fd5 refactor: Improve resource filter templates and styles; enhance layout and dynamic taxonomy handling 2025-03-27 14:31:57 -05:00
Keith Solomon
a983351446 refactor: Update resource filter templates and styles; remove unused files and add new styles 2025-03-26 15:51:55 -05:00
Keith Solomon
22b6e37331 feature: Enhance release workflow to extract version and create a zip file for release #release
Some checks failed
Add release / Create release (push) Has been cancelled
2025-03-26 09:34:40 -05:00
15 changed files with 677 additions and 751 deletions

View File

@@ -43,15 +43,28 @@ jobs:
run: |
echo "Tag to be created: ${{ steps.release-tag.outputs.tag }}"
- name: Extract latest changelog entry
- name: Extract latest changelog entry and version
id: extract-changelog
run: |
# Extract changelog for latest version section
version=$(awk '/^## /{ print $2; exit }' CHANGELOG.md)
changelog=$(awk '/^## /{i++} i==1{print}' CHANGELOG.md)
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "changelog<<EOF" >> "$GITHUB_OUTPUT"
echo "$changelog" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Create renamed zip file
run: |
REPO_NAME=$(basename -s .git `git config --get remote.origin.url`)
VERSION=${{ steps.extract-changelog.outputs.version }}
ZIP_NAME="${REPO_NAME}-${VERSION}.zip"
# Create a temporary clone without .git
mkdir package
git archive --format=zip HEAD -o "$ZIP_NAME"
echo "zip_name=$ZIP_NAME" >> "$GITHUB_ENV"
- name: Create release
id: create_release
@@ -60,5 +73,6 @@ jobs:
tag_name: ${{ steps.release-tag.outputs.tag }}
name: Release ${{ steps.release-tag.outputs.tag }}
body: ${{ steps.extract-changelog.outputs.changelog }}
files: ${{ env.zip_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.DS_Store
bak/

View File

@@ -1,5 +1,17 @@
# Changelog
## 1.6.1 - 2025-03-29
- Fixed bug in filter removal functionality
- Updated templates for a better starting point for customization
## 1.6.0 - 2025-03-29
- Implement full admin configuarability
- Update action to build releases automatically
- Clean up unneeded files
- Update templates to use new admin settings
## 1.5.4 - 2025-03-03
- Fix count issue

View File

@@ -13,7 +13,7 @@ The Content Filter Plugin is a WordPress plugin designed to enhance content disc
## Installation
1. Clone this repo or download the plugin files and place them in the `wp-content/plugins/resource-filter` directory.
1. Clone this repo or download the [latest release](https://github.com/Vincent-Design-Inc/resource-filter/releases) and place them in the `wp-content/plugins/resource-filter` directory.
2. Log in to your WordPress admin dashboard.
3. Navigate to **Plugins** > **Installed Plugins**.
4. Activate the **Content Filter** plugin.
@@ -35,7 +35,8 @@ Add one of the following shortcodes to your page or post content where you want
The plugin is designed to work out of the box with minimal configuration. However, you can customize the following:
- **Templates**: Override the default templates and styles by placing your custom versions in your theme's `resource-filter` directory. You will need to add `"./resource-filter/**/*.{php,vue,js,cjs}",` to the `content` object in your `tailwind-config.js` file to compile the custom styles.
- **Templates**: Override the default templates and styles by placing your custom versions in your theme's `resource-filter` directory. You will need to add `"./resource-filter/**/*.{php,css}",` to the `content` object in your `tailwind-config.js` or `@source "./resource-filter/**/*.{php,css}` in your main css file is using Tailwind 4 to compile the custom styles.
- **Template Files**:
- `filter-form.php` - Main form template
- `filter-homepage.php` - Secondary form template for the homepage or other uses
@@ -43,18 +44,20 @@ The plugin is designed to work out of the box with minimal configuration. Howeve
- `resource-results.php` - Template for the search results
- `style.css` - Custom styles for the filter system
- **Taxonomies**: Ensure your WordPress site has the `resource_type` and `resource_subject` taxonomies set up for the `resource` post type.
## Roadmap
- ~~Repo updates~~
- ~~Admin configuration page~~
- ~~tag close functionality~~
- ~~general reset button~~
- ~~pagination~~
## Changelog
## 1.6.1 - 2025-03-29
- Fixed bug in filter removal functionality
- Updated templates for a better starting point for customization
## 1.6.0 - 2025-03-29
- Implement full admin configuarability
- Update action to build releases automatically
- Clean up unneeded files
- Update templates to use new admin settings
## 1.5.4 - 2025-03-03
- Fix count issue

View File

@@ -1,176 +0,0 @@
jQuery(document).ready(function ($) {
/** Triggers filtering and pagination of resources based on the current form state.
*
* @param {number} [paged=1] - The page number to query.
*/
function triggerFiltering(paged = 1) {
let searchTerm = $('#search').val();
let selectedTypes = $('input[name="resource_type[]"]:checked')
.map(function () {
return $(this).closest('label').text().trim();
})
.get();
let selectedSubjects = $('input[name="resource_subject[]"]:checked')
.map(function () {
return $(this).closest('label').text().trim();
})
.get();
let appliedFilters = [];
// Search Term
if (searchTerm) {
appliedFilters.push(
`<span class="filter-item" data-type="search" data-value="${searchTerm}">
<strong>Search:</strong> "${searchTerm}"
<button class="remove-filter" aria-label="Remove Search">×</button>
</span>`
);
}
// Resource Types
$('input[name="resource_type[]"]:checked').each(function () {
const slug = $(this).val(); // Get the slug
const name = $(this).closest('label').text().trim(); // Get the name
appliedFilters.push(
`<span class="filter-item" data-type="resource_type" data-value="${slug}">
<strong>Type:</strong> ${name}
<button class="remove-filter" aria-label="Remove Type ${name}">×</button>
</span>`
);
});
// Resource Subjects
selectedSubjects.forEach(function (subject) {
appliedFilters.push(
`<span class="filter-item" data-type="resource_subject" data-value="${subject}">
<strong>Subject:</strong> ${subject}
<button class="remove-filter" aria-label="Remove Subject ${subject}">×</button>
</span>`
);
});
$('#applied-filters').html(
appliedFilters.length ? appliedFilters.join(' ') : 'None'
);
let formData = {
action: 'filter_resources',
nonce: resourceFilterAjax.nonce,
search: $('#search').val(),
resource_type: $('input[name="resource_type[]"]:checked')
.map(function () {
return this.value;
})
.get(),
resource_subject: $('input[name="resource_subject[]"]:checked')
.map(function () {
return this.value;
})
.get(),
sort_order: $('#sort-order').val(),
paged: paged,
};
$.post(resourceFilterAjax.ajaxurl, formData, function (response) {
response = JSON.parse(response);
$('#resource-results').html(response.html);
$('#result-count').text(response.count);
// Update pagination
if (response.pagination && response.pagination.length > 0) {
// Clear and update pagination container
if (!$('.pagination').length) {
$('#resource-results').after('<div class="pagination"></div>');
}
$('.pagination').html(response.pagination.join(''));
} else {
$('.pagination').html(''); // Clear pagination if no links are needed
}
});
}
// Handle filter removal
$(document).on('click', '.remove-filter', function (e) {
e.preventDefault();
let $filter = $(this).closest('.filter-item');
let filterType = $filter.data('type');
let filterValue = $filter.data('value');
// Remove the corresponding filter
if (filterType === 'search') {
$('#search').val('');
} else if (filterType === 'resource_type') {
$('input[name="resource_type[]"]:checked').each(function () {
if ($(this).val() === filterValue) { // Match the slug, not the name
$(this).prop('checked', false);
}
});
} else if (filterType === 'resource_subject') {
$('input[name="resource_subject[]"]:checked').each(function () {
if ($(this).closest('label').text().trim() === filterValue) {
$(this).prop('checked', false);
}
});
}
// Re-trigger filtering after removing the filter
triggerFiltering(1);
});
// Handle form submission
$('#resource-filter').on('submit', function (e) {
e.preventDefault();
triggerFiltering();
});
// Handle sort order change
$('#sort-order').on('change', function () {
triggerFiltering();
});
// Handle pagination click
$(document).on('click', '.pagination a', function (e) {
e.preventDefault();
// Extract the page number from the link
let pagedMatch = $(this).attr('href').match(/paged=(\d+)/);
let paged = pagedMatch ? pagedMatch[1] : 1; // Default to page 1 if no match is found
// Trigger filtering for the selected page
triggerFiltering(paged);
});
});
document.addEventListener('DOMContentLoaded', function () {
// Toggle dropdown visibility
document.querySelectorAll('.custom-dropdown .dropdown-toggle').forEach(function (button) {
button.addEventListener('click', function () {
const dropdown = this.parentElement;
// Close all other dropdowns
document.querySelectorAll('.custom-dropdown').forEach(function (otherDropdown) {
if (otherDropdown !== dropdown) {
otherDropdown.classList.remove('open');
}
});
// Toggle the current dropdown
dropdown.classList.toggle('open');
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', function (event) {
if (!event.target.closest('.custom-dropdown')) {
document.querySelectorAll('.custom-dropdown').forEach(function (dropdown) {
dropdown.classList.remove('open');
});
}
});
});

View File

@@ -6,93 +6,82 @@ jQuery(document).ready(function ($) {
*
* @param {number} paged The current page number (default is 1).
*/
function triggerFiltering(paged = 1) {
window.triggerFiltering = function (paged = 1) { // Attach to the window object
let searchTerm = $('#search').val();
let appliedFilters = [];
let appliedFilters = [];
let typeFilters = [];
let subjectFilters = [];
let selectedTypes = $('input[name="resource_type[]"]:checked')
.map(function () {
return $(this).closest('label').text().trim();
})
.get();
let selectedSubjects = $('input[name="resource_subject[]"]:checked')
.map(function () {
return $(this).closest('label').text().trim();
})
.get();
// Search Term
if (searchTerm) {
appliedFilters.push(
`<span class="filter-item" data-type="search" data-value="${searchTerm}">
<strong>Search:</strong> "${searchTerm}"
<button class="remove-filter" aria-label="Remove Search">×</button>
<strong>Search:</strong> ${searchTerm}
<button class="remove-filter" aria-label="Remove search term">×</button>
</span>`
);
}
// Resource Types
$('input[name="resource_type[]"]:checked').each(function () {
const slug = $(this).val(); // Get the slug
const name = $(this).closest('label').text().trim(); // Get the name
// Collect selected taxonomy filters dynamically
let taxonomyFilters = {};
$('input[type="checkbox"]:checked').each(function () {
let taxonomy = $(this).attr('name').replace('[]', ''); // Extract taxonomy name
appliedFilters.push(
`<span class="filter-item" data-type="resource_type" data-value="${slug}">
<strong>Type:</strong> ${name}
<button class="remove-filter" aria-label="Remove ${name}">×</button>
</span>`
);
if (!taxonomyFilters[taxonomy]) {
taxonomyFilters[taxonomy] = [];
}
typeFilters.push(
`${name}`
);
taxonomyFilters[taxonomy].push({
value: $(this).val(),
text: $(this).closest('label').text().trim() // Get the text associated with the checkbox
});
});
// Resource Subjects
selectedSubjects.forEach(function (subject) {
appliedFilters.push(
`<span class="filter-item" data-type="resource_subject" data-value="${subject}">
<strong>Subject:</strong> ${subject}
<button class="remove-filter" aria-label="Remove ${subject}">×</button>
</span>`
);
const toTitleCase = (phrase) => {
return phrase
.toLowerCase()
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
subjectFilters.push(
`${subject}`
);
});
// Build applied filters for display
let dropdownFilters = [];
let finalFilters = {};
$('#applied-filters').html(
appliedFilters.length ? appliedFilters.join(' ') : 'None'
);
for (let taxonomy in taxonomyFilters) {
taxonomyFilters[taxonomy].forEach(function (term) {
let taxName = toTitleCase(taxonomy);
$('#type_text').html(
typeFilters.length ? typeFilters.join(', ') : 'Resource Type'
);
$('#subject_text').html(
subjectFilters.length ? subjectFilters.join(', ') : 'Subject Tags'
);
appliedFilters.push(
`<span class="filter-item" data-type="${taxonomy}" data-value="${term.value}">
<strong>${taxName}:</strong> ${term.text}
<button class="remove-filter" aria-label="Remove ${term.text}">×</button>
</span>`
);
dropdownFilters.push(term.text);
if (!finalFilters[taxonomy]) {
finalFilters[taxonomy] = [];
}
finalFilters[taxonomy].push(
term.value,
);
$(`#${taxonomy}_text`).html(
dropdownFilters ? dropdownFilters.join(', ') : taxName
);
});
}
$('#applied-filters').html(appliedFilters.length ? appliedFilters.join(' ') : 'None');
let formData = {
action: 'filter_resources',
nonce: resourceFilterAjax.nonce,
search: searchTerm,
paged: paged,
sort_order: $('#sort-order').val(),
resource_type: $('input[name="resource_type[]"]:checked')
.map(function () {
return this.value;
})
.get(),
resource_subject: $('input[name="resource_subject[]"]:checked')
.map(function () {
return this.value;
})
.get(),
sort_order: $('#sortOrder').val(),
...finalFilters, // Include taxonomy filters dynamically
};
// Perform AJAX request
@@ -109,10 +98,11 @@ jQuery(document).ready(function ($) {
$('.pagination').html('');
}
});
}
};
// Handle sort order change
$('#sort-order').on('change', function () {
$('#sortOrder').on('change', function () {
triggerFiltering();
});
@@ -148,18 +138,13 @@ jQuery(document).ready(function ($) {
// Remove the corresponding filter
if (filterType === 'search') {
$('#search').val('');
} else if (filterType === 'resource_type') {
$('input[name="resource_type[]"]:checked').each(function () {
} else {
// Dynamically handle taxonomy filters
$(`input[name="${filterType}[]"]:checked`).each(function () {
if ($(this).val() === filterValue) { // Match the slug, not the name
$(this).prop('checked', false);
}
});
} else if (filterType === 'resource_subject') {
$('input[name="resource_subject[]"]:checked').each(function () {
if ($(this).closest('label').text().trim() === filterValue) {
$(this).prop('checked', false);
}
});
}
// Re-trigger filtering after removing the filter
@@ -173,15 +158,17 @@ document.addEventListener('DOMContentLoaded', function () {
button.addEventListener('click', function () {
const dropdown = this.parentElement;
// Close all other dropdowns
// Close all other dropdowns and update aria-expanded
document.querySelectorAll('.custom-dropdown').forEach(function (otherDropdown) {
if (otherDropdown !== dropdown) {
otherDropdown.classList.remove('open');
otherDropdown.querySelector('.dropdown-toggle').setAttribute('aria-expanded', 'false');
}
});
// Toggle the current dropdown
dropdown.classList.toggle('open');
// Toggle the current dropdown and update aria-expanded
const isOpen = dropdown.classList.toggle('open');
this.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
});

View File

@@ -1,171 +0,0 @@
.search-text {
display: flex;
gap: 0;
margin-bottom: 1.25rem;
.full-width {
border: 1px solid #ccc;
border-right: none;
font-size: 1rem;
padding: .5rem;
width: 100%;
}
button {
border: 1px solid #ccc;
font-size: 1rem;
padding: .5rem 1rem;
}
}
.search-tax {
align-items: flex-start;
display: flex;
gap: 1rem;
margin-bottom: 1.25rem;
position: relative;
.filter-options { padding-top: .5rem; }
.filter-options label {
display: block;
margin-bottom: 5px;
}
/* Dropdown Container */
.custom-dropdown {
display: inline-block;
position: relative;
width: 100%;
}
/* Dropdown Button */
.custom-dropdown .dropdown-toggle {
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
padding: 10px 15px;
}
/* Dropdown Menu (Hidden by Default) */
.custom-dropdown .dropdown-menu {
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: none;
left: 0;
max-height: fit-content;
overflow-y: auto;
padding: 10px;
position: absolute;
top: 100%;
width: 100%;
z-index: 10;
}
/* Show Dropdown Menu */
.custom-dropdown.open .dropdown-menu {
display: grid;
gap: .5rem;
grid-template-columns: repeat(auto-fill, minmax(15ch, 1fr));
}
/* Checkbox Labels */
.dropdown-menu label {
display: block;
margin-bottom: 8px;
}
.dropdown-menu input[type="checkbox"] {
margin-right: 8px;
}
}
#resource-filter-summary {
display: flex;
gap: .5rem;
justify-content: space-between;
margin-bottom: 1.25rem;
width: 100%;
p { margin: 0; padding: 0; }
}
#resource-results {
display: grid;
gap: 1rem;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
#applied-filters {
margin-top: 15px;
font-size: 14px;
}
.filter-item {
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 20px;
display: inline-block;
margin: 5px;
padding: 5px 10px;
}
.filter-item button.remove-filter {
background: none;
border: none;
color: #0073aa;
cursor: pointer;
font-size: 16px;
margin-left: 5px;
}
.filter-item button.remove-filter:hover { color: #d63638; }
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
.pagination ul {
list-style: none;
display: flex;
gap: 8px;
padding: 0;
}
.pagination a,
.pagination span {
margin: 0 5px;
padding: 8px 12px;
text-decoration: none;
color: #0073aa;
border: 1px solid #ddd;
border-radius: 4px;
}
.pagination a:hover {
background: #0073aa;
color: #fff;
}
.pagination .current {
background: #0073aa;
color: #fff;
border-color: #0073aa;
}
@media (min-width: 768px) { /* Tailwind 'md' breakpoint */
#resource-results {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) { /* Tailwind 'lg' breakpoint */
#resource-results {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}

View File

@@ -4,8 +4,9 @@
* Plugin URI: https://github.com/Vincent-Design-Inc/resource-filter
* Update URI: https://github.com/Vincent-Design-Inc/resource-filter
* Description: Adds filtering for the content typed by various taxonomies.
* Version: 1.5.4
* Version: 1.6.1
* Author: Keith Solomon
* Author URI: https://vincentdesign.ca
*/
if (!defined('ABSPATH')) { exit; } // Prevent direct access
@@ -67,13 +68,13 @@ class ContentFilterPlugin {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_admin_referer('content_filter_settings');
$post_types = isset($_POST['post_types']) ? array_map('sanitize_text_field', $_POST['post_types']) : [];
$postTypes = isset($_POST['post_types']) ? array_map('sanitize_text_field', $_POST['post_types']) : [];
$taxonomies = isset($_POST['taxonomies']) ? array_map('sanitize_text_field', $_POST['taxonomies']) : [];
$posts_per_page = isset($_POST['posts_per_page']) ? intval($_POST['posts_per_page']) : 12;
$homepage_taxonomy = isset($_POST['homepage_taxonomy']) ? sanitize_text_field($_POST['homepage_taxonomy']) : '';
update_option('content_filter_homepage_taxonomy', $homepage_taxonomy);
update_option('content_filter_post_types', $post_types);
update_option('content_filter_post_types', $postTypes);
update_option('content_filter_taxonomies', $taxonomies);
update_option('content_filter_posts_per_page', $posts_per_page);
@@ -81,7 +82,7 @@ class ContentFilterPlugin {
}
// Get saved options
$post_types = get_option('content_filter_post_types', []);
$postTypes = get_option('content_filter_post_types', []);
$taxonomies = get_option('content_filter_taxonomies', []);
$posts_per_page = get_option('content_filter_posts_per_page', 12);
$homepage_taxonomy = get_option('content_filter_homepage_taxonomy', '');
@@ -99,7 +100,7 @@ class ContentFilterPlugin {
<p>Select the post types to include in the filter.</p>
<?php foreach ($all_post_types as $post_type): ?>
<label>
<input type="checkbox" name="post_types[]" value="<?php echo esc_attr($post_type->name); ?>" <?php checked(in_array($post_type->name, $post_types)); ?>>
<input type="checkbox" name="post_types[]" value="<?php echo esc_attr($post_type->name); ?>" <?php checked(in_array($post_type->name, $postTypes)); ?>>
<?php echo esc_html($post_type->labels->singular_name); ?>
</label><br>
<?php endforeach; ?>
@@ -129,7 +130,7 @@ class ContentFilterPlugin {
<?php endforeach; ?>
</select>
<p><input type="submit" class="button-primary" value="Save Settings"></p>
<p style="margin-top: 1.5rem;"><input type="submit" class="button-primary" value="Save Settings"></p>
</form>
</div>
<?php
@@ -153,7 +154,7 @@ class ContentFilterPlugin {
wp_enqueue_style('content-filter-style', $theme_stylesheet_url, [], filemtime($theme_stylesheet));
} else {
// Fall back to the plugin's stylesheet
wp_enqueue_style('content-filter-style', plugins_url('assets/style.css', __FILE__), [], filemtime(plugin_dir_path(__FILE__) . 'assets/style.css'));
wp_enqueue_style('content-filter-style', plugins_url('templates/style.css', __FILE__), [], filemtime(plugin_dir_path(__FILE__) . 'templates/style.css'));
}
// Load script only if the shortcode is present on the page
@@ -184,7 +185,8 @@ class ContentFilterPlugin {
$query = $this->getQuery();
define('RF_TOTAL_RESOURCES', $query->found_posts);
global $postsTotal;
$postsTotal = $query->found_posts;
ob_start();
@@ -216,6 +218,7 @@ class ContentFilterPlugin {
private function getTemplate($type) {
if ($type === 'homepage') {
$homepage_taxonomy = get_option('content_filter_homepage_taxonomy', '');
if (!empty($homepage_taxonomy) && taxonomy_exists($homepage_taxonomy)) {
$GLOBALS['homepage_taxonomy'] = $homepage_taxonomy;
return 'filter-homepage.php';
@@ -290,56 +293,40 @@ class ContentFilterPlugin {
* @return WP_Query The query object
*/
private function getQuery() {
$postTypes = get_option('content_filter_post_types', ['post']); // Get selected post types from admin
$postCount = get_option('content_filter_posts_per_page', 12); // Get posts per page from admin
$strSearch = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$sort_order = isset($_POST['sort_order']) ? sanitize_text_field($_POST['sort_order']) : 'date_desc';
$query_args = [
'post_type' => get_option('content_filter_post_types', ['post']),
'posts_per_page' => get_option('content_filter_posts_per_page', 12),
'post_type' => $postTypes,
'posts_per_page' => $postCount,
'paged' => max(1, get_query_var('paged', 1)), // Get current page number
'tax_query' => $this->buildDynamicTaxQuery(),
's' => isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '',
's' => $strSearch,
];
// Sorting logic
$query_args = $this->applySorting($query_args, $sort_order);
$tax_query = [];
$query = new WP_Query($query_args);
$query->set('count', $query->found_posts);
if (!empty($_POST['resource_type'])) {
$resType = is_array($_POST['resource_type']) ? array_map('sanitize_text_field', $_POST['resource_type']) : sanitize_text_field($_POST['resource_type']);
$query_args['tax_query'][] = [
'taxonomy' => 'resource_type',
'field' => 'slug',
'terms' => $resType,
'operator' => 'IN'
];
}
if (!empty($_POST['resource_subject'])) {
$query_args['tax_query'][] = [
'taxonomy' => 'resource_subject',
'field' => 'slug',
'terms' => array_map('sanitize_text_field', $_POST['resource_subject']),
'operator' => 'IN'
];
}
if (!empty($tax_query)) {
$query_args['tax_query'] = [
'relation' => 'AND', // Both filters must match
...$tax_query
];
}
return new WP_Query($query_args);
return $query;
} else {
return new WP_Query([
'post_type' => get_option('content_filter_post_types', ['post']),
'posts_per_page' => get_option('content_filter_posts_per_page', 12),
$query = new WP_Query([
'post_type' => $postTypes,
'posts_per_page' => $postCount,
'paged' => max(1, get_query_var('paged', 1)), // Get current page number
'tax_query' => $this->buildDynamicTaxQuery(),
's' => $strSearch,
]);
$query->set('count', $query->found_posts);
return $query;
}
}
@@ -365,27 +352,27 @@ class ContentFilterPlugin {
switch ($sort_order) {
case 'date_asc':
$query_args['orderby'] = 'date';
$query_args['order'] = 'ASC';
$query_args['order'] = 'ASC';
break;
case 'date_desc':
$query_args['orderby'] = 'date';
$query_args['order'] = 'DESC';
$query_args['order'] = 'DESC';
break;
case 'title_asc':
$query_args['orderby'] = 'title';
$query_args['order'] = 'ASC';
$query_args['order'] = 'ASC';
break;
case 'title_desc':
$query_args['orderby'] = 'title';
$query_args['order'] = 'DESC';
$query_args['order'] = 'DESC';
break;
default:
$query_args['orderby'] = 'date';
$query_args['order'] = 'DESC';
$query_args['order'] = 'DESC';
}
return $query_args;
@@ -469,20 +456,21 @@ class ContentFilterPlugin {
* @since 1.0.0
*/
private function buildQueryArgs() {
$post_types = get_option('content_filter_post_types', []);
$postTypes = get_option('content_filter_post_types', []);
$postCount = get_option('content_filter_posts_per_page', 12); // Get posts per page from admin
$strSearch = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
$sort_order = isset($_POST['sort_order']) ? sanitize_text_field($_POST['sort_order']) : 'date_desc';
$query_args = [
'post_type' => !empty($post_types) ? $post_types : ['post'],
'posts_per_page' => get_option('content_filter_posts_per_page', 12),
'post_type' => !empty($postTypes) ? $postTypes : ['post'],
'posts_per_page' => $postCount,
'paged' => isset($_POST['paged']) ? intval($_POST['paged']) : 1, // Get current page number
'tax_query' => $this->buildDynamicTaxQuery(),
's' => isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '',
's' => $strSearch,
];
$query_args = $this->applySorting($query_args, $sort_order);
$query_args['tax_query'] = $this->buildTaxQuery();
$query_args['tax_query'] = $this->buildDynamicTaxQuery();
return $query_args;
}
@@ -498,7 +486,7 @@ class ContentFilterPlugin {
* @since 1.4.0
*/
private function buildDynamicTaxQuery() {
$taxonomies = get_option('content_filter_taxonomies', []);
$taxonomies = get_option('content_filter_taxonomies', []); // Get selected taxonomies from admin
$tax_query = [];
if (!empty($taxonomies)) {
@@ -526,50 +514,6 @@ class ContentFilterPlugin {
return [];
}
/** Builds a taxonomy query array for filtering resources by type and subject.
*
* Constructs a taxonomy query based on the 'resource_type' and 'resource_subject'
* POST parameters. The function checks if these parameters are present and
* properly sanitizes them before adding them to the query. If both parameters
* are provided, the query will require both conditions to match using an 'AND'
* relation.
*
* @return array The constructed tax query array, or an empty array if no filters are applied.
*/
private function buildTaxQuery() {
$tax_query = [];
if (!empty($_POST['resource_type'])) {
$resType = is_array($_POST['resource_type']) ? array_map('sanitize_text_field', $_POST['resource_type']) : sanitize_text_field($_POST['resource_type']);
$tax_query[] = [
'taxonomy' => 'resource_type',
'field' => 'slug',
'terms' => $resType,
'operator' => 'IN'
];
}
if (!empty($_POST['resource_subject'])) {
$tax_query[] = [
'taxonomy' => 'resource_subject',
'field' => 'slug',
'terms' => array_map('sanitize_text_field', $_POST['resource_subject']),
'operator' => 'IN'
];
}
if (!empty($tax_query)) {
return [
'relation' => 'AND', // Both filters must match
...$tax_query
];
}
return $tax_query;
}
/** Sends an AJAX response with resource filtering results.
*
* Constructs a response array containing the number of resources found,
@@ -583,31 +527,38 @@ class ContentFilterPlugin {
private function sendAjaxResponse($query) {
$response = [
'count' => $query->found_posts,
'filters' => [
'search' => isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '',
'resource_type' => !empty($_POST['resource_type']) ? sanitize_text_field($_POST['resource_type']) : '',
'resource_subject' => !empty($_POST['resource_subject']) ? sanitize_text_field($_POST['resource_subject']) : ''
'count' => $query->found_posts,
'filters' => [
'search' => isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '',
],
'html' => ob_get_clean(),
'pagination' => paginate_links([
'total' => $query->max_num_pages,
'current' => isset($_POST['paged']) ? intval($_POST['paged']) : 1,
'format' => '?paged=%#%',
'add_args' => [], // Pass additional query arguments
'html' => ob_get_clean(),
'pagination' => paginate_links([
'total' => $query->max_num_pages,
'current' => isset($_POST['paged']) ? intval($_POST['paged']) : 1,
'format' => '?paged=%#%',
'add_args' => [], // Pass additional query arguments
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'type' => 'array',
'type' => 'array',
])
];
// Include dynamic taxonomy filters in the response
$taxonomies = get_option('content_filter_taxonomies', []);
foreach ($taxonomies as $taxonomy) {
if (!empty($_POST[$taxonomy])) {
$response['filters'][$taxonomy] = array_map('sanitize_text_field', (array) $_POST[$taxonomy]);
}
}
echo json_encode($response);
wp_die();
}
}
new ContentFilterPlugin();
$gitHubUpdater = new GitHubUpdater(__FILE__);
$gitHubUpdater = new GitHubUpdater(plugin_dir_path(__FILE__).'resource-filter.php');
$gitHubUpdater->setChangelog('CHANGELOG.md');
$gitHubUpdater->setPluginIcon('assets/icon-256x256.png');
$gitHubUpdater->setPluginBannerLarge('assets/banner.jpg');

View File

@@ -1,48 +0,0 @@
<?php
if (!defined('ABSPATH')) { exit; } // Prevent direct access
$resource_types = get_terms(['taxonomy' => 'resource_type', 'hide_empty' => true]);
$resource_subjects = get_terms(['taxonomy' => 'resource_subject', 'hide_empty' => true]);
?>
<!-- Theme override -->
<form id="resource-filter">
<!-- Search Field-->
<div class="search-text">
<input class="full-width" type="text" id="search" name="search" placeholder="Search resources..." value="<?php echo isset($search) ? esc_attr($search) : ''; ?>">
<button type="reset" id="clear-search">&times;</button>
<button type="submit">Filter</button>
</div>
<div class="search-tax">
<!-- Resource Type Filters -->
<details>
<summary>Resource Type</summary>
<div class="filter-options">
<?php foreach ($resource_types as $type) : ?>
<label>
<input type="checkbox" name="resource_type[]" value="<?php echo esc_attr($type->slug); ?>"
<?php echo (isset($_POST['resource_type']) && $_POST['resource_type'] === $type->slug) ? 'checked' : ''; ?>>
<?php echo esc_html($type->name); ?>
</label>
<?php endforeach; ?>
</div>
</details>
<!-- Resource Subject Filters -->
<details>
<summary>Resource Subject</summary>
<div class="filter-options">
<?php foreach ($resource_subjects as $subject) : ?>
<label>
<input type="checkbox" name="resource_subject[]" value="<?php echo esc_attr($subject->slug); ?>">
<?php echo esc_html($subject->name); ?>
</label>
<?php endforeach; ?>
</div>
</details>
</div>
</form>

View File

@@ -1,59 +1,73 @@
<?php
if (!defined('ABSPATH')) { exit; } // Prevent direct access
$resource_types = get_terms(['taxonomy' => 'resource_type', 'hide_empty' => true]);
$resource_subjects = get_terms(['taxonomy' => 'resource_subject', 'hide_empty' => true]);
$selected_taxonomies = get_option('content_filter_taxonomies', []); // Get selected taxonomies
?>
<form class="px-4 sm:px-0" id="resource-filter">
<!-- Search Field -->
<!-- Search Form - Plugin -->
<div class="search-text">
<input class="bg-[#F8F8F8] border-[#8B8B8B] border-2" type="text" id="search" name="search" placeholder="Search resources..."
value="<?php echo isset($search) ? esc_attr($search) : ''; ?>">
<button class="bg-[#3B65D4] text-white" type="submit">Search</button>
<button type="reset" id="clear-search">&times;</button>
<div class="search-input-wrapper flex-grow">
<input class="full-width bg-[#F8F8F8] border-[#8B8B8B] border rounded-xl" type="text" id="search" name="search"
placeholder="Search resources..." value="<?php echo isset($search) ? esc_attr($search) : ''; ?>">
<button type="reset" id="clear-search">&times;</button>
</div>
<div class="flex justify-center md:justify-start">
<button class="btn btn-primary bg-primary text-white rounded-xl w-full md:w-auto px-4 py-2" type="submit">Search</button>
</div>
</div>
<div class="search-tax">
<!-- Resource Type Filters -->
<div class="custom-dropdown">
<button type="button" class="dropdown-toggle">Resource Type</button>
<div class="dropdown-menu">
<?php foreach ($resource_types as $type) : ?>
<label>
<input type="checkbox" name="resource_type[]" value="<?php echo esc_attr($type->slug); ?>" <?php echo
(isset($_POST['resource_type']) && in_array($type->slug, (array) $_POST['resource_type'])) ? 'checked' :
'';
?>>
<?php echo esc_html($type->name); ?>
</label>
<?php endforeach; ?>
</div>
</div>
<?php
foreach ($selected_taxonomies as $taxonomy):
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => true]);
$taxonomy_obj = get_taxonomy($taxonomy);
<!-- Resource Subject Filters -->
<div class="custom-dropdown">
<button type="button" class="dropdown-toggle">Resource Subject</button>
<div class="dropdown-menu">
<?php foreach ($resource_subjects as $subject) : ?>
<label>
<input type="checkbox" name="resource_subject[]" value="<?php echo esc_attr($subject->slug); ?>" <?php echo
(isset($_POST['resource_subject']) && in_array($subject->slug, (array) $_POST['resource_subject'])) ?
'checked' : ''; ?>>
<?php echo esc_html($subject->name); ?>
</label>
<?php endforeach; ?>
</div>
</div>
<!-- Sort Container -->
<div id="sort-container" class="flex items-start">
<label class="pt-2" for="sort-order">Sort:</label>
<select class="ml-2 bg-white border-[#CCC] border-2" id="sort-order">
<option value="date_desc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : 'date_desc', 'date_desc'); ?>>Newest First</option>
<option value="date_asc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : 'date_desc', 'date_asc'); ?>>Oldest First</option>
<option value="title_asc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : 'date_desc', 'title_asc'); ?>>Title (A-Z)</option>
<option value="title_desc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : 'date_desc', 'title_desc'); ?>>Title (Z-A)</option>
</select>
</div>
if (!empty($terms) && !empty($taxonomy_obj)): ?>
<div class="custom-dropdown">
<button id="<?php echo esc_attr($taxonomy); ?>_toggle" type="button" class="dropdown-toggle" aria-haspopup="true" aria-expanded="false">
<div id="<?php echo esc_attr($taxonomy); ?>_text" class="dropdown-text"><?php echo esc_html($taxonomy_obj->labels->singular_name); ?></div>
</button>
<div class="dropdown-menu taxonomy-filter" data-taxonomy="<?php echo esc_attr($taxonomy); ?>">
<?php foreach ($terms as $term): ?>
<label>
<input type="checkbox" name="<?php echo esc_attr($taxonomy); ?>[]" value="<?php echo esc_attr($term->slug); ?>" <?php echo (isset($_POST[$taxonomy]) && in_array($term->slug, (array) $_POST[$taxonomy])) ? 'checked' : ''; ?>>
<?php echo esc_html($term->name); ?>
</label>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('resource-filter');
const resetButton = document.getElementById('clear-search');
resetButton.addEventListener('click', function (e) {
e.preventDefault(); // Prevent the default reset behavior
// Clear all input fields
form.reset();
// Clear all checkboxes
const checkboxes = form.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
// Reset dropdown text to taxonomy name
document.querySelectorAll('.custom-dropdown').forEach(function (dropdown) {
const taxonomy = dropdown.querySelector('.dropdown-toggle').id.replace('_toggle', '');
const taxonomyName = taxonomy.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); // Convert to title case
dropdown.querySelector('.dropdown-text').textContent = taxonomyName;
});
// Trigger filtering without reloading the page
if (typeof triggerFiltering === 'function') {
triggerFiltering(1); // Call the function with the first page
}
});
});
</script>

View File

@@ -1,20 +0,0 @@
<?php if (!defined('ABSPATH')) { exit; } ?>
<form class="flex w-full" id="homepage-filter" action="<?php echo site_url('/browse-resources/'); ?>" method="POST">
<?php
$resource_types = get_terms(['taxonomy' => 'resource_type']);
if (!empty($resource_types)) :
?>
<select class="w-fit" name="resource_type">
<option value="">All Types</option>
<?php foreach ($resource_types as $type) : ?>
<option value="<?php echo esc_attr($type->slug); ?>"><?php echo esc_html($type->name); ?></option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<input class="full-width" type="text" name="search" placeholder="Search resources...">
<button class="btn btn-primary" type="submit">Search</button>
</form>

View File

@@ -2,7 +2,8 @@
if (!defined('ABSPATH')) { exit; } // Prevent direct access
// Get count from AJAX or direct POST
$count = isset($resTotal) ? esc_html($resTotal) : 0;
global $postsTotal;
$count = isset($postsTotal) ? esc_html($postsTotal) : 0;
// Initialize filters array
$filters = [];
@@ -16,37 +17,36 @@ if (!empty($_POST['search'])) {
];
}
// Handle the "Resource Type" filter
if (!empty($_POST['resource_type'])) {
$selected_types = is_array($_POST['resource_type']) ? $_POST['resource_type'] : [$_POST['resource_type']];
$filters[] = [
'type' => 'resource_type',
'value' => esc_html(implode(',', $selected_types)),
'label' => '<strong>Type:</strong> ' . esc_html(implode(', ', $selected_types))
];
}
// Get selected taxonomies from admin settings
$selectedTaxonomies = get_option('content_filter_taxonomies', []);
// Handle the "Resource Subject" filter
if (!empty($_POST['resource_subject'])) {
$selected_subjects = is_array($_POST['resource_subject']) ? $_POST['resource_subject'] : [$_POST['resource_subject']];
$filters[] = [
'type' => 'resource_subject',
'value' => esc_html(implode(',', $selected_subjects)),
'label' => '<strong>Subject:</strong> ' . esc_html(implode(', ', $selected_subjects))
];
// Handle dynamic taxonomy filters
foreach ($selectedTaxonomies as $taxonomy) {
if (!empty($_POST[$taxonomy])) {
$selectedTerms = is_array($_POST[$taxonomy]) ? $_POST[$taxonomy] : [$_POST[$taxonomy]];
$taxonomyObj = get_taxonomy($taxonomy);
if ($taxonomyObj) {
$filters[] = [
'type' => $taxonomy,
'value' => esc_html(implode(',', $selectedTerms)),
'label' => '<strong>' . esc_html($taxonomyObj->labels->singular_name) . ':</strong> ' . esc_html(implode(', ', $selectedTerms))
];
}
}
}
// Display filters as HTML
$filter_html = '';
$filterHtml = '';
if (!empty($filters)) {
foreach ($filters as $filter) {
$filter_html .= '<span class="filter-item" data-type="' . esc_attr($filter['type']) . '" data-value="' . esc_attr($filter['value']) . '">'
$filterHtml .= '<span class="filter-item" data-type="' . esc_attr($filter['type']) . '" data-value="' . esc_attr($filter['value']) . '">'
. $filter['label']
. ' <button class="remove-filter" aria-label="Remove ' . esc_attr($filter['type']) . '">×</button>'
. '</span> ';
}
} else {
$filter_html = 'None';
$filterHtml = 'None';
}
?>
@@ -55,21 +55,21 @@ if (!empty($filters)) {
<p><strong>Showing <span id="result-count"><?php echo esc_html($count); ?></span> resource(s)</strong></p>
<div class="sort-filters flex items-start gap-4">
<!-- Sort Container -->
<div id="sort-container">
<label for="sort-order">Sort by:</label>
<select id="sort-order">
<option value="date_desc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : '', 'date_desc'); ?>>Newest First</option>
<option value="date_asc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : '', 'date_asc'); ?>>Oldest First</option>
<option value="title_asc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : '', 'title_asc'); ?>>Title (A-Z)</option>
<option value="title_desc" <?php selected(isset($_GET['sort_order']) ? $_GET['sort_order'] : '', 'title_desc'); ?>>Title (Z-A)</option>
</select>
</div>
<!-- Applied Filters -->
<p>
<strong>Filters applied:</strong><br>
<span id="applied-filters"><?php echo $filter_html; ?></span>
<span id="applied-filters"><?php echo $filterHtml; ?></span>
</p>
</div>
<!-- Sort Container -->
<div id="sort-container">
<label for="sortOrder"><strong>Sort by:</strong></label>
<select id="sortOrder" name="sortOrder">
<option value="date_desc" <?php selected(isset($_GET['sortOrder']) ? $_GET['sortOrder'] : '', 'date_desc'); ?>>Newest First</option>
<option value="date_asc" <?php selected(isset($_GET['sortOrder']) ? $_GET['sortOrder'] : '', 'date_asc'); ?>>Oldest First</option>
<option value="title_asc" <?php selected(isset($_GET['sortOrder']) ? $_GET['sortOrder'] : '', 'title_asc'); ?>>Title (A-Z)</option>
<option value="title_desc" <?php selected(isset($_GET['sortOrder']) ? $_GET['sortOrder'] : '', 'title_desc'); ?>>Title (Z-A)</option>
</select>
</div>
</div>

View File

@@ -1,38 +0,0 @@
<?php
if (!defined('ABSPATH')) { exit; } // Prevent direct access
if (!empty($resources)) :
foreach ($resources as $resource) :
$postID = $resource->ID;
$postTitle = get_the_title($postID);
$postLink = get_permalink($postID);
?>
<div class="resource-item border border-primary-500 p-4 rounded">
<h3 class="text-22px font-semibold leading-2 my-0 py-0"><a class="text-indigo-400" href="<?php echo esc_url($postLink); ?>"><?php echo esc_html($postTitle); ?></a></h3>
<div class="flex flex-col mt-8">
<p class="text-14px leading-tight my-0 py-0"><strong>Resource Type:</strong> <?php echo esc_html(get_the_terms($postID, 'resource_type')[0]->name); ?></p>
<p class="text-14px leading-tight my-0 py-0">
<strong>Resource Subject(s):</strong>
<?php
$subjects = get_the_terms($postID, 'resource_subject');
$count = count($subjects);
$i = 1;
foreach ($subjects as $subject) {
if ($i === $count) {
echo esc_html($subject->name);
} else {
echo esc_html($subject->name) . ', ';
}
$i++;
}
?>
</p>
</div>
</div>
<?php endforeach; ?>
<?php else : ?>
<p>No resources found.</p>
<?php endif; ?>

View File

@@ -1,34 +1,38 @@
<?php
if (!defined('ABSPATH')) { exit; } // Prevent direct access
// Define dynamic post type and taxonomy
$postType = isset($postType) ? $postType : 'post'; // Default to 'post' if not set
$taxonomy = isset($taxonomy) ? $taxonomy : 'category'; // Default to 'category' if not set
if (!empty($resources)) :
foreach ($resources as $resource) :
$postID = $resource->ID;
$postTitle = get_the_title($postID);
$postLink = get_permalink($postID);
$terms = get_the_terms($postID, 'resource_type');
$postType = get_post_type($postID);
$terms = get_the_terms($postID, $taxonomy);
$img = has_post_thumbnail($postID) ? get_the_post_thumbnail_url($postID, 'full') : get_field('admin', 'option')['imgDefault']['url'] ?? '';
?>
<div class="px-4 sm:px-0">
<?php if (has_post_thumbnail($postID)) : ?>
<div class="block bg-[#F3F3F3] h-[32rem]">
<a href="<?php echo esc_url($postLink); ?>">
<img src="<?php echo esc_url(get_the_post_thumbnail_url($postID)); ?>" alt="<?php echo esc_attr($postTitle); ?>"
class="flex justify-center items-center w-full h-full object-contain">
</a>
</div>
<?php endif; ?>
<div class="block mb-4">
<a href="<?php echo esc_url($postLink); ?>">
<img src="<?php echo esc_url($img); ?>" alt="<?php echo esc_attr($postTitle); ?>" class="flex justify-center items-center w-full h-full object-contain">
</a>
</div>
<p class="text-sm font-thin uppercase">
<?php echo $terms ? esc_html(strtolower($terms[0]->name)) : ''; ?>
<p class="text-sm font-thin uppercase px-2 mb-1!">
Post Type: <?php echo $postType; ?>
</p>
<div class="flex">
<div class="flex px-2">
<div>
<h3 class="text-lg"><a class="" href="<?php echo esc_url($postLink); ?>"><?php echo esc_html($postTitle); ?></a></h3>
</div>
<div class="flex items-center ml-auto pr-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 ml-2">
<div class="flex ml-auto mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</div>
@@ -36,5 +40,5 @@ if (!empty($resources)) :
</div>
<?php endforeach; ?>
<?php else : ?>
<p>No resources found.</p>
<p>No results.</p>
<?php endif; ?>

393
templates/style.css Normal file
View File

@@ -0,0 +1,393 @@
.search-text {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.25rem;
position: relative;
}
@media (min-width: 768px) {
.search-text {
align-items: flex-start;
flex-direction: row;
}
}
.search-input-wrapper {
display: flex;
width: 100%;
}
.full-width {
align-items: center;
background: #fff;
border: 1px solid #ccc;
border-right: none;
border-radius: 10px 0 0 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
display: flex;
font-size: 0.95rem;
font-weight: 500;
padding: 12px 15px;
transition: all 0.25s ease-in-out;
width: 100%;
}
.full-width:focus {
border-color: #4E6ACA;
box-shadow: 0 2px 8px rgba(78, 106, 202, 0.25);
outline: none;
}
.full-width:hover {
border-color: #4E6ACA;
box-shadow: 0 2px 5px rgba(78, 106, 202, 0.15);
}
#clear-search {
align-items: center;
background: #fff;
border: 1px solid #ccc;
border-radius: 0 10px 10px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
display: flex;
font-size: 1rem;
justify-content: center;
padding: .75rem 1rem;
transition: all 0.25s ease-in-out;
}
#clear-search:hover {
border-color: #4E6ACA;
box-shadow: 0 2px 5px rgba(78, 106, 202, 0.15);
color: #4E6ACA;
}
.search-tax {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
margin-bottom: 1.25rem;
position: relative;
}
@media (min-width: 640px) {
.search-tax {
grid-template-columns: repeat(1, 1fr);
}
}
@media (min-width: 1024px) {
.search-tax {
grid-template-columns: repeat(3, 1fr);
}
}
.filter-options {
padding-top: .5rem;
}
.filter-options label {
display: block;
margin-bottom: 5px;
}
/* Dropdown Container */
.custom-dropdown {
position: relative;
width: 100%;
margin-bottom: 1rem;
}
.dropdown-toggle {
align-items: center;
background: #fff;
border: 1px solid #ccc;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
cursor: pointer;
display: flex;
font-size: 0.95rem;
font-weight: 500;
justify-content: space-between;
max-width: 16rem;
padding: 12px 15px;
transition: all 0.25s ease-in-out;
width: 100%;
}
.dropdown-toggle:hover {
border-color: #4E6ACA;
box-shadow: 0 2px 5px rgba(78, 106, 202, 0.15);
}
.dropdown-toggle::after {
color: #666;
content: '\e804';
font-family: "fontello", sans-serif;
font-size: 0.8rem;
margin-left: 0.5rem;
transition: transform 0.3s ease, color 0.2s ease;
}
.custom-dropdown.open .dropdown-toggle {
border-color: #4E6ACA;
box-shadow: 0 2px 8px rgba(78, 106, 202, 0.25);
}
.custom-dropdown.open .dropdown-toggle::after {
color: #4E6ACA;
content: '\e804';
transform: rotate(180deg);
}
/* Dropdown Menu (Hidden by Default) */
.dropdown-menu {
animation: fadeIn 0.25s ease;
background: #fff;
border: 1px solid #ccc;
border-radius: 10px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
display: none;
left: 0;
max-height: 350px;
overflow-y: auto;
padding: 15px;
position: absolute;
top: calc(100% + 5px);
width: 100%;
z-index: 50 !important;
}
/* Improved responsive widths */
@media (min-width: 640px) {
.dropdown-menu {
max-width: 750px;
width: 200%;
}
}
@media (min-width: 1280px) {
.dropdown-menu {
max-width: 800px;
width: 250%;
}
}
.dropdown-menu::-webkit-scrollbar {
width: 8px;
}
.dropdown-menu::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.dropdown-menu::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
.dropdown-menu::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
/* Checkbox Labels - Improved touch target sizes */
.dropdown-menu label {
align-items: center;
border-radius: 6px;
cursor: pointer;
display: flex;
font-size: 0.85rem;
margin-bottom: 4px;
padding: 10px;
transition: all 0.2s ease;
}
.dropdown-menu label:hover {
background: #f5f7ff;
}
.dropdown-menu input[type="checkbox"] {
accent-color: #4E6ACA;
cursor: pointer;
margin-right: 0.75rem;
min-width: 16px;
transform: scale(1.2);
}
.custom-dropdown.open .dropdown-menu {
display: grid;
gap: 0.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.custom-dropdown.open .dropdown-menu {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.custom-dropdown.open .dropdown-menu {
grid-template-columns: repeat(2, minmax(150px, 1fr));
}
}
@media (min-width: 1280px) {
.custom-dropdown.open .dropdown-menu {
grid-template-columns: repeat(4, minmax(150px, 1fr));
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-text {
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
#sort-container {
width: fit-content;
}
#sortOrder {
border: 1px solid #ccc;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
cursor: pointer;
display:flex;
font-size: 0.95rem;
font-weight: 500;
max-width: fit-content;
padding: 2px 4px;
transition: all 0.25s ease-in-out;
width: 100%;
}
#resource-filter-summary {
display: flex;
gap: .5rem;
justify-content: space-between;
margin-bottom: 1.25rem;
width: 100%;
p {
margin: 0;
padding: 0;
@apply leading-5;
&:first-child {
@apply leading-none;
}
}
#applied-filters {
@apply italic;
font-size: 14px;
margin-top: 15px;
.filter-item {
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 20px;
display: inline-block;
margin: 5px;
padding: 5px 10px;
button.remove-filter {
background: none;
border: none;
color: #0073aa;
cursor: pointer;
font-size: 16px;
margin-left: 5px;
&:hover {
color: #d63638;
}
}
}
}
#sort-container {
@apply m-0 p-0 leading-5;
label {
@apply font-bold;
}
select {
@apply mt-1;
}
}
}
#resource-results {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
ul {
display: flex;
list-style: none;
gap: 8px;
padding: 0;
}
a,
span {
border: 1px solid #ddd;
border-radius: 4px;
color: #4E6ACA;
margin: 0 5px;
padding: 8px 12px;
text-decoration: none;
}
a:hover {
background: #4E6ACA;
color: #fff;
}
.current {
background: #4E6ACA;
border-color: #4E6ACA;
color: #fff;
padding: 8px 12px;
}
}
@media (min-width: 768px) {
/* Tailwind 'md' breakpoint */
#resource-results {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
/* Tailwind 'lg' breakpoint */
#resource-results {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}