forked from yvv/decision
- Le vote se déplie sous l'article, façon accordéon, l'URL pour état (?vote=) :
niveau d'eau, totaux, seuil et voix manquantes, ton vote et où il en est
(enregistré, inscrit sur la chaîne, en attente, à reconfirmer, non compté),
inertie et formule. Le reste d'une version (coquilles, traductions,
signalements, historique) passe dans l'onglet « Détails » du panneau.
- Le panier : article par article, « tout pour / tout contre » par section ou
pour tout le document, jamais pour un article à plusieurs versions ; un seul
geste (une écriture, ou une transaction).
- La signature : le portefeuille de la page (injectedWeb3), polkadot-api chargé
au moment de signer ; sans portefeuille, Ğecko par lien et QR ; la page
attend la confirmation. Un bulletin que le portefeuille n'a pas signé est
lâché aussitôt (DELETE /api/ballots/{id}).
- Pastilles d'ambiance : flèches dans le groupe, anneau de focus pointillé à
la couleur d'accent de la page (3:1 au moins), distinct du choix.
- e2e : chaîne simulée (indexeur et nœud), faux portefeuille ; C21–C24, et
C4, C5, C6, C11, C17, C19, C20, M1 mis à jour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 lines
1.6 KiB
TypeScript
44 lines
1.6 KiB
TypeScript
// What the vote under an article and the basket share: one way to send votes
|
|
// to the server (managed groups), and the news of what entered into force.
|
|
|
|
import { api, ApiError } from './api';
|
|
import type { DocState } from './doc.svelte';
|
|
import { i18n, t, type Key } from './i18n/index.svelte';
|
|
import { toasts } from './toast.svelte';
|
|
import type { Entered } from './types';
|
|
|
|
/** A line of a vote: one version, for or against, or null to withdraw one's vote. */
|
|
export interface Line {
|
|
proposalId: string;
|
|
choice: 'for' | 'against' | null;
|
|
}
|
|
|
|
/** Cents of Ğ1, as the person reads them: « 0,1 » in French. */
|
|
export function g1Amount(cents: number): string {
|
|
return i18n.number(cents / 100);
|
|
}
|
|
|
|
export function errorText(e: unknown): string {
|
|
return t(`error.${e instanceof ApiError ? e.code : 'internal'}` as Key);
|
|
}
|
|
|
|
/** Tells, one by one, the texts that just entered into force. */
|
|
export function announce(doc: DocState, entered: Entered[]) {
|
|
for (const e of entered) {
|
|
const u = doc.unit(e.unitId);
|
|
const title = u?.inForce?.title ?? u?.code ?? '';
|
|
const key = e.kind === 'remove' ? 'vote.entered.remove' : e.kind === 'inertia' ? 'vote.entered.inertia' : 'vote.entered';
|
|
toasts.show(t(key, { title }), 'rise', 6000);
|
|
}
|
|
}
|
|
|
|
/** In a managed group: every line in one write on the server. Returns how many were written. */
|
|
export async function voteDirect(doc: DocState, lines: Line[]): Promise<number> {
|
|
const id = doc.view?.doc.id;
|
|
if (!id) return 0;
|
|
const r = await api.post<{ count: number; entered: Entered[] }>(`/docs/${id}/votes`, { lines });
|
|
await doc.reload();
|
|
announce(doc, r.entered);
|
|
return r.count;
|
|
}
|