feat(mobile): paginador CSS-only en agenda, logs, inhabitants,
trending, calendars, banking, blockchain - Nuevo helper src/views/mobile_pager.js — renderMobilePager(opts) genera el paginador con radios + flechas + cells de 2 botones. - activity_view: refactor para usar el helper. - agenda_view, logs_view, inhabitants_view, trending_view, calendars_view, banking_views, blockchain_view: añaden el mobilePager y marcan el contenedor desktop con .actpager-desktop-only. - mobile.css: .actpager-desktop-only se oculta en móvil junto al grid antiguo de activity. modules_view (4 botones) y stats_view (3 modos) no necesitan paginación: caben en una fila. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb04d4cb64
commit
13161b2158
10 changed files with 204 additions and 68 deletions
|
|
@ -399,8 +399,9 @@ textarea, input, select {
|
|||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* En móvil ocultamos el grid antiguo; usa el paginador (.actpager) */
|
||||
.activity-filter-grid {
|
||||
/* En móvil ocultamos los grids de filtros antiguos; se usa el paginador (.actpager) */
|
||||
.activity-filter-grid,
|
||||
.actpager-desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1701,46 +1701,14 @@ exports.activityView = (actions, filter, userId, q = '') => {
|
|||
});
|
||||
}
|
||||
|
||||
// Mobile paginator (CSS-only, no JS): 2 buttons per page with arrow labels
|
||||
const buildBtn = (cls, t, lab) => form({ method: 'GET', action: '/activity', class: cls },
|
||||
input({ type: 'hidden', name: 'filter', value: t }),
|
||||
button({ type: 'submit', class: filter === t ? 'filter-btn active' : 'filter-btn' }, lab)
|
||||
);
|
||||
const PAGE_SIZE = 2;
|
||||
const totalPages = Math.ceil(activityTypes.length / PAGE_SIZE);
|
||||
const pageCells = [];
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const slice = activityTypes.slice(i * PAGE_SIZE, (i + 1) * PAGE_SIZE);
|
||||
pageCells.push(div({ class: 'actpager-cell' },
|
||||
slice.map(({ type, label }) => buildBtn('actpager-form', type, label))
|
||||
));
|
||||
}
|
||||
const radios = [];
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const attrs = { type: 'radio', name: 'actp', id: `actp${i}`, class: `actpager-radio actpager-r${i}` };
|
||||
if (i === 0) attrs.checked = 'checked';
|
||||
radios.push(input(attrs));
|
||||
}
|
||||
const haxe = require('../server/node_modules/hyperaxe');
|
||||
const arrowSets = [];
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const prev = i > 0
|
||||
? haxe.label({ for: `actp${i - 1}`, class: 'actpager-arrow' }, '‹')
|
||||
: haxe.span({ class: 'actpager-arrow disabled' }, '‹');
|
||||
const next = i < totalPages - 1
|
||||
? haxe.label({ for: `actp${i + 1}`, class: 'actpager-arrow' }, '›')
|
||||
: haxe.span({ class: 'actpager-arrow disabled' }, '›');
|
||||
arrowSets.push(div({ class: `actpager-arrows actpager-a${i}` }, prev, next));
|
||||
}
|
||||
const mobilePager = section({ class: 'actpager', style: 'display:none' },
|
||||
...radios,
|
||||
div({ class: 'actpager-frame' },
|
||||
div({ class: 'actpager-clip' },
|
||||
div({ class: 'actpager-row' }, ...pageCells)
|
||||
),
|
||||
div({ class: 'actpager-controls' }, ...arrowSets)
|
||||
)
|
||||
);
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
const mobilePager = renderMobilePager({
|
||||
items: activityTypes,
|
||||
idPrefix: 'actp',
|
||||
formAction: '/activity',
|
||||
currentFilter: filter,
|
||||
pageSize: 2
|
||||
});
|
||||
|
||||
let html = template(
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -202,6 +202,29 @@ const renderAgendaItem = (item, userId, filter) => {
|
|||
exports.agendaView = async (data, filter) => {
|
||||
const { items = [], counts: _c = {} } = data || {};
|
||||
const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, discarded: 0, ..._c };
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
const agendaFilters = [
|
||||
{ type: 'all', label: `${i18n.agendaFilterAll} (${counts.all})` },
|
||||
{ type: 'open', label: `${i18n.agendaFilterOpen} (${counts.open})` },
|
||||
{ type: 'closed', label: `${i18n.agendaFilterClosed} (${counts.closed})` },
|
||||
{ type: 'events', label: `${i18n.agendaFilterEvents} (${counts.events})` },
|
||||
{ type: 'tasks', label: `${i18n.agendaFilterTasks} (${counts.tasks})` },
|
||||
{ type: 'reports', label: `${i18n.agendaFilterReports} (${counts.reports})` },
|
||||
{ type: 'tribes', label: `${i18n.agendaFilterTribes} (${counts.tribes})` },
|
||||
{ type: 'jobs', label: `${i18n.agendaFilterJobs} (${counts.jobs})` },
|
||||
{ type: 'market', label: `${i18n.agendaFilterMarket} (${counts.market})` },
|
||||
{ type: 'projects', label: `${i18n.agendaFilterProjects} (${counts.projects})` },
|
||||
{ type: 'calendars', label: `${i18n.agendaFilterCalendars || 'CALENDARS'} (${counts.calendars})` },
|
||||
{ type: 'transfers', label: `${i18n.agendaFilterTransfers} (${counts.transfers})` },
|
||||
{ type: 'discarded', label: `DISCARDED (${counts.discarded})` }
|
||||
];
|
||||
const mobilePager = renderMobilePager({
|
||||
items: agendaFilters,
|
||||
idPrefix: 'agp',
|
||||
formAction: '/agenda',
|
||||
currentFilter: filter,
|
||||
pageSize: 2
|
||||
});
|
||||
return template(
|
||||
i18n.agendaTitle,
|
||||
section(
|
||||
|
|
@ -209,7 +232,8 @@ exports.agendaView = async (data, filter) => {
|
|||
h2(i18n.agendaTitle),
|
||||
p(i18n.agendaDescription)
|
||||
),
|
||||
div({ class: 'filters' },
|
||||
mobilePager,
|
||||
div({ class: 'filters actpager-desktop-only' },
|
||||
form({ method: 'GET', action: '/agenda' },
|
||||
button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' },
|
||||
`${i18n.agendaFilterAll} (${counts.all})`),
|
||||
|
|
|
|||
|
|
@ -312,13 +312,22 @@ const renderAddresses = (data, userId) => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderBankingView = (data, filter, userId, isPub) =>
|
||||
template(
|
||||
const renderBankingView = (data, filter, userId, isPub) => {
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
const bankingFilters = ["overview","exchange","mine","pending","closed","claimed","expired","epochs","rules","addresses"];
|
||||
const items = bankingFilters.map(m => ({ type: m, label: (FILTER_LABELS[m] || m).toUpperCase() }));
|
||||
const bankingMobilePager = renderMobilePager({
|
||||
items, idPrefix: 'bnkp', formAction: '/banking', currentFilter: filter, pageSize: 2
|
||||
});
|
||||
return template(
|
||||
i18n.banking,
|
||||
section(
|
||||
div({ class: "tags-header" }, h2(i18n.banking), p(i18n.bankingDescription)),
|
||||
data.flash ? div({ class: "flash-banner" }, p(flashText(data.flash) || data.flash)) : null,
|
||||
generateFilterButtons(["overview","exchange","mine","pending","closed","claimed","expired","epochs","rules","addresses"], filter, "/banking"),
|
||||
bankingMobilePager,
|
||||
div({ class: "actpager-desktop-only" },
|
||||
generateFilterButtons(bankingFilters, filter, "/banking")
|
||||
),
|
||||
filter === "overview"
|
||||
? div(
|
||||
renderOverviewSummaryTable(data.summary || {}, data.rules),
|
||||
|
|
@ -339,6 +348,7 @@ const renderBankingView = (data, filter, userId, isPub) =>
|
|||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderSingleAllocationView = (alloc, userId) => {
|
||||
if (!alloc) return template(i18n.banking, section(div(p(i18n.bankNoAllocations))));
|
||||
|
|
|
|||
|
|
@ -296,7 +296,21 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi
|
|||
h2(i18n.blockchain),
|
||||
p(i18n.blockchainDescription)
|
||||
),
|
||||
div({ class: 'mode-buttons-row' },
|
||||
(() => {
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
const allFilters = [...BASE_FILTERS, ...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3, ...CAT_BLOCK4];
|
||||
return renderMobilePager({
|
||||
items: allFilters.map(m => ({ type: m, label: (FILTER_LABELS[m] || m).toUpperCase() })),
|
||||
idPrefix: 'bxp',
|
||||
formAction: '/blockexplorer',
|
||||
currentFilter: filter,
|
||||
pageSize: 2,
|
||||
extraHidden: Object.fromEntries(
|
||||
(search && typeof search === 'object' ? Object.entries(search) : []).filter(([_,v]) => v != null && v !== '')
|
||||
)
|
||||
});
|
||||
})(),
|
||||
div({ class: 'mode-buttons-row actpager-desktop-only' },
|
||||
div({ class: 'filter-column' },
|
||||
generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer', search)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -26,20 +26,27 @@ const renderCalendarFavoriteToggle = (cal, returnTo) =>
|
|||
button({ type: "submit", class: "tribe-action-btn" }, cal.isFavorite ? (i18n.calendarRemoveFavorite || "Remove Favorite") : (i18n.calendarAddFavorite || "Add Favorite"))
|
||||
)
|
||||
|
||||
const renderModeButtons = (currentFilter) =>
|
||||
div({ class: "tribe-mode-buttons" },
|
||||
["all", "mine", "recent", "favorites", "open", "closed"].map(f =>
|
||||
const renderModeButtons = (currentFilter) => {
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
const filters = ["all", "mine", "recent", "favorites", "open", "closed"];
|
||||
const items = filters.map(f => ({ type: f, label: i18n[`calendarFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase() }));
|
||||
return [
|
||||
renderMobilePager({ items, idPrefix: 'calp', formAction: '/calendars', currentFilter, pageSize: 2 }),
|
||||
div({ class: "tribe-mode-buttons actpager-desktop-only" },
|
||||
filters.map(f =>
|
||||
form({ method: "GET", action: "/calendars" },
|
||||
input({ type: "hidden", name: "filter", value: f }),
|
||||
button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" },
|
||||
i18n[`calendarFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase())
|
||||
)
|
||||
),
|
||||
form({ method: "GET", action: "/calendars" },
|
||||
input({ type: "hidden", name: "filter", value: f }),
|
||||
button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" },
|
||||
i18n[`calendarFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase())
|
||||
input({ type: "hidden", name: "filter", value: "create" }),
|
||||
button({ type: "submit", class: "create-button" }, i18n.calendarCreate || "Create Calendar")
|
||||
)
|
||||
),
|
||||
form({ method: "GET", action: "/calendars" },
|
||||
input({ type: "hidden", name: "filter", value: "create" }),
|
||||
button({ type: "submit", class: "create-button" }, i18n.calendarCreate || "Create Calendar")
|
||||
)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
const renderStatus = (cal) => {
|
||||
if (cal.isClosed) return span({ class: "pad-status-closed" }, i18n.calendarStatusClosed || "CLOSED")
|
||||
|
|
|
|||
|
|
@ -223,7 +223,17 @@ exports.inhabitantsView = async (inhabitants, filter, query, currentUserId) => {
|
|||
button({ type: 'submit' }, i18n.applyFilters)
|
||||
)
|
||||
),
|
||||
div({ class: 'inhabitant-action' },
|
||||
(() => {
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
return renderMobilePager({
|
||||
items: filters.map(mode => ({ type: mode, label: i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode })),
|
||||
idPrefix: 'inhp',
|
||||
formAction: '/inhabitants',
|
||||
currentFilter: filter,
|
||||
pageSize: 2
|
||||
});
|
||||
})(),
|
||||
div({ class: 'inhabitant-action actpager-desktop-only' },
|
||||
...generateFilterButtons(filters, filter)
|
||||
),
|
||||
filter === 'GALLERY'
|
||||
|
|
|
|||
|
|
@ -18,14 +18,26 @@ const filterLabel = (f) => {
|
|||
return map[f] || f.toUpperCase();
|
||||
};
|
||||
|
||||
const renderFilterBar = (current) =>
|
||||
div({ class: "logs-toolbar" },
|
||||
form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" },
|
||||
FILTERS.map(f =>
|
||||
button({
|
||||
type: "submit", name: "filter", value: f,
|
||||
class: current === f ? "filter-btn active" : "filter-btn"
|
||||
}, filterLabel(f))
|
||||
const renderFilterBar = (current) => {
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
const items = FILTERS.map(f => ({ type: f, label: filterLabel(f) }));
|
||||
const mobilePager = renderMobilePager({
|
||||
items,
|
||||
idPrefix: 'logp',
|
||||
formAction: '/logs',
|
||||
currentFilter: current,
|
||||
pageSize: 2
|
||||
});
|
||||
return div({ class: "logs-toolbar" },
|
||||
mobilePager,
|
||||
div({ class: "actpager-desktop-only logs-toolbar-desktop" },
|
||||
form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" },
|
||||
FILTERS.map(f =>
|
||||
button({
|
||||
type: "submit", name: "filter", value: f,
|
||||
class: current === f ? "filter-btn active" : "filter-btn"
|
||||
}, filterLabel(f))
|
||||
)
|
||||
)
|
||||
),
|
||||
form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" },
|
||||
|
|
@ -36,6 +48,7 @@ const renderFilterBar = (current) =>
|
|||
button({ type: "submit", class: "create-button" }, i18n.logsExport || 'Export Logs')
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderSearchBox = (current, search) => {
|
||||
const q = search || {};
|
||||
|
|
|
|||
76
nodejs-project/nodejs-project/src/views/mobile_pager.js
Normal file
76
nodejs-project/nodejs-project/src/views/mobile_pager.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"use strict";
|
||||
const { div, input, span, label, form, button, section, a } = require('../server/node_modules/hyperaxe');
|
||||
|
||||
function renderMobilePager(opts) {
|
||||
const items = Array.isArray(opts.items) ? opts.items : [];
|
||||
const idPrefix = opts.idPrefix || 'actp';
|
||||
const formAction = opts.formAction || '';
|
||||
const currentFilter = opts.currentFilter;
|
||||
const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : 2;
|
||||
const paramName = opts.paramName || 'filter';
|
||||
const buttonClass = opts.buttonClass || 'filter-btn';
|
||||
const useLinks = !!opts.useLinks;
|
||||
const linkBuilder = typeof opts.linkBuilder === 'function' ? opts.linkBuilder : null;
|
||||
const extraHidden = opts.extraHidden && typeof opts.extraHidden === 'object' ? opts.extraHidden : null;
|
||||
|
||||
if (!items.length) return null;
|
||||
|
||||
const totalPages = Math.ceil(items.length / pageSize);
|
||||
|
||||
const radios = [];
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const attrs = { type: 'radio', name: idPrefix, id: `${idPrefix}${i}`, class: `actpager-radio actpager-r${i}` };
|
||||
if (i === 0) attrs.checked = 'checked';
|
||||
radios.push(input(attrs));
|
||||
}
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const slice = items.slice(i * pageSize, (i + 1) * pageSize);
|
||||
cells.push(div({ class: 'actpager-cell' },
|
||||
slice.map(item => {
|
||||
const t = item.type ?? item.value;
|
||||
const lbl = item.label ?? item.text ?? String(t);
|
||||
const isActive = item.active != null ? !!item.active : (currentFilter === t);
|
||||
const cls = `${buttonClass}${isActive ? ' active' : ''}`;
|
||||
if (useLinks || linkBuilder) {
|
||||
const href = linkBuilder ? linkBuilder(item) : (item.href || `${formAction}?${paramName}=${encodeURIComponent(t)}`);
|
||||
return a({ href, class: cls }, lbl);
|
||||
}
|
||||
const hiddens = [input({ type: 'hidden', name: paramName, value: t })];
|
||||
if (extraHidden) {
|
||||
for (const k of Object.keys(extraHidden)) {
|
||||
hiddens.push(input({ type: 'hidden', name: k, value: extraHidden[k] }));
|
||||
}
|
||||
}
|
||||
return form({ method: 'GET', action: formAction, class: 'actpager-form' },
|
||||
...hiddens,
|
||||
button({ type: 'submit', class: cls }, lbl)
|
||||
);
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
const arrowSets = [];
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const prev = i > 0
|
||||
? label({ for: `${idPrefix}${i - 1}`, class: 'actpager-arrow' }, '‹')
|
||||
: span({ class: 'actpager-arrow disabled' }, '‹');
|
||||
const next = i < totalPages - 1
|
||||
? label({ for: `${idPrefix}${i + 1}`, class: 'actpager-arrow' }, '›')
|
||||
: span({ class: 'actpager-arrow disabled' }, '›');
|
||||
arrowSets.push(div({ class: `actpager-arrows actpager-a${i}` }, prev, next));
|
||||
}
|
||||
|
||||
return section({ class: 'actpager', style: 'display:none' },
|
||||
...radios,
|
||||
div({ class: 'actpager-frame' },
|
||||
div({ class: 'actpager-clip' },
|
||||
div({ class: 'actpager-row' }, ...cells)
|
||||
),
|
||||
div({ class: 'actpager-controls' }, ...arrowSets)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { renderMobilePager };
|
||||
|
|
@ -277,12 +277,25 @@ exports.trendingView = (items, filter, categories = opinionCategories) => {
|
|||
|
||||
const hasDocument = filteredItems.some(item => item.value.content.type === 'document');
|
||||
|
||||
const allTrendingFilters = [
|
||||
...baseFilters.map(m => ({ type: m, label: i18n[m + 'Button'] || m })),
|
||||
...contentFilters.flat().map(m => ({ type: m, label: i18n[m + 'Button'] || m }))
|
||||
];
|
||||
const { renderMobilePager } = require('./mobile_pager');
|
||||
const trendingMobilePager = renderMobilePager({
|
||||
items: allTrendingFilters,
|
||||
idPrefix: 'trp',
|
||||
formAction: '/trending',
|
||||
currentFilter: filter,
|
||||
pageSize: 2
|
||||
});
|
||||
let html = template(
|
||||
title,
|
||||
section(
|
||||
header,
|
||||
trendingMobilePager,
|
||||
div(
|
||||
{ class: 'mode-buttons' },
|
||||
{ class: 'mode-buttons actpager-desktop-only' },
|
||||
generateFilterButtons(baseFilters, filter, '/trending'),
|
||||
...contentFilters.map(row =>
|
||||
div({ style: 'display:flex;flex-direction:column;gap:8px;' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue