source: Klonkt/src/views/partials/profile-sheet.ejs@ 127f87e

main
Last change on this file since 127f87e was a995698, checked in by Robin Genis <roboburr@…>, 2 months ago

fix(ui): hide Fediverse + Notifications nav when AP/federation is off

The fedi nav item + the notifications bell (topnav + mobile profile sheet) were gated only on
canManageFedi, so they stayed visible with federation switched off. Now also gated on apEnabled,
consistent with the rest of the fedi UI which already hides when off. (The profile-sheet
Notifications item was previously ungated entirely.)

  • Property mode set to 100644
File size: 16.1 KB
Line 
1<%
2// Profile bottom sheet — mobile-only.
3//
4// Triggered by tapping any element with [data-profile-sheet-toggle]
5// (the Profiel tab in bottom-tab.ejs sets this).
6//
7// UX matches .audio-sheet for consistency: slide-up from bottom,
8// drag-to-dismiss, blur backdrop, safe-area-aware. Hidden ≥768px
9// (desktop uses the user dropdown in top-nav).
10//
11// Extension point: add more <li> blocks in any of the menu groups.
12
13if (!user) return;
14
15const _isAdmin = (user.role === 'god' || user.role === 'admin');
16const _canPost = !!((typeof canMutate === 'undefined' || canMutate)
17 && typeof permissions !== 'undefined'
18 && permissions
19 && permissions.canCreatePost
20 && permissions.canCreatePost(user, site));
21const _roleLabel = (user.role === 'god' || user.role === 'admin') ? 'beheerder'
22 : (user.role === 'editor') ? 'redacteur'
23 : '';
24// Site-scoped link (post-aanmaken) heeft in hub de /user/<slug>-prefix nodig.
25const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
26%>
27
28<div class="profile-sheet" id="profile-sheet" aria-hidden="true" role="dialog" aria-label="Profiel menu">
29 <div class="profile-sheet-backdrop" id="profile-sheet-backdrop"></div>
30 <div class="profile-sheet-panel">
31
32 <div class="profile-sheet-drag-zone" id="profile-sheet-drag-zone">
33 <button type="button" class="profile-sheet-handle" id="profile-sheet-close" aria-label="Sluiten"></button>
34 </div>
35
36 <!-- Identity header -->
37 <div class="profile-sheet-header">
38 <% if (user.avatar_url) { %>
39 <img class="profile-sheet-avatar profile-sheet-avatar-img" src="<%= user.avatar_url %>" alt="" aria-hidden="true">
40 <% } else { %>
41 <div class="profile-sheet-avatar" aria-hidden="true"><%= user.username.charAt(0).toUpperCase() %></div>
42 <% } %>
43 <div class="profile-sheet-meta">
44 <div class="profile-sheet-name"><%= user.username %></div>
45 <% if (_roleLabel) { %><div class="profile-sheet-role"><%= _roleLabel %></div><% } %>
46 </div>
47 </div>
48
49 <!-- Group 1: primary destinations -->
50 <ul class="profile-sheet-menu" role="menu">
51 <li role="none">
52 <a href="/account" class="profile-sheet-item" role="menuitem" data-close-sheet>
53 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
54 <span class="psi-label">Account</span>
55 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
56 </a>
57 </li>
58 <% if (typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
59 <li role="none">
60 <a href="/notifications" class="profile-sheet-item" role="menuitem" data-close-sheet>
61 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
62 <span class="psi-label"><%= t('nav.notifications') %><% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %> (<%= notifUnread %>)<% } %></span>
63 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
64 </a>
65 </li>
66 <% } %>
67 <% if (typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
68 <li role="none">
69 <a href="/news" class="profile-sheet-item" role="menuitem" data-close-sheet
70 hx-get="/news?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/news" hx-indicator="#pcms-loading">
71 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
72 <span class="psi-label"><%= t('nav.fediverse') %></span>
73 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
74 </a>
75 </li>
76 <% } %>
77 <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
78 <li role="none">
79 <a href="/admin" class="profile-sheet-item" role="menuitem" data-close-sheet>
80 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z"/></svg>
81 <span class="psi-label">Beheer</span>
82 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
83 </a>
84 </li>
85 <% } %>
86 <% if (_canPost) { %>
87 <li role="none">
88 <a href="<%= _base %>/posts/new" class="profile-sheet-item" role="menuitem" data-close-sheet
89 hx-get="<%= _base %>/posts/new?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
90 hx-push-url="<%= _base %>/posts/new" hx-indicator="#pcms-loading">
91 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
92 <span class="psi-label">Nieuwe post</span>
93 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
94 </a>
95 </li>
96 <% } %>
97 </ul>
98
99 <div class="profile-sheet-divider" role="separator"></div>
100
101 <!-- Group 2: preferences (inline toggles) -->
102 <ul class="profile-sheet-menu" role="menu">
103 <li role="none">
104 <button type="button" class="profile-sheet-item" id="profile-sheet-theme" role="menuitemcheckbox">
105 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
106 <span class="psi-label">Thema</span>
107 <span class="psi-meta" id="profile-sheet-theme-state">Donker</span>
108 </button>
109 </li>
110 </ul>
111
112 <div class="profile-sheet-divider" role="separator"></div>
113
114 <!-- Group 3: destructive actions -->
115 <ul class="profile-sheet-menu" role="menu">
116 <li role="none">
117 <a href="/auth/logout" class="profile-sheet-item is-destructive" role="menuitem">
118 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
119 <span class="psi-label">Uitloggen</span>
120 </a>
121 </li>
122 </ul>
123
124 </div>
125</div>
126
127<style>
128/* ============================================================
129 PROFILE SHEET — mobile bottom-sheet menu
130 Hidden ≥768px (desktop uses .user-menu dropdown in top-nav).
131 ============================================================ */
132
133.profile-sheet {
134 position: fixed;
135 inset: 0;
136 z-index: 1100; /* same as audio-sheet — modal layer */
137 pointer-events: none;
138 opacity: 0;
139 transition: opacity .2s ease;
140}
141.profile-sheet.is-open {
142 pointer-events: auto;
143 opacity: 1;
144}
145
146@media (min-width: 768px) {
147 /* Desktop has its own user-menu dropdown */
148 .profile-sheet { display: none !important; }
149}
150
151body.profile-sheet-locked { overflow: hidden; }
152
153.profile-sheet-backdrop {
154 position: absolute;
155 inset: 0;
156 background: color-mix(in srgb, var(--ink, #000) 35%, transparent);
157 backdrop-filter: blur(6px);
158 -webkit-backdrop-filter: blur(6px);
159}
160
161.profile-sheet-panel {
162 position: absolute;
163 left: 0; right: 0; bottom: 0;
164 background: var(--paper);
165 color: var(--ink);
166 border-top-left-radius: 22px;
167 border-top-right-radius: 22px;
168 box-shadow: 0 -20px 60px rgba(0, 0, 0, .25);
169 padding-bottom: max(.75rem, env(safe-area-inset-bottom, 0));
170 transform: translateY(100%);
171 transition: transform .3s cubic-bezier(.22, .9, .3, 1);
172 display: flex;
173 flex-direction: column;
174 max-height: 85vh;
175 overflow-y: auto;
176 overscroll-behavior: contain;
177}
178.profile-sheet.is-open .profile-sheet-panel {
179 transform: translateY(var(--pcms-drag-y, 0px));
180}
181.profile-sheet-panel.is-dragging { transition: none; will-change: transform; }
182
183[data-theme="dark"] .profile-sheet-panel {
184 box-shadow: 0 -20px 60px rgba(0, 0, 0, .5);
185}
186
187/* ── Drag handle zone ───────────────────────────── */
188.profile-sheet-drag-zone {
189 flex-shrink: 0;
190 padding: 12px 16px 4px;
191 /* MUST be 'none' so JS pointer events get the gesture (see audio-sheet) */
192 touch-action: none;
193 user-select: none;
194 -webkit-user-select: none;
195 cursor: grab;
196}
197.profile-sheet-drag-zone:active { cursor: grabbing; }
198@media (hover: hover) and (pointer: fine) {
199 /* On desktop the sheet isn't shown anyway, but be safe */
200 .profile-sheet-drag-zone { touch-action: auto; cursor: default; }
201}
202
203.profile-sheet-handle {
204 display: block;
205 margin: 0 auto;
206 width: 44px;
207 height: 4px;
208 border-radius: 2px;
209 background: color-mix(in srgb, var(--ink, #000) 25%, transparent);
210 border: 0;
211 padding: 0;
212 position: relative;
213}
214.profile-sheet-handle::before {
215 /* Larger invisible tap target on the handle itself */
216 content: "";
217 position: absolute;
218 inset: -16px -32px;
219}
220
221/* ── Identity header ────────────────────────────── */
222.profile-sheet-header {
223 display: flex;
224 align-items: center;
225 gap: 14px;
226 padding: 8px 20px 18px;
227}
228.profile-sheet-avatar {
229 width: 48px;
230 height: 48px;
231 display: flex;
232 align-items: center;
233 justify-content: center;
234 border-radius: 50%;
235 background: var(--paper-2); /* neutral so transparent avatars blend */
236 color: var(--accent); /* fallback initial color */
237 font-family: var(--font-ui);
238 font-weight: 700;
239 font-size: 20px;
240 flex-shrink: 0;
241 overflow: hidden;
242}
243.profile-sheet-avatar-img {
244 /* When rendered as <img>, fill the round container edge-to-edge. */
245 object-fit: cover;
246}
247.profile-sheet-meta { min-width: 0; }
248.profile-sheet-name {
249 font-family: var(--font-display, var(--font-ui));
250 font-size: 1.2rem;
251 font-weight: 600;
252 line-height: 1.2;
253 color: var(--ink);
254}
255.profile-sheet-role {
256 font-family: var(--font-ui);
257 font-size: .82rem;
258 color: var(--ink-soft);
259 margin-top: 3px;
260 text-transform: lowercase;
261 letter-spacing: .02em;
262}
263
264/* ── Menu list ──────────────────────────────────── */
265.profile-sheet-menu {
266 list-style: none;
267 margin: 0;
268 padding: 0;
269}
270.profile-sheet-divider {
271 height: 1px;
272 background: var(--rule);
273 margin: 4px 20px;
274}
275.profile-sheet-item {
276 display: flex;
277 align-items: center;
278 gap: 14px;
279 width: 100%;
280 min-height: 52px;
281 padding: 12px 20px;
282 background: transparent;
283 border: 0;
284 color: var(--ink);
285 text-decoration: none;
286 font-family: var(--font-ui);
287 font-size: 1rem;
288 text-align: left;
289 cursor: pointer;
290 -webkit-tap-highlight-color: transparent;
291 transition: background .12s ease, transform .12s ease;
292}
293.profile-sheet-item:active {
294 background: var(--paper-2);
295 transform: scale(0.99);
296}
297@media (hover: hover) {
298 .profile-sheet-item:hover { background: var(--paper-2); }
299}
300
301.psi-icon {
302 width: 22px;
303 height: 22px;
304 flex-shrink: 0;
305 color: var(--ink-soft);
306}
307.psi-label {
308 flex: 1;
309 min-width: 0;
310}
311.psi-chev {
312 width: 16px;
313 height: 16px;
314 color: var(--ink-muted);
315 flex-shrink: 0;
316}
317.psi-meta {
318 font-size: .85rem;
319 color: var(--ink-soft);
320 font-family: var(--font-ui);
321}
322
323.profile-sheet-item.is-destructive { color: #dc2626; }
324.profile-sheet-item.is-destructive .psi-icon { color: currentColor; }
325[data-theme="dark"] .profile-sheet-item.is-destructive { color: #f87171; }
326</style>
327
328<script>
329(function() {
330 const sheet = document.getElementById('profile-sheet');
331 if (!sheet) return;
332
333 const backdrop = document.getElementById('profile-sheet-backdrop');
334 const panel = sheet.querySelector('.profile-sheet-panel');
335 const closeBtn = document.getElementById('profile-sheet-close');
336 const dragZone = document.getElementById('profile-sheet-drag-zone');
337 const themeBtn = document.getElementById('profile-sheet-theme');
338 const themeLbl = document.getElementById('profile-sheet-theme-state');
339
340 function openSheet() {
341 sheet.classList.add('is-open');
342 sheet.setAttribute('aria-hidden', 'false');
343 document.body.classList.add('profile-sheet-locked');
344 syncTheme();
345 }
346 function closeSheet() {
347 sheet.classList.remove('is-open');
348 sheet.setAttribute('aria-hidden', 'true');
349 document.body.classList.remove('profile-sheet-locked');
350 panel.style.removeProperty('--pcms-drag-y');
351 }
352
353 // Open: any element with [data-profile-sheet-toggle]
354 document.addEventListener('click', function(e) {
355 const trigger = e.target.closest('[data-profile-sheet-toggle]');
356 if (trigger) {
357 e.preventDefault();
358 openSheet();
359 }
360 });
361
362 // Close: backdrop tap, handle tap, ESC, or any [data-close-sheet] item
363 if (backdrop) backdrop.addEventListener('click', closeSheet);
364 if (closeBtn) closeBtn.addEventListener('click', closeSheet);
365 document.addEventListener('keydown', (e) => {
366 if (e.key === 'Escape' && sheet.classList.contains('is-open')) closeSheet();
367 });
368
369 // Menu items that navigate: close synchronously before nav fires
370 sheet.querySelectorAll('[data-close-sheet]').forEach((el) => {
371 el.addEventListener('click', closeSheet);
372 });
373
374 // Drag-down-to-close on touch.
375 // Uses --pcms-drag-y custom property rather than overwriting the panel's
376 // transform string. This composes correctly with any horizontal centering
377 // (currently none here, but the pattern matches audio-sheet for safety).
378 let startY = 0, lastY = 0, dragging = false;
379 function onDown(e) {
380 if (e.pointerType !== 'touch') return;
381 if (panel.scrollTop > 0) return;
382 startY = lastY = e.clientY;
383 dragging = true;
384 panel.classList.add('is-dragging');
385 }
386 function onMove(e) {
387 if (!dragging) return;
388 lastY = e.clientY;
389 const dy = Math.max(0, lastY - startY);
390 panel.style.setProperty('--pcms-drag-y', dy + 'px');
391 }
392 function onUp() {
393 if (!dragging) return;
394 dragging = false;
395 panel.classList.remove('is-dragging');
396 const dy = lastY - startY;
397 if (dy > 80) closeSheet();
398 else panel.style.removeProperty('--pcms-drag-y');
399 }
400 if (window.PointerEvent && dragZone) {
401 dragZone.addEventListener('pointerdown', onDown);
402 document.addEventListener('pointermove', onMove);
403 document.addEventListener('pointerup', onUp);
404 document.addEventListener('pointercancel', onUp);
405 }
406
407 // Theme toggle inside sheet — syncs label
408 function syncTheme() {
409 if (!themeLbl) return;
410 const t = document.documentElement.getAttribute('data-theme') || 'dark';
411 themeLbl.textContent = t === 'dark' ? 'Donker' : 'Licht';
412 }
413 if (themeBtn) {
414 themeBtn.addEventListener('click', function() {
415 const cur = document.documentElement.getAttribute('data-theme') || 'dark';
416 const next = cur === 'dark' ? 'light' : 'dark';
417 document.documentElement.setAttribute('data-theme', next);
418 try { localStorage.setItem('pcms-theme', next); } catch (e) {}
419 syncTheme();
420 });
421 }
422})();
423</script>
Note: See TracBrowser for help on using the repository browser.