openvibers/openvibe.mediahub Commits

Pics: a picture's action bar, the gallery's search field fixed, and a designed empty gallery (#359)

OP OpenVibers committed 4 days ago · 58609416f8 · 4 files, +55 −8

* A picture's page: one action bar (who, views, Like with its count, Share), make-something buttons with glyphs, comments as a card

Like is a heart that fills in Pics pink when pressed, its count beside it; the script changes only the word. Edit with AI, Make a meme and Take a photo are quiet kit buttons with their glyphs; on a phone the buttons share the width. The comments card holds its one button in its head.

* Gallery: the search field loses the stale boxed-input rule; an empty gallery or search is the kit's empty state

A leftover .ovp-gallery-search input rule at the end of pics.css drew a box inside the rounded field in production. Empty: an icon, a title, one line and Your pictures (or Clear the search).

Changes

ddiff --git a/public/css/pics.css b/public/css/pics.css
iindex 9862ad0..ccf65be 100644
---- a/public/css/pics.css
++++ b/public/css/pics.css
@@@ -247,7 +247,7 @@
.ovp-gc-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0; }
.ovp-tag { display: inline-flex; align-items: center; min-height: 26px; padding: 0 10px; border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); color: var(--ovp-dim); font-size: 12.5px; font-weight: 600; text-decoration: none; }
.ovp-tag:hover { background: color-mix(in srgb, var(--ovp-pink) 18%, transparent); color: var(--text-primary, #e6edf7); }
--.ovp-gallery-empty { padding: 30px 0; }
++.ovp-gallery-empty { margin: 8px 0 24px; }
.ovp-drop--compact .ovp-drop-icon, .ovp-drop--compact .ovp-drop-fields { display: none; }
.ovp-drop--compact .ovp-drop-inner { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 14px; }
.ovp-drop--compact .ovp-drop-title { margin: 0; font-size: 16px; }
@@@ -270,5 +270,37 @@
.ovp-drop--compact .ovp-drop-inner p { margin: 0; }
.ovp-drop--signin.ovp-drop--compact .ovp-drop-title { display: none; }
.ovp-drop--signin.ovp-drop--compact .ovp-drop-hint { display: block; flex: 1; }
--.ovp-gallery-search input { background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border); border-radius: 8px; min-height: 44px; padding: 8px 12px; }
.ovp-btn { min-height: 44px; }
++
++/* The picture's action bar (owner's design pass, 11-09): who published it and how many saw it on the left, Like (a heart
++ that fills in Pics pink, with its count) and Share on the right; under it, quiet buttons to make something with the
++ picture; the comments as a card whose head holds the one button. */
++.ovp-pic-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 16px; margin: 14px 0 10px; }
++.ovp-pic-who { display: flex; align-items: center; gap: 10px; min-width: 0; }
++.ovp-pic-av { display: grid; place-items: center; flex: none; width: 40px; height: 40px; border-radius: 50%; background: color-mix(in srgb, var(--ovp-pink) 26%, var(--ovp-card)); color: var(--text-primary, #e6edf7); font-weight: 800; }
++.ovp-pic-who-txt { display: grid; min-width: 0; line-height: 1.25; }
++.ovp-pic-who-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.ovp-pic-who-txt small { color: var(--ovp-dim); font-size: 13px; }
++.ovp-pic-acts { display: flex; align-items: center; gap: 8px; margin-left: auto; }
++.ovp-glyph { display: inline-grid; place-items: center; flex: none; }
++.ovp-glyph svg { display: block; }
++.ovp-like { gap: 8px; }
++.ovp-like-n { padding-left: 8px; border-left: 1px solid var(--ovp-line); color: var(--ovp-dim); font-variant-numeric: tabular-nums; }
++.ovp-like[aria-pressed="true"] { border-color: color-mix(in srgb, var(--ovp-pink) 60%, transparent); background: color-mix(in srgb, var(--ovp-pink) 14%, transparent); }
++.ovp-like[aria-pressed="true"] .ovp-glyph { color: var(--ovp-pink); }
++.ovp-like[aria-pressed="true"] .ovp-glyph svg { fill: currentColor; }
++.ovp-pic-status:empty { display: none; }
++.ovp-pic-status { margin: 0 0 8px; color: var(--ovp-dim); font-size: 14px; }
++.ovp-pic-make { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 14px; }
++.ovp-pic-make .ovk-btn, .ovp-comments-head .ovk-btn { gap: 8px; }
++.ovp-comments { padding: 14px 16px; border: 1px solid var(--ovp-line); border-radius: 16px; background: var(--ovp-card); }
++.ovp-comments-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
++.ovp-comments-head h2 { margin: 0; }
++.ovp-comments-list:empty { display: none; }
++.ovp-comments-list { margin-top: 12px; display: grid; gap: 8px; }
++.ovp-comments-list p { margin: 0; padding: 10px 12px; border-radius: 12px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); overflow-wrap: anywhere; }
++@media (max-width: 560px) {
++ .ovp-pic-acts { margin-left: 0; width: 100%; }
++ .ovp-pic-acts > * { flex: 1; justify-content: center; }
++ .ovp-pic-make .ovk-btn { flex: 1 1 auto; justify-content: center; }
++}
ddiff --git a/public/js/pics-gallery.js b/public/js/pics-gallery.js
iindex 27335e3..2c9dd6d 100644
---- a/public/js/pics-gallery.js
++++ b/public/js/pics-gallery.js
@@@ -48,7 +48,9 @@
if (!response.ok) throw new Error(response.status === 401 ? 'Sign in to like this picture.' : 'Could not update your like. Try again.');
var data = await response.json();
document.querySelector('[data-like-count]').textContent = data.likes;
-- like.setAttribute('aria-pressed', String(!removing)); like.textContent = removing ? 'Like' : 'Liked'; status.textContent = '';
++ like.setAttribute('aria-pressed', String(!removing));
++ var word = like.querySelector('[data-like-word]'); if (word) word.textContent = removing ? 'Like' : 'Liked'; else like.textContent = removing ? 'Like' : 'Liked';
++ status.textContent = '';
} catch (error) { status.textContent = error.message; }
finally { like.disabled = false; }
});
ddiff --git a/server/pics/gallery.js b/server/pics/gallery.js
iindex 428a7b9..1c0623c 100644
---- a/server/pics/gallery.js
++++ b/server/pics/gallery.js
@@@ -9,6 +9,7 @@
*/
const { html, raw } = require('../render/html');
const glyphs = require('openvibe-shared/glyphs');
++const kit = require('openvibe-shared/kit');
const labelsUi = require('../render/labels-ui');
const PUBLIC = "i.listed = true AND i.state = 'active' AND i.audience = 'public' AND EXISTS (SELECT 1 FROM shares sh WHERE sh.service = 'mediahub' AND sh.kind = 'picture' AND sh.resource_id = i.id AND sh.audience = 'public')";
const isPublic = (row) => Boolean(row.listed && row.state === 'active' && row.audience === 'public');
@@@ -76,7 +77,9 @@ function render(pics, result) {
<nav class="ovp-gallery-tabs" aria-label="Gallery order">${['popular', 'new', 'rising'].map((sort) => html`<a href="${href(sort)}" ${sort === result.sort ? raw('aria-current="page"') : ''}>${sort[0].toUpperCase() + sort.slice(1)}</a>`)}</nav></div>
${result.tag ? html`<p class="ovp-gallery-tagged">Tagged <b>#${result.tag}</b> <a href="${href(result.sort).replace(/&?tag=[^&]*/, '')}">Clear</a></p>` : ''}
<div class="ovp-gallery" data-gallery>${cards(pics, result.rows)}</div>
--${!result.rows.length ? html`<p class="ovp-gallery-empty">${result.q || result.tag ? 'No public pictures match yet. Try another search.' : 'The gallery is ready for its first public picture. Upload one, then choose Publish to gallery on its page.'}</p>` : ''}
++${!result.rows.length ? raw(`<div class="ovp-gallery-empty">${result.q || result.tag
++ ? kit.empty({ icon: 'search', title: 'No public pictures match yet', text: 'Try another word or tag, or see everything published so far.', action: { href: '/', label: 'Clear the search', icon: 'image' } })
++ : kit.empty({ icon: 'image', title: 'The gallery is waiting for its first picture', text: 'Upload a picture, then choose Publish to gallery on its page. Until you do, it stays unlisted.', action: { href: '/mine', label: 'Your pictures', icon: 'image' } })}</div>`) : ''}
${result.next ? html`<a class="ovp-btn" data-gallery-next href="${href(result.sort, result.next)}">More pictures</a>` : ''}<p role="status" data-gallery-status></p></section>`;
}
module.exports = { PUBLIC, isPublic, tags, parseTags, options, browse, counts, publish, wire, cards, render };
ddiff --git a/server/pics/pages.js b/server/pics/pages.js
iindex 4d03bbd..1de4e36 100644
---- a/server/pics/pages.js
++++ b/server/pics/pages.js
@@@ -41,6 +41,9 @@ const { createPicsApi } = require('./api');
const { createPicVariants } = require('./variants');
const kit = require('openvibe-shared/kit');
const gallery = require('./gallery');
++const glyphs = require('openvibe-shared/glyphs');
++/** A line glyph from Shared's set, decorative (its button says what it does). */
++const glyph = (name, size = 16) => raw(`<span class="ovp-glyph" aria-hidden="true">${glyphs.svg(name, { size })}</span>`);
const showcase = require('openvibe-shared/showcase');
const { PATHS: POLICY_PATHS, policyPage } = require('./policy');
@@@ -402,13 +405,20 @@ ${hidden ? notice(row.state === 'hidden' ? 'This picture is hidden while staff l
${labelsUi.chips(labelsUi.inForce(row))}
<figure class="ovp-figure"${labelsUi.attrs(labelsUi.inForce(row), { owner: row.owner, opened: true })}><a href="/i/${row.slug}.${row.ext}"><img src="/i/${row.slug}.${row.ext}" alt="${pics.altOf(row)}" width="${row.width}" height="${row.height}" decoding="async"></a></figure>
<p class="ovp-meta">${Number(row.width)} × ${Number(row.height)} · ${KIND[row.ext] || row.ext}${row.animated ? ' (animated)' : ''} · ${formatBytes(row.bytes)} · uploaded ${time(row.created_at)}</p>
--${raw(shareButton({ url: L.page, title, text: row.description || `${title}: a picture on ${SITE_NAME}.`, image: L.direct, kind: 'picture', id: row.id }))}
--${gallery.isPublic(row) ? html`<p>${row.author_name || 'OpenVibe member'} · ${(await gallery.counts(s, row.id)).views} views · <span data-like-count>${(await gallery.counts(s, row.id)).likes}</span> likes</p><button class="ovp-btn" data-like="${row.slug}">Like</button><p role="status" data-like-status></p>` : ''}
--${!hidden ? html`<p class="ovp-gallery-tabs"><a class="ovp-btn" href="https://openvibe.studio/image-tools?picture=${row.slug}">Edit with AI</a><a class="ovp-btn" href="https://openvibe.meme/make?picture=${row.slug}">Make a meme</a><a class="ovp-btn" href="https://openvibe.cam/">Take a photo</a></p>` : ''}
++${await (async () => {
++ const pub = gallery.isPublic(row);
++ const n = pub ? await gallery.counts(s, row.id) : null;
++ const who = row.author_name || 'OpenVibe member';
++ const share = raw(shareButton({ url: L.page, title, text: row.description || `${title}: a picture on ${SITE_NAME}.`, image: L.direct, kind: 'picture', id: row.id }));
++ return html`<div class="ovp-pic-bar">${pub ? html`<div class="ovp-pic-who"><span class="ovp-pic-av" aria-hidden="true">${who.trim().charAt(0).toUpperCase() || 'O'}</span><span class="ovp-pic-who-txt"><b>${who}</b><small>${n.views} ${n.views === 1 ? 'view' : 'views'}</small></span></div>` : ''}
++<div class="ovp-pic-acts">${pub ? html`<button type="button" class="ovk-btn ovk-btn--quiet ovp-like" data-like="${row.slug}" aria-pressed="false">${glyph('heart', 18)}<span data-like-word>Like</span><span class="ovp-like-n" data-like-count>${n.likes}</span></button>` : ''}${share}</div></div>
++${pub ? html`<p class="ovp-pic-status" role="status" data-like-status></p>` : ''}
++${!hidden ? html`<nav class="ovp-pic-make" aria-label="Make something with this picture"><a class="ovk-btn ovk-btn--quiet" href="https://openvibe.studio/image-tools?picture=${row.slug}">${glyph('spark', 17)}<span>Edit with AI</span></a><a class="ovk-btn ovk-btn--quiet" href="https://openvibe.meme/make?picture=${row.slug}">${glyph('laugh', 17)}<span>Make a meme</span></a><a class="ovk-btn ovk-btn--quiet" href="https://openvibe.cam/">${glyph('camera', 17)}<span>Take a photo</span></a></nav>` : ''}`;
++})()}
${row.description ? html`<p class="ovp-desc">${row.description}</p>` : ''}
${row.ai_caption && row.alt_source === 'ai' ? html`<p class="ovp-ai"><span class="ovp-ai-label">Described by AI</span> ${row.ai_caption}</p>` : ''}
</article>
--${gallery.isPublic(row) ? html`<section class="ovp-section"><h2>Comments</h2><button class="ovp-btn" data-comments="${row.id}">Show Community discussion</button><div data-comments-list aria-live="polite"></div></section>` : ''}
++${gallery.isPublic(row) ? html`<section class="ovp-section ovp-comments" aria-labelledby="h-comments"><div class="ovp-comments-head"><h2 id="h-comments">Comments</h2><button type="button" class="ovk-btn ovk-btn--quiet" data-comments="${row.id}">${glyph('chat', 17)}<span>Show the discussion</span></button></div><div class="ovp-comments-list" data-comments-list aria-live="polite"></div></section>` : ''}
<section class="ovp-share" aria-labelledby="h-share"><h2 id="h-share">Share and embed</h2>
<p class="ovp-share-lede">These links follow the audience you choose in Share. Private pictures are visible only to you. The direct link is the picture itself, at full size — no page around it.</p>
<div class="ovp-link-grid">