Skip to main content

max / makenotwork

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