Skip to main content

max / makenotwork

12.1 KB · 228 lines History Blame Raw
1 {# The sidebar rides along on every results swap, out-of-band, so its counts
2 and drill-down always describe the results below. Without this the sidebar
3 is stale from the first filter click until a full page load, which is fatal
4 for faceted browsing: the numbers are what the user steers by.
5
6 htmx restores focus after a swap to an element with the same id, so every
7 control in the sidebar carries a stable one and keyboard position survives.
8 The typeahead input additionally keeps its value across the swap, since that
9 value is the reader's and the server does not render it. It is described
10 rather than written here: `Field::keeping_value` says the fact and the
11 webview renderer emits the attribute (quasicoherent `a135f898`).
12
13 Guarded on `oob_sidebar`: the full page includes this same partial inside
14 #results-container, and rendering the sidebar there too would emit it twice
15 with duplicate ids. Only the standalone /discover/results response sets it. #}
16 {# #total-count sits in the page form, outside #results-container, so a plain
17 swap leaves it reading the previous filter's total. It said "13 items" beside
18 a list showing 3. #}
19 {# Search results say how well they matched, in two places.
20
21 .results-tier-break is the one heading, and it sits exactly where the claim
22 it makes becomes true: above it, every row holds at least one word that was
23 typed; below it, every row holds none and is here on character overlap alone.
24 The query orders the tiers and flags the first row of the lower one. A page
25 made entirely of near matches carries the heading at the top, which is the
26 honest rendering under pagination. No heading when not searching, and none
27 for 1-2 character searches, which are substring-only and have no boundary.
28
29 .row-match-note is the per-row half: "Matched 3 of 5 words" on a row holding
30 some but not all of them. It cannot be a heading, because coverage varies row
31 to row and one page can hold a 4-of-5 beside a 1-of-5. Rows with every word
32 say nothing, since that is the unremarkable case; rows with none say nothing
33 either, because the heading above them already did.
34
35 Grid view needs `grid-column: 1 / -1` on .results-tier-break to span the row.
36 Discover has no CSS yet, so these are class hooks like the rest of the sidebar. #}
37 {% if oob_sidebar %}
38 <span class="table-meta" id="total-count" hx-swap-oob="true">{{ count_label }}</span>
39 {% endif %}
40 {% if oob_sidebar && mode == "items" %}
41 <div hx-swap-oob="outerHTML:#discover-sidebar">
42 {% include "partials/discover_sidebar.html" %}
43 </div>
44 {% endif %}
45
46 {# The region a filter change replaces, whole. Its id is here rather than on a
47 wrapper in the page, so the answer to /discover/results IS the region and an
48 outer swap keeps the element every control targets. The out-of-band elements
49 above stay outside it: htmx only honours hx-swap-oob at the top level of a
50 response. #}
51 <div id="results-container">
52 <div class="results-container results-list" id="results-container-inner">
53 <!-- List View -->
54 <div class="results-table well" id="results-table">
55 {% if mode == "projects" %}
56 {% for project in projects %}
57 {% if project.starts_fuzzy_block %}
58 <div class="results-tier-break">
59 <span class="results-tier-label">Near matches</span>
60 <span class="results-tier-hint">None of your words are in these. The spelling is close.</span>
61 </div>
62 {% endif %}
63 <a href="/p/{{ project.slug }}" class="table-row project-row">
64 <span class="row-info"><span class="row-name">{{ project.title }}</span><span class="row-creator">{{ project.creator }}</span>{% if project.match_label.is_some() %}<span class="row-match-note">{{ project.match_label.as_deref().unwrap_or("") }}</span>{% endif %}</span>
65 <span class="row-category">{{ project.category_name.as_deref().unwrap_or(&project.project_type) }}</span>
66 <span class="row-items">{{ project.item_count }}</span>
67 <span class="row-date">{{ project.date }}</span>
68 </a>
69 {% endfor %}
70 {% if projects.is_empty() %}
71 <div class="empty-state">
72 {% if is_search %}
73 <p>Nothing matched your search.</p>
74 <p class="empty-state-hint">Try fewer words, or check the spelling. You can also <a href="/discover">clear all filters</a>.</p>
75 {% else %}
76 <p>No projects found matching your filters.</p>
77 <p class="empty-state-hint">Try <a href="/discover">clearing all filters</a>.</p>
78 {% endif %}
79 </div>
80 {% endif %}
81 {% else %}
82 {% for item in items %}
83 {% if item.starts_fuzzy_block %}
84 <div class="results-tier-break">
85 <span class="results-tier-label">Near matches</span>
86 <span class="results-tier-hint">None of your words are in these. The spelling is close.</span>
87 </div>
88 {% endif %}
89 <div class="table-row table-row-item">
90 <a href="{% if item.is_free %}/i/{{ item.id }}{% else %}/purchase/{{ item.id }}{% endif %}" class="table-row-link">
91 <span class="row-type">{{ item.item_type }}</span>
92 <span class="row-info"><span class="row-name">{{ item.name }}</span><span class="row-creator">{{ item.project }} &middot; {{ item.creator }}</span>{% if item.match_label.is_some() %}<span class="row-match-note">{{ item.match_label.as_deref().unwrap_or("") }}</span>{% endif %}</span>
93 <span class="row-category">{{ item.primary_tag }}</span>
94 <span class="row-price">{{ item.price }}</span>
95 <span class="row-date">{{ item.date }}</span>
96 <span class="discover-row-ai-tier">{{ item.ai_tier }}</span>
97 </a>
98 {% if is_authenticated %}
99 <button class="row-save collection-picker-anchor" data-collection-trigger data-item-id="{{ item.id }}"
100 data-action="onOpenCollectionPicker" data-stop title="Save to collection" aria-label="Save to collection">+</button>
101 {% endif %}
102 </div>
103 {% endfor %}
104 {% if items.is_empty() %}
105 <div class="empty-state">
106 {% if is_search %}
107 <p>Nothing matched your search.</p>
108 <p class="empty-state-hint">Try fewer words, or check the spelling. You can also <a href="/discover">clear all filters</a>.</p>
109 {% else %}
110 <p>No items found matching your filters.</p>
111 <p class="empty-state-hint">Try <a href="/discover">clearing all filters</a>.</p>
112 {% endif %}
113 </div>
114 {% endif %}
115 {% endif %}
116 </div>
117
118 <!-- Grid View -->
119 <div class="results-grid" id="results-grid">
120 {% if mode == "projects" %}
121 {% for project in projects %}
122 {% if project.starts_fuzzy_block %}
123 <div class="results-tier-break">
124 <span class="results-tier-label">Near matches</span>
125 <span class="results-tier-hint">None of your words are in these. The spelling is close.</span>
126 </div>
127 {% endif %}
128 <a href="/p/{{ project.slug }}" class="grid-card">
129 <div class="grid-card-content">
130 <div class="grid-card-title">{{ project.title }}</div>
131 <div class="grid-card-meta">{{ project.creator }}</div>
132 {% if project.match_label.is_some() %}<span class="row-match-note">{{ project.match_label.as_deref().unwrap_or("") }}</span>{% endif %}
133 <div class="grid-card-footer">
134 <span class="grid-card-category">{{ project.category_name.as_deref().unwrap_or(&project.project_type) }}</span>
135 <span class="grid-card-stat">{{ project.item_count }} items</span>
136 </div>
137 </div>
138 </a>
139 {% endfor %}
140 {% if projects.is_empty() %}
141 <div class="empty-state">
142 {% if is_search %}
143 <p>Nothing matched your search.</p>
144 <p class="empty-state-hint">Try fewer words, or check the spelling. You can also <a href="/discover">clear all filters</a>.</p>
145 {% else %}
146 <p>No projects found matching your filters.</p>
147 <p class="empty-state-hint">Try <a href="/discover">clearing all filters</a>.</p>
148 {% endif %}
149 </div>
150 {% endif %}
151 {% else %}
152 {% for item in items %}
153 {% if item.starts_fuzzy_block %}
154 <div class="results-tier-break">
155 <span class="results-tier-label">Near matches</span>
156 <span class="results-tier-hint">None of your words are in these. The spelling is close.</span>
157 </div>
158 {% endif %}
159 <div class="grid-card discover-grid-card">
160 {% if is_authenticated %}
161 <button class="grid-card-save collection-picker-anchor" data-collection-trigger data-item-id="{{ item.id }}"
162 data-action="onOpenCollectionPicker" data-stop title="Save to collection" aria-label="Save to collection">+</button>
163 {% endif %}
164 <a href="{% if item.is_free %}/i/{{ item.id }}{% else %}/purchase/{{ item.id }}{% endif %}" class="discover-card-link">
165 <div class="grid-card-thumbnail">{{ item.item_type }}</div>
166 <div class="grid-card-content">
167 <div class="grid-card-title">{{ item.name }}</div>
168 <div class="grid-card-meta">{{ item.project }} &middot; {{ item.creator }}</div>
169 {% if item.match_label.is_some() %}<span class="row-match-note">{{ item.match_label.as_deref().unwrap_or("") }}</span>{% endif %}
170 <div class="grid-card-footer">
171 <span class="grid-card-category">{{ item.primary_tag }}</span>
172 <span class="grid-card-price">{{ item.price }}</span>
173 <span class="discover-row-ai-tier">{{ item.ai_tier }}</span>
174 </div>
175 </div>
176 </a>
177 </div>
178 {% endfor %}
179 {% if items.is_empty() %}
180 <div class="empty-state">
181 {% if is_search %}
182 <p>Nothing matched your search.</p>
183 <p class="empty-state-hint">Try fewer words, or check the spelling. You can also <a href="/discover">clear all filters</a>.</p>
184 {% else %}
185 <p>No items found matching your filters.</p>
186 <p class="empty-state-hint">Try <a href="/discover">clearing all filters</a>.</p>
187 {% endif %}
188 </div>
189 {% endif %}
190 {% endif %}
191 </div>
192 </div>
193
194 <div class="table-footer">
195 <span class="page-info">Showing {{ showing_start }}-{{ showing_end }} of {{ total_items }}</span>
196 <div class="pagination">
197 {% if total_pages > 0 %}
198 <button {% if current_page <= 1 %}disabled{% endif %}
199 hx-get="/discover/results"
200 hx-target="#results-container" hx-swap="outerHTML"
201 hx-include=".discover-filter, #q"
202 hx-vals='{"page": {{ current_page - 1 }}}'
203 {% if current_page <= 1 %}hx-disable="this"{% endif %}>Prev</button>
204 {% for p in pagination_range %}
205 <button {% if *p == current_page %}class="is-selected"{% endif %}
206 hx-get="/discover/results"
207 hx-target="#results-container" hx-swap="outerHTML"
208 hx-include=".discover-filter, #q"
209 hx-vals='{"page": {{ p }}}'>{{ p }}</button>
210 {% endfor %}
211 {% if total_pages > 5 %}
212 <button disabled>...</button>
213 <button hx-get="/discover/results"
214 hx-target="#results-container" hx-swap="outerHTML"
215 hx-include=".discover-filter, #q"
216 hx-vals='{"page": {{ total_pages }}}'>{{ total_pages }}</button>
217 {% endif %}
218 <button {% if current_page >= total_pages || total_pages == 0 %}disabled{% endif %}
219 hx-get="/discover/results"
220 hx-target="#results-container" hx-swap="outerHTML"
221 hx-include=".discover-filter, #q"
222 hx-vals='{"page": {{ current_page + 1 }}}'
223 {% if current_page >= total_pages || total_pages == 0 %}hx-disable="this"{% endif %}>Next</button>
224 {% endif %}
225 </div>
226 </div>
227 </div>
228