source: Klonkt/src/views/partials/profile-header.ejs@ 00a2bcd

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

feat(profile): click the profile photo to open it in a lightbox

When the header has a real photo it becomes a lightbox trigger (enlarged view, click/Esc
to close) instead of a home link; the name still links home. No photo → unchanged home
link with the fallback avatar.

  • Property mode set to 100644
File size: 13.2 KB
Line 
1<!-- Profile header (v9 structure)
2 Body class controls collapse:
3 .on-home → full size, avatar + bio + links
4 .on-post → compact, just avatar + name
5 .on-special → compact (archive/search/tag pages)
6 .on-admin → hidden (admin pages don't show profile)
7-->
8
9<%
10// Show profile-header unless we're on an admin page or the site explicitly
11// disabled it via profile_enabled=0.
12const _showProfile = site
13 && (site.profile_enabled === undefined || site.profile_enabled !== 0)
14 && !(typeof bodyClass !== 'undefined' && bodyClass.indexOf('on-admin') >= 0);
15
16// profile_name and profile_bio used to be optional overrides exposed in the
17// admin form. P62 removed those form fields — title and description are the
18// single source of truth. We still consult the override columns first for
19// backward compat with any legacy data, but the form no longer writes them.
20const _displayName = (site && (site.profile_name || site.title)) || '';
21const _bioText = (site && (site.profile_bio || site.description || site.tagline)) || '';
22
23// Parse profile_links JSON column once.
24let _profileLinks = [];
25if (site && site.profile_links) {
26 try { _profileLinks = JSON.parse(site.profile_links) || []; } catch (e) { _profileLinks = []; }
27}
28%>
29
30<% if (_showProfile) { %>
31<aside class="profile-header" aria-label="Site profile">
32 <div class="container profile-header-inner">
33
34 <% var _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; %>
35 <% var _avatarUrl = site.profile_photo || ((typeof siteOwnerAvatar !== 'undefined' && siteOwnerAvatar) || ''); %>
36 <% if (_avatarUrl) { %>
37 <%# With a real photo: clicking opens a lightbox (enlarged view) instead of going home. %>
38 <button type="button" class="profile-photo" data-lightbox="<%= _avatarUrl %>" aria-label="<%= _displayName || 'Profile photo' %>">
39 <img src="<%= _avatarUrl %>" alt="">
40 </button>
41 <% } else { %>
42 <a class="profile-photo" href="<%= _base %>/"
43 hx-get="<%= _base %>/?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
44 hx-push-url="<%= _base %>/" hx-indicator="#pcms-loading" aria-label="Home">
45 <% if (site.enable_audio_player && (typeof audioEnabled === 'undefined' || audioEnabled)) { %>
46 <span class="profile-photo-fallback profile-photo-fallback--music" aria-hidden="true">
47 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
48 <path d="M9 17V5l12-2v12"/>
49 <circle cx="6" cy="17" r="3"/>
50 <circle cx="18" cy="15" r="3"/>
51 </svg>
52 </span>
53 <% } else { %>
54 <span class="profile-photo-fallback" aria-hidden="true"><%= (_displayName || '?').charAt(0).toUpperCase() %></span>
55 <% } %>
56 </a>
57 <% } %>
58
59 <div class="profile-info">
60 <h2 class="profile-name">
61 <a href="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/"
62 hx-get="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/?partial=1"
63 hx-target="#pcms-main" hx-swap="innerHTML"
64 hx-push-url="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/"
65 hx-indicator="#pcms-loading">
66 <%= _displayName %>
67 </a>
68 </h2>
69 <% if (_bioText) { %>
70 <p class="profile-bio"><%= _bioText %></p>
71 <% } %>
72
73 <%# Follow-via-fediverse: a compact icon in the links row (keeps the header
74 short), and hidden for the owner — you don't follow yourself. %>
75 <% var _showFollow = (typeof canManageFedi === 'undefined' || !canManageFedi) && site && site.slug && (typeof apEnabled === 'undefined' || apEnabled); %>
76 <% if (_profileLinks.length || _showFollow) { %>
77 <% const _platforms = (typeof platforms_catalog !== 'undefined' && platforms_catalog) || {}; %>
78 <ul class="profile-links" aria-label="External links">
79 <% _profileLinks.forEach(function(l) {
80 const meta = _platforms[l.platform];
81 if (!meta) return; %>
82 <li>
83 <a class="profile-link profile-link--<%= l.platform %>"
84 href="<%= l.platform === 'email' && l.url.indexOf('mailto:') !== 0 ? ('mailto:' + l.url) : l.url %>"
85 target="<%= l.platform === 'email' ? '_self' : '_blank' %>"
86 rel="noopener noreferrer"
87 aria-label="<%= meta.label %>"
88 title="<%= meta.label %>"
89 style="--brand: <%= meta.brand %>">
90 <%- meta.svg %>
91 </a>
92 </li>
93 <% }); %>
94 <% if (_showFollow) { %>
95 <li style="order:-1">
96 <button type="button" class="profile-link profile-fedi-link" data-fedi-actor-slug="<%= site.slug %>"
97 aria-label="<%= t('fedi.profile_follow') %>" title="<%= t('fedi.profile_follow') %>"><svg viewBox="151 31 530 530" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><g transform="translate(6.6789703,-32.495842)"><path d="M257.1,266.8c-4.9,9.4-12.6,17.1-22.1,22l121.3,121.8l29.2-14.8L257.1,266.8z M417.1,427.4l-29.2,14.8l61.5,61.7c4.9-9.4,12.6-17.1,22.1-22L417.1,427.4z"/><path d="M557.5,315l-68.7,34.8l5.1,32.4l77.7-39.4C564.1,335.2,559.2,325.5,557.5,315z M448.9,370l-162.4,82.3c7.5,7.6,12.4,17.3,14.1,27.8L454,402.4L448.9,370z"/><path d="M396.7,167.3l-78.4,153l23.1,23.2l83-162C414,179.7,404.3,174.7,396.7,167.3z M298,360l-39.7,77.5c10.5,1.8,20.2,6.7,27.7,14.2l35.1-68.5L298,360z"/><path d="M234.3,289.1c-8,4-16.9,5.9-25.8,5.4c-1.7-0.1-3.3-0.3-5-0.5l23.2,148.2c8-4,16.9-5.9,25.8-5.4c1.7,0.1,3.3,0.3,5,0.5L234.3,289.1z"/><path d="M300.8,480.8c0.5,3.4,0.7,6.9,0.5,10.4c-0.4,7.1-2.3,14-5.5,20.4l148.2,23.8c-0.5-3.4-0.7-6.9-0.5-10.4c0.4-7.1,2.3-14,5.5-20.4L300.8,480.8z"/><path d="M572.1,343.3l-68.4,133.6c10.5,1.8,20.2,6.7,27.7,14.2l68.4-133.6C589.4,355.8,579.7,350.8,572.1,343.3z"/><path d="M478.8,154.4c-4.9,9.4-12.6,17.1-22.1,22l105.9,106.4c4.9-9.4,12.6-17.1,22.1-22L478.8,154.4z"/><path d="M382.1,138.9l-133.9,67.9c7.5,7.6,12.4,17.3,14.1,27.8l133.9-67.9C388.7,159.1,383.8,149.4,382.1,138.9z"/><path d="M456.5,176.6c-8.1,4.1-17.1,6.1-26.2,5.6c-1.5-0.1-3-0.3-4.5-0.5l11.9,76l32.4,5.2L456.5,176.6z M444.5,301.8l28.1,179.6c7.9-3.9,16.7-5.7,25.4-5.2c1.8,0.1,3.6,0.3,5.4,0.6L476.8,307L444.5,301.8z"/><path d="M262.4,235.2c0.6,3.5,0.7,7,0.6,10.6c-0.4,7-2.2,13.9-5.4,20.2l76,12.2l14.9-29.2L262.4,235.2z M392.7,256.1l-14.9,29.2l179.6,28.9c-0.5-3.4-0.7-6.9-0.5-10.3c0.4-7.1,2.3-14.1,5.5-20.5L392.7,256.1z"/><circle cx="433" cy="130.6" r="47"/><circle cx="608.4" cy="306.6" r="47"/><circle cx="495.1" cy="527.8" r="47"/><circle cx="249.7" cy="488.4" r="47"/><circle cx="211.3" cy="242.9" r="47"/></g></svg></button>
98 </li>
99 <% } %>
100 </ul>
101 <% } %>
102 </div>
103
104 </div>
105</aside>
106<script>
107(function () {
108 if (window.__pcmsFediFollowWired) return;
109 window.__pcmsFediFollowWired = true;
110 document.addEventListener('click', function (e) {
111 var b = e.target.closest && e.target.closest('.profile-fedi-link');
112 if (!b) return;
113 var raw = window.prompt(<%- JSON.stringify(t('fedi.remote_prompt')) %>, '');
114 if (!raw) return;
115 // Strip a full @user@host handle (and any scheme/path) down to the server host.
116 var server = raw.trim().replace(/^@?[^@\s]*@/, '').replace(/^https?:\/\//i, '').replace(/\/.*$/, '').trim();
117 if (!server) return;
118 var actor = location.origin + '/ap/users/' + encodeURIComponent(b.getAttribute('data-fedi-actor-slug') || '');
119 location.href = 'https://' + server + '/authorize_interaction?uri=' + encodeURIComponent(actor);
120 });
121})();
122
123/* Lightbox: click the profile photo to see it enlarged. */
124(function () {
125 if (window.__pcmsLightboxWired) return; window.__pcmsLightboxWired = true;
126 var lb = null;
127 function close(){ if (lb) lb.classList.remove('is-open'); }
128 document.addEventListener('click', function (e) {
129 var t = e.target.closest && e.target.closest('.profile-photo[data-lightbox]');
130 if (!t) return;
131 e.preventDefault();
132 var url = t.getAttribute('data-lightbox'); if (!url) return;
133 if (!lb) {
134 lb = document.createElement('div'); lb.className = 'pcms-lightbox';
135 lb.innerHTML = '<img alt=""><button type="button" class="pcms-lightbox-close" aria-label="Sluiten">&times;</button>';
136 document.body.appendChild(lb);
137 lb.addEventListener('click', function (ev) { if (ev.target === lb || (ev.target.closest && ev.target.closest('.pcms-lightbox-close'))) close(); });
138 }
139 lb.querySelector('img').src = url;
140 lb.classList.add('is-open');
141 });
142 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
143})();
144</script>
145<% } %>
146
147<style>
148/* Profile header (v9 collapse pattern) */
149.profile-header {
150 border-bottom: 1px solid var(--rule);
151 background: var(--paper);
152 padding: 1.5rem 0;
153 transition: padding 250ms cubic-bezier(.4,0,.2,1);
154}
155.profile-header-inner {
156 max-width: 800px;
157 margin: 0 auto;
158 padding: 0 1rem;
159 display: flex;
160 align-items: center;
161 gap: 1.25rem;
162 transition: gap 250ms;
163}
164.profile-photo {
165 flex-shrink: 0;
166 display: block;
167 width: 80px;
168 height: 80px;
169 padding: 0;
170 border-radius: 50%;
171 overflow: hidden;
172 background: var(--paper-2);
173 border: 2px solid var(--rule);
174 transition: width 250ms, height 250ms;
175 cursor: pointer;
176 font: inherit;
177 -webkit-appearance: none;
178 appearance: none;
179}
180.profile-photo[data-lightbox] { cursor: zoom-in; }
181.profile-photo:hover {
182 border-color: var(--accent);
183}
184.profile-photo img {
185 width: 100%;
186 height: 100%;
187 object-fit: cover;
188 pointer-events: none;
189}
190.profile-photo-fallback {
191 display: flex;
192 align-items: center;
193 justify-content: center;
194 width: 100%;
195 height: 100%;
196 font-family: var(--font-display, serif);
197 font-size: 2rem;
198 font-weight: 700;
199 color: var(--ink-soft);
200 background: var(--paper-2);
201 pointer-events: none;
202}
203.profile-photo-fallback--music {
204 color: var(--accent);
205}
206.profile-photo-fallback--music svg {
207 width: 50%;
208 height: 50%;
209}
210.profile-info {
211 flex: 1;
212 min-width: 0;
213}
214.profile-name {
215 font-family: var(--font-display, serif);
216 font-size: 1.75rem;
217 margin: 0 0 0.25rem;
218 line-height: 1.2;
219}
220.profile-name a {
221 color: var(--ink);
222 text-decoration: none;
223}
224.profile-bio {
225 margin: 0;
226 color: var(--ink-soft);
227 font-size: 0.95rem;
228 line-height: 1.4;
229}
230
231/* Compact mode on post / special pages */
232.on-post .profile-header,
233.on-special .profile-header,
234.on-search .profile-header,
235.on-archive .profile-header {
236 padding: 0.75rem 0;
237}
238.on-post .profile-photo,
239.on-special .profile-photo,
240.on-search .profile-photo,
241.on-archive .profile-photo {
242 width: 40px;
243 height: 40px;
244 border-width: 1px;
245}
246.on-post .profile-photo-fallback,
247.on-special .profile-photo-fallback,
248.on-search .profile-photo-fallback,
249.on-archive .profile-photo-fallback {
250 font-size: 1rem;
251}
252.on-post .profile-name,
253.on-special .profile-name,
254.on-search .profile-name,
255.on-archive .profile-name {
256 font-size: 1.1rem;
257 margin: 0;
258}
259.on-post .profile-bio,
260.on-special .profile-bio,
261.on-search .profile-bio,
262.on-archive .profile-bio {
263 display: none;
264}
265
266/* Hide on admin pages */
267.on-admin .profile-header { display: none; }
268
269/* Mobile tweaks */
270@media (max-width: 600px) {
271 .profile-photo { width: 64px; height: 64px; }
272 .profile-photo-fallback { font-size: 1.5rem; }
273 .profile-name { font-size: 1.4rem; }
274}
275
276/* Social / streaming icon row */
277.profile-links {
278 list-style: none;
279 margin: 0.5rem 0 0;
280 padding: 0;
281 display: flex;
282 gap: 0.5rem;
283 flex-wrap: wrap;
284}
285.profile-link {
286 display: inline-flex;
287 align-items: center;
288 justify-content: center;
289 width: 36px;
290 height: 36px;
291 border-radius: 50%;
292 background: var(--paper-2);
293 color: var(--ink-soft);
294 text-decoration: none;
295 transition: background 120ms, color 120ms, transform 100ms ease;
296}
297.profile-link:hover {
298 background: var(--brand, var(--accent));
299 color: white;
300 transform: translateY(-1px);
301}
302.profile-link svg { width: 18px; height: 18px; display: block; }
303
304/* Compact mode collapses social links too (post pages) */
305.on-post .profile-links,
306.on-special .profile-links,
307.on-archive .profile-links { display: none; }
308
309/* Follow-via-fediverse: compact icon button, sits in the links row */
310.profile-fedi-link { border: 0; padding: 0; background: var(--paper-2); cursor: pointer; -webkit-appearance: none; appearance: none; }
311/* The fediverse logo is an open line-network, so it reads smaller than a solid
312 glyph at 18px — render it larger INSIDE the same 36px circle (circle unchanged). */
313.profile-fedi-link svg { width: 22px; height: 22px; }
314
315/* Profile-photo lightbox overlay */
316.pcms-lightbox { position: fixed; inset: 0; z-index: 2147483600; display: none;
317 align-items: center; justify-content: center; padding: 4vmin;
318 background: rgba(0,0,0,.86); cursor: zoom-out; }
319.pcms-lightbox.is-open { display: flex; }
320.pcms-lightbox img { max-width: 92vw; max-height: 92vh; border-radius: 12px;
321 box-shadow: 0 10px 50px rgba(0,0,0,.55); cursor: default; }
322.pcms-lightbox-close { position: fixed; top: 1rem; right: 1.1rem; width: 44px; height: 44px;
323 border: 0; border-radius: 50%; background: rgba(255,255,255,.14); color: #fff;
324 font-size: 1.9rem; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; }
325.pcms-lightbox-close:hover { background: rgba(255,255,255,.26); }
326</style>
Note: See TracBrowser for help on using the repository browser.