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

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

Hub mobile: Home button goes to the hub root instead of the artist sub-home

In hub mode, _siteUrlBase set the mobile Home button to /user/<slug>/
(the sub-home of the artist). Now Home in hub always goes to the hub root
(/) with a full nav (resets the shell context), matching the desktop brand
link. Active state only on the real hub root. Solo/circle unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

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