feat: add QR share button to each pub row in invites view

- Import details, summary from hyperaxe
- renderPubTable converted to async, generates QR SVG per pub key
- Each row: details/summary collapsible QR panel below the key link
  (same pattern as profile and tribe invite QR, no JS required)
- All three renderPubTable calls updated with await
- QR falls back silently if key is missing or generation fails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SITO 2026-05-01 02:33:02 +02:00
parent 16f9189e61
commit 12acd6fd20

View file

@ -1,4 +1,4 @@
const { form, button, div, h2, h3, p, section, ul, li, a, br, hr, input, span, table, tr, td } = require("../server/node_modules/hyperaxe"); const { form, button, div, h2, h3, p, section, ul, li, a, br, hr, input, span, table, tr, td, details, summary } = require("../server/node_modules/hyperaxe");
const QRCode = require('../server/node_modules/qrcode'); const QRCode = require('../server/node_modules/qrcode');
const path = require("path"); const path = require("path");
const fs = require('fs'); const fs = require('fs');
@ -75,16 +75,31 @@ const invitesView = async ({ invitesEnabled }) => {
const activePubs = filteredPubs.filter(pubItem => !hasError(pubItem)); const activePubs = filteredPubs.filter(pubItem => !hasError(pubItem));
const unreachablePubs = pubs.filter(hasError); const unreachablePubs = pubs.filter(hasError);
const renderPubTable = (items, actionFn) => table({ class: 'block-info-table' }, const renderPubTable = async (items, actionFn) => {
pubTableHeader(), const rows = await Promise.all(items.map(async pubItem => {
items.map(pubItem => tr( let qrSvg = '';
td(pubItem.host || '—'), try {
td(String(pubItem.port || 8008)), if (pubItem.key) qrSvg = await QRCode.toString(pubItem.key, { type: 'svg' });
td(String(pubItem.announcers || 0)), } catch {}
td(a({ href: encodePubLink(pubItem.key), class: 'user-link' }, pubItem.key)), return tr(
td(actionFn(pubItem)) td(pubItem.host || '—'),
)) td(String(pubItem.port || 8008)),
); td(String(pubItem.announcers || 0)),
td(
a({ href: encodePubLink(pubItem.key), class: 'user-link' }, pubItem.key),
qrSvg ? details({ class: 'qr-share-details' },
summary({ class: 'qr-share-btn' }, '⬡ QR'),
div({ class: 'qr-share-panel' },
div({ class: 'qr-code', innerHTML: qrSvg }),
p({ class: 'qr-share-id' }, pubItem.key)
)
) : null
),
td(actionFn(pubItem))
);
}));
return table({ class: 'block-info-table' }, pubTableHeader(), ...rows);
};
const title = i18n.invites; const title = i18n.invites;
const description = i18n.invitesDescription; const description = i18n.invitesDescription;
@ -130,7 +145,7 @@ const invitesView = async ({ invitesEnabled }) => {
hr(), hr(),
h2(`${i18n.invitesAcceptedInvites} (${activePubs.length})`), h2(`${i18n.invitesAcceptedInvites} (${activePubs.length})`),
activePubs.length activePubs.length
? renderPubTable(activePubs, pubItem => ? await renderPubTable(activePubs, pubItem =>
form({ action: '/settings/invite/unfollow', method: 'post' }, form({ action: '/settings/invite/unfollow', method: 'post' },
input({ type: 'hidden', name: 'key', value: pubItem.key }), input({ type: 'hidden', name: 'key', value: pubItem.key }),
button({ type: 'submit' }, i18n.invitesUnfollow) button({ type: 'submit' }, i18n.invitesUnfollow)
@ -140,7 +155,7 @@ const invitesView = async ({ invitesEnabled }) => {
hr(), hr(),
h2(`${i18n.invitesUnfollowedInvites} (${unfollowed.length})`), h2(`${i18n.invitesUnfollowedInvites} (${unfollowed.length})`),
unfollowed.length unfollowed.length
? renderPubTable(unfollowed, pubItem => ? await renderPubTable(unfollowed, pubItem =>
form({ action: '/settings/invite/follow', method: 'post' }, form({ action: '/settings/invite/follow', method: 'post' },
input({ type: 'hidden', name: 'key', value: pubItem.key }), input({ type: 'hidden', name: 'key', value: pubItem.key }),
input({ type: 'hidden', name: 'host', value: pubItem.host || '' }), input({ type: 'hidden', name: 'host', value: pubItem.host || '' }),
@ -152,7 +167,7 @@ const invitesView = async ({ invitesEnabled }) => {
hr(), hr(),
h2(`${i18n.invitesUnreachablePubs} (${unreachablePubs.length})`), h2(`${i18n.invitesUnreachablePubs} (${unreachablePubs.length})`),
unreachablePubs.length unreachablePubs.length
? renderPubTable(unreachablePubs, pubItem => ? await renderPubTable(unreachablePubs, pubItem =>
div({ class: 'error-box' }, div({ class: 'error-box' },
p({ class: 'error-title' }, i18n.errorDetails), p({ class: 'error-title' }, i18n.errorDetails),
p({ class: 'error-pre' }, String(pubItem.error || i18n.genericError)) p({ class: 'error-pre' }, String(pubItem.error || i18n.genericError))