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

main
Last change on this file since 156baa3 was 156baa3, checked in by roboburr <roboburr@…>, 5 weeks ago

De chrome en vier schermen met servergegevens (shaer-bqr, stap 3e)

topnav en profile-sheet naar mod/chrome.js; admin-media, admin-videos, paid-gate,
paid-passkey en admin-playlists naar eigen modules.

TWEE MANIEREN om servergegevens bij een module te krijgen, en de keuze is niet
willekeurig:

OP HET ELEMENT waar de tekst bij EEN ding hoort. De zoekteksten staan nu op

#search-overlay, de twee themalabels op het label zelf. Dat is
ook robuuster: bij een htmx-navigatie wordt de chrome
vervangen, en dan komt de tekst gewoon mee.

VIA pageData() waar het om paginabrede gegevens gaat (csrf, slug, een blob,

een setje meldingen).

De chrome kon niet via pageData: die leeft langer dan een pagina.

DRIE FOUTEN ONDERWEG, alle drie gevangen voordat er iets stuk ging:

  1. Ik zocht de eerste </script> in het HELE bestand in plaats van die na het openende tag. paid-gate en paid-passkey hebben een <script src> erboven, dus het "blok" werd leeg en er werd een lege module geschreven. Teruggezet uit git en de zoekfunctie kreeg een offset plus een controle dat het blok niet leeg is.
  2. Een zoekterm sloot te vroeg af: de tekst van delete_failed loopt door in dezelfde JS-string (": " + err.message), dus het patroon met de sluitquote matchte niet.
  3. Een eerdere poging verving nul regels en schreef toch. Nu telt de helper de vervangingen en weigert als het er niet exact zoveel zijn als opgegeven.

Die controle op "geen enkele module bevat nog EJS" draait mee, en er is er nu een
bij die een verdacht lege module afvangt -- precies wat fout 1 opleverde.

Templates compileren, suite 565/565.

  • Property mode set to 100644
File size: 12.9 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" data-dark="<%= t('nav.dark') %>" data-light="<%= t('nav.light') %>"><%= 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<%# Het script van het profielblad staat in assets/js/mod/chrome.js (shaer-bqr). %>
Note: See TracBrowser for help on using the repository browser.