source: Klonkt/src/views/partials/bottom-tab.ejs@ 8ef8c3e

main
Last change on this file since 8ef8c3e was 834bcc3, checked in by Robin Genis <roboburr@…>, 3 months ago

i18n: translate Dutch code comments to English across src/

Comments in routes/services/views/config/middleware/assets translated to
English for the public repo. A few dev-facing throw/console message strings
were Englished too. No user-facing UI strings or i18n dictionary values changed
(src/services/i18n.js untouched). Logic unchanged.

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

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