diff --git a/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css b/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css index b9c6b6b7..e9cfecda 100644 --- a/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css +++ b/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css @@ -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; } diff --git a/nodejs-project/nodejs-project/src/views/activity_view.js b/nodejs-project/nodejs-project/src/views/activity_view.js index 1539e66b..d178e61e 100644 --- a/nodejs-project/nodejs-project/src/views/activity_view.js +++ b/nodejs-project/nodejs-project/src/views/activity_view.js @@ -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, diff --git a/nodejs-project/nodejs-project/src/views/agenda_view.js b/nodejs-project/nodejs-project/src/views/agenda_view.js index feb865d4..4a5bd5ae 100644 --- a/nodejs-project/nodejs-project/src/views/agenda_view.js +++ b/nodejs-project/nodejs-project/src/views/agenda_view.js @@ -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})`), diff --git a/nodejs-project/nodejs-project/src/views/banking_views.js b/nodejs-project/nodejs-project/src/views/banking_views.js index 9f4e7489..7667a159 100644 --- a/nodejs-project/nodejs-project/src/views/banking_views.js +++ b/nodejs-project/nodejs-project/src/views/banking_views.js @@ -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)))); diff --git a/nodejs-project/nodejs-project/src/views/blockchain_view.js b/nodejs-project/nodejs-project/src/views/blockchain_view.js index 1433ac07..22a98adc 100644 --- a/nodejs-project/nodejs-project/src/views/blockchain_view.js +++ b/nodejs-project/nodejs-project/src/views/blockchain_view.js @@ -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) ), diff --git a/nodejs-project/nodejs-project/src/views/calendars_view.js b/nodejs-project/nodejs-project/src/views/calendars_view.js index 91914464..a463abc5 100644 --- a/nodejs-project/nodejs-project/src/views/calendars_view.js +++ b/nodejs-project/nodejs-project/src/views/calendars_view.js @@ -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") diff --git a/nodejs-project/nodejs-project/src/views/inhabitants_view.js b/nodejs-project/nodejs-project/src/views/inhabitants_view.js index b3e29d32..3bc5f9f0 100644 --- a/nodejs-project/nodejs-project/src/views/inhabitants_view.js +++ b/nodejs-project/nodejs-project/src/views/inhabitants_view.js @@ -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' diff --git a/nodejs-project/nodejs-project/src/views/logs_view.js b/nodejs-project/nodejs-project/src/views/logs_view.js index c389d5ed..330ec13e 100644 --- a/nodejs-project/nodejs-project/src/views/logs_view.js +++ b/nodejs-project/nodejs-project/src/views/logs_view.js @@ -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 || {}; diff --git a/nodejs-project/nodejs-project/src/views/mobile_pager.js b/nodejs-project/nodejs-project/src/views/mobile_pager.js new file mode 100644 index 00000000..0942df63 --- /dev/null +++ b/nodejs-project/nodejs-project/src/views/mobile_pager.js @@ -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 }; diff --git a/nodejs-project/nodejs-project/src/views/trending_view.js b/nodejs-project/nodejs-project/src/views/trending_view.js index 3937a93e..7073180e 100644 --- a/nodejs-project/nodejs-project/src/views/trending_view.js +++ b/nodejs-project/nodejs-project/src/views/trending_view.js @@ -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;' },