source: Klonkt/src/views/pages/post.ejs@ 8cdb377

main
Last change on this file since 8cdb377 was 1e2e9e7, checked in by roboburr <roboburr@…>, 3 months ago

ui(fan-gate): show Newer/Older post navigation on the fan gate too

The fan gate (login wall of a fan_only post, e.g. /fans) was a dead end:
no navigation. Nav is now in a shared partial partials/post-nav.ejs, used
by both post.ejs and fan-gate.ejs. posts.js calculates the neighbours via
a shared postNeighbors() helper, also in the fan-gate branch.

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

  • Property mode set to 100644
File size: 21.2 KB
RevLine 
[b94c08e]1<%
2// In hub-mode is de site bereikbaar onder /user/<slug>; alle site-scoped links
3// (tag, edit, delete, comments) moeten met die prefix anders vallen ze terug op
4// de primaire site → 404 / verkeerde site. In solo is _base = ''.
5const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
6%>
[7bc636b]7<article class="container post-page">
[74544fb]8
[1e2e9e7]9 <%# Navigatie over ALLE posts — BOVEN de post (gedeeld partial). %>
10 <%- include('../partials/post-nav', { newerPost: newerPost, olderPost: olderPost }) %>
[74544fb]11
[7bc636b]12 <% if (post.cover_image_url) { %>
13 <figure class="post-cover">
14 <img src="<%= post.cover_image_url %>" alt="">
15 </figure>
16 <% } %>
17
18 <header class="post-header">
19 <h1 class="post-title"><%= post.title %></h1>
20 <div class="post-meta">
21 <a class="author" href="/users/<%= encodeURIComponent(post.author_username) %>"
22 hx-get="/users/<%= encodeURIComponent(post.author_username) %>?partial=1"
23 hx-target="#pcms-main" hx-swap="innerHTML"
24 hx-push-url="/users/<%= encodeURIComponent(post.author_username) %>"
25 hx-indicator="#pcms-loading"><%= post.author_username %></a>
26 <span class="meta-sep">·</span>
27 <time datetime="<%= post.published_at || post.created_at %>"><%= formatDate(post.published_at || post.created_at) %></time>
28 <% if (post.status !== 'published') { %>
29 <span class="meta-sep">·</span>
30 <span class="status-badge status-<%= post.status %>"><%= post.status %></span>
31 <% } %>
32 </div>
33 </header>
34
35 <div class="post-content">
36 <%- post.content_html %>
37 </div>
38
39 <% if (post.tags && post.tags.length > 0) { %>
40 <div class="post-tags">
41 <% post.tags.forEach(t => { %>
[b94c08e]42 <a class="tag" href="<%= _base %>/tag/<%= encodeURIComponent(t) %>"
43 hx-get="<%= _base %>/tag/<%= encodeURIComponent(t) %>?partial=1"
[7bc636b]44 hx-target="#pcms-main" hx-swap="innerHTML"
[b94c08e]45 hx-push-url="<%= _base %>/tag/<%= encodeURIComponent(t) %>"
[7bc636b]46 hx-indicator="#pcms-loading">#<%= t %></a>
47 <% }); %>
48 </div>
49 <% } %>
50
51 <!-- Inline admin actions: only visible if user has permission -->
52 <% if (user && (permissions.canEditPost(user, post, site) || permissions.canDeletePost(user, post, site))) { %>
53 <aside class="post-actions">
54 <% if (permissions.canEditPost(user, post, site)) { %>
[b94c08e]55 <a href="<%= _base %>/posts/<%= post.slug %>/edit" class="btn"
56 hx-get="<%= _base %>/posts/<%= post.slug %>/edit?partial=1" hx-target="#pcms-main"
57 hx-push-url="<%= _base %>/posts/<%= post.slug %>/edit" hx-indicator="#pcms-loading">
[7bc636b]58 ✏️ Edit
59 </a>
60 <% } %>
61 <% if (permissions.canDeletePost(user, post, site)) { %>
[b94c08e]62 <form method="post" action="<%= _base %>/posts/<%= post.slug %>/delete" style="display:inline" onsubmit="return confirm('Delete this post?')">
[7bc636b]63 <button type="submit" class="btn btn-danger">🗑 Delete</button>
64 </form>
65 <% } %>
66 </aside>
67 <% } %>
68
69 <!-- Comments -->
70 <section class="post-comments" id="comments">
71 <h2 class="comments-heading">
72 <%= totalComments %> comment<%= totalComments === 1 ? '' : 's' %>
73 </h2>
74
75 <% if (typeof currentPath === 'string' && currentPath && currentPath.indexOf('/' + post.slug) === 0) { %>
76 <% /* If the URL has ?pending=1, the previous submit landed in the queue */ %>
77 <% } %>
78 <script>
79 (function() {
80 if (location.search.indexOf('pending=1') !== -1) {
81 var s = document.createElement('div');
82 s.className = 'comments-pending-flash';
83 s.textContent = 'Your comment is awaiting moderation. It will appear once an admin approves it.';
84 document.currentScript.parentNode.insertBefore(s, document.currentScript);
85 }
86 })();
87 </script>
88
89 <% if (!comments || !comments.length) { %>
[b94c08e]90 <p class="comments-empty">No comments yet.<% if (!user) { %> <a href="/auth/login?next=<%= encodeURIComponent(_base + '/' + post.slug + '#comments') %>">Log in</a> to start the conversation.<% } %></p>
[7bc636b]91 <% } else { %>
92 <ol class="comments-list">
93 <% comments.forEach(function(c) { %>
94 <li class="comment" id="comment-<%= c.id %>">
95 <div class="comment-avatar">
96 <% if (c.author_avatar) { %>
97 <img src="<%= c.author_avatar %>" alt="">
98 <% } else { %>
99 <span><%= c.author_username.charAt(0).toUpperCase() %></span>
100 <% } %>
101 </div>
102 <div class="comment-body">
103 <div class="comment-meta">
104 <a class="comment-author" href="/users/<%= encodeURIComponent(c.author_username) %>"><%= c.author_username %></a>
105 <span class="comment-time" title="<%= c.created_at %>"><%= formatDateTime(c.created_at) %></span>
106 </div>
107 <div class="comment-content"><%= c.content %></div>
108 <div class="comment-actions">
[8afbdd6]109 <% if (user && canMutate) { %>
[7bc636b]110 <button type="button" class="comment-reply-btn" data-reply-to="<%= c.id %>">Reply</button>
111 <% } %>
112 <% if (user && permissions.canDeleteComment(user, c, site)) { %>
[b94c08e]113 <form method="post" action="<%= _base %>/comments/<%= c.id %>/delete" style="display:inline" onsubmit="return confirm('Delete this comment?')">
[7bc636b]114 <button type="submit" class="comment-delete-btn">Delete</button>
115 </form>
116 <% } %>
117 </div>
118
119 <!-- Inline reply form (hidden by default) -->
[8afbdd6]120 <% if (user && canMutate) { %>
[b94c08e]121 <form method="post" action="<%= _base %>/comments" class="comment-reply-form" hidden data-reply-form-for="<%= c.id %>">
[7bc636b]122 <input type="hidden" name="post_slug" value="<%= post.slug %>">
123 <input type="hidden" name="parent_comment_id" value="<%= c.id %>">
124 <textarea name="content" rows="3" maxlength="4000" placeholder="Reply to <%= c.author_username %>…" required></textarea>
125 <div class="comment-reply-form-actions">
126 <button type="submit" class="btn btn-primary">Reply</button>
127 <button type="button" class="btn comment-cancel-reply">Cancel</button>
128 </div>
129 </form>
130 <% } %>
131
132 <!-- Replies (1 level deep) -->
133 <% if (c.replies && c.replies.length) { %>
134 <ol class="comment-replies">
135 <% c.replies.forEach(function(r) { %>
136 <li class="comment comment-reply" id="comment-<%= r.id %>">
137 <div class="comment-avatar">
138 <% if (r.author_avatar) { %>
139 <img src="<%= r.author_avatar %>" alt="">
140 <% } else { %>
141 <span><%= r.author_username.charAt(0).toUpperCase() %></span>
142 <% } %>
143 </div>
144 <div class="comment-body">
145 <div class="comment-meta">
146 <a class="comment-author" href="/users/<%= encodeURIComponent(r.author_username) %>"><%= r.author_username %></a>
147 <span class="comment-time"><%= formatDateTime(r.created_at) %></span>
148 </div>
149 <div class="comment-content"><%= r.content %></div>
150 <div class="comment-actions">
151 <% if (user && permissions.canDeleteComment(user, r, site)) { %>
[b94c08e]152 <form method="post" action="<%= _base %>/comments/<%= r.id %>/delete" style="display:inline" onsubmit="return confirm('Delete this comment?')">
[7bc636b]153 <button type="submit" class="comment-delete-btn">Delete</button>
154 </form>
155 <% } %>
156 </div>
157 </div>
158 </li>
159 <% }); %>
160 </ol>
161 <% } %>
162 </div>
163 </li>
164 <% }); %>
165 </ol>
166 <% } %>
167
168 <!-- Top-level comment form (only for logged-in users for now) -->
[8afbdd6]169 <% if (user && canMutate) { %>
[b94c08e]170 <form method="post" action="<%= _base %>/comments" class="comment-form">
[7bc636b]171 <input type="hidden" name="post_slug" value="<%= post.slug %>">
172 <label class="comment-form-label">
173 <span>Add a comment as <strong><%= user.username %></strong></span>
174 <textarea name="content" rows="3" maxlength="4000" placeholder="Share your thoughts…" required></textarea>
175 </label>
176 <button type="submit" class="btn btn-primary">Post comment</button>
177 </form>
[8afbdd6]178 <% } else if (!user) { %>
[7bc636b]179 <p class="comments-login-cta">
[b94c08e]180 <a href="/auth/login?next=<%= encodeURIComponent(_base + '/' + post.slug + '#comments') %>" class="btn">Log in to comment</a>
[7bc636b]181 </p>
[8afbdd6]182 <% } else { %>
183 <p class="comments-login-cta" style="color:var(--ink-muted)">👁️ Kijker-modus — reageren is uitgeschakeld.</p>
[7bc636b]184 <% } %>
185 </section>
186
187 <!-- Reply form toggle -->
188 <script>
189 (function() {
190 document.querySelectorAll('.comment-reply-btn').forEach(function(btn) {
191 btn.addEventListener('click', function() {
192 var id = btn.dataset.replyTo;
193 var form = document.querySelector('[data-reply-form-for="' + id + '"]');
194 if (form) {
195 form.hidden = !form.hidden;
196 if (!form.hidden) form.querySelector('textarea').focus();
197 }
198 });
199 });
200 document.querySelectorAll('.comment-cancel-reply').forEach(function(btn) {
201 btn.addEventListener('click', function() {
202 btn.closest('.comment-reply-form').hidden = true;
203 });
204 });
205 })();
206 </script>
207
208 <%# ── Related posts (3-card grid) ─────────────────────────────
209 Always shown if at least one related post exists. Selection logic
210 lives server-side: same-tag posts ranked by overlap, falling back
211 to most-recent if there's no tag overlap. %>
212 <% if (relatedPosts && relatedPosts.length > 0) { %>
213 <section class="post-related" aria-labelledby="related-heading">
214 <h2 class="post-related-heading" id="related-heading">Gerelateerde posts</h2>
215 <ul class="post-related-grid">
216 <% relatedPosts.forEach(function(rp) { %>
217 <li class="post-related-card">
[d54dade]218 <a href="<%= rp._urlBase || '' %>/<%= rp.slug %>"<% if (!rp._urlBase) { %>
[7bc636b]219 hx-get="/<%= rp.slug %>?partial=1" hx-target="#pcms-main"
[d54dade]220 hx-push-url="/<%= rp.slug %>" hx-indicator="#pcms-loading"<% } %>>
[7bc636b]221 <% if (rp.cover_image_url) { %>
222 <span class="post-related-cover"
223 style="background-image: url('<%= rp.cover_image_url %>')"
224 aria-hidden="true"></span>
225 <% } else { %>
226 <span class="post-related-cover post-related-cover--empty" aria-hidden="true"></span>
227 <% } %>
228 <span class="post-related-meta">
229 <span class="post-related-date"><%= formatDate(rp.published_at) %></span>
230 <span class="post-related-title"><%= rp.title %></span>
231 </span>
232 </a>
233 </li>
234 <% }); %>
235 </ul>
236 </section>
237 <% } %>
238
239</article>
240<style>
[8e29f77]241.post-page { max-width: 720px; margin: 0.5rem auto 2rem; padding: 0 1rem; }
[7bc636b]242.post-cover { margin: 0 0 2rem; border-radius: 8px; overflow: hidden; }
243.post-cover img { width: 100%; height: auto; display: block; }
244.post-header { margin-bottom: 2rem; }
245.post-title { font-family: var(--font-display, serif); font-size: 2.5rem; line-height: 1.1; margin: 0 0 1rem; }
246@media (max-width: 600px) { .post-title { font-size: 1.85rem; } }
247.post-meta { font-size: 0.9rem; color: var(--ink-muted); }
248.author { font-weight: 500; color: var(--ink-soft); text-decoration: none; }
249.author:hover { color: var(--accent); }
250.post-content { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); }
251.post-content p { margin: 1.2em 0; }
252.post-content h2 { font-family: var(--font-display, serif); font-size: 1.6rem; margin: 2rem 0 1rem; }
253.post-content h3 { font-family: var(--font-display, serif); font-size: 1.3rem; margin: 1.5rem 0 0.75rem; }
254.post-content a { color: var(--accent); }
255.post-content code { background: var(--paper-2); padding: 0.1em 0.4em; border-radius: 3px; font-family: var(--font-mono, monospace); font-size: 0.9em; }
256.post-content pre { background: var(--paper-2); padding: 1rem; border-radius: 6px; overflow-x: auto; }
257.post-content blockquote { border-left: 3px solid var(--accent); padding-left: 1rem; margin: 1.5rem 0; color: var(--ink-soft); font-style: italic; }
258.post-content img { max-width: 100%; height: auto; border-radius: 6px; }
259.post-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 2rem 0; }
260.tag { background: var(--paper-2); color: var(--ink-soft); padding: 0.2rem 0.6rem; border-radius: 999px; font-size: 0.85rem; text-decoration: none; transition: background 120ms, color 120ms; }
261.tag:hover { background: var(--accent); color: white; }
262.post-actions { display: flex; gap: 0.5rem; margin: 2rem 0; padding: 1rem; background: var(--paper-2); border-radius: 6px; flex-wrap: wrap; }
263
264/* ============================================================
265 Comments
266 ============================================================ */
267.post-comments {
268 margin-top: 3rem;
269 padding-top: 2rem;
270 border-top: 1px solid var(--rule);
271}
272.comments-heading {
273 font-family: var(--font-display, serif);
274 font-size: 1.5rem;
275 margin: 0 0 1.5rem;
276}
277.comments-empty { color: var(--ink-muted, var(--ink-soft)); margin: 0 0 1.5rem; }
278.comments-pending-flash {
279 background: rgba(40, 160, 90, 0.15);
280 color: #2a9d5e;
281 border: 1px solid rgba(40, 160, 90, 0.3);
282 padding: 0.75rem 1rem;
283 border-radius: 6px;
284 margin-bottom: 1rem;
285 font-size: 0.9rem;
286}
287.comments-empty a { color: var(--accent); }
288
289.comments-list, .comment-replies {
290 list-style: none;
291 padding: 0;
292 margin: 0 0 2rem;
293}
294
295.comment {
296 display: flex;
297 gap: 0.85rem;
298 padding: 1rem 0;
299 border-top: 1px solid var(--rule);
300}
301.comments-list > .comment:first-child { border-top: 0; padding-top: 0; }
302
303.comment-avatar {
304 flex-shrink: 0;
305 width: 40px;
306 height: 40px;
307 border-radius: 50%;
308 background: var(--paper-2);
309 border: 1px solid var(--rule);
310 overflow: hidden;
311 display: inline-flex;
312 align-items: center;
313 justify-content: center;
314}
315.comment-avatar img { width: 100%; height: 100%; object-fit: cover; }
316.comment-avatar span {
317 font-family: var(--font-display, serif);
318 font-weight: 700;
319 color: var(--accent);
320}
321
322.comment-body { flex: 1; min-width: 0; }
323.comment-meta {
324 display: flex;
325 gap: 0.5rem;
326 align-items: baseline;
327 font-size: 0.85rem;
328 margin-bottom: 0.3rem;
329}
330.comment-author { color: var(--ink); font-weight: 600; text-decoration: none; }
331.comment-author:hover { color: var(--accent); }
332.comment-time { color: var(--ink-muted, var(--ink-soft)); }
333.comment-content {
334 white-space: pre-wrap;
335 word-wrap: break-word;
336 color: var(--ink);
337 line-height: 1.5;
338 font-size: 0.95rem;
339}
340
341.comment-actions {
342 display: flex;
343 gap: 0.5rem;
344 margin-top: 0.4rem;
345}
346.comment-reply-btn, .comment-delete-btn {
347 background: none;
348 border: 0;
349 color: var(--ink-muted, var(--ink-soft));
350 font-size: 0.8rem;
351 cursor: pointer;
352 padding: 0.2rem 0;
353 font-family: inherit;
354 text-transform: uppercase;
355 letter-spacing: 0.04em;
356}
357.comment-reply-btn:hover { color: var(--accent); }
358.comment-delete-btn:hover { color: #c33; }
359
360.comment-replies {
361 margin-top: 1rem;
362 margin-left: 0;
363 border-left: 2px solid var(--rule);
364 padding-left: 1rem;
365}
366.comment-reply { padding: 0.75rem 0; }
367.comment-replies > .comment-reply:first-child { border-top: 0; padding-top: 0; }
368.comment-reply .comment-avatar { width: 32px; height: 32px; }
369.comment-reply .comment-avatar span { font-size: 0.85rem; }
370
371.comment-form, .comment-reply-form {
372 display: flex;
373 flex-direction: column;
374 gap: 0.6rem;
375 background: var(--paper-2);
376 border: 1px solid var(--rule);
377 border-radius: 8px;
378 padding: 1rem;
379 margin-top: 1rem;
380}
381.comment-form-label { display: flex; flex-direction: column; gap: 0.4rem; }
382.comment-form-label > span { font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
383.comment-form textarea, .comment-reply-form textarea {
384 width: 100%;
385 resize: vertical;
386 min-height: 70px;
387 padding: 0.65rem 0.85rem;
388 border: 1px solid var(--rule);
389 border-radius: 5px;
390 background: var(--paper);
391 color: var(--ink);
392 font-family: inherit;
393 font-size: 0.95rem;
394 line-height: 1.4;
395 box-sizing: border-box;
396}
397.comment-form button { align-self: flex-start; }
398.comment-reply-form-actions { display: flex; gap: 0.5rem; }
399
400.comments-login-cta { margin-top: 1.5rem; text-align: center; }
401
402/* ─── Related posts (3-card grid above pinned-nav) ─────────────── */
403.post-related {
404 margin: 3rem auto 1.5rem;
405 /* Break out of the 720px article column so the cards have room to breathe.
406 Cap at 1100 to match other wide content. */
407 max-width: 1100px;
408 padding: 0 1rem;
409}
410.post-related-heading {
411 font-family: var(--font-ui, system-ui), sans-serif;
412 font-size: 0.75rem;
413 font-weight: 600;
414 text-transform: uppercase;
415 letter-spacing: 0.15em;
416 color: var(--ink-muted, var(--ink-soft));
417 margin: 0 0 1rem;
418}
419.post-related-grid {
420 list-style: none;
421 padding: 0; margin: 0;
422 display: grid;
423 grid-template-columns: repeat(3, minmax(0, 1fr));
424 gap: 1rem;
425}
426@media (max-width: 880px) { .post-related-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
427@media (max-width: 540px) { .post-related-grid { grid-template-columns: 1fr; } }
428
429.post-related-card {
430 background: var(--paper);
431 border: 1px solid var(--rule);
432 border-radius: 10px;
433 overflow: hidden;
434 transition: border-color 150ms, transform 150ms;
435}
436.post-related-card:hover { border-color: var(--accent); transform: translateY(-2px); }
437.post-related-card a {
438 display: block;
439 color: inherit;
440 text-decoration: none;
441}
442.post-related-cover {
443 display: block;
444 aspect-ratio: 16 / 10;
445 background: var(--paper-2) center/cover no-repeat;
446}
447.post-related-cover--empty {
448 background-image: linear-gradient(135deg,
449 color-mix(in srgb, var(--accent) 12%, var(--paper-2)),
450 var(--paper-2));
451}
452.post-related-meta {
453 display: flex; flex-direction: column; gap: 0.4rem;
454 padding: 0.85rem 1rem 1.1rem;
455}
456.post-related-date {
457 font-size: 0.78rem;
458 color: var(--ink-muted, var(--ink-soft));
459 font-variant-numeric: tabular-nums;
460}
461.post-related-title {
462 font-family: var(--font-display, serif);
463 font-size: 1.1rem;
464 font-weight: 600;
465 color: var(--ink);
466 line-height: 1.25;
467}
468
469/* ─── Pinned navigation cards (v9 style) ───────────────────────── */
470.post-pinned-nav {
471 display: grid;
472 grid-template-columns: 1fr 1fr;
473 gap: 1rem;
474 margin: 1.5rem auto 2rem;
475 max-width: 1100px;
476 padding: 0 1rem;
477}
478@media (max-width: 600px) {
479 .post-pinned-nav { grid-template-columns: 1fr; }
480}
481
482.post-pinned-card {
483 display: flex; align-items: center; gap: 1rem;
484 padding: 1rem 1.25rem;
485 border: 1.5px solid color-mix(in srgb, var(--accent) 50%, transparent);
486 background: color-mix(in srgb, var(--accent) 6%, var(--paper));
487 border-radius: 10px;
488 text-decoration: none;
489 color: var(--ink);
490 transition: border-color 150ms, background 150ms, transform 150ms;
491 min-height: 88px;
492}
493.post-pinned-card:hover {
494 border-color: var(--accent);
495 background: color-mix(in srgb, var(--accent) 11%, var(--paper));
496 transform: translateY(-2px);
497}
498.post-pinned-arrow {
499 font-size: 1.6rem;
500 color: var(--accent);
501 flex-shrink: 0;
502 line-height: 1;
503}
504.post-pinned-card--next {
505 /* "Volgende" card aligns text to the right with the arrow on the right
506 side, mirroring v9 — the arrow is on the OPPOSITE end from the body. */
507 flex-direction: row-reverse;
508 text-align: right;
509}
510.post-pinned-body {
511 display: flex; flex-direction: column; gap: 0.25rem;
512 flex: 1; min-width: 0;
513}
514.post-pinned-label {
515 font-family: var(--font-ui, system-ui), sans-serif;
516 font-size: 0.7rem; font-weight: 600;
517 text-transform: uppercase;
518 letter-spacing: 0.1em;
519 color: var(--accent);
520}
521.post-pinned-title {
522 font-family: var(--font-display, serif);
523 font-size: 1.05rem; font-weight: 600;
524 color: var(--ink);
525 line-height: 1.25;
526 overflow: hidden;
527 text-overflow: ellipsis;
528 display: -webkit-box;
529 -webkit-line-clamp: 2;
530 -webkit-box-orient: vertical;
531}
532.post-pinned-cta {
533 font-size: 0.78rem;
534 color: var(--accent);
535 font-weight: 500;
536}
537
538/* Edge-of-stack indicator: dashed border, muted styling. */
539.post-pinned-card--edge {
540 flex-direction: column;
541 align-items: flex-start;
542 justify-content: center;
543 gap: 0.35rem;
544 border-style: dashed;
545 background: transparent;
546 color: var(--ink-muted, var(--ink-soft));
547 cursor: default;
548}
549.post-pinned-card--edge:hover {
550 /* Edge cards aren't clickable — undo the lift effect. */
551 transform: none;
552 border-color: color-mix(in srgb, var(--accent) 50%, transparent);
553 background: transparent;
554}
555.post-pinned-edge-label {
556 font-family: var(--font-ui, system-ui), sans-serif;
557 font-size: 0.72rem;
558 font-weight: 600;
559 text-transform: uppercase;
560 letter-spacing: 0.15em;
561 color: var(--accent);
562}
563.post-pinned-edge-title {
564 font-family: var(--font-display, serif);
565 font-size: 1.05rem;
566 font-weight: 600;
567 color: var(--ink);
568}
569</style>
Note: See TracBrowser for help on using the repository browser.