Skip to content

Commit

Permalink
website(refactor): revisit transparency page (#634)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkue authored Nov 16, 2023
1 parent 0f64d42 commit 3e76008
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 147 deletions.
15 changes: 8 additions & 7 deletions shared/locales/de/website-transparency.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"amount": "{{ value, currency }}",
"amount-since-march-2020": "Insgesamt seit März 2020",
"future-payouts": "Künftige Auszahlungen: {{ value, currency }}",
"past-payouts": "Vergangene Auszahlungen: {{ value, currency }}",
"section-1": {
"title-1": "Transparenz schafft Vertrauen.",
"title-2": "Vertrauen schafft Solidarität.",
Expand All @@ -14,11 +11,14 @@
"section-2": {
"title": "Wie wird Social Income finanziert?",
"donations": "Spenden",
"amount-since-march-2020": "Insgesamt seit März 2020",
"contributions-from": "{{ value, currency }} von",
"individuals": "{{ count }} Individuen",
"institutions": "{{ count }} Institutionen",
"used-for-fees": "Für Zustellgebühren von Individualspenden verwendet: {{ value, currency }}",
"used-for-operating-costs": "Für Betriebsgebühren verwendet: {{ value, currency }}"
"past-payouts": "Vergangene Auszahlungen: {{ value, currency }}",
"future-payouts": "Künftige Auszahlungen: {{ value, currency }}",
"past-costs": "Bereits verwendet um Kosten zu decken: {{ value, currency }}",
"future-costs": "Um kommende Kosten zu decken: {{ value, currency }}"
},
"section-3": {
"title": "Von wo kommen die Spender:innen?",
Expand All @@ -29,10 +29,12 @@
"section-4": {
"title": "Wie werden Spenden ausbezahlt?",
"subtitle": "Individuelle Spenden werden zu 100% ausbezahlt. Institutionell Spenden werden gebraucht um die operativen Kosten zu decken.",
"amount-since-march-2020": "Insgesamt seit März 2020",
"expenses": "Ausgaben",
"payments-total": "{{ value, currency }} ausbezahlt an {{ recipientsCount }} Empfänger:innen",
"payments-last-month": "Letzter Monat: {{ value, currency }} an {{ recipientsCount }} Empfänger:innen",
"total-costs": "{{ value, currency }} fees and costs",
"future-payouts": "Künftige Auszahlungen: {{ value, currency }}",
"total-costs": "{{ value, currency }} Gebühren und Kosten",
"payment-fees": "Zustellgebühren: {{ value, currency }}",
"payment-fees-tooltip": "Dies beinhaltet die Transaktionsgebühren von Zahlungsportalen wie Stripe.",
"transaction-fees": "Transaktionsgebühren: {{ value, currency }}",
Expand All @@ -41,7 +43,6 @@
"operating-costs-tooltip": "Dies beinhaltet Kosten für Saläre, Marketing, Fundraising.",
"other-costs": "Andere Kosten: {{ value, currency }}",
"other-costs-tooltip": "??",

"reserves": "Reserven",
"covers-payments": "Reserven decken Auszahlungen für",
"covers-payments-1": "{{ recipientsCount }} Empfänger:innen für {{ monthsCount }} Monate",
Expand Down
14 changes: 7 additions & 7 deletions shared/locales/en/website-transparency.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"amount": "{{ value, currency }}",
"amount-since-march-2020": "Total amount since March 2020",
"future-payouts": "To be paid out: {{ value, currency }}",
"past-payouts": "Already paid out: {{ value, currency }}",
"section-1": {
"title-1": "Transparency builds trust.",
"title-2": "Trust builds solidarity.",
Expand All @@ -14,11 +11,14 @@
"section-2": {
"title": "How is Social Income funded?",
"donations": "Donations",
"amount-since-march-2020": "Total amount since March 2020",
"contributions-from": "{{ value, currency }} contributions from",
"individuals": "{{ count }} Individuals",
"institutions": "{{ count }} Institutions",
"used-for-fees": "Used to cover payment fees: {{ value, currency }}",
"used-for-operating-costs": "Used to cover operational costs: {{ value, currency }}"
"past-payouts": "Already paid out: {{ value, currency }}",
"future-payouts": "To be paid out: {{ value, currency }}",
"past-costs": "Already used to cover costs: {{ value, currency }}",
"future-costs": "To be used to cover costs: {{ value, currency }}"
},
"section-3": {
"title": "Where are our contributors from?",
Expand All @@ -29,10 +29,11 @@
"section-4": {
"title": "How are the funds used?",
"subtitle": "Recurring individual donations are always paid out to 100%. Institutional donations are used to cover our operational costs.",
"amount-since-march-2020": "Total amount since March 2020",
"expenses": "Expenses",
"payments-total": "{{ value, currency }} paid out to {{ recipientsCount }} recipients",
"payments-last-month": "Last month: {{ value, currency }} to {{ recipientsCount }} recipients",

"future-payouts": "To be paid out: {{ value, currency }}",
"total-costs": "{{ value, currency }} fees and costs",
"payment-fees": "Payment fees: {{ value, currency }}",
"payment-fees-tooltip": "This includes transaction fees charged by payment providers like Stripe.",
Expand All @@ -43,7 +44,6 @@
"other-costs": "Other costs: {{ value, currency }}",
"other-costs-tooltip": "??",
"reserves": "Reserves",

"covers-payments": "This covers payments for",
"covers-payments-1": "{{ recipientsCount }} recipients for {{ monthsCount }} months",
"covers-payments-2": "{{ recipientsCount }} recipients for 1 month",
Expand Down
52 changes: 25 additions & 27 deletions shared/src/utils/stats/ContributionStatsCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@ export class ContributionStatsCalculator {
* ContributionStatsCalculator with the flattened intermediate data structure.
* @param firestoreAdmin
* @param currency
* @param contributionFilter
*/
static async build(firestoreAdmin: FirestoreAdmin, currency: string): Promise<ContributionStatsCalculator> {
static async build(
firestoreAdmin: FirestoreAdmin,
currency: string,
contributionFilter = (c: Contribution) => c.status === StatusKey.SUCCEEDED,
): Promise<ContributionStatsCalculator> {
const exchangeRate = await getLatestExchangeRate(firestoreAdmin, currency);

const getContributionsForUser = async (userId: string): Promise<Contribution[]> => {
Expand All @@ -71,32 +76,25 @@ export class ContributionStatsCalculator {
.map(async (userDoc) => {
const user = userDoc.data();
const contributions = await getContributionsForUser(userDoc.id);
return contributions
.filter(
(contribution) =>
contribution.status == StatusKey.SUCCEEDED ||
contribution.status == StatusKey.UNKNOWN ||
contribution.status == undefined,
)
.map((contribution) => {
const created = contribution.created.toDate();
if (created.getFullYear() < 2020) {
console.log(userDoc.id, created);
}
return {
userId: userDoc.id,
isInstitution: Boolean(user.institution),
country: user.location?.toUpperCase() ?? 'CH',
amount: contribution.amount_chf * exchangeRate,
paymentFees: contribution.fees_chf * exchangeRate,
source: contribution.source,
currency: contribution.currency.toUpperCase() ?? '',
month: DateTime.fromObject({
year: created.getFullYear(),
month: created.getMonth() + 1, // month is indexed from 0 in JS
}).toFormat('yyyy-MM'),
} as ContributionStatsEntry;
});
return contributions.filter(contributionFilter).map((contribution) => {
const created = contribution.created.toDate();
if (created.getFullYear() < 2020) {
console.log(userDoc.id, created);
}
return {
userId: userDoc.id,
isInstitution: Boolean(user.institution),
country: user.location?.toUpperCase() ?? 'CH',
amount: contribution.amount_chf * exchangeRate,
paymentFees: contribution.fees_chf * exchangeRate,
source: contribution.source,
currency: contribution.currency.toUpperCase() ?? '',
month: DateTime.fromObject({
year: created.getFullYear(),
month: created.getMonth() + 1, // month is indexed from 0 in JS
}).toFormat('yyyy-MM'),
} as ContributionStatsEntry;
});
}),
);
return new ContributionStatsCalculator(_(contributions.flat()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { Typography } from '@socialincome/ui';
import { Card, CardContent, Typography } from '@socialincome/ui';
import _ from 'lodash';
import { ReactElement } from 'react';

type TransparencyCardProps = {
sectionTitle: string;
title: string;
text: string;

firstIcon?: ReactElement;
firstContent?: ReactElement;

secondIcon?: ReactElement;
secondContent?: ReactElement;
};
Expand All @@ -18,30 +16,30 @@ export function InfoCard(
{ sectionTitle, title, text, firstIcon, firstContent, secondIcon, secondContent }: TransparencyCardProps,
) {
return (
<div className="divide-neutral grid grid-cols-1 items-center divide-y rounded-lg bg-neutral-200 py-8 md:grid-cols-2 md:divide-x md:divide-y-0">
<div className="space-y-2 p-8">
<Typography size="2xl">{sectionTitle}</Typography>
<Typography size="3xl" weight="bold">
{title}
</Typography>
<Typography size="2xl" weight="medium">
{text}
</Typography>
</div>
<div className="flex flex-col space-y-8 p-8">
{!_.isNil(firstIcon) && !_.isNil(firstContent) && (
<div className="grid grid-cols-9">
<div>{firstIcon}</div>
<div className="col-span-8">{firstContent}</div>
</div>
)}
{!_.isNil(secondIcon) && !_.isNil(secondContent) && (
<div className="grid grid-cols-9">
<div>{secondIcon}</div>
<div className="col-span-8">{secondContent}</div>
</div>
)}
</div>
</div>
<Card>
<CardContent className="grid grid-cols-1 items-center divide-y py-8 md:grid-cols-2 md:divide-x md:divide-y-0">
<div className="space-y-2 p-8">
<Typography size="2xl">{sectionTitle}</Typography>
<Typography size="3xl" weight="bold">
{title}
</Typography>
<Typography size="2xl">{text}</Typography>
</div>
<div className="flex flex-col space-y-8 p-8">
{!_.isNil(firstIcon) && !_.isNil(firstContent) && (
<div className="grid grid-cols-9">
<div>{firstIcon}</div>
<div className="col-span-8">{firstContent}</div>
</div>
)}
{!_.isNil(secondIcon) && !_.isNil(secondContent) && (
<div className="grid grid-cols-9">
<div>{secondIcon}</div>
<div className="col-span-8">{secondContent}</div>
</div>
)}
</div>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,12 @@ export default async function Page({ params }: TransparencyPageProps) {
const { contributionStats, paymentStats } = await getStats(currency);

return (
<div>
<div className="flex flex-col space-y-16 py-8">
<CurrencyRedirect currency={currency} />
<div className="flex flex-col space-y-16">
<Section1 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
<Section2 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
<Section3 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
<Section4 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
</div>
<Section1 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
<Section2 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
<Section3 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
<Section4 params={params} contributionStats={contributionStats} paymentStats={paymentStats} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { Typography } from '@socialincome/ui';
import { Card, CardContent, Typography } from '@socialincome/ui';
import _ from 'lodash';
import { SectionProps } from './page';

Expand All @@ -25,23 +25,27 @@ export async function Section1({ params, paymentStats, contributionStats }: Sect
];

return (
<div className="flex flex-col justify-center space-y-2">
<div className="my-8">
<Typography weight="bold" size="4xl" lineHeight="tight">
<div>
<div className="mb-8">
<Typography weight="bold" size="4xl">
{translator.t('section-1.title-1')}
</Typography>
<Typography weight="bold" size="4xl" lineHeight="tight">
<Typography weight="bold" size="4xl" color="secondary">
{translator.t('section-1.title-2')}
</Typography>
</div>
<Typography color="muted-foreground">{translator.t('section-1.since-march-2020')}</Typography>
{cards.map((card, index) => (
<div key={index} className="border-neutral rounded-lg border px-4 py-6 duration-200 hover:scale-[102%]">
<Typography size="xl" weight="normal">
{card}
</Typography>
</div>
))}
<div className="flex flex-col space-y-2">
<Typography color="muted-foreground">{translator.t('section-1.since-march-2020')}</Typography>
{cards.map((card, index) => (
<Card key={index} className="duration-200 hover:scale-[102%]">
<CardContent className="py-8">
<Typography size="xl" weight="normal">
{card}
</Typography>
</CardContent>
</Card>
))}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,24 @@ import { SectionProps } from './page';
export async function Section2({ params, contributionStats, paymentStats }: SectionProps) {
const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-transparency'] });
const paymentFees = _.sumBy(contributionStats.totalPaymentFeesByIsInstitution, 'amount');

// TODO: Calculate these costs dynamically
const transactionFees = 8600;
const operatingCosts = 9300;
const otherCosts = 9600;
const totalCosts = paymentFees + transactionFees + operatingCosts + otherCosts;

return (
<div className="flex flex-col space-y-8">
<Typography weight="bold" size="3xl">
<div>
<Typography weight="bold" size="3xl" className="mb-8">
{translator.t('section-2.title')}
</Typography>
<InfoCard
sectionTitle={translator.t('section-2.donations')}
title={translator.t('amount', {
context: { value: contributionStats.totalContributionsAmount, currency: params.currency },
})}
text={translator.t('amount-since-march-2020')}
text={translator.t('section-2.amount-since-march-2020')}
firstIcon={<HeartIcon className="h-8 w-8" />}
firstContent={
<div className="flex flex-col space-y-1">
Expand All @@ -31,23 +34,23 @@ export async function Section2({ params, contributionStats, paymentStats }: Sect
{translator.t('section-2.contributions-from', {
context: { value: contributionStats.totalIndividualContributionsAmount, currency: params.currency },
})}
<Badge className="mx-1">
{translator.t('section-2.individuals', {
context: { count: contributionStats.totalIndividualContributorsCount },
})}
</Badge>
</Typography>
<Badge className="mx-1">
{translator.t('section-2.individuals', {
context: { count: contributionStats.totalIndividualContributorsCount },
})}
</Badge>
</div>
<Typography>
{translator.t('past-payouts', {
{translator.t('section-2.past-payouts', {
context: {
value: paymentStats.totalPaymentsAmount,
currency: params.currency,
},
})}
</Typography>
<Typography>
{translator.t('future-payouts', {
{translator.t('section-2.future-payouts', {
context: {
value: contributionStats.totalIndividualContributionsAmount - paymentStats.totalPaymentsAmount,
currency: params.currency,
Expand All @@ -67,22 +70,25 @@ export async function Section2({ params, contributionStats, paymentStats }: Sect
currency: params.currency,
},
})}
<Badge className="mx-1">
{translator.t('section-2.institutions', {
context: { count: contributionStats.totalInstitutionalContributorsCount },
})}
</Badge>
</Typography>
<Badge className="mx-1">
{translator.t('section-2.institutions', {
context: { count: contributionStats.totalInstitutionalContributorsCount },
})}
</Badge>
</div>
<Typography>
{translator.t('section-2.used-for-fees', {
context: { value: paymentFees, currency: params.currency },
{translator.t('section-2.past-costs', {
context: {
value: totalCosts,
currency: params.currency,
},
})}
</Typography>
<Typography>
{translator.t('section-2.used-for-operating-costs', {
{translator.t('section-2.future-costs', {
context: {
value: paymentFees + transactionFees + operatingCosts + otherCosts,
value: contributionStats.totalInstitutionalContributionsAmount - totalCosts,
currency: params.currency,
},
})}
Expand Down
Loading

0 comments on commit 3e76008

Please sign in to comment.