source: Klonkt/src/views/pages/circle-feed.ejs@ b0ccd0d

main
Last change on this file since b0ccd0d was 520e477, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: Load more on Solo + Cirkel, page 72 (klonkt-demo-r9u, slice 3/4)

The home feed (Solo) and the Cirkel feed now page in 72s instead of a
hard 30/80 cap. Both render two containers (list + grid, CSS-toggled),
so the append fragment (partials/home-append) sends the post-cards as
the primary beforeend swap into #post-list and OOB-appends the same
posts as tiles into #grid-tiles. htmx 1.9.12 unwraps the OOB wrapper's
children on a positional swap, so the tiles land as direct grid items
(the display:contents wrapper is a belt-and-braces fallback). The
button lives once below both views and OOB-replaces itself with the
next offset, dropping on the last page. Pinned posts stay on page 1
only. getCirkelPosts gains an offset arg; FEED_PAGE hoisted to the top
of posts.js.

Browser-verified both feeds: 72 -> 144 -> 150 in list AND grid, then
the button disappears.

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

  • Property mode set to 100644
File size: 8.3 KB
Line 
1<div class="container cirkel-hero">
2 <span class="cirkel-badge" aria-hidden="true">
3 <svg viewBox="0 0 100 100" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
4 <defs>
5 <marker id="cf-a" viewBox="0 0 10 10" refX="6.5" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M1.5 1.5 L8.5 5 L1.5 8.5 Z" fill="currentColor" stroke="none"/></marker>
6 </defs>
7 <line x1="28.95" y1="43.5" x2="63.05" y2="26.5" stroke-width="2.6" marker-start="url(#cf-a)" marker-end="url(#cf-a)"/>
8 <line x1="72" y1="32" x2="72" y2="66" stroke-width="2.6" marker-end="url(#cf-a)"/>
9 <line x1="28.8" y1="52.7" x2="63.2" y2="71.3" stroke-width="2.6" marker-end="url(#cf-a)"/>
10 <circle cx="20" cy="48" r="7" stroke-width="2.4"/>
11 <circle cx="72" cy="22" r="7" stroke-width="2.4"/>
12 <circle cx="72" cy="76" r="7" stroke-width="2.4"/>
13 </svg>
14 </span>
15 <h1><%= t('cfeed.title') %></h1>
16 <p class="cirkel-sub"><%= t('cfeed.sub') %></p>
17 <% if (typeof sites !== 'undefined' && sites && sites.length) { %>
18 <% if (sites.length <= 5) { %>
19 <div class="cirkel-members">
20 <% sites.forEach(function(s){ %>
21 <% if (s.url) { %><a class="cirkel-member" href="<%= s.url %>" target="_blank" rel="noopener" title="<%= s.name %>"><% } else { %><span class="cirkel-member" title="<%= s.name %>"><% } %>
22 <span class="cirkel-avatar"<% if (s.avatar) { %> style="background-image:url('<%= s.avatar %>');background-color:transparent"<% } %>><% if (!s.avatar) { %><%= (s.name || '?').charAt(0).toUpperCase() %><% } %></span>
23 <span class="cirkel-member-name"><%= s.name %></span>
24 <% if (s.url) { %></a><% } else { %></span><% } %>
25 <% }); %>
26 </div>
27 <% } else { %>
28 <%# More than 5: the count pill becomes a button that opens a modal listing every
29 member (avatar + name + link). CSS-only via a hidden checkbox — no JS, so it's
30 CSP- (no inline handler) and htmx-safe. Checkbox must precede .cirkel-modal so
31 the `:checked ~` sibling selector matches. %>
32 <input type="checkbox" id="cirkel-more-cb" class="cirkel-more-cb" hidden>
33 <label for="cirkel-more-cb" class="cirkel-count cirkel-count-btn" role="button" tabindex="0"><%= t('cfeed.count', { n: sites.length }) %></label>
34 <div class="cirkel-modal" role="dialog" aria-modal="true" aria-label="<%= t('cfeed.title') %>">
35 <label for="cirkel-more-cb" class="cirkel-modal-backdrop" aria-label="<%= t('cfeed.close') %>"></label>
36 <div class="cirkel-modal-card">
37 <label for="cirkel-more-cb" class="cirkel-modal-x" aria-label="<%= t('cfeed.close') %>" role="button" tabindex="0">&times;</label>
38 <h2 class="cirkel-modal-h"><%= t('cfeed.count', { n: sites.length }) %></h2>
39 <ul class="cirkel-modal-list">
40 <% sites.forEach(function(s){ %>
41 <li>
42 <% if (s.url) { %><a class="cirkel-modal-link" href="<%= s.url %>" target="_blank" rel="noopener"><% } else { %><span class="cirkel-modal-link"><% } %>
43 <span class="cirkel-avatar"<% if (s.avatar) { %> style="background-image:url('<%= s.avatar %>');background-color:transparent"<% } %>><% if (!s.avatar) { %><%= (s.name || '?').charAt(0).toUpperCase() %><% } %></span>
44 <span class="cirkel-member-name"><%= s.name %></span>
45 <% if (s.url) { %><span class="cirkel-modal-go" aria-hidden="true">↗</span><% } %>
46 <% if (s.url) { %></a><% } else { %></span><% } %>
47 </li>
48 <% }); %>
49 </ul>
50 </div>
51 </div>
52 <% } %>
53 <% } %>
54</div>
55
56<% if (!posts.length) { %>
57 <div class="container">
58 <p class="cirkel-empty"><%= t('cfeed.empty') %></p>
59 </div>
60<% } else { %>
61
62 <%# Same view as the home page: timeline (post-card) + grid (post-tile),
63 switched by the view-switcher via body[data-feed-view]. The remote posts
64 are shaped to the local post format in circle.js (with external_url,
65 so cards link to the source site). %>
66
67 <!-- ===== TIMELINE (standaard) ===== -->
68 <div class="container feed-timeline">
69 <ul class="post-list feed-timeline-list" id="post-list">
70 <% posts.forEach(function(post){ %>
71 <%- include('../partials/post-card', { post: post }) %>
72 <% }); %>
73 </ul>
74 </div>
75
76 <!-- ===== GRID (body[data-feed-view="grid"]) ===== -->
77 <section class="feed-grid container" aria-label="<%= t('cfeed.grid_view') %>">
78 <div class="grid-tiles" id="grid-tiles">
79 <% posts.forEach(function(post){ %>
80 <%- include('../partials/post-tile', { post: post }) %>
81 <% }); %>
82 </div>
83 </section>
84
85 <% if (typeof hasMore !== 'undefined') { %>
86 <div class="container feed-more-wrap">
87 <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#post-list', morePath: '/cirkel' }) %>
88 </div>
89 <% } %>
90
91<% } %>
92
93<style>
94.cirkel-hero { text-align: center; max-width: 640px; margin: 2.5rem auto 2rem; }
95.cirkel-badge { display: block; width: 100px; height: 100px; margin: 0 auto .7rem; color: var(--accent);
96 filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 65%, transparent)) drop-shadow(0 0 1px var(--accent)); }
97.cirkel-badge svg { width: 100%; height: 100%; }
98.cirkel-hero h1 { font-family: var(--font-display, serif); font-size: 2.4rem; margin: 0 0 .3rem; }
99.cirkel-sub { color: var(--ink-muted); margin: 0 0 1.25rem; font-size: 1.02rem; }
100.cirkel-members { display: flex; flex-wrap: wrap; gap: .55rem; justify-content: center; }
101.cirkel-member { display: inline-flex; align-items: center; gap: .45rem;
102 padding: .28rem .8rem .28rem .3rem; border: 1px solid var(--rule); border-radius: 999px;
103 text-decoration: none; color: var(--ink); background: var(--paper-2);
104 transition: border-color .15s, transform .15s; }
105.cirkel-member:hover { border-color: var(--accent); transform: translateY(-1px); }
106.cirkel-avatar { width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
107 background-size: cover; background-position: center; background-color: var(--accent);
108 color: #fff; display: inline-flex; align-items: center; justify-content: center;
109 font-weight: 700; font-size: .82rem; }
110.cirkel-member-name { font-size: .9rem; font-weight: 600; }
111.cirkel-count { display: inline-block; color: var(--ink-muted); font-size: .92rem; font-weight: 600;
112 padding: .35rem 1rem; border: 1px solid var(--rule); border-radius: 999px; background: var(--paper-2); }
113.cirkel-count-btn { cursor: pointer; user-select: none; transition: border-color .15s, color .15s, transform .15s; }
114.cirkel-count-btn:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
115.cirkel-empty { color: var(--ink-muted); text-align: center; margin: 2rem auto; }
116.cirkel-empty a { color: var(--accent); }
117/* CSS-only "more members" modal (toggled by the hidden checkbox, no JS) */
118.cirkel-more-cb { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }
119.cirkel-modal { display: none; position: fixed; inset: 0; z-index: 1000; align-items: center; justify-content: center; padding: 1rem; }
120.cirkel-more-cb:checked ~ .cirkel-modal { display: flex; }
121.cirkel-modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,.45); cursor: pointer; }
122.cirkel-modal-card { position: relative; background: var(--paper); color: var(--ink); border-radius: 18px;
123 padding: 1.4rem 1.2rem; width: min(420px, 92vw); max-height: 78vh; overflow-y: auto; text-align: left;
124 box-shadow: 0 20px 60px rgba(0,0,0,.32); }
125.cirkel-modal-x { position: absolute; top: .55rem; right: .65rem; width: 34px; height: 34px; border-radius: 50%;
126 display: inline-flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.45rem;
127 line-height: 1; color: var(--ink-muted); background: var(--paper-2); }
128.cirkel-modal-x:hover { color: var(--ink); }
129.cirkel-modal-h { font-family: var(--font-display, serif); font-size: 1.3rem; margin: .1rem 0 1rem; text-align: center; }
130.cirkel-modal-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .25rem; }
131.cirkel-modal-link { display: flex; align-items: center; gap: .6rem; padding: .45rem .5rem; border-radius: 12px;
132 text-decoration: none; color: var(--ink); transition: background .15s; }
133.cirkel-modal-link:hover { background: var(--paper-2); }
134.cirkel-modal-go { margin-left: auto; color: var(--accent); font-weight: 700; }
135</style>
Note: See TracBrowser for help on using the repository browser.