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

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

fix(i18n): localize the mobile profile-sheet menu labels

The mobile profile menu had hardcoded Dutch labels (Account, Beheer, Nieuwe post, Thema,
Donker/Licht, Uitloggen) so they didn't translate on EN/DE sites. They now use t().

  • src/views/partials/profile-sheet.ejs — Account/Admin/New post/Logout reuse the existing nav.* keys; the theme item + its JS state label use new nav.theme_label/nav.dark/nav.light
  • src/services/i18n.js — add nav.theme_label, nav.dark, nav.light (nl/en/de)

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

  • Property mode set to 100644
File size: 16.2 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="<%= avatar(user.avatar_url, 128) %>" 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"><%= t('nav.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"><%= t('nav.admin') %></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"><%= t('nav.new_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"><%= t('nav.theme_label') %></span>
107 <span class="psi-meta" id="profile-sheet-theme-state"><%= t('nav.dark') %></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"><%= t('nav.logout') %></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' ? '<%= t('nav.dark') %>' : '<%= t('nav.light') %>';
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.