Skip to content

Commit 1e89a7d

Browse files
committed
feat: add min-max filtering relational logic
1 parent 6a5802b commit 1e89a7d

6 files changed

Lines changed: 91 additions & 19 deletions

File tree

core/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,6 +1752,8 @@ def get_context_data(self, **kwargs):
17521752
"options": [
17531753
("alphabetical", "Alphabetical"),
17541754
("popular", "Most Popular"),
1755+
("updated", "Recently Updated"),
1756+
("release", "Release Date"),
17551757
],
17561758
"selected": "alphabetical",
17571759
},

libraries/views.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,20 +128,14 @@ def get_v3_context_data(self, queryset=None, **kwargs):
128128
context = {}
129129
view_str = self.kwargs.get("library_view_str")
130130

131-
cpp_values = sorted(
132-
{
133-
v
134-
for lv in (queryset or [])
135-
for v in (lv.cpp_standard_minimum, lv.cpp_standard_maximum)
136-
if v
137-
},
138-
key=lambda v: int(v) if v.isdigit() else 0,
131+
cpp_options = [("all", "All")] + list(
132+
LibraryVersion.CPP_STANDARD_DISPLAY_NAMES.items()
139133
)
140-
cpp_options = [("all", "All")] + [(v, f"C++{v}") for v in cpp_values]
141134

142135
tiers_present = sorted(
143136
{lv.library.tier for lv in (queryset or []) if lv.library.tier is not None}
144137
)
138+
145139
grading_options = [("all", "All")] + [
146140
(Tier(t).label.lower(), Tier(t).label) for t in tiers_present
147141
]

static/css/v3/forms.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,16 @@
393393
text-decoration: underline;
394394
}
395395

396+
.dropdown__item--disabled {
397+
opacity: 0.4;
398+
cursor: not-allowed;
399+
}
400+
401+
.dropdown__item--disabled:hover,
402+
.dropdown__item--disabled:focus {
403+
background-color: transparent;
404+
}
405+
396406
.field--error .dropdown__trigger {
397407
background-color: var(--color-surface-error-weak, #fdf2f2);
398408
border-color: var(--color-stroke-error, #d53f3f33);

templates/v3/includes/_field_dropdown.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
<div class="field{% if error %} field--error{% endif %}{% if extra_class %} {{ extra_class }}{% endif %}"
2121
{% if alpine_error %}:class="{ 'field--error': {{ alpine_error }} }"{% endif %}
2222
{% if not exclude_from_clear %}@clear-all-filters.window="clear()"{% endif %}
23+
@field-disabled-values.window="if ($event.detail.name === '{{ name }}') disabledValues = $event.detail.values"
24+
@field-set.window="if ($event.detail.name === '{{ name }}') { selected = $event.detail.value; selectedLabel = $event.detail.label }"
2325
{# djlint:off #}
2426
x-data="{
2527
jsReady: false,
2628
open: false,
2729
selected: '{{ selected|default:'' }}',
2830
selectedLabel: '',
2931
defaultValue: '{{ default|default:'' }}',
32+
disabledValues: [],
3033
init() {
3134
this.jsReady = true;
3235
if (this.selected) {
@@ -122,9 +125,10 @@
122125
<div class="dropdown__list" x-ref="list">
123126
{% for value, label in options %}
124127
<div class="dropdown__item"
125-
:class="{ 'dropdown__item--selected': selected === '{{ value }}' }"
128+
:class="{ 'dropdown__item--selected': selected === '{{ value }}', 'dropdown__item--disabled': disabledValues.includes('{{ value }}') }"
126129
role="option"
127130
:aria-selected="selected === '{{ value }}'"
131+
:aria-disabled="disabledValues.includes('{{ value }}')"
128132
data-value="{{ value }}"
129133
data-label="{{ label }}"
130134
@click="choose('{{ value }}', '{{ label }}')"

templates/v3/includes/_library_item.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
{% url 'libraries-list' category_slug=cat.slug library_view_str=variant version_slug=version_str as cat_url %}
3535
{% include "v3/includes/_category_tag.html" with tag_label=cat.label url=cat_url variant=cat.variant only %}
3636
{% endfor %}
37-
<span class="version-tag version-tag--default library-item__cpp-version">C++ {{ cpp_version|default:"03"|cut:"C++" }}</span>
37+
<span class="version-tag version-tag--default library-item__cpp-version">{% if cpp_version %}C++ {{ cpp_version|cut:"C++" }}{% else %}Unknown{% endif %}</span>
3838
</div>
3939
</div>
4040

@@ -43,7 +43,7 @@
4343
</div>
4444

4545
<div class="library-item__actions">
46-
<span class="version-tag version-tag--default library-item__cpp-version--desktop">C++ {{ cpp_version|default:"03"|cut:"C++" }}</span>
46+
<span class="version-tag version-tag--default library-item__cpp-version--desktop">{% if cpp_version %}C++ {{ cpp_version|cut:"C++" }}{% else %}Unknown{% endif %}</span>
4747
{% include "v3/includes/_button.html" with url=doc_url label="" icon_name="documentation" icon_size=32 icon_viewbox="0 0 16 16" style="icon-library" aria_label="View documentation for "|add:library_name %}
4848
</div>
4949

templates/v3/library_page.html

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,22 @@
3636
/* Defined inline so the factory is on window before Alpine evaluates x-data
3737
on this page — defer-script load ordering can otherwise race Alpine.start. */
3838
window.libraryFilter = function () {
39-
function cppNum(value) {
40-
const n = parseInt(value, 10);
41-
return Number.isFinite(n) ? n : 0;
39+
const CPP_STANDARDS = [
40+
{ key: '98', year: 1998, label: 'C++98' },
41+
{ key: '03', year: 2003, label: 'C++03' },
42+
{ key: '11', year: 2011, label: 'C++11' },
43+
{ key: '14', year: 2014, label: 'C++14' },
44+
{ key: '17', year: 2017, label: 'C++17' },
45+
{ key: '20', year: 2020, label: 'C++20' },
46+
{ key: '23', year: 2023, label: 'C++23' },
47+
];
48+
const CPP_OPTION_VALUES = CPP_STANDARDS.map((s) => s.key);
49+
const CPP_LABEL_MAP = Object.fromEntries(
50+
[['all', 'All'], ...CPP_STANDARDS.map((s) => [s.key, s.label])]
51+
);
52+
function cppRank(value) {
53+
const digits = String(value || '').match(/\d+/)?.[0];
54+
return CPP_STANDARDS.find((s) => s.key === digits)?.year ?? 0;
4255
}
4356
return {
4457
dataset: [],
@@ -98,8 +111,22 @@
98111
this.$el.addEventListener('field-change', (event) => {
99112
const { name, value } = event.detail;
100113
if (name === 'grading') this.grading = value || 'all';
101-
else if (name === 'min_cpp') this.minCpp = value || 'all';
102-
else if (name === 'max_cpp') this.maxCpp = value || 'all';
114+
else if (name === 'min_cpp') {
115+
this.minCpp = value || 'all';
116+
if (this.minCpp !== 'all' && this.maxCpp !== 'all'
117+
&& cppRank(this.minCpp) > cppRank(this.maxCpp)) {
118+
this.resetField('max_cpp', 'all');
119+
}
120+
this.updateCppConstraints();
121+
}
122+
else if (name === 'max_cpp') {
123+
this.maxCpp = value || 'all';
124+
if (this.maxCpp !== 'all' && this.minCpp !== 'all'
125+
&& cppRank(this.maxCpp) < cppRank(this.minCpp)) {
126+
this.resetField('min_cpp', 'all');
127+
}
128+
this.updateCppConstraints();
129+
}
103130
else if (name === 'q') this.query = value || '';
104131
else if (name === 'sort') this.sort = value || this.defaults.sort;
105132
else if (name === 'view') {
@@ -109,6 +136,21 @@
109136
this.apply();
110137
});
111138

139+
// Auto-correct invalid combinations loaded from URL params, then push
140+
// initial disabled-value constraints to both cpp dropdowns. Defer to
141+
// $nextTick so the dropdown components have mounted their listeners.
142+
if (this.minCpp !== 'all' && this.maxCpp !== 'all'
143+
&& cppRank(this.minCpp) > cppRank(this.maxCpp)) {
144+
this.maxCpp = 'all';
145+
}
146+
this.$nextTick(() => {
147+
if (this.maxCpp === 'all') {
148+
window.dispatchEvent(new CustomEvent('field-set',
149+
{ detail: { name: 'max_cpp', value: 'all', label: 'All' } }));
150+
}
151+
this.updateCppConstraints();
152+
});
153+
112154
this.observeCategorySelections();
113155
document.querySelectorAll('[data-slug]').forEach((el) => {
114156
const siblings = Array.from(el.parentElement.children).filter((c) => c.dataset.slug);
@@ -117,6 +159,26 @@
117159
this.apply();
118160
},
119161

162+
updateCppConstraints() {
163+
const minDisabled = (this.maxCpp && this.maxCpp !== 'all')
164+
? CPP_OPTION_VALUES.filter((v) => cppRank(v) > cppRank(this.maxCpp))
165+
: [];
166+
const maxDisabled = (this.minCpp && this.minCpp !== 'all')
167+
? CPP_OPTION_VALUES.filter((v) => cppRank(v) < cppRank(this.minCpp))
168+
: [];
169+
window.dispatchEvent(new CustomEvent('field-disabled-values',
170+
{ detail: { name: 'min_cpp', values: minDisabled } }));
171+
window.dispatchEvent(new CustomEvent('field-disabled-values',
172+
{ detail: { name: 'max_cpp', values: maxDisabled } }));
173+
},
174+
175+
resetField(name, value) {
176+
if (name === 'min_cpp') this.minCpp = value;
177+
else if (name === 'max_cpp') this.maxCpp = value;
178+
window.dispatchEvent(new CustomEvent('field-set',
179+
{ detail: { name, value, label: CPP_LABEL_MAP[value] || '' } }));
180+
},
181+
120182
handleViewChange(value) {
121183
if (!value) return;
122184
const url = new URL(window.location.href);
@@ -252,8 +314,8 @@
252314
matches(data) {
253315
if (this.searchHits && !this.searchHits.has(data.slug)) return false;
254316
if (this.grading !== 'all' && data.tier !== this.grading) return false;
255-
if (this.minCpp !== 'all' && data.cpp_min && cppNum(data.cpp_min) > cppNum(this.minCpp)) return false;
256-
if (this.maxCpp !== 'all' && data.cpp_max && cppNum(data.cpp_max) < cppNum(this.maxCpp)) return false;
317+
if (this.minCpp !== 'all' && data.cpp_min && cppRank(data.cpp_min) > cppRank(this.minCpp)) return false;
318+
if (this.maxCpp !== 'all' && data.cpp_max && cppRank(data.cpp_max) < cppRank(this.maxCpp)) return false;
257319
if (this.categories.length > 0) {
258320
const hit = data.category_slugs.some((s) => this.categories.includes(s));
259321
if (!hit) return false;

0 commit comments

Comments
 (0)