movil: usabilidad tras la prueba en el Pixel
Lo que salio de recorrer la app como alguien de la acampada: - borrar (tribus y todo lo que usa renderContentActions) pide un segundo toque; la X va la ultima de la fila - Karvan: Llamar empieza solo con voz (si se negaba la camara no arrancaba nada); botones con icono y nombre; textos traducidos; ayuda para invitar - votaciones: tipo Si/No, semaforo o termometro (el servidor ya aceptaba options); resultados en barras; aviso a quien la crea de que no puede votar - workflow activistas con tribus, parlamento, votaciones y eventos - seccion apagada: /modules dice cual era y la activa de un toque - Modulos en el movil: agrupados como el lanzador, interruptores y Guardar fijo - tribus en filas compactas; secciones de una tribu en tira de fichas + Mas - lanzador y barra con iconos SVG en vez de letras vai - Ajustes: idioma y aspecto primero; idiomas de un toque - aviso de bienvenida solo en la portada y en una linea; Ahora no al final - pie de una linea con el estado de sincronizacion en claro - mobile_ux.css para los dos skins: un margen de 14px (eran 74 por lado en las fichas), Anton solo en titulos y boton principal, buscador en una linea, la barra de abajo se esconde con el teclado
This commit is contained in:
parent
2e3ff1e79f
commit
4dd573662c
14 changed files with 1279 additions and 54 deletions
|
|
@ -1,4 +1,4 @@
|
|||
const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe");
|
||||
const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span, progress } = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
|
|
@ -7,6 +7,15 @@ const { renderTribeOriginChip } = require("./clearnet_view");
|
|||
|
||||
const userId = config.keys.id;
|
||||
const VOTE_QUORUM = 2;
|
||||
const isMobile = process.env.OASIS_MOBILE === "1";
|
||||
|
||||
// tipos de votacion en el movil: el servidor ya guarda las opciones que se le
|
||||
// pasen (campo options), aqui solo se ofrecen las que usan las asambleas
|
||||
const VOTE_KINDS = [
|
||||
{ key: "classic", options: null },
|
||||
{ key: "traffic", options: ["GREEN", "YELLOW", "RED"] },
|
||||
{ key: "scale", options: ["1", "2", "3", "4", "5"] }
|
||||
];
|
||||
|
||||
const safeArray = (v) => Array.isArray(v) ? v : [];
|
||||
|
||||
|
|
@ -21,7 +30,7 @@ const computeVoteOutcome = (baseCounts, voteOptions, totalVotesNum) => {
|
|||
const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount);
|
||||
if (maxCount === 0 || topOpts.length > 1) return noResult;
|
||||
const winner = topOpts[0];
|
||||
const variant = winner === "YES" ? "yes" : winner === "NO" ? "no" : "pending";
|
||||
const variant = (winner === "YES" || winner === "GREEN") ? "yes" : (winner === "NO" || winner === "RED") ? "no" : "pending";
|
||||
return { text: voteLabel(winner), variant };
|
||||
};
|
||||
|
||||
|
|
@ -157,6 +166,22 @@ exports.renderVoteOptionsBlock = (v, voteOptions, action, returnTo, canVote) =>
|
|||
];
|
||||
};
|
||||
|
||||
// en el movil los resultados son barras, una por opcion: la tabla de 6 columnas
|
||||
// no cabia y partia las palabras por silabas
|
||||
const renderVoteBars = (voteOptions, baseCounts) => {
|
||||
const total = voteOptions.reduce((s, o) => s + (Number(baseCounts[o]) || 0), 0);
|
||||
return div({ class: "vote-bars" },
|
||||
...voteOptions.map((opt) => {
|
||||
const n = Number(baseCounts[opt]) || 0;
|
||||
return div({ class: `vote-bar vote-bar-${String(opt).toLowerCase()}` },
|
||||
span({ class: "vote-bar-label" }, voteLabel(opt)),
|
||||
span({ class: "vote-bar-count" }, String(n)),
|
||||
progress({ class: "vote-bar-meter", max: String(Math.max(total, 1)), value: String(n) })
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, activeFilter, params = {}) => {
|
||||
const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault;
|
||||
const baseCounts = voteOptions.reduce((acc, opt) => {
|
||||
|
|
@ -212,7 +237,12 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
|||
|
||||
const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo);
|
||||
|
||||
const ownOpenNote = isMobile && !voteButtonsNode && isVoteAuthor(v) && normalizeStatus(v.status) === "OPEN"
|
||||
? p({ class: "vote-own-note" }, i18n.voteOwnNote || "You created this vote, so you cannot vote in it.")
|
||||
: null;
|
||||
|
||||
const voteMain = div({ class: "tribe-main" },
|
||||
ownOpenNote,
|
||||
voteButtonsNode
|
||||
? div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"),
|
||||
|
|
@ -224,7 +254,7 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
|||
`${i18n.voteResults || "Results"}: `,
|
||||
span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text)
|
||||
),
|
||||
div({ class: "vote-table" },
|
||||
isMobile ? renderVoteBars(voteOptions, baseCounts) : div({ class: "vote-table" },
|
||||
table(
|
||||
tr(...voteOptions.map((opt) => th(voteLabel(opt)))),
|
||||
tr(...voteOptions.map((opt) => td(baseCounts[opt])))
|
||||
|
|
@ -247,6 +277,22 @@ const renderCommentsSection = (voteId, comments, activeFilter) => {
|
|||
});
|
||||
};
|
||||
|
||||
const renderVoteKindPicker = (current) => {
|
||||
const cur = String(current || "");
|
||||
return div({ class: "vote-kind" },
|
||||
label({ class: "vote-kind-title" }, i18n.voteKindLabel || "Type of vote"),
|
||||
...VOTE_KINDS.map((k, i) => {
|
||||
const value = k.options ? k.options.join(",") : "";
|
||||
const checked = cur ? cur === value : i === 0;
|
||||
return label({ class: "vote-kind-option" },
|
||||
input(Object.assign({ type: "radio", name: "options", value }, checked ? { checked: "checked" } : {})),
|
||||
span({ class: "vote-kind-name" }, i18n["voteKind_" + k.key] || k.key),
|
||||
span({ class: "vote-kind-hint" }, i18n["voteKindHint_" + k.key] || "")
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => {
|
||||
const list = Array.isArray(votes) ? votes : [votes];
|
||||
|
||||
|
|
@ -351,6 +397,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam,
|
|||
}), br(), br(),
|
||||
label(i18n.voteTagsLabel), br(),
|
||||
input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(),
|
||||
isMobile && mode === "create" ? renderVoteKindPicker(draft.options) : null,
|
||||
button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue