From b7861ae321daad25b4dbfcc14d92d439e57b602d Mon Sep 17 00:00:00 2001 From: Patrick McClurg Date: Mon, 27 Nov 2023 15:27:27 +0100 Subject: [PATCH] explainer text for selection process --- draws_upcoming/aurora.json | 6 - .../draws/10-aurora-2023-10-31.txt | 2 +- recipients_selection/draws/2-testlist | 1 - recipients_selection/lists/2-testlist | 4 - shared/locales/de/website-selection.json | 5 - shared/locales/en/website-selection.json | 5 - .../(website)/selection/drawCard.tsx | 112 --------------- .../[country]/(website)/selection/page.tsx | 100 ------------- .../[country]/(website)/selection/state.ts | 63 -------- .../recipient-selection/drawCard.tsx | 136 ++++++++++++++++++ .../recipient-selection/explainer.tsx | 120 ++++++++++++++++ .../transparency/recipient-selection/page.tsx | 59 +++++++- .../transparency/recipient-selection/state.ts | 60 ++++++++ website/src/components/navbar/navbar.tsx | 6 +- 14 files changed, 376 insertions(+), 303 deletions(-) delete mode 100644 draws_upcoming/aurora.json delete mode 100644 recipients_selection/draws/2-testlist delete mode 100644 recipients_selection/lists/2-testlist delete mode 100644 website/src/app/[lang]/[country]/(website)/selection/drawCard.tsx delete mode 100644 website/src/app/[lang]/[country]/(website)/selection/page.tsx delete mode 100644 website/src/app/[lang]/[country]/(website)/selection/state.ts create mode 100644 website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/drawCard.tsx create mode 100644 website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/explainer.tsx create mode 100644 website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/state.ts diff --git a/draws_upcoming/aurora.json b/draws_upcoming/aurora.json deleted file mode 100644 index 5c3da6ae1..000000000 --- a/draws_upcoming/aurora.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "time": "2023/10/15", - "name": "Aurora Draw", - "count": 10, - "total": 400 -} diff --git a/recipients_selection/draws/10-aurora-2023-10-31.txt b/recipients_selection/draws/10-aurora-2023-10-31.txt index abc511d59..f1e22bf2c 100644 --- a/recipients_selection/draws/10-aurora-2023-10-31.txt +++ b/recipients_selection/draws/10-aurora-2023-10-31.txt @@ -1 +1 @@ -{"time":1698779251122,"name":"","total":567,"round":3444941,"hashedInput":"753eb631c9aa540310eb7314d9b4c137bdf053c026faefe0014f189d75612092","winners":["651cb050678d54b000df571ccd78bc0fa89b2c43782414de66df104535457424","40a62f179c7440140225c6950bb364e6fffe5c7e36ca6cf276a7eedc90f87207","c45031211e6edbcc616bf3d2a73cdc31076b325ecc8c7bd940db2492eb8c1e31","5eca1cd5f4f403ac9819c378e3985199c50d2404ae7a896cd9800c1453acffd6","9d82ee01c6d50cf12e24c877646153210e90a1f3f71a14898a2e75ca8f845bb4","09b403c75ad8e1fe3efe4c3870a91ec7daf8221e2c47f076fc585948b8573be4","b16cf5bd32d3bed96dda82a23d35556f6ec436ee7ba4640c46e7248668a479ad","5b418305ba948acc1de234f82916dc247ffa4811c0ac10735d211e0c1a30e16c","2f12c10e1ab50ac952828f1e177c63565383fe2161ef2dbcbdc9d5e81db69815","6055cc0c460460f23f81ede9849cc5ca8131b991deaa8fde0e1ba6a424f48f08"],"randomness":"69db1f1d13ea50e52814746f59ab9753144942528321bbc3768e543324ffb9dc"} \ No newline at end of file +{"time":1698779251122,"totalCount":567,"round":3444941,"winners":["651cb050678d54b000df571ccd78bc0fa89b2c43782414de66df104535457424","40a62f179c7440140225c6950bb364e6fffe5c7e36ca6cf276a7eedc90f87207","c45031211e6edbcc616bf3d2a73cdc31076b325ecc8c7bd940db2492eb8c1e31","5eca1cd5f4f403ac9819c378e3985199c50d2404ae7a896cd9800c1453acffd6","9d82ee01c6d50cf12e24c877646153210e90a1f3f71a14898a2e75ca8f845bb4","09b403c75ad8e1fe3efe4c3870a91ec7daf8221e2c47f076fc585948b8573be4","b16cf5bd32d3bed96dda82a23d35556f6ec436ee7ba4640c46e7248668a479ad","5b418305ba948acc1de234f82916dc247ffa4811c0ac10735d211e0c1a30e16c","2f12c10e1ab50ac952828f1e177c63565383fe2161ef2dbcbdc9d5e81db69815","6055cc0c460460f23f81ede9849cc5ca8131b991deaa8fde0e1ba6a424f48f08"],"randomness":"69db1f1d13ea50e52814746f59ab9753144942528321bbc3768e543324ffb9dc"} \ No newline at end of file diff --git a/recipients_selection/draws/2-testlist b/recipients_selection/draws/2-testlist deleted file mode 100644 index baccec690..000000000 --- a/recipients_selection/draws/2-testlist +++ /dev/null @@ -1 +0,0 @@ -{"time":1698779281090,"name":"","total":2,"round":0,"hashedInput":"f3352d643b5f692dc4d22efed58ee6cb25a81fc9f3ff7a68b146622335c6e4c5","winners":["267bdf7cf143790e41f721799d7b32c55c986c80402bada7cbcfd8bf0886586c","68d83e3231f56d9d66cd903775249bc0d7ded79c4de1c7f7d789a99e6232b4b9"],"randomness":""} \ No newline at end of file diff --git a/recipients_selection/lists/2-testlist b/recipients_selection/lists/2-testlist deleted file mode 100644 index c3dfecc0f..000000000 --- a/recipients_selection/lists/2-testlist +++ /dev/null @@ -1,4 +0,0 @@ -267bdf7cf143790e41f721799d7b32c55c986c80402bada7cbcfd8bf0886586c -68d83e3231f56d9d66cd903775249bc0d7ded79c4de1c7f7d789a99e6232b4b9 -267bdf7cf143790e41f721799d7b32c55c986c80402bada7cbcfd8bf0886586c -68d83e3231f56d9d66cd903775249bc0d7ded79c4de1c7f7d789a99e6232b4b9 diff --git a/shared/locales/de/website-selection.json b/shared/locales/de/website-selection.json index 62e9b0f25..11678ae6e 100644 --- a/shared/locales/de/website-selection.json +++ b/shared/locales/de/website-selection.json @@ -1,10 +1,5 @@ { - "upcoming": "In Zukunft", "past": "Zurückliegend", - "read-draws": "Um mehr über die Auswahlverfahren zu lesen, besuchen Sie", - "read-draws-link": "some website", - "read-draws-2": "um zu herausfinde, wie sie funktionieren", - "future-draws": "Zukünftliche Auswahlverfahren werden hier veröffentlicht, und sind abhängig von den finanzielle Möglichkeiten des Social Incomes.", "random-number": "Zusätzliche Zahl", "confirm-drand": "Bestätigen bei drand", "confirm-github": "Bestätigen bei Github", diff --git a/shared/locales/en/website-selection.json b/shared/locales/en/website-selection.json index 202c85fc2..15a69b6a5 100644 --- a/shared/locales/en/website-selection.json +++ b/shared/locales/en/website-selection.json @@ -1,10 +1,5 @@ { - "upcoming": "Upcoming", "past": "Past", - "read-draws": "To read more about draws visit", - "read-draws-link": "some website", - "read-draws-2": "to find about about how they work", - "future-draws": "Future draws for new recipients are announced here and depend on the financial possibilities of Social Income.", "random-number": "Random number", "confirm-drand": "Confirm on drand", "confirm-github": "Confirm on Github", diff --git a/website/src/app/[lang]/[country]/(website)/selection/drawCard.tsx b/website/src/app/[lang]/[country]/(website)/selection/drawCard.tsx deleted file mode 100644 index 3ba161267..000000000 --- a/website/src/app/[lang]/[country]/(website)/selection/drawCard.tsx +++ /dev/null @@ -1,112 +0,0 @@ -'use client'; -import { FutureDraw, PastDraw } from '@/app/[lang]/[country]/(website)/selection/state'; -import { Disclosure } from '@headlessui/react'; -import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'; -import { Translator } from '@socialincome/shared/src/utils/i18n'; -import { Typography } from '@socialincome/ui'; -import * as React from 'react'; - -export interface DrawCardsProps { - summary: React.ReactNode; - detail?: React.ReactNode; -} - -// a collapsible element rendering the summary, and optionally a button to open the detailed view -export function DrawCard(props: DrawCardsProps) { - return ( - - {({ open }) => ( -
-
-
{props.summary}
-
- {!!props.detail ? ( - - {open ? ( - - ) : ( - - )} - - ) : ( - /* this is a filthy hack for spacing, but is actually responsive by some sorcery */ -           - )} -
-
- {!!props.detail && {props.detail}} -
- )} -
- ); -} - -export type DrawSummaryProps = { - translations: { - from: string; - }; - draw: PastDraw | FutureDraw; - translator?: Translator; -}; - -export function DrawSummary({ draw, translations }: DrawSummaryProps) { - return ( -
-
- - {new Date(draw.time).toLocaleDateString()} - -
-
- - {draw.name} - -
-
- - {draw.count} {translations.from} {draw.total} - -
-
- ); -} - -export type DrawDetailProps = { - translations: { - randomNumber: string; - longlist: string; - people: string; - confirmDrand: string; - confirmGithub: string; - }; - draw: PastDraw; -}; - -export function DrawDetail({ draw, translations }: DrawDetailProps) { - return ( -
-
-
- {translations.randomNumber}: - {draw.drandRandomness} -
-
- - {translations.confirmDrand} - -
-
-
-
- {translations.people}: - {translations.longlist} -
-
- - {translations.confirmGithub} - -
-
-
- ); -} diff --git a/website/src/app/[lang]/[country]/(website)/selection/page.tsx b/website/src/app/[lang]/[country]/(website)/selection/page.tsx deleted file mode 100644 index 90c1fc156..000000000 --- a/website/src/app/[lang]/[country]/(website)/selection/page.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { DefaultPageProps } from '@/app/[lang]/[country]'; -import { DrawCard, DrawDetail, DrawSummary } from '@/app/[lang]/[country]/(website)/selection/drawCard'; -import { loadFutureDraws, loadPastDraws } from '@/app/[lang]/[country]/(website)/selection/state'; -import { Translator } from '@socialincome/shared/src/utils/i18n'; -import { BaseContainer, Typography } from '@socialincome/ui'; - -export default async function Page(props: DefaultPageProps) { - const translator = await Translator.getInstance({ language: props.params.lang, namespaces: 'website-selection' }); - - const futureDraws = await loadFutureDraws().catch(_ => []); - const pastDraws = await loadPastDraws().catch(_ => []); - - return ( - - - - - {translator.t('upcoming')} - - - {futureDraws.length === 0 && {translator.t('none-scheduled')}} - - {translator.t('future-draws')} - {futureDraws.map((draw) => ( - - } - /> - ))} - - - {translator.t('past')} - - - {pastDraws.length === 0 && {translator.t('none-completed')}} - - {pastDraws.map((draw) => ( - - } - detail={ - - } - /> - ))} - - ); -} - -type IntroTextProps = { - translations: { - readDraws: string; - readDraws2: string; - readDrawsLink: string; - }; -}; - -function IntroText({ translations }: IntroTextProps) { - return ( -
- - {translations.readDraws}  - - {translations.readDrawsLink} - -   - {translations.readDraws2} - -
- ); -} diff --git a/website/src/app/[lang]/[country]/(website)/selection/state.ts b/website/src/app/[lang]/[country]/(website)/selection/state.ts deleted file mode 100644 index 260aeba58..000000000 --- a/website/src/app/[lang]/[country]/(website)/selection/state.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as fs from 'fs/promises'; - -export type FutureDraw = { - time: number; - name: string; - count: number; - total: number; -}; - -export type PastDraw = { - time: number; - name: string; - count: number; - total: number; - drandRound: number; - drandRandomness: string; - filename: string; -}; - -type DrawFile = { - time: number; - name: string; - total: number; - hashedInput: string; - winners: Array; - randomness: string; - round: number; -}; - -export async function loadPastDraws(): Promise> { - const drawsPath = '../draws'; - const files = await fs.readdir(drawsPath); - const draws: Array = []; - - for (const file of files) { - const drawContents = await fs.readFile(`${drawsPath}/${file}`); - const drawFile: DrawFile = JSON.parse(drawContents.toString()); - draws.push({ - time: drawFile.time, - name: drawFile.name, - total: drawFile.total, - drandRound: drawFile.round, - drandRandomness: drawFile.randomness, - count: drawFile.winners.length, - filename: file, - }); - } - - return draws; -} - -export async function loadFutureDraws(): Promise> { - const futureDrawsPath = '../draws_upcoming'; - const files = await fs.readdir(futureDrawsPath); - const draws: Array = []; - - for (const file of files) { - const drawContents = await fs.readFile(`${futureDrawsPath}/${file}`); - draws.push(JSON.parse(drawContents.toString())); - } - - return draws; -} diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/drawCard.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/drawCard.tsx new file mode 100644 index 000000000..899f10bec --- /dev/null +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/drawCard.tsx @@ -0,0 +1,136 @@ +'use client'; +import {FutureDraw, PastDraw} from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/state'; +import {Disclosure} from '@headlessui/react'; +import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/20/solid'; +import {Translator} from '@socialincome/shared/src/utils/i18n'; +import {Card, Typography} from '@socialincome/ui'; +import * as React from 'react'; + +export interface DrawCardsProps { + summary: React.ReactNode; + detail?: React.ReactNode; +} + +// a collapsible element rendering the summary, and optionally a button to open the detailed view +export function DrawCard(props: DrawCardsProps) { + return ( + + {({open}) => ( + +
+
{props.summary}
+
+ {!!props.detail ? ( + + {open ? ( + + ) : ( + + )} + + ) : ( + /* this is a filthy hack for spacing, but is actually responsive by some sorcery */ +           + )} +
+
+ {!!props.detail && {props.detail}} +
+ )} +
+ ); +} + +// this details the fields that are in each draw's card +export function DrawHeader() { + return ( +
+
+ + Date + +
+
+ + Draw name + +
+
+ + Details + +
+
+ ) +} + +export type DrawSummaryProps = { + translations: { + from: string; + }; + draw: PastDraw | FutureDraw; + translator?: Translator; +}; + +export function DrawSummary({draw, translations}: DrawSummaryProps) { + return ( +
+
+ + {new Date(draw.time).toDateString()} + +
+
+ + {draw.name} + +
+
+ + {draw.count} {translations.from} {draw.total} + +
+
+ ); +} + +export type DrawDetailProps = { + translations: { + randomNumber: string; + longlist: string; + people: string; + confirmDrand: string; + confirmGithub: string; + }; + draw: PastDraw; +}; + +export function DrawDetail({draw, translations}: DrawDetailProps) { + return ( +
+
+
+ {translations.randomNumber}: + {draw.drandRandomness} +
+ +
+
+
+ {translations.people}: + {translations.longlist} +
+ +
+
+ ); +} diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/explainer.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/explainer.tsx new file mode 100644 index 000000000..b8ec48de4 --- /dev/null +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/explainer.tsx @@ -0,0 +1,120 @@ +import {Card, CardContent, Typography} from "@socialincome/ui" + +export function Explainer() { + return ( +
+ + Selecting Recipients + +

Social Income, like any UBI project, has limited financial resources. Choosing who gets help and who + doesn't is tough. We want everyone to understand how we select our recipients. Since November 2023, + we’ve employed a new way to randomly select people from poor communities, thanks to our partnership + with drand.

+
+ + Our selection process in 3 steps: + +
+ + Step 1: Finding potential recipients + +

We team up with a variety of international and local NGOs who support marginalized communities and are + familiar with poverty at the local level. After visiting and positively assessing these NGOs – as well + as the areas and communities they support – we may request that they provide us with a list of all + potential recipients.

+

Potential recipients are people living in poverty with whom they maintain direct + contact. Depending on the communities supported, those lists can include anything from 100 to 1000 + names.

+ +
+ + Step 2: Selecting at random and with utmost transparency + +

The lists we are + working with during selection contain minimal information about potential recipients. They are + subsequently + anonymized (hashed) and uploaded to our open-source + repository. With the help of a random number generated and published by drand, a mechanism is set in place to select a predetermined + number of individuals from the list. This process can be mathematically traced back and verified without + compromising recipients’ data.

+ + +
+

+ 🔎 Upcoming: allowing everyone to easily verify for themselves whether or not they're on one + of the NGOs' lists or whether they have been chosen. +

+
+
+
+ + Step 3: Communicating transparently + +

After a draw, we reach out to both the NGO and the community directly to share the results. The + beneficiaries who were selected – upon confirming their participation – then provide additional + information and are onboarded for our 3-year program

+
+ + Frequently asked questions + +
+ + Why do we select randomly out of a pool of qualified recipients? + +

The purpose of employing this random selection method is to achieve several objectives. Firstly, it helps + prevent bias in our recipient selection, ensuring that relatives or acquaintances of individuals + involved in the process are not given preferential treatment. Secondly, it aims to avoid tensions + between recipients and non-recipients, as well as any potential conflicts between recipients and our + organization. Lastly, we strive to incorporate technology where it is feasible and beneficial to the + process.

+ +
+ + Who can influence the draft? + +

By relying on an unpredictable random value from drand, set to be emitted in the future, it's not + possible for anyone to sway the results of the selection process. This inherent randomness can be + utilized to confirm the integrity of the selection. To qualify for Social Income, an individual must + first be on a list provided by an NGO. While this does present a preliminary condition, it aligns with + our foundational principle: ensuring that Social Income reaches those most in need.

+ +
+ + How do we avoid bias and tensions? + +

To ensure we don't inadvertently prioritize a specific ethnic group, gender, or occupation, we + collaborate with diverse NGOs that have varied missions. In line with our 'do no harm' policy, + we strive to prevent tensions among recipients, non-recipients, communities, and the NGOs. Our random + selection plays a pivotal role in achieving this. It ensures that those related to or acquainted with + individuals in the process aren't shown favoritism, which could lead to conflicts.

+ +
+ + What is happening during the selection process? + +

We utilize the random number provided by drand, which is publicly accessible, to pick a set number of + individuals from our list. Here's how it's done: we first organize the hashed list of + recipients. Then, using the randomness element from drand, we convert it into a position on the list + through a key derivation function. + For those keen on the technical details, the function and its associated processes are documented in + our Github repository.

+ +
+ + What is drand and who is behind it? + +

The drand project generates random numbers that everyone can trust. It can be applied to create truly + random and verifiable drafts. Initiated in 2017 at EPFL by Nicolas Gailly, drand received support from + Philipp Jovanovic and was guided by Bryan Ford. It has since become independently managed and maintains + a network called the League of Entropy with + EPFL, UCL, Cloudflare, Kudelski Security, the University of Chile, and Protocol Labs. Current core + maintainers of the open source project are the CEO, CSO and CTO of Randamu, respectively Erick Watson, Yolan Romailler, and Patrick McClurg.

+
+
+ ) +} \ No newline at end of file diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx index 6c4ab854f..0a6c4b672 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx @@ -1,5 +1,58 @@ -import { Typography } from '@socialincome/ui'; +import {DefaultPageProps} from '@/app/[lang]/[region]'; +import { + DrawCard, + DrawDetail, DrawHeader, + DrawSummary +} from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/drawCard'; +import {loadPastDraws} from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/state'; +import {Translator} from '@socialincome/shared/src/utils/i18n'; +import {BaseContainer, Typography} from '@socialincome/ui'; +import {Explainer} from "@/app/[lang]/[region]/(website)/transparency/recipient-selection/explainer" -export default async function Page() { - return Coming soon; +export default async function Page(props: DefaultPageProps) { + const translator = await Translator.getInstance({language: props.params.lang, namespaces: 'website-selection'}); + + const pastDraws = await loadPastDraws().catch(_ => []); + // sort the draws in descending order by time + pastDraws.sort((a, b) => b.time - a.time); + + return ( + + + + {translator.t('past')} + + + {pastDraws.length === 0 && {translator.t('none-completed')}} + + + + {pastDraws.map((draw) => ( + + } + detail={ + + } + /> + ))} + + ); } + diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/state.ts b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/state.ts new file mode 100644 index 000000000..5dade2fae --- /dev/null +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/state.ts @@ -0,0 +1,60 @@ +import * as fs from 'fs/promises'; + +export type FutureDraw = { + time: number; + name: string; + count: number; + total: number; +}; + +export type PastDraw = { + time: number; + name: string; + count: number; + total: number; + drandRound: number; + drandRandomness: string; + filename: string; +}; + +type DrawFile = { + time: number; + totalCount: number; + winners: Array; + randomness: string; + round: number; +}; + +export async function loadPastDraws(): Promise> { + const drawsPath = '../recipients_selection/draws'; + const files = await fs.readdir(drawsPath); + const draws: Array = []; + + for (const file of files) { + const drawContents = await fs.readFile(`${drawsPath}/${file}`); + const drawFile: DrawFile = JSON.parse(drawContents.toString()); + + draws.push({ + time: drawFile.time, + name: extractDrawName(file), + total: drawFile.totalCount, + drandRound: drawFile.round, + drandRandomness: drawFile.randomness, + count: drawFile.winners.length, + filename: file, + }); + } + + return draws +} + +// extracts the name from a file of format `{count}-{name}-{date}.txt` and capitalises the first letter +function extractDrawName(filename: string): string { + const drawNameMatch = filename.match(/\d-([A-Za-z \-]+)-.*\.txt/); + if (drawNameMatch == null || drawNameMatch.length < 2) { + return ""; + } + const unsanitisedName = drawNameMatch[1]; + const withSpaces = unsanitisedName.replaceAll("-", " "); + return withSpaces.slice(0, 1).toUpperCase() + withSpaces.slice(1).toLowerCase(); +} diff --git a/website/src/components/navbar/navbar.tsx b/website/src/components/navbar/navbar.tsx index 3c17adb36..f9e6e7715 100644 --- a/website/src/components/navbar/navbar.tsx +++ b/website/src/components/navbar/navbar.tsx @@ -31,9 +31,9 @@ export default async function Navbar({ lang, region, showNavigation = true }: Na code: lang, translation: translator.t(`languages.${lang}`), }))} - regions={websiteRegions.map((country) => ({ - code: country, - translation: translator.t(`regions.${country}`), + regions={websiteRegions.map((region) => ({ + code: region, + translation: translator.t(`regions.${region}`), }))} currencies={websiteCurrencies.map((currency) => ({ code: currency,