diff --git a/shared/locales/de/website-donate.json b/shared/locales/de/website-donate.json new file mode 100644 index 000000000..dd9f0b906 --- /dev/null +++ b/shared/locales/de/website-donate.json @@ -0,0 +1,35 @@ +{ + "amount-currency": "{{ amount, currency }}", + "title": "Was ist dein durchschnittliches monatliches Einkommen?", + "how-to-pay": "Wie möchtest du bezahlen?", + "amount": "Betrag", + "button-text": "Einen Unterschied machen", + "donation-impact": { + "monthly-contribution": "Dein monatlicher Beitrag:", + "direct-payout": "Dein Beitrag wird direkt auf das Mobiltelefon der Social Income Empfänger:innen ausgezahlt.", + "your-impact": "Dein Einfluss:", + "0": "{{ amount, currency }} legt den Grundstein für eine bedeutende Sache. Es ermöglicht uns, einer Person in Not ein Social Income zu bieten.", + "1": "{{ amount, currency }} deckt mehr als ein Drittel eines Social Incomes für eine bedürftige Person.", + "2": "{{ amount, currency }} finanziert mehr als die Hälfte eines Social Incomes für eine Person.", + "3": "Großartig! Dein monatlicher Beitrag von {{ amount, currency }} ermöglicht es mindestens einer bedürftigen Person ein vollständiges Social Income bereitzustellen.", + "4": "{{ amount, currency }} ermöglicht ein Social Income für mehr als zwei bedürftige Personen.", + "5": "Wunderbar! Dein Beitrag von {{ amount, currency }} finanziert ein Social Income für mehr als drei bedürftige Personen.", + "6": "Dein Beitrag von {{ amount, currency }} sichert ein Social Income für mehr als vier bedürftige Personen.", + "7": "Dein Beitrag von {{ amount, currency }} sichert ein Social Income für mehr als fünf bedürftige Personen.", + "8": "Dein monatlicher Beitrag von {{ amount, currency }} ist außergewöhnlich, ebenso wie dein Einfluss." + }, + "donation-interval": { + "1": { + "title": "Monatlich", + "text": "{{ amount, currency }} jeden Monat zahlen" + }, + "3": { + "title": "Quartalsweise", + "text": "{{ amount, currency }} alle 3 Monate zahlen" + }, + "12": { + "title": "Jährlich", + "text": "{{ amount, currency }} jedes Jahr zahlen" + } + } +} diff --git a/shared/locales/de/website-me.json b/shared/locales/de/website-me.json index 4e0289f7c..46cf4a943 100644 --- a/shared/locales/de/website-me.json +++ b/shared/locales/de/website-me.json @@ -12,22 +12,23 @@ } }, "contributions": { - "amount": "Amount", - "date": "Date", - "source": "Source", + "amount": "Betrag", + "date": "Datum", + "source": "Quelle", "total": "Total", + "one-time-payment": "Neue einmalige Spende", "amount-currency": "{{ amount, currency }}", "sources": { "benevity": "Benevity", - "cash": "Cash", + "cash": "Bar", "stripe": "Stripe", - "wire-transfer": "Wire Transfer" + "wire-transfer": "Banküberweisung" } }, "subscriptions": { - "amount": "Amount", - "date": "Date", - "source": "Source", + "amount": "Betrag", + "date": "Datum", + "source": "Quelle", "total": "Total", "amount-currency": "{{ amount, currency }}", "interval": "Interval", diff --git a/shared/locales/en/website-donate.json b/shared/locales/en/website-donate.json new file mode 100644 index 000000000..91c24d022 --- /dev/null +++ b/shared/locales/en/website-donate.json @@ -0,0 +1,35 @@ +{ + "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", + "donation-impact": { + "monthly-contribution": "Your monthly contribution:", + "direct-payout": "The people in need receive your contribution directly on their mobile phones.", + "your-impact": "Your impact:", + "0": "{{ amount, currency }} lays the groundwork for a significant cause. It enables us to provide a Social Income to an individual facing hardship.", + "1": "{{ amount, currency }} pays for more than a third of a Social Income for one person in need.", + "2": "{{ amount, currency }} funds more than half of someone’s Social Income.", + "3": "Great! Your monthly contribution of {{ amount, currency }} will allow at least one person in need to receive a full Social Income.", + "4": "{{ amount, currency }} provides a Social Income for more than two people in need.", + "5": "Wonderful! Your contribution of {{ amount, currency }} funds a Social Income for more than three people in need.", + "6": "Your contribution of {{ amount, currency }} sustains a Social Income for more than four people in need.", + "7": "Your contribution of {{ amount, currency }} sustains a Social Income for more than five people in need.", + "8": "Your monthly contribution of {{ amount, currency }} is extraordinary, as well as your impact." + }, + "donation-interval": { + "1": { + "title": "Monthly", + "text": "Pay {{ amount, currency }} every month" + }, + "3": { + "title": "Quarterly", + "text": "Pay {{ amount, currency }} every 3 months" + }, + "12": { + "title": "Yearly", + "text": "Pay {{ amount, currency }} every year" + } + } +} diff --git a/shared/locales/en/website-me.json b/shared/locales/en/website-me.json index 0d0cbd0ae..92b89fac0 100644 --- a/shared/locales/en/website-me.json +++ b/shared/locales/en/website-me.json @@ -16,6 +16,7 @@ "date": "Date", "source": "Source", "total": "Total", + "one-time-payment": "New one-time donation", "amount-currency": "{{ amount, currency }}", "sources": { "benevity": "Benevity", diff --git a/ui/src/components/button.tsx b/ui/src/components/button.tsx index 1f758ab60..3c7a48b59 100644 --- a/ui/src/components/button.tsx +++ b/ui/src/components/button.tsx @@ -42,7 +42,7 @@ const Button = React.forwardRef( const Comp = asChild ? Slot : 'button'; return ( - {Icon && } + {Icon && } {children} ); diff --git a/ui/src/components/typography/typography.tsx b/ui/src/components/typography/typography.tsx index 3a53698bf..c45a84175 100644 --- a/ui/src/components/typography/typography.tsx +++ b/ui/src/components/typography/typography.tsx @@ -41,6 +41,8 @@ const FONT_COLOR_MAP: { [key in FontColor]: string } = { 'destructive-foreground': 'text-destructive-foreground', muted: 'text-muted', 'muted-foreground': 'text-muted-foreground', + card: 'text-card', + 'card-foreground': 'text-card-foreground', popover: 'text-popover', 'popover-foreground': 'text-popover-foreground', }; @@ -67,26 +69,17 @@ export type TypographyProps = { } & ComponentPropsWithoutRef; export function Typography( - { - as, - size = 'md', - weight = 'normal', - color = 'foreground', - lineHeight = 'normal', - className, - children, - ...props - }: TypographyProps, + { as, size, weight, color, lineHeight, className, children, ...props }: TypographyProps, ) { const Component = as || 'p'; return ( ; diff --git a/website/src/app/[lang]/[region]/(website)/(home)/section-1-form.tsx b/website/src/app/[lang]/[region]/(website)/(home)/section-1-form.tsx index 144c80fed..1249686b0 100644 --- a/website/src/app/[lang]/[region]/(website)/(home)/section-1-form.tsx +++ b/website/src/app/[lang]/[region]/(website)/(home)/section-1-form.tsx @@ -38,7 +38,7 @@ export default function Section1Form({ translations, lang, region }: Section1Inp }); const onSubmit = (values: FormSchema) => { - router.push(`/donate/individual?amount=${(Number(values.amount) / 100).toFixed(2)}`); + router.push(`/${lang}/${region}/donate/individual?amount=${Number(values.amount).toFixed(0)}`); }; return ( @@ -51,7 +51,7 @@ export default function Section1Form({ translations, lang, region }: Section1Inp control={form.control} name="amount" render={({ field }) => ( - + diff --git a/website/src/app/[lang]/[region]/(website)/about-us/(sections)/team.tsx b/website/src/app/[lang]/[region]/(website)/about-us/(sections)/team.tsx index 8853c938f..bde558166 100644 --- a/website/src/app/[lang]/[region]/(website)/about-us/(sections)/team.tsx +++ b/website/src/app/[lang]/[region]/(website)/about-us/(sections)/team.tsx @@ -107,7 +107,7 @@ const groups: Group[] = [ export default async function Team({ params }: DefaultPageProps) { const translator = await Translator.getInstance({ language: params.lang, namespaces: ['countries', 'website-team'] }); return ( - + {translator.t('header')} diff --git a/website/src/app/[lang]/[region]/(website)/me/payments/page.tsx b/website/src/app/[lang]/[region]/(website)/me/payments/page.tsx index dd2e0a8f9..3e3861fa3 100644 --- a/website/src/app/[lang]/[region]/(website)/me/payments/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/payments/page.tsx @@ -1,20 +1,29 @@ import { DefaultPageProps } from '@/app/[lang]/[region]'; import { ContributionsTable } from '@/app/[lang]/[region]/(website)/me/payments/contributions-table'; +import { PlusCircleIcon } from '@heroicons/react/24/outline'; import { Translator } from '@socialincome/shared/src/utils/i18n'; +import { Button } from '@socialincome/ui'; +import Link from 'next/link'; -export default async function Page({ params }: DefaultPageProps) { - const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-me'] }); +export default async function Page({ params: { lang, region } }: DefaultPageProps) { + const translator = await Translator.getInstance({ language: lang, namespaces: ['website-me'] }); return ( -
+
+ + +
); } diff --git a/website/src/app/[lang]/[region]/(website)/me/personal-info/page.tsx b/website/src/app/[lang]/[region]/(website)/me/personal-info/page.tsx index f9cf80d5a..45f05b0cd 100644 --- a/website/src/app/[lang]/[region]/(website)/me/personal-info/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/personal-info/page.tsx @@ -111,7 +111,7 @@ export default function Page({ params }: DefaultPageProps) { First name - + @@ -124,7 +124,7 @@ export default function Page({ params }: DefaultPageProps) { Last name - + @@ -137,7 +137,7 @@ export default function Page({ params }: DefaultPageProps) { Email - + @@ -173,7 +173,7 @@ export default function Page({ params }: DefaultPageProps) { Street - + @@ -186,7 +186,7 @@ export default function Page({ params }: DefaultPageProps) { Number - + @@ -201,7 +201,7 @@ export default function Page({ params }: DefaultPageProps) { Zip - + @@ -214,7 +214,7 @@ export default function Page({ params }: DefaultPageProps) { City - + @@ -226,7 +226,7 @@ export default function Page({ params }: DefaultPageProps) { name="language" render={({ field }) => ( - Language + Communication Language + + + )} + /> + +
+ {form.watch('monthlyIncome') > 0 && ( + + + + + + + {translations.howToPay} + + ( + + + + + + + + + + + )} + /> + + + + + + )} + + + + ); +} diff --git a/website/src/app/[lang]/[region]/donate/individual/page.tsx b/website/src/app/[lang]/[region]/donate/individual/page.tsx index f9786df9d..058660a86 100644 --- a/website/src/app/[lang]/[region]/donate/individual/page.tsx +++ b/website/src/app/[lang]/[region]/donate/individual/page.tsx @@ -1,172 +1,30 @@ -'use client'; - import { DefaultPageProps } from '@/app/[lang]/[region]'; -import { CreatePaymentData } from '@/app/api/stripe/checkout/new-payment/route'; -import { CheckCircleIcon } from '@heroicons/react/24/outline'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { - BaseContainer, - Button, - Form, - FormControl, - FormField, - FormItem, - FormMessage, - Input, - RadioGroup, - Typography, -} from '@socialincome/ui'; -import classNames from 'classnames'; -import { useRouter } from 'next/navigation'; -import { UseFormReturn, useForm } from 'react-hook-form'; -import { useUser } from 'reactfire'; -import Stripe from 'stripe'; -import * as z from 'zod'; - -// TODO: i18n -type RadioGroupFormItem = { - active: boolean; - value: '1' | '3' | '12'; - title: string; - description: string; - form: UseFormReturn<{ amount: number; intervalCount: '1' | '3' | '12' }, any, undefined>; -}; +import { DonationForm } from '@/app/[lang]/[region]/donate/individual/donation-form'; +import { Translator } from '@socialincome/shared/src/utils/i18n'; +import { BaseContainer } from '@socialincome/ui'; -function RadioGroupFormItem({ active, title, value, form, description }: RadioGroupFormItem) { - const updateForm = () => { - const annualAmount = (form.getValues('amount') / Number(form.getValues('intervalCount'))) * 12; - form.setValue('amount', (Number(value) / 12) * annualAmount); - form.setValue('intervalCount', value); - }; +export default async function Page({ params: { lang, region }, searchParams }: DefaultPageProps) { + const amount = Number(searchParams.amount) || undefined; + const translator = await Translator.getInstance({ language: lang, namespaces: ['website-donate'] }); return ( - - + -
- - - {title} - {description} - - -
-
-
- ); -} - -export default function Page({ params, searchParams }: DefaultPageProps) { - const router = useRouter(); - const { data: authUser } = useUser(); - - const formSchema = z.object({ - amount: z.coerce.number(), - intervalCount: z.enum(['1', '3', '12']), - }); - type FormSchema = z.infer; - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { intervalCount: '1', amount: Number(searchParams.amount) || 50 }, - }); - - const onSubmit = async (values: FormSchema) => { - const authToken = await authUser?.getIdToken(true); - const data: CreatePaymentData = { - amount: values.amount * 100, // The amount is in cents, so we need to multiply by 100 to get the correct amount. - intervalCount: Number(values.intervalCount), - successUrl: `${window.location.origin}/${params.lang}/${params.region}/donate/success?stripeCheckoutSessionId={CHECKOUT_SESSION_ID}`, - recurring: true, - firebaseAuthToken: authToken, - }; - const response = await fetch('/api/stripe/checkout/new-payment', { - method: 'POST', - body: JSON.stringify(data), - }); - const { url } = (await response.json()) as Stripe.Response; - - // This sends the user to stripe.com where payment is completed - if (url) router.push(url); - }; - - return ( - - - How would you like to pay? - -
- - ( - - - - - - - - - - - )} - /> -
- ( - - - Amount - - - - - - - )} - /> -
- - - + }} + />
); } diff --git a/website/src/app/[lang]/[region]/donate/one-time/page.tsx b/website/src/app/[lang]/[region]/donate/one-time/page.tsx index 39f46c271..e213e6faf 100644 --- a/website/src/app/[lang]/[region]/donate/one-time/page.tsx +++ b/website/src/app/[lang]/[region]/donate/one-time/page.tsx @@ -5,7 +5,7 @@ import { BaseContainer, Typography } from '@socialincome/ui'; // TODO: i18n export default function Page({ params }: DefaultPageProps) { return ( - +
Your Donation @@ -13,8 +13,7 @@ export default function Page({ params }: DefaultPageProps) { Make a one-time donation to Social Income - -
+
- {Icon && } - + + {Icon && } + {label} @@ -43,7 +42,7 @@ export default async function Footer({ lang, region }: DefaultParams) { namespaces: ['common', 'website-common', 'website-me'], }); - const supportedTranslatedLanguages = websiteLanguages.map((lang) => { + const supportedTranslatedLanguages = (['de', 'en'] as WebsiteLanguage[]).map((lang) => { return { translation: translator.t(`languages.${lang}`), code: lang }; }); const supportedTranslatedCountries = websiteRegions.map((region) => { @@ -51,7 +50,7 @@ export default async function Footer({ lang, region }: DefaultParams) { }); return ( - +
@@ -66,8 +65,8 @@ export default async function Footer({ lang, region }: DefaultParams) {

-
- +
+ Connect @@ -90,10 +89,16 @@ export default async function Footer({ lang, region }: DefaultParams) { url="https://www.linkedin.com/company/socialincome" target="_blank" /> - + + {/**/}
-
- +
+ Resources @@ -106,26 +111,26 @@ export default async function Footer({ lang, region }: DefaultParams) { url={`/${lang}/${region}/terms-and-conditions`} />
-
- +
+ Our Work - + - +
-
- +
+ About us - - - - + + + +
-
+
{ - const options = langParser.parse(request.headers.get('Accept-Language') || 'en'); - // TODO: make sure country is supported - // TODO: save country/language/currency in cookie if manually updated +export const findBestLocale = ( + request: NextRequest, +): { + region: WebsiteRegion; + language: WebsiteLanguage; +} => { + /** + * Check if the user has set a language and region cookie, and if they are valid. If so, return them. + * Otherwise, try to find the best locale from the Accept-Language header, and if that fails, return the default locale. + */ + if ( + request.cookies.has(LANGUAGE_COOKIE) && + websiteLanguages.includes(request.cookies.get(LANGUAGE_COOKIE)!.value as WebsiteLanguage) && + request.cookies.has(REGION_COOKIE) && + websiteRegions.includes(request.cookies.get(REGION_COOKIE)!.value as WebsiteRegion) + ) { + return { + language: request.cookies.get(LANGUAGE_COOKIE)!.value as WebsiteLanguage, + region: request.cookies.get(REGION_COOKIE)!.value as WebsiteRegion, + }; + } - const requestCountry = request.geo?.country; + const options = langParser.parse(request.headers.get('Accept-Language') || 'en'); + const requestRegion = request.geo?.country; const bestOption = options.find( (option) => option.code && @@ -39,7 +58,7 @@ export const findBestLocale = (request: NextRequest): { region: WebsiteRegion; l return { language: (bestOption?.code as WebsiteLanguage) || defaultLanguage, region: - (websiteLanguages.includes(requestCountry as WebsiteLanguage) && (requestCountry as WebsiteRegion)) || + (websiteRegions.includes(requestRegion as WebsiteRegion) && (requestRegion as WebsiteRegion)) || (bestOption?.region as WebsiteRegion) || defaultRegion, };