feat: add price filter and dynamic slider to catalog
All checks were successful
Build and Push Docker Image / build (push) Successful in 41s

- Implemented JavaScript-based price slider for catalog filtering
- Updated catalog views to handle min/max price inputs and compute slider bounds
- Enhanced templates (`index.html`, `side-bar.html`) with filtering form and updated URLs
- Added CSS for price slider styling and form layout
- Introduced utility functions for URL building and price parsing
- Expanded tests for price filtering, sliders, and sidebar active states
- Updated context processor and header for dynamic navigation with price filters
This commit is contained in:
2026-05-16 11:12:24 +02:00
parent 52cbc1c2b3
commit 3d356c7d6a
11 changed files with 579 additions and 11 deletions

View File

@@ -50,6 +50,116 @@ aside {
line-height: 1.4;
}
.catalog-filter-form {
display: flex;
flex-direction: column;
}
.catalog-price-slider {
display: grid;
gap: 14px;
}
.catalog-price-slider__values {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: #554971;
font-size: 16px;
line-height: 1.2;
}
.catalog-price-slider__controls {
position: relative;
height: 32px;
}
.catalog-price-slider__track {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 4px;
transform: translateY(-50%);
background: rgba(99, 118, 141, 0.18);
}
.catalog-price-slider__track-fill {
position: absolute;
top: 0;
bottom: 0;
background: #63768d;
}
.catalog-price-slider__range {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 32px;
margin: 0;
pointer-events: none;
appearance: none;
background: transparent;
}
.catalog-price-slider__range::-webkit-slider-thumb {
width: 18px;
height: 18px;
border: 2px solid #63768d;
border-radius: 50%;
background: #ffffff;
appearance: none;
pointer-events: auto;
cursor: pointer;
box-shadow: 0 0 0 3px rgba(99, 118, 141, 0.08);
}
.catalog-price-slider__range::-moz-range-thumb {
width: 18px;
height: 18px;
border: 2px solid #63768d;
border-radius: 50%;
background: #ffffff;
pointer-events: auto;
cursor: pointer;
box-shadow: 0 0 0 3px rgba(99, 118, 141, 0.08);
}
.catalog-price-slider__range::-webkit-slider-runnable-track {
height: 4px;
background: transparent;
}
.catalog-price-slider__range::-moz-range-track {
height: 4px;
background: transparent;
}
.catalog-price-slider__range:focus-visible {
outline: none;
}
.catalog-filter-submit {
min-height: 42px;
margin-top: 16px;
border: 1px solid #63768d;
background: #63768d;
color: #ffffff;
font-family: 'Noah', sans-serif;
font-size: 15px;
line-height: 1;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
}
.catalog-filter-submit:hover {
background: #554971;
border-color: #554971;
}
.item-list {
flex: 1 1 auto;
min-width: 0;

View File

@@ -0,0 +1,61 @@
document.addEventListener('DOMContentLoaded', function () {
const priceFormatter = new Intl.NumberFormat('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const formatQueryValue = function (value) {
return value.toFixed(2).replace(/\.?0+$/, '');
};
document.querySelectorAll('[data-price-slider]').forEach(function (slider) {
const minBound = Number.parseFloat(slider.dataset.minBound || '0');
const maxBound = Number.parseFloat(slider.dataset.maxBound || slider.dataset.minBound || '0');
const minRange = slider.querySelector('[data-price-range-min]');
const maxRange = slider.querySelector('[data-price-range-max]');
const minHidden = slider.querySelector('[data-price-hidden-min]');
const maxHidden = slider.querySelector('[data-price-hidden-max]');
const minLabel = slider.querySelector('[data-price-label-min]');
const maxLabel = slider.querySelector('[data-price-label-max]');
const trackFill = slider.querySelector('[data-price-track-fill]');
if (!minRange || !maxRange || !minHidden || !maxHidden || !minLabel || !maxLabel || !trackFill) {
return;
}
const updateSlider = function () {
const minValue = Number.parseFloat(minRange.value);
const maxValue = Number.parseFloat(maxRange.value);
const rangeSpan = maxBound - minBound;
const left = rangeSpan > 0 ? ((minValue - minBound) / rangeSpan) * 100 : 0;
const right = rangeSpan > 0 ? ((maxValue - minBound) / rangeSpan) * 100 : 100;
trackFill.style.left = left + '%';
trackFill.style.width = Math.max(right - left, 0) + '%';
minLabel.textContent = priceFormatter.format(minValue) + ' руб.';
maxLabel.textContent = priceFormatter.format(maxValue) + ' руб.';
minHidden.value = minValue <= minBound ? '' : formatQueryValue(minValue);
maxHidden.value = maxValue >= maxBound ? '' : formatQueryValue(maxValue);
};
minRange.addEventListener('input', function () {
if (Number.parseFloat(minRange.value) > Number.parseFloat(maxRange.value)) {
minRange.value = maxRange.value;
}
updateSlider();
});
maxRange.addEventListener('input', function () {
if (Number.parseFloat(maxRange.value) < Number.parseFloat(minRange.value)) {
maxRange.value = minRange.value;
}
updateSlider();
});
updateSlider();
});
});