diff --git a/admin/src/collections/Campaigns.ts b/admin/src/collections/Campaigns.ts index e0d46b1e7..fb0f6c1cc 100644 --- a/admin/src/collections/Campaigns.ts +++ b/admin/src/collections/Campaigns.ts @@ -104,5 +104,17 @@ export const campaignsCollection = buildAuditedCollection({ enumValues: campaignStatusEnumValues, validation: { required: true }, }, + metadata_description: { + dataType: 'string', + name: 'Metadata Description', + }, + metadata_ogImage: { + dataType: 'string', + name: 'Metadata Open Graph Image Path', + }, + metadata_twitterImage: { + dataType: 'string', + name: 'Metadata Twitter Image Path', + }, }), }); diff --git a/shared/locales/de/website-donate.json b/shared/locales/de/website-donate.json index 232ed32c2..a7d38feb6 100644 --- a/shared/locales/de/website-donate.json +++ b/shared/locales/de/website-donate.json @@ -1,12 +1,10 @@ { - "metadata": { - "title": "Mein Beitrag | Social Income" - }, "amount-currency": "{{ amount, currency }}", "title": "Was ist dein durchschnittliches monatliches Einkommen?", "how-to-pay": "Wie möchtest du bezahlen?", "amount": "Betrag", "button-text": "Mache einen Unterschied", + "button-text-short": "Jetzt spenden", "donation-impact": { "monthly-contribution": "Dein monatlicher Beitrag:", "direct-payout": "Dein Beitrag wird direkt auf das Mobiltelefon der Social Income Empfänger:innen ausgezahlt.", @@ -92,7 +90,12 @@ }, "days-left_zero": "Letzter Tag", "days-left_one": "Noch einen Tag bis zum Spendeschluss", - "days-left_other": "{{ count }} verbleibende Tage:", - "ended": "Die Kampagne ist geschlossen. Keine weiteren Spenden möglich." + "days-left_other": "{{ count }} verbleibende Tage", + "ended": "Die Kampagne ist beendet. Für reguläre Spenden verwende bitte die Hauptspendeseite.", + "card-title": "Mein Beitrag", + "about-si-title": "Über Social Income", + "about-si-text-1": "Social Income ist eine NGO mit Sitz in der Schweiz, die bedingungslose Geldüberweisungen per Mobiltelefon an Menschen in multidimensionaler Armut in Westafrika bereitstellt.", + "about-si-text-2": "Seit 2020 führt Social Income ein zeitlich unbegrenztes Programm für ein universelles Grundeinkommen in Sierra Leone durch.", + "about-si-link": "Fragen und Antworten" } } diff --git a/shared/locales/en/website-donate.json b/shared/locales/en/website-donate.json index d949b9551..f1c8bb71b 100644 --- a/shared/locales/en/website-donate.json +++ b/shared/locales/en/website-donate.json @@ -1,12 +1,10 @@ { - "metadata": { - "title": "My Contribution | Social Income" - }, "amount-currency": "{{ amount, currency }}", "title": "What's your average monthly income?", "how-to-pay": "How would you like to pay?", "amount": "Amount", "button-text": "Make a Difference", + "button-text-short": "Donate Now", "donation-impact": { "monthly-contribution": "Your monthly contribution", "direct-payout": "The people in need receive your contribution directly on their mobile phones.", @@ -92,7 +90,12 @@ }, "days-left_zero": "Last day left to contribute", "days-left_one": "1 day left to contribute", - "days-left_other": "{{ count }} days left to contribute:", - "ended": "The campaign ended, no more donations possible." + "days-left_other": "{{ count }} days left to contribute", + "ended": "The campaign has ended. For regular donations, please use the main donation page.", + "card-title": "My Contribution", + "about-si-title": "About Social Income", + "about-si-text-1": "Social Income is a nonprofit organization based in Switzerland that provides unconditional cash transfers via mobile phone to people living in multidimensional poverty in West Africa.", + "about-si-text-2": "Since 2020, Social Income has been running an open-ended universal basic income program in Sierra Leone.", + "about-si-link": "Frequently Asked Questions" } } diff --git a/shared/src/types/campaign.ts b/shared/src/types/campaign.ts index 306e8a69c..cf0d11dfa 100644 --- a/shared/src/types/campaign.ts +++ b/shared/src/types/campaign.ts @@ -18,6 +18,9 @@ export type Campaign = { goal_currency?: Currency; end_date: Timestamp; status: CampaignStatus; + metadata_description?: string; + metadata_ogImage?: string; + metadata_twitterImage?: string; }; export enum CampaignStatus { diff --git a/ui/src/components/progress.tsx b/ui/src/components/progress.tsx index 003ca7945..b24bdb19d 100644 --- a/ui/src/components/progress.tsx +++ b/ui/src/components/progress.tsx @@ -10,13 +10,18 @@ const Progress = React.forwardRef< >(({ className, value, ...props }, ref) => ( - +
+ +
)); Progress.displayName = ProgressPrimitive.Root.displayName; diff --git a/website/public/assets/metadata/og/campaign-ismatu-en.png b/website/public/assets/metadata/og/campaign-ismatu-en.png new file mode 100644 index 000000000..6af08aca6 Binary files /dev/null and b/website/public/assets/metadata/og/campaign-ismatu-en.png differ diff --git a/website/public/assets/metadata/twitter/campaign-ismatu-en.png b/website/public/assets/metadata/twitter/campaign-ismatu-en.png new file mode 100644 index 000000000..8efedc177 Binary files /dev/null and b/website/public/assets/metadata/twitter/campaign-ismatu-en.png differ diff --git a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx index e2fcb86be..f76e82f04 100644 --- a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx @@ -1,13 +1,24 @@ import { DefaultPageProps } from '@/app/[lang]/[region]'; -import { Video } from '@/app/[lang]/[region]/(website)/(home)/(sections)/video'; import OneTimeDonationForm from '@/app/[lang]/[region]/donate/one-time/one-time-donation-form'; +import { VimeoVideo } from '@/components/vimeo-video'; import { firestoreAdmin } from '@/firebase-admin'; import { WebsiteLanguage, WebsiteRegion } from '@/i18n'; +import { getMetadata } from '@/metadata'; import { CAMPAIGN_FIRESTORE_PATH, Campaign, CampaignStatus } from '@socialincome/shared/src/types/campaign'; import { daysUntilTs } from '@socialincome/shared/src/utils/date'; import { getLatestExchangeRate } from '@socialincome/shared/src/utils/exchangeRates'; import { Translator } from '@socialincome/shared/src/utils/i18n'; -import { BaseContainer, Typography } from '@socialincome/ui'; +import { + BaseContainer, + Popover, + PopoverContent, + PopoverTrigger, + Table, + TableBody, + TableCell, + TableRow, + Typography, +} from '@socialincome/ui'; import { Progress } from '@socialincome/ui/src/components/progress'; export type CampaignPageProps = { @@ -18,8 +29,36 @@ export type CampaignPageProps = { }; } & DefaultPageProps; +export async function generateMetadata({ params }: CampaignPageProps) { + const campaignDoc = await firestoreAdmin.collection(CAMPAIGN_FIRESTORE_PATH).doc(params.campaign).get(); + const campaign = campaignDoc.data(); + const campaignMetadata = + campaign?.metadata_description && campaign?.metadata_ogImage && campaign?.metadata_twitterImage + ? { + title: campaign?.title, + description: campaign?.metadata_description, + openGraph: { + title: campaign?.title, + description: campaign?.metadata_description, + images: campaign?.metadata_ogImage, + }, + twitter: { + title: campaign?.title, + card: 'summary_large_image', + site: '@so_income', + creator: '@so_income', + images: campaign?.metadata_twitterImage, + }, + } + : undefined; + return getMetadata(params.lang, 'website-donate', campaignMetadata); +} + export default async function Page({ params }: CampaignPageProps) { - const translator = await Translator.getInstance({ language: params.lang, namespaces: 'website-donate' }); + const translator = await Translator.getInstance({ + language: params.lang, + namespaces: ['website-donate', 'website-videos'], + }); const campaignDoc = await firestoreAdmin.collection(CAMPAIGN_FIRESTORE_PATH).doc(params.campaign).get(); const campaign = campaignDoc.data(); @@ -47,127 +86,221 @@ export default async function Page({ params }: CampaignPageProps) { return ( <> - -
- - {translator.t('campaign.by', { context: { creator: campaign.creator_name } })} - - - {campaign.title} - -
-
- -
- - {campaign.description} - -
-
-
- {!campaign.goal && ( -
- - {translator?.t('campaign.without-goal.collected', { - context: { - count: contributions, - amount: amountCollected, - currency: campaign.goal_currency, - total: campaign.goal, - }, - })} + +
+
+
+
+ + {translator.t('campaign.by', { context: { creator: campaign.creator_name } })}
- )} - {percentageCollected !== undefined && ( -
-
-
- {translator.t('campaign.with-goal.collected-percentage', { - context: { - percentage: percentageCollected, - }, - })} +
+ + {campaign.title} + +
+
+ + {campaign.description} + +
+
+ {!campaign.goal && ( +
+ + {translator?.t('campaign.without-goal.collected', { + context: { + count: contributions, + amount: amountCollected, + currency: campaign.goal_currency, + total: campaign.goal, + }, + })} +
-
{translator.t('campaign.with-goal.goal-title')}
-
-
- -
-
-
- {translator.t('campaign.with-goal.collected-amount', { - context: { - count: contributions, - amount: amountCollected, - currency: campaign.goal_currency, - }, - })} + )} + {percentageCollected !== undefined && ( +
+
+
+ + {translator.t('campaign.with-goal.collected-percentage', { + context: { + percentage: percentageCollected, + }, + })} + +
+
+ + {translator.t('campaign.with-goal.goal-title')} + +
+
+
+ +
+
+
+ + {translator.t('campaign.with-goal.collected-amount', { + context: { + count: contributions, + amount: amountCollected, + currency: campaign.goal_currency, + }, + })} + +
+
+ + {translator.t('campaign.with-goal.goal-amount', { + context: { + amount: campaign.goal, + currency: campaign.goal_currency, + }, + })} + +
+
-
- {translator.t('campaign.with-goal.goal-amount', { - context: { - amount: campaign.goal, - currency: campaign.goal_currency, - }, - })} + )} +
+
+ {daysLeft >= 0 && ( + <> +
+
+
+ + {translator.t('campaign.card-title')} + +
+
+ +
+ {daysLeft >= 0 && ( + <> +
+ + {translator?.t('campaign.days-left', { context: { count: daysLeft } })} + +
+ + )} + {daysLeft < 0 && ( +
+ + {translator?.t('campaign.ended', { context: { count: daysLeft } })} + +
+ )}
+ + )} + {daysLeft < 0 && ( +
+ + {translator?.t('campaign.ended', { context: { count: daysLeft } })} +
)}
- {daysLeft >= 0 && ( - <> -
- - {translator?.t('campaign.days-left', { context: { count: daysLeft } })} +
+ + {campaign.second_description && campaign.third_description && ( + +
+
+
+ + {campaign.second_description_title}
-
- +
+ + {campaign.second_description} + +
+
+
+
+ + {campaign.third_description_title} + +
+
+ + {campaign.third_description} +
- - )} - {daysLeft < 0 && ( -
- - {translator?.t('campaign.ended', { context: { count: daysLeft } })} -
- )} -
- -