Skip to content

Commit 2296c34

Browse files
authored
feat(web-ui): add optional ballot text summary (#56)
1 parent 3535a04 commit 2296c34

File tree

4 files changed

+76
-32
lines changed

4 files changed

+76
-32
lines changed

packages/web-ui/src/App.svelte

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
<script lang="ts">
2+
import BallotSummary from "./BallotSummary.svelte";
23
import CopyEncryptedBallotForm from "./CopyEncryptedBallotForm.svelte";
34
import FillBallotForm from "./FillBallotForm.svelte";
45
import GitHubCredentials from "./GitHubCredentials.svelte";
56
import FindPrForm from "./FindPRForm.svelte";
67
7-
let encryptDataPromise = new Promise(() => {}) as Promise<never>;
8+
import encryptData from "@node-core/caritat-crypto/encrypt";
9+
import uint8ArrayToBase64 from "./uint8ArrayToBase64.ts";
10+
const textEncoder =
11+
typeof TextEncoder === "undefined" ? { encode() {} } : new TextEncoder();
12+
13+
let ballot: string | undefined;
14+
let encryptDataPromise = new Promise<never>(() => {});
15+
let shouldSummarize = false;
16+
let ballotSummary = new Promise<never>(() => {});
817
918
let url = globalThis.location?.hash.slice(1);
1019
@@ -23,16 +32,32 @@
2332
step = url ? Math.max(step, 1) : 0;
2433
});
2534
26-
function registerEncryptedBallot(promise) {
27-
encryptDataPromise = promise;
28-
promise.then(
29-
() => {
30-
step = 2;
31-
},
32-
() => {
33-
step = Math.min(step, 1);
34-
}
35-
);
35+
function maybeUpdateSummary() {
36+
ballotSummary = shouldSummarize && ballot ? (async () => {
37+
// Lazy-loading as the summary is only a nice-to-have.
38+
const { getSummarizedBallot } = await import("./ballotSummary.ts");
39+
return getSummarizedBallot(ballot);
40+
})() : new Promise<never>(() => {});
41+
}
42+
function registerBallot(ballotContent, publicKey) {
43+
encryptDataPromise = (async () => {
44+
const { encryptedSecret, saltedCiphertext } = await encryptData(
45+
textEncoder.encode(ballotContent) as Uint8Array,
46+
await publicKey
47+
);
48+
return JSON.stringify({
49+
encryptedSecret: uint8ArrayToBase64(new Uint8Array(encryptedSecret)),
50+
data: uint8ArrayToBase64(saltedCiphertext),
51+
});
52+
})();
53+
ballot = ballotContent;
54+
step = 2;
55+
maybeUpdateSummary();
56+
}
57+
58+
function onSummaryToggle(e) {
59+
shouldSummarize = e.newState === 'open';
60+
maybeUpdateSummary();
3661
}
3762
</script>
3863

@@ -48,8 +73,11 @@
4873
<FindPrForm {url} />
4974
</details>
5075
<details open={step === 1}>
51-
<FillBallotForm {url} {username} {token} {registerEncryptedBallot} />
76+
<FillBallotForm {url} {username} {token} {registerBallot} />
77+
</details>
78+
<details open={shouldSummarize} on:toggle={onSummaryToggle}>
79+
<BallotSummary {ballotSummary} />
5280
</details>
5381
<details open={step === 2}>
54-
<CopyEncryptedBallotForm {encryptDataPromise} {url} />
82+
<CopyEncryptedBallotForm {ballot} {encryptDataPromise} {url} />
5583
</details>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
export let ballotSummary: Promise<string | never>;
3+
</script>
4+
5+
<summary>Ballot summary (requires downloading YAML parser)</summary>
6+
7+
{#await ballotSummary}
8+
<textarea readonly>Getting summary… </textarea>
9+
{:then data}
10+
<textarea readonly>{data}</textarea>
11+
{:catch error}
12+
An error occurred: {error?.message ?? error}
13+
{/await}
14+

packages/web-ui/src/FillBallotForm.svelte

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,16 @@
11
<script lang="ts">
22
import { beforeUpdate } from "svelte";
33
4-
import encryptData from "@node-core/caritat-crypto/encrypt";
5-
import uint8ArrayToBase64 from "./uint8ArrayToBase64.ts";
64
import fetchFromGitHub from "./fetchDataFromGitHub.ts";
75
8-
export let url, username, token, registerEncryptedBallot;
6+
export let url, username, token, registerBallot;
97
108
let fetchedBallot: Promise<string>, fetchedPublicKey;
119
12-
const textEncoder =
13-
typeof TextEncoder === "undefined" ? { encode() {} } : new TextEncoder();
14-
1510
function onSubmit(this: HTMLFormElement, event: SubmitEvent) {
1611
event.preventDefault();
1712
const textarea = this.elements.namedItem("ballot") as HTMLInputElement;
18-
registerEncryptedBallot(
19-
(async () => {
20-
const { encryptedSecret, saltedCiphertext } = await encryptData(
21-
textEncoder.encode(textarea.value) as Uint8Array,
22-
await fetchedPublicKey
23-
);
24-
return JSON.stringify({
25-
encryptedSecret: uint8ArrayToBase64(new Uint8Array(encryptedSecret)),
26-
data: uint8ArrayToBase64(saltedCiphertext),
27-
});
28-
})()
29-
);
13+
registerBallot(textarea.value, fetchedPublicKey);
3014
}
3115
3216
fetchedBallot = fetchedPublicKey = Promise.reject("no data");
@@ -47,7 +31,7 @@
4731
{#await fetchedPublicKey}
4832
<button type="submit" disabled>Loading public key…</button>
4933
{:then}
50-
<button type="submit">Encrypt ballot</button>
34+
<button type="submit">Next</button>
5135
{/await}
5236
</form>
5337
{:catch error}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as yaml from "js-yaml";
2+
import {
3+
getSummarizedBallot as _getSummarizedBallot,
4+
summarizeCondorcetBallotForVoter,
5+
} from "@node-core/caritat/summary/condorcet";
6+
7+
export function getSummarizedBallot(ballotStr: string) {
8+
const ballot = yaml.load(ballotStr) as { preferences: Array<{ title: string; score: number }> };
9+
if (!Array.isArray(ballot?.preferences)) {
10+
throw new Error("Ballot does not contain a list of preferences");
11+
}
12+
return summarizeCondorcetBallotForVoter(
13+
_getSummarizedBallot({
14+
voter: {},
15+
preferences: ballot.preferences.map(({ title, score }) => [title, score]),
16+
}),
17+
);
18+
}

0 commit comments

Comments
 (0)