source: Klonkt/src/views/pages/search.ejs@ 9eb6e1b

main
Last change on this file since 9eb6e1b was 9eb6e1b, checked in by Robin <roboburr@…>, 7 hours ago

Een fediverse-post ziet eruit als een post, ook in de zoekbalk

Plak je een link in de zoekbalk, dan laadt de uitklaplijst nu de post zelf
in plaats van te zoeken naar de tekst van de link. Dezelfde kaart staat op de
zoekpagina en op /authorize_interaction, dus één render op drie plekken.

DE KAART. Wat eromheen stond is teruggebracht tot een kop met gezicht, de
inhoud, en één rij knoppen: Reageer, Like, Boost en het origineel. Weg zijn de
zin "je reageert op", de aparte knop naar het origineel, de twee grote pillen,
"of reageer:" en het antwoordvenster dat altijd openstond. Antwoorden klapt
open als je erom vraagt; melden staat onderaan en niet in de weg.

Foto's staan in een raster met vaste rijhoogtes. Met verhoudingen groeide het
mee met de breedte, en werd op een desktop de eerste van drie foto's zo hoog
dat de knoppen onder de vouw van de uitklaplijst verdwenen.

EEN INHOUDSWAARSCHUWING WORDT GERESPECTEERD. De oude render toonde alles open,
ook wat de schrijver achter een waarschuwing had gezet.

MOBIEL. Elke knop minstens 44 punten. Op een smal scherm vallen de woorden weg
en blijven de tekens staan, gelijk verdeeld; de woorden blijven bestaan voor
wie het scherm voorgelezen krijgt. Nagemeten op 375 pixels: geen horizontale
overloop, kleinste tikdoel 44, de knoppen in beeld zonder te scrollen.

DE PREVIEW IS COMPACT. Waarderen en boosten kan ter plekke; antwoorden en
melden gaan naar de volle pagina, met ?reply=1 of ?report=1 zodat dat deel
daar al openstaat. Een editor in een uitklaplijst is op een telefoon geen plek
om te schrijven. Zonder JS doet dezelfde link hetzelfde.

/search/remote levert het fragment, achter dezelfde grens als de volle
pagina: alleen wie de site beheert laat ophalen. Die grens weegt hier zwaarder,
want deze route wordt bij elke toetsaanslag geraakt; zonder hem is hij een
haalservice die iedereen deze server op adressen naar keuze laat afsturen.
Voor een bezoeker 204, en dan valt de zoekbalk terug op de gewone suggesties.
Elke toetsaanslag krijgt een volgnummer, zodat een traag antwoord op een oude
vraag niet overschrijft wat je inmiddels hebt getypt.

Waarderen en boosten ter plekke verhuisde van authorize-interaction.js naar
chrome.js: de kaart staat nu op elke pagina, via de zoekbalk. De zoekpagina
stuurde je bij een like eerder weg naar /authorize_interaction.

Onderweg: /authorize_interaction haalt nu ook ondertekend op, net als de
zoekbalk. Anders zegt die pagina "niet gevonden" over een post voor volgers die
de preview wel liet zien.

Getoetst: de preview met de post, ondertekend; wat er in de compacte kaart wel
en niet staat; dat een bezoeker via de preview niets laat ophalen; dat een
zoekterm niets ophaalt; de waarschuwing; het venster op verzoek. Tegenproef:
rechtencheck eruit, waarschuwing genegeerd, compact genegeerd en anoniem
ophalen laten elk hun eigen toets omvallen. Volledige suite 1283 groen.

Het gedrag in chrome.js is in de browser nagelopen met een nagebootste
server: herkennen, laden, invoegen, de volgorde bij een traag antwoord, de
terugval bij 204, waarderen en boosten heen en terug, en het antwoordvenster
open en dicht met de focus op de goede plek.

shaer-utpi

Co-Authored-By: Claude Opus 5.5 <noreply@…>

  • Property mode set to 100644
File size: 9.1 KB
Line 
1<% const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; %>
2<% const _tracks = (typeof tracks !== 'undefined' && tracks) ? tracks : []; %>
3<% const _events = (typeof events !== 'undefined' && events) ? events : []; %>
4<% const _pages = (typeof pages !== 'undefined' && pages) ? pages : []; %>
5<div class="container search-page">
6 <h1><%= t('nav.search') %></h1>
7
8 <form method="get" action="<%= _base %>/search" class="search-page-form">
9 <input
10 type="search"
11 name="q"
12 value="<%= query %>"
13 placeholder="<%= t('search.placeholder') %>"
14 autocomplete="off"
15 autofocus
16 aria-label="<%= t('nav.search') %>">
17 <button type="submit" class="btn btn-primary"><%= t('search.button') %></button>
18 </form>
19
20 <% if (queryError) { %>
21 <p class="search-error"><%= t('search.error') %></p>
22 <% } %>
23
24 <% if (query && !queryError) { %>
25 <p class="search-meta">
26 <%= t(total === 1 ? 'search.results_one' : 'search.results_other', { n: total, q: query }) %>
27 </p>
28 <% } %>
29
30 <%# Een geplakt adres is geen zoekterm maar een aanwijzing (shaer-utpi): het
31 object van de andere server staat bovenaan, met dezelfde knoppen als op
32 /authorize_interaction -- dezelfde partial, dus één render en één plek
33 waar een poort of een sanitizer kan wonen. %>
34 <% if (typeof remoteUri !== 'undefined' && remoteUri) { %>
35 <h2 class="search-section-title"><%= t('search.section_fediverse') %></h2>
36 <div class="auth-interact search-remote">
37 <% if (!mayLookup) { %>
38 <%# Ophalen is een uitgaand verzoek namens deze server; dat doen we niet
39 op verzoek van een willekeurige bezoeker. %>
40 <p class="auth-interact-note"><%= t('search.fedi_signin') %></p>
41 <% } else if (remote && remoteKind === 'note') { %>
42 <%- include('../partials/remote-object', { target: remote, uri: remoteUri, reacted: remoteReacted, siteTitle: siteTitle, cancelUrl: _base + '/search?q=' + encodeURIComponent(query) }) %>
43 <% } else if (remote && remoteKind === 'actor') { %>
44 <%- include('../partials/remote-actor', { actor: remote, siteTitle: siteTitle }) %>
45 <% } else { %>
46 <p class="auth-interact-note"><%= t('fedi.remote_notfound') %></p>
47 <% } %>
48 </div>
49 <%- include('../partials/fedi-interact-styles') %>
50 <% } %>
51
52 <% if (_tracks.length) { %>
53 <h2 class="search-section-title"><%= t('search.section_tracks') %></h2>
54 <% var _inPost = t('search.in_post'); /* t() vóór de loop ophalen: function(t) overschaduwt 'm */ %>
55 <div class="search-tracks">
56 <% _tracks.forEach(function(t) {
57 const tj = JSON.stringify({ id: t.id, url: t.url, title: t.title, artist: t.artist || '', cover: t.cover || '' })
58 .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;'); %>
59 <div class="post-audio-track search-track" id="track-<%= t.id %>"
60 data-pcms-track-id="<%= t.id %>" data-pcms-track-url="<%= t.url %>"
61 data-pcms-track='<%- tj %>'>
62 <button type="button" class="pat-play" aria-label="Speel <%= t.title %>">
63 <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
64 </button>
65 <% if (t.cover) { %>
66 <span class="search-track-cover" style="background-image:url('<%= t.cover %>')" aria-hidden="true"></span>
67 <% } %>
68 <div class="pat-info">
69 <div class="pat-title"><%= t.title %></div>
70 <% if (t.artist || t.album) { %>
71 <div class="pat-artist">
72 <%= t.artist %><% if (t.artist && t.album) { %> &middot; <% } %><%= t.album %>
73 </div>
74 <% } %>
75 </div>
76 <% if (t.postUrl) { %>
77 <a class="search-track-link"
78 href="<%= t.postUrl %>"
79 hx-get="<%= t.postUrl %>?partial=1"
80 hx-target="#pcms-main"
81 hx-swap="innerHTML"
82 hx-push-url="<%= t.postUrl %>"
83 aria-label="<%= _inPost %>"><%= _inPost %></a>
84 <% } %>
85 </div>
86 <% }); %>
87 </div>
88 <% } %>
89
90 <% if (results && results.length) { %>
91 <% if (_tracks.length) { %><h2 class="search-section-title"><%= t('search.section_posts') %></h2><% } %>
92 <ol class="search-results">
93 <% results.forEach(function(r) { %>
94 <li class="search-result">
95 <h3 class="search-result-title">
96 <a href="<%= _base %>/<%= r.slug %>"
97 hx-get="<%= _base %>/<%= r.slug %>?partial=1"
98 hx-target="#pcms-main"
99 hx-swap="innerHTML"
100 hx-push-url="<%= _base %>/<%= r.slug %>"
101 hx-indicator="#pcms-loading">
102 <%= r.title || '(zonder titel)' %>
103 </a>
104 </h3>
105 <p class="search-result-meta">
106 <%= r.author_username %>
107 <% if (r.published_at) { %> &middot; <%= formatDate(r.published_at) %><% } %>
108 </p>
109 <p class="search-result-snippet"><%- r.snippet %></p>
110 </li>
111 <% }); %>
112 </ol>
113 <% } %>
114
115 <% if (_events.length) { %>
116 <h2 class="search-section-title"><%= t('search.section_events') %></h2>
117 <ol class="search-results">
118 <% _events.forEach(function(ev) { %>
119 <li class="search-result">
120 <h3 class="search-result-title"><a href="<%= ev.url %>"><%= ev.where || '—' %></a></h3>
121 <p class="search-result-meta"><%= [ev.date, ev.time].filter(Boolean).join(' ') %></p>
122 </li>
123 <% }); %>
124 </ol>
125 <% } %>
126
127 <% if (_pages.length) { %>
128 <h2 class="search-section-title"><%= t('search.section_pages') %></h2>
129 <ul class="search-pages">
130 <% _pages.forEach(function(pg) { %>
131 <li><a class="search-page-link" href="<%= pg.url %>"><%= pg.label %> <span aria-hidden="true">→</span></a></li>
132 <% }); %>
133 </ul>
134 <% } %>
135
136 <% if (query && !queryError && !results.length && !_tracks.length && !_events.length && !_pages.length) { %>
137 <p class="search-empty"><%= t('search.empty') %></p>
138 <% } %>
139</div>
140
141<style>
142.search-page { max-width: 720px; margin: 3rem auto; padding: 0 1rem; }
143.search-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 1.5rem; }
144
145.search-page-form {
146 display: flex;
147 gap: 0.5rem;
148 margin-bottom: 1.5rem;
149}
150.search-page-form input {
151 flex: 1;
152 padding: 0.6rem 0.9rem;
153 border: 1px solid var(--rule);
154 border-radius: 5px;
155 background: var(--paper-2);
156 color: var(--ink);
157 font-size: 1rem;
158}
159.search-page-form input:focus {
160 outline: none;
161 border-color: var(--accent);
162}
163
164.search-meta {
165 color: var(--ink-muted, var(--ink-soft));
166 font-size: 0.9rem;
167 margin: 0 0 1rem;
168}
169.search-section-title {
170 font-family: var(--font-display, serif);
171 font-size: 1.1rem;
172 margin: 1.75rem 0 0.75rem;
173 color: var(--ink-soft);
174 text-transform: uppercase;
175 letter-spacing: 0.05em;
176}
177.search-error {
178 background: rgba(200, 60, 60, 0.15);
179 color: #c33;
180 padding: 0.75rem 1rem;
181 border-radius: 6px;
182 border: 1px solid rgba(200, 60, 60, 0.3);
183}
184.search-empty {
185 color: var(--ink-muted, var(--ink-soft));
186 text-align: center;
187 margin: 3rem 0;
188}
189
190/* Nummer-resultaten */
191.search-tracks { display: flex; flex-direction: column; gap: 0.25rem; }
192.search-track.post-audio-track {
193 display: flex;
194 align-items: center;
195 gap: 0.75rem;
196 padding: 0.5rem 0.6rem;
197 border-radius: 8px;
198}
199.search-track .search-track-cover {
200 width: 38px; height: 38px;
201 flex: 0 0 38px;
202 border-radius: 5px;
203 background-size: cover;
204 background-position: center;
205 background-color: var(--paper-2);
206}
207.search-track .pat-info { flex: 1; min-width: 0; }
208.search-track .pat-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
209.search-track .pat-artist { font-size: 0.82rem; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
210.search-track-link {
211 flex: 0 0 auto;
212 font-size: 0.82rem;
213 color: var(--accent);
214 text-decoration: none;
215 white-space: nowrap;
216}
217.search-track-link:hover { text-decoration: underline; }
218
219.search-results {
220 list-style: none;
221 margin: 0;
222 padding: 0;
223}
224.search-result {
225 padding: 1rem 0;
226 border-bottom: 1px solid var(--rule);
227}
228.search-result:last-child { border-bottom: 0; }
229
230.search-result-title {
231 font-family: var(--font-display, serif);
232 font-size: 1.25rem;
233 margin: 0 0 0.25rem;
234}
235.search-result-title a {
236 color: var(--ink);
237 text-decoration: none;
238}
239.search-result-title a:hover { color: var(--accent); }
240
241.search-result-meta {
242 margin: 0 0 0.5rem;
243 color: var(--ink-muted, var(--ink-soft));
244 font-size: 0.85rem;
245}
246
247.search-result-snippet {
248 margin: 0;
249 color: var(--ink-soft);
250 line-height: 1.5;
251}
252.search-result-snippet mark {
253 background: var(--accent);
254 color: white;
255 padding: 0.05em 0.25em;
256 border-radius: 3px;
257}
258.search-pages { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
259.search-page-link {
260 display: inline-flex; align-items: center; gap: 0.3rem;
261 padding: 0.45rem 0.8rem; border: 1px solid var(--rule); border-radius: 999px;
262 color: var(--ink); text-decoration: none; font-size: 0.9rem; background: var(--paper-2);
263}
264.search-page-link:hover { border-color: var(--accent); color: var(--accent); }
265</style>
Note: See TracBrowser for help on using the repository browser.