source: Klonkt/src/views/partials/search-results.ejs@ ca260d1

main
Last change on this file since ca260d1 was fd8faf5, checked in by Robin <roboburr@…>, 11 hours ago

Zoeken is één vlak: de balk bovenaan, de resultaten eronder, en @naam@server

Robins ontwerp van 30-9: de aparte resultatenpagina is niet meer nodig als het
vak onder de zoekbalk blijft staan. De balk en het vak eronder zijn samen het
hele zoeken, en het vak krijgt alle ruimte die er onder de balk is.

EEN VORM, TWEE INGANGEN. partials/search-surface.ejs is de balk plus het vak;
de overlay op elke pagina gebruikt hem, en /search?q=... ook. De resultaten
zelf staan in partials/search-results.ejs, en het live vak haalt precies die
render op (/search/results). De pagina bevat dus teken voor teken wat het vak
toont; een toets bewaakt dat, want twee renders lopen gegarandeerd uit de pas.
/search blijft bestaan zodat een gedeelde link en Enter zonder JavaScript op
hetzelfde beeld uitkomen.

/search/suggest (JSON, dat de browser tot een eigen lijstje bouwde, anders dan
de pagina) en /search/remote (alleen het adres) zijn weg; niets anders
gebruikte ze. remote-preview.ejs ging mee.

HET VLAK. Vaste positie over het hele scherm, de balk bovenaan, het vak vult
de rest en scrolt zelf. Op z-index 1060: de vorige overlay lag op 200, onder de
tabbalk (1050), zodat op een telefoon het onderste stuk resultaten wegviel. Enter
blijft staan en zoekt meteen, want het vak ís de resultaten. Op de pagina beweegt
het adres mee (/search?q=...), zodat wat je ziet te delen blijft; sluiten is daar
teruggaan. Balk en resultaten staan op één lijn: een scrollbalk alleen rechts
schoof de kolom anders zo'n 9 pixels naar links.

WEBFINGER. @naam@server (met of zonder @ vooraan, zoals Mastodon) haalt het
profiel op, met de volgknop. Zonder eerst een post te proberen: een handle is
nooit een post. De server moet een echte domeinnaam zijn; "@robin" blijft een
naam om naar te zoeken. WebFinger zelf is openbaar en loopt via safeFetch;
het profiel erachter gaat ondertekend, en alleen namens een ingelogde
beheerder, dezelfde grens als een geplakte link.

Onderweg gevonden: search-results.ejs verwees kaal naar queryError, en een
aanroeper die hem vergat liet de hele render omvallen. Alles gaat nu via
locals. In de app gaf gather hem altijd mee, dus live ging het goed; het was
één vergeten veld van een kapotte zoekpagina.

Nagelopen in de browser op 375 en 1280 pixels: de balk bovenaan, het vak tot
de onderrand, tabbalk bedekt, geen horizontale overloop, tikdoelen 44, balk en
resultaten op één lijn. Het gedrag met de echte chrome.js: openen vers en met
focus, typen, Enter zonder weg te gaan, de wachtregel bij een link en een
handle, een traag antwoord dat niets overschrijft, een resultaat aantikken en
Escape sluiten, en het adres dat op de pagina meebeweegt.

Getoetst: pagina en vak teken voor teken gelijk, de handle-herkenning, een
handle ondertekend en zonder post-poging, en niet ingelogd geen enkel verzoek.
Tegenproef: pagina laten afwijken, handle niet herkennen, bij een handle toch
een post proberen en de inlogcontrole eruit laten elk hun toetsen omvallen.
Volledige suite 1292 groen.

shaer-utpi

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

  • Property mode set to 100644
File size: 6.2 KB
Line 
1<%#
2 Wat er onder de zoekbalk staat. EEN render voor beide ingangen: het live
3 resultatenvak (via /search/results) en de pagina /search?q=... zelf. Zo zien
4 ze er per constructie hetzelfde uit; twee renders lopen gegarandeerd uit de
5 pas.
6
7 Verwacht: query, results (posts), tracks, events, pages, queryError,
8 remoteUri, remote, remoteKind, mayLookup, remoteReacted, siteTitle,
9 siteUrlBase, formatDate, t.
10
11 GEEN SCRIPT hierin: het live vak zet dit via innerHTML neer, en daar draait
12 niets. Gedrag (afspelen, waarderen, htmx-links) luistert op document.
13%>
14<%
15 const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
16 const _results = (typeof results !== 'undefined' && results) ? results : [];
17 const _tracks = (typeof tracks !== 'undefined' && tracks) ? tracks : [];
18 const _events = (typeof events !== 'undefined' && events) ? events : [];
19 const _pages = (typeof pages !== 'undefined' && pages) ? pages : [];
20 const _remote = (typeof remoteUri !== 'undefined' && remoteUri) ? remoteUri : '';
21 // Alles via locals: een veld dat een aanroeper vergeet mag de render niet
22 // laten omvallen (een kale verwijzing naar iets ongedefinieerds gooit).
23 const _queryError = !!locals.queryError;
24 const _mayLookup = !!locals.mayLookup;
25 const _found = locals.remote || null;
26 const _kind = locals.remoteKind || null;
27 const _reacted = locals.remoteReacted || { liked: false, boosted: false };
28 const _siteTitle = locals.siteTitle || '';
29 const _inPost = t('search.in_post');
30 const _any = _results.length || _tracks.length || _events.length || _pages.length;
31%>
32<% if (locals.query) { %>
33<div class="sr">
34 <% if (_remote) { %>
35 <%# Een geplakt adres is geen zoekterm maar een aanwijzing (shaer-utpi).
36 Ophalen doen we alleen namens een ingelogde beheerder van de site. %>
37 <section class="sr-section sr-fediverse">
38 <h2 class="sr-title"><%= t('search.section_fediverse') %></h2>
39 <% if (!_mayLookup) { %>
40 <p class="sr-note"><%= t('search.fedi_signin') %></p>
41 <% } else if (_found && _kind === 'note') { %>
42 <%- include('./remote-object', { target: _found, uri: _remote, reacted: _reacted, siteTitle: _siteTitle, compact: true }) %>
43 <% } else if (_found && _kind === 'actor') { %>
44 <%- include('./remote-actor', { actor: _found, siteTitle: _siteTitle, compact: true }) %>
45 <% } else { %>
46 <p class="sr-note"><%= t('fedi.remote_notfound') %></p>
47 <% } %>
48 <% if (_found) { %><%- include('./fedi-interact-styles') %><% } %>
49 </section>
50 <% } %>
51
52 <% if (_queryError) { %>
53 <p class="sr-note"><%= t('search.error') %></p>
54 <% } %>
55
56 <% if (_tracks.length) { %>
57 <section class="sr-section">
58 <h2 class="sr-title"><%= t('search.section_tracks') %></h2>
59 <div class="sr-tracks">
60 <% _tracks.forEach(function (tr) {
61 const tj = JSON.stringify({ id: tr.id, url: tr.url, title: tr.title, artist: tr.artist || '', cover: tr.cover || '' })
62 .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;'); %>
63 <div class="post-audio-track sr-track" id="track-<%= tr.id %>"
64 data-pcms-track-id="<%= tr.id %>" data-pcms-track-url="<%= tr.url %>"
65 data-pcms-track='<%- tj %>'>
66 <button type="button" class="pat-play" aria-label="<%= tr.title %>">
67 <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
68 </button>
69 <% if (tr.cover) { %>
70 <span class="sr-track-cover" style="background-image:url('<%= tr.cover %>')" aria-hidden="true"></span>
71 <% } %>
72 <div class="pat-info">
73 <div class="pat-title"><%= tr.title %></div>
74 <% if (tr.artist || tr.album) { %>
75 <div class="pat-artist"><%= tr.artist %><% if (tr.artist && tr.album) { %> &middot; <% } %><%= tr.album %></div>
76 <% } %>
77 </div>
78 <% if (tr.postUrl) { %>
79 <a class="sr-track-link" href="<%= tr.postUrl %>"
80 hx-get="<%= tr.postUrl %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="<%= tr.postUrl %>"><%= _inPost %></a>
81 <% } %>
82 </div>
83 <% }); %>
84 </div>
85 </section>
86 <% } %>
87
88 <% if (_results.length) { %>
89 <section class="sr-section">
90 <h2 class="sr-title"><%= t('search.section_posts') %></h2>
91 <ol class="sr-list">
92 <% _results.forEach(function (r) { %>
93 <li>
94 <a class="sr-item" href="<%= _base %>/<%= r.slug %>"
95 hx-get="<%= _base %>/<%= r.slug %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
96 hx-push-url="<%= _base %>/<%= r.slug %>" hx-indicator="#pcms-loading">
97 <span class="sr-item-title"><%= r.title || '(zonder titel)' %></span>
98 <span class="sr-item-meta"><%= r.author_username %><% if (r.published_at) { %> &middot; <%= formatDate(r.published_at) %><% } %></span>
99 <% if (r.snippet) { %><span class="sr-item-snippet"><%- r.snippet %></span><% } %>
100 </a>
101 </li>
102 <% }); %>
103 </ol>
104 </section>
105 <% } %>
106
107 <% if (_events.length) { %>
108 <section class="sr-section">
109 <h2 class="sr-title"><%= t('search.section_events') %></h2>
110 <ol class="sr-list">
111 <% _events.forEach(function (ev) { %>
112 <li>
113 <a class="sr-item" href="<%= ev.url %>">
114 <span class="sr-item-title"><%= ev.where || ev.date %></span>
115 <span class="sr-item-meta"><%= [ev.date, ev.time].filter(Boolean).join(' ') %></span>
116 </a>
117 </li>
118 <% }); %>
119 </ol>
120 </section>
121 <% } %>
122
123 <% if (_pages.length) { %>
124 <section class="sr-section">
125 <h2 class="sr-title"><%= t('search.section_pages') %></h2>
126 <ul class="sr-list">
127 <% _pages.forEach(function (pg) { %>
128 <li><a class="sr-item sr-item-row" href="<%= pg.url %>"><span class="sr-item-title"><%= pg.label %></span><span aria-hidden="true">→</span></a></li>
129 <% }); %>
130 </ul>
131 </section>
132 <% } %>
133
134 <% if (!_queryError && !_any && !_remote) { %>
135 <p class="sr-empty"><%= t('search.empty') %></p>
136 <% } %>
137</div>
138<% } %>
Note: See TracBrowser for help on using the repository browser.