source: Klonkt/src/views/pages/search.ejs@ 3ceaef6

main
Last change on this file since 3ceaef6 was 3ceaef6, checked in by Robin <roboburr@…>, 6 hours ago

Zoeken op een adres: het object erachter, met de knoppen erbij

Plak je een link naar een fediverse-post of -profiel in de zoekbalk, dan is dat
geen zoekterm maar een aanwijzing. Klonkt haalt hem op en zet hem bovenaan de
resultaten, met dezelfde knoppen als /authorize_interaction: antwoorden,
waarderen, boosten, stemmen, volgen en melden.

De weg erheen was vooral opruimen. De render en de formulieren stonden als 200
regels in authorize-interaction.ejs, en er een tweede kopie van maken voor de
zoekpagina zou een tweede plek zijn waar een poort, een sanitizer of een knop
vergeten kan worden. Ze staan nu in partials/remote-object.ejs en
partials/remote-actor.ejs, met de opmaak in een derde. De pagina die ze had
gebruikt ze nu ook, dus er is één render en de formulieren posten in beide
gevallen naar /authorize_interaction/..., waar het werk en de rechtencontrole
al zaten.

DRIE DINGEN DIE VASTLIGGEN, en elk om een eigen reden:

Het schema is verplicht. "soundfabrics.nl" is een geldige zoekterm -- iemand
zoekt naar de naam -- en een kale domeinnaam als adres opvatten maakt van elke
zoekopdracht met een punt erin een netwerkverzoek. Met ​https:// ervoor is er
geen twijfel over de bedoeling.

Ophalen mag alleen een beheerder van de site laten doen. Dat is dezelfde grens
als /authorize_interaction, en met opzet: ophalen is een uitgaand verzoek dat
deze server namens de bezoeker doet. Zonder die grens is de zoekbalk een
haalservice waarmee een vreemde deze server laat aankloppen op adressen naar
keuze. Een bezoeker die toch een link plakt krijgt de gewone zoekresultaten en
een zin die zegt waarom er verder niets staat.

De ophaling gaat ONDERTEKEND als de site. Een post die alleen voor volgers
zichtbaar is weigert een anonieme GET, en dan lijkt een bestaande post te
ontbreken -- dezelfde val als bij het antwoorden op een post van een vriend.

Bijgekomen zonder dat erom gevraagd is, en het is het vermelden waard: het
profiel toont nu zijn avatar. Die hadden we al in handen (actor_icon uit
resolveRemoteActor) en hij stond op geen van beide schermen.

Getoetst: de herkenning zelf, dat een zoekterm GEEN verzoek wordt, dat de post
met zijn vier formulieren binnenkomt, dat een profiel de volgknop geeft, dat
een bezoeker niets laat ophalen, en dat /authorize_interaction na de verhuizing
nog steeds hetzelfde toont -- die pagina had zelf geen enkele toets, dus dat
was de bodem die er onder moest. Tegenproef per zaak.

Nog open: een @naam@server in de zoekbalk (webfinger kan het al, dus klein
werk) en een hint in de live-suggesties terwijl je plakt.

shaer-utpi

  • 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' }) %>
43 <% } else if (remote && remoteKind === 'actor') { %>
44 <%- include('../partials/remote-actor', { actor: remote, cancelUrl: _base + '/search', 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.