source: Klonkt/src/views/partials/bottom-tab.ejs@ 72ec6a4

main
Last change on this file since 72ec6a4 was 72ec6a4, checked in by Robin <roboburr@…>, 6 weeks ago

Hub-modus en guardian-lite eruit

Robins besluit (31-7): een instance is een eigenaar. Twee dingen weg.

HUB-MODUS was al dood: getTenancy() gaf sinds 24-6 hardcoded 'solo'
terug, dus elke tenancy === 'hub'-tak was onbereikbaar. Nu ook echt
verwijderd: getTenancy/setTenancy zelf, de /user/:slug-routing in
resolveSite, de hub-takken in admin, zoeken, audio, posts (neighbours
en related over alle sites), download, en de push-prefix. In de views
verdwijnen de hub-tagline, de hub-navigatie, de sites- en
users-tabellen (die kwamen alleen in hub-modus gevuld en verwezen nu
naar locals die niemand meer meegeeft), de eigenaar-toewijzing bij een
site, de /user/-slugprefix, het hub-brandblok en de hub-thuisknop.

GUARDIAN-LITE was de laatste multi-user-rest: /guardian/invite gaf een
link waarmee iemand via /guardian/join een echte user plus een site met
guardian_only=1 aanmaakte. Dat zette andermans wachtwoordhash, sessie
en PRIVATE actor-sleutel in jouw database, waardoor een verhuizing of
export nooit netjes kon (shaer-qw6q). Routes, formulier, kolom en
uitnodigingstabel zijn weg. Het guardian-DASHBOARD blijft: dat is
FEP-633c en werkt voor guardians met een eigen Klonkt. Bestaande
installaties houden kolom en tabel ongebruikt; nieuwe krijgen ze niet.

Changed files:
src/services/SettingsService.js

  • getTenancy/setTenancy verwijderd; kop herschreven

src/middleware/site.js, src/middleware/render.js

  • /user/:slug-routing weg; tenancy en hubTitle uit de locals

src/routes/admin.js, admin-settings.js, audio.js, download.js,
src/routes/posts.js, search.js

  • hub-takken en hub-queries weg; postNeighbors zonder isHub

src/services/ActivityPubService.js

  • pushPrefix is nu gewoon ; getTenancy-import weg

src/routes/guardian.js

  • /invite en /join verwijderd (dashboard blijft), imports opgeschoond

src/config/database.js

  • guardian_only-kolom en ap_guardian_invites-tabel niet meer aangemaakt

src/views/pages/admin.ejs, admin-users.ejs, admin-site-edit.ejs,
src/views/pages/guardian.ejs, partials/topnav.ejs, chrome.ejs, bottom-tab.ejs

  • alle hub-takken en de uitnodigingsknop weg

remarks: 320 regels weg, 63 erbij. Suite 372 groen; alle 85 templates
compileren; en met een wegwerp-kopie van de database daadwerkelijk
gedraaid en ingelogd: /, /admin, /admin/users, /admin/sites,
/admin/settings, /admin/sites/demo/edit, /admin/media en /guardian
geven alle 200 zonder fouten in het log, en /guardian/invite en
/guardian/join geven nu 404.

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

  • Property mode set to 100644
File size: 11.8 KB
Line 
1<%
2// Bottom-tab navigation — mobile-only (hidden ≥768px via CSS).
3//
4// Tabs:
5// logged-out: Home · Zoek · Inloggen (3 tabs)
6// logged-in: Home · Zoek · [Plus] · Profiel
7// The Plus tab is a FAB-style center action (accent bg, slightly raised).
8// It only appears for users with create-post permission.
9
10const _siteUrlBase = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
11const _path = (typeof currentPath !== 'undefined' && currentPath) ? currentPath : '/';
12const _canPost = !!(user && (typeof canMutate === 'undefined' || canMutate)
13 && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site));
14const _ownProfile = user ? ('/users/' + user.username) : null;
15const _homeHref = _siteUrlBase + '/';
16
17// Strip site prefix for cleaner matching
18function _normalize(p) {
19 if (_siteUrlBase && p.indexOf(_siteUrlBase) === 0) return p.slice(_siteUrlBase.length) || '/';
20 return p;
21}
22const _p = _normalize(_path);
23
24let _active = null;
25if (_p === '/' || _p === '') _active = 'home';
26else if (_p.indexOf('/search') === 0 || _p.indexOf('/tag/') === 0 || _p.indexOf('/type/') === 0) _active = 'search';
27else if (_p === '/posts/new' || /^\/posts\/[^/]+\/edit$/.test(_p)) _active = 'create';
28else if (_p.indexOf('/account') === 0 || (_ownProfile && _p.indexOf(_ownProfile) === 0)) _active = 'profile';
29else if (_p.indexOf('/auth/login') === 0) _active = 'login';
30%>
31
32<nav class="bottom-tab" aria-label="Hoofdnavigatie">
33
34 <!-- Home -->
35 <%# htmx navigation (no full reload): the persistent audio player in document.body
36 survives, so music does not skip when navigating. On mobile the top bar is hidden
37 anyway, so a context reset via full-load is not needed here. %>
38 <a class="bottom-tab-item<%= _active === 'home' ? ' is-active' : '' %>"
39 href="<%= _homeHref %>"
40 hx-get="<%= _homeHref %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
41 hx-push-url="<%= _homeHref %>" hx-indicator="#pcms-loading"
42 aria-label="Home" <%= _active === 'home' ? 'aria-current="page"' : '' %>>
43 <svg class="bottom-tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
44 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
45 <polyline points="9 22 9 12 15 12 15 22"/>
46 </svg>
47 <span class="bottom-tab-label"><%= t('tab.home') %></span>
48 </a>
49
50 <!-- Zoek -->
51 <button type="button" class="bottom-tab-item<%= _active === 'search' ? ' is-active' : '' %>"
52 id="bottom-tab-search-toggle" aria-label="<%= t('nav.search') %>">
53 <svg class="bottom-tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
54 <circle cx="11" cy="11" r="7"/>
55 <line x1="21" y1="21" x2="16.65" y2="16.65"/>
56 </svg>
57 <span class="bottom-tab-label"><%= t('tab.search') %></span>
58 </button>
59
60 <!-- Plus (FAB-style, only when user can post) -->
61 <% if (_canPost) { %>
62 <a class="bottom-tab-item bottom-tab-fab<%= _active === 'create' ? ' is-active' : '' %>"
63 href="<%= _siteUrlBase %>/posts/new"
64 hx-get="<%= _siteUrlBase %>/posts/new?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
65 hx-push-url="<%= _siteUrlBase %>/posts/new" hx-indicator="#pcms-loading"
66 aria-label="Nieuwe post">
67 <svg class="bottom-tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
68 <line x1="12" y1="5" x2="12" y2="19"/>
69 <line x1="5" y1="12" x2="19" y2="12"/>
70 </svg>
71 <span class="bottom-tab-label"><%= t('tab.write') %></span>
72 </a>
73 <% } %>
74
75
76 <!-- Profiel / Inloggen -->
77 <% if (user) { %>
78 <button type="button"
79 class="bottom-tab-item<%= _active === 'profile' ? ' is-active' : '' %>"
80 data-profile-sheet-toggle
81 aria-label="Profiel menu" aria-haspopup="dialog">
82 <% if (user.avatar_url) { %>
83 <img class="bottom-tab-avatar bottom-tab-avatar-img" src="<%= avatar(user.avatar_url, 128) %>" alt="">
84 <% } else { %>
85 <span class="bottom-tab-avatar"><%= user.username.charAt(0).toUpperCase() %></span>
86 <% } %>
87 <span class="bottom-tab-label"><%= t('tab.profile') %></span>
88 <% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %>
89 <span class="bottom-tab-badge" aria-label="<%= notifUnread %> ongelezen meldingen"><%= notifUnread > 99 ? '99+' : notifUnread %></span>
90 <% } %>
91 </button>
92 <% } else { %>
93 <a class="bottom-tab-item<%= _active === 'login' ? ' is-active' : '' %>"
94 href="/auth/login"
95 hx-get="/auth/login?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
96 hx-push-url="/auth/login" hx-indicator="#pcms-loading"
97 aria-label="Inloggen">
98 <svg class="bottom-tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
99 <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
100 <polyline points="10 17 15 12 10 7"/>
101 <line x1="15" y1="12" x2="3" y2="12"/>
102 </svg>
103 <span class="bottom-tab-label"><%= t('nav.login') %></span>
104 </a>
105 <% } %>
106
107</nav>
108
109<style>
110/* ============================================================
111 BOTTOM-TAB NAVIGATION (mobile-first)
112 - Visible by default (mobile)
113 - Hidden ≥768px (desktop uses .masthead instead)
114 - Sticks to bottom of viewport, respects safe-area-inset
115 ============================================================ */
116
117.bottom-tab {
118 position: fixed;
119 bottom: 0;
120 left: 0;
121 right: 0;
122 /* z-index 1050: above mini-player (1000), below open sheet (1100).
123 This makes the FAB (Schrijven knop) overlay the mini-player when
124 they overlap visually. */
125 z-index: 1050;
126 display: flex;
127 align-items: stretch;
128 justify-content: space-around;
129 background: color-mix(in srgb, var(--paper) 92%, transparent);
130 backdrop-filter: blur(16px) saturate(140%);
131 -webkit-backdrop-filter: blur(16px) saturate(140%);
132 border-top: 1px solid var(--rule);
133 padding-bottom: env(safe-area-inset-bottom, 0);
134 font-family: var(--font-ui);
135 /* Subtle shadow so the bar feels "lifted" above content */
136 box-shadow: 0 -2px 12px color-mix(in srgb, var(--ink) 5%, transparent);
137}
138
139.bottom-tab-item {
140 flex: 1 1 0;
141 min-height: 56px;
142 /* Tap target ≥44px guaranteed (icon-row 24 + label 12 + padding) */
143 display: flex;
144 flex-direction: column;
145 align-items: center;
146 justify-content: center;
147 gap: 2px;
148 padding: 6px 4px;
149 border: 0;
150 background: transparent;
151 color: var(--ink-muted);
152 text-decoration: none;
153 cursor: pointer;
154 font-family: inherit;
155 position: relative;
156 transition: color 120ms ease, transform 120ms ease;
157 -webkit-tap-highlight-color: transparent;
158}
159
160.bottom-tab-icon {
161 width: 24px;
162 height: 24px;
163 flex-shrink: 0;
164}
165
166.bottom-tab-label {
167 font-size: 10px;
168 font-weight: 500;
169 letter-spacing: 0.01em;
170 line-height: 1;
171 white-space: nowrap;
172}
173
174/* Pressed/active feedback — replaces hover for touch */
175.bottom-tab-item:active {
176 transform: scale(0.92);
177 color: var(--ink);
178}
179
180/* Selected tab */
181.bottom-tab-item.is-active {
182 color: var(--accent);
183}
184.bottom-tab-item.is-active::before {
185 content: '';
186 position: absolute;
187 top: 0;
188 left: 50%;
189 transform: translateX(-50%);
190 width: 28px;
191 height: 2px;
192 background: var(--accent);
193 border-radius: 0 0 2px 2px;
194}
195
196/* Hover only on devices that actually have it */
197@media (hover: hover) {
198 .bottom-tab-item:hover { color: var(--ink); }
199 .bottom-tab-item.is-active:hover { color: var(--accent); }
200}
201
202/* ── FAB-style Plus tab ───────────────────────────────
203 The FAB icon is taken OUT of the flex flow with position:absolute so it
204 cannot inflate the bar height or shift the label baseline. The label
205 stays in flex flow with a margin-top equal to (icon + gap) to mimic
206 a regular icon's contribution — that way center-alignment matches the
207 other tabs exactly. */
208.bottom-tab-fab {
209 position: relative;
210}
211.bottom-tab-fab .bottom-tab-icon {
212 position: absolute;
213 left: 50%;
214 top: -12px; /* overhang above the bar */
215 transform: translateX(-50%);
216 width: 44px;
217 height: 44px;
218 padding: 10px;
219 background: var(--accent);
220 color: #fff;
221 border-radius: 50%;
222 box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 35%, transparent);
223 transition: transform 120ms ease, box-shadow 120ms ease;
224 margin: 0; /* clear any inherited margin */
225}
226.bottom-tab-fab:active .bottom-tab-icon {
227 transform: translateX(-50%) scale(0.92);
228 box-shadow: 0 2px 6px color-mix(in srgb, var(--accent) 35%, transparent);
229}
230.bottom-tab-fab .bottom-tab-label {
231 /* Mimic the space a regular 24px icon + 2px gap would occupy, so the
232 label sits at the same vertical position as labels in sibling tabs. */
233 margin-top: 26px;
234 color: var(--ink-muted);
235}
236.bottom-tab-fab.is-active .bottom-tab-label { color: var(--accent); }
237.bottom-tab-fab.is-active::before { display: none; } /* FAB doesn't need top-bar indicator */
238
239/* ── Avatar in profile tab ──────────────────────────── */
240.bottom-tab-avatar {
241 display: inline-flex;
242 align-items: center;
243 justify-content: center;
244 width: 24px;
245 height: 24px;
246 border-radius: 50%;
247 background: var(--paper-2);
248 color: var(--ink-soft);
249 font-size: 11px;
250 font-weight: 600;
251 line-height: 1;
252 overflow: hidden;
253 object-fit: cover;
254}
255.bottom-tab-avatar-img {
256 /* When rendered as <img>, the same dimensions + object-fit give the
257 same circular crop as the letter span. */
258 display: block;
259}
260/* Active-state used to tint the avatar with --accent. With user-uploaded
261 avatars that override looks broken (orange wash over the photo). The
262 active state is already signalled by the label color, which is enough. */
263
264/* ── Unread badge ───────────────────────────────────── */
265.bottom-tab-badge {
266 position: absolute;
267 top: 4px;
268 left: calc(50% + 8px);
269 min-width: 18px;
270 height: 18px;
271 padding: 0 5px;
272 border-radius: 9px;
273 background: var(--accent);
274 color: #fff;
275 font-size: 10px;
276 font-weight: 700;
277 line-height: 18px;
278 text-align: center;
279 border: 2px solid var(--paper);
280 pointer-events: none;
281}
282
283/* ============================================================
284 HIDE on desktop — top-nav takes over
285 ============================================================ */
286@media (min-width: 768px) {
287 .bottom-tab { display: none; }
288}
289
290/* ============================================================
291 When bottom-tab is present, body needs bottom padding so
292 content doesn't sit underneath it. Applied via body class.
293 ============================================================ */
294@media (max-width: 767px) {
295 body.has-bottom-tab .pcms-main {
296 /* 56 (tab height) + safe-area + breathing room */
297 padding-bottom: calc(72px + env(safe-area-inset-bottom, 0));
298 }
299 /* The .has-audio-player override (extra padding for stacked bars) lives
300 in audio.css — keeps the audio-related rules together. */
301}
302</style>
303
304<script>
305// Bottom-tab search button → opens #search-overlay. Using event delegation + looking
306// up the overlay on EVERY click: the overlay lives in the chrome that is OOB-replaced
307// during htmx navigation, so a once-captured reference goes stale
308// (button did nothing afterwards). Guard against double-wiring.
309(function() {
310 if (window.__btSearchWired) return;
311 window.__btSearchWired = true;
312 document.addEventListener('click', function(e) {
313 if (!e.target.closest || !e.target.closest('#bottom-tab-search-toggle')) return;
314 var o = document.getElementById('search-overlay');
315 if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); }
316 });
317})();
318</script>
Note: See TracBrowser for help on using the repository browser.