diff --git a/shared/locales/de/website-newsletter.json b/shared/locales/de/website-newsletter.json index 6fdefc9c1..83e4bff7b 100644 --- a/shared/locales/de/website-newsletter.json +++ b/shared/locales/de/website-newsletter.json @@ -18,5 +18,12 @@ "submit-button": "Beim Newsletter anmelden", "newsletter-updated-toast": "Du wurdest zum Newsletter hinzugefügt", "newsletter-error-toast": "Du konntest leider nicht zum Newsletter hinzugefügt werden" + }, + "popup": { + "information-label": "Melde dich für unseren Newsletter an und werde Teil der Social Income Community!", + "toast-success": "Danke für das Abonnieren des Social Income Newsletters!", + "toast-failure": "Oops! Etwas ist schief gelaufen. Bitte versuche es nochmals, oder kontaktiere uns auf hello@socialincome.org", + "email-placeholder": "Deine E-Mail Adresse", + "button-subscribe": "Abonnieren" } } diff --git a/shared/locales/en/website-newsletter.json b/shared/locales/en/website-newsletter.json index 01976a92a..ed3172900 100644 --- a/shared/locales/en/website-newsletter.json +++ b/shared/locales/en/website-newsletter.json @@ -18,5 +18,12 @@ "submit-button": "Subscribe to newsletter", "newsletter-updated-toast": "You have been added to the newsletter", "newsletter-error-toast": "We weren't able to add you to our newsletter" + }, + "popup": { + "information-label": "Join our newsletter and become part of the Social Income Community!", + "toast-success": "Thank you for subscribing to our newsletter!", + "toast-failure": "Something went wrong. Try again or contact us at hello@socialincome.org", + "email-placeholder": "Your email", + "button-subscribe": "Subscribe" } } diff --git a/shared/src/mailchimp/MailchimpAPI.ts b/shared/src/mailchimp/MailchimpAPI.ts index ba3fe54c4..5df44b0ee 100644 --- a/shared/src/mailchimp/MailchimpAPI.ts +++ b/shared/src/mailchimp/MailchimpAPI.ts @@ -1,7 +1,7 @@ import mailchimp, { Status } from '@mailchimp/mailchimp_marketing'; import { CountryCode } from '../types/country'; -export type MailchimpSubscriptionData = { +export type NewsletterSubscriptionData = { email: string; status: Status; language?: 'de' | 'en'; @@ -26,7 +26,7 @@ export class MailchimpAPI { } }; - upsertSubscription = async (data: MailchimpSubscriptionData, listId: string) => { + upsertSubscription = async (data: NewsletterSubscriptionData, listId: string) => { const subscriber = await this.getSubscriber(data.email, listId); if (subscriber === null) { return this.createSubscription(data, listId); @@ -44,7 +44,7 @@ export class MailchimpAPI { } }; - createSubscription = async (data: MailchimpSubscriptionData, listId: string) => { + createSubscription = async (data: NewsletterSubscriptionData, listId: string) => { await mailchimp.lists.addListMember(listId, { email_address: data.email, status: data.status, diff --git a/website/.env.sample b/website/.env.sample new file mode 100644 index 000000000..352390986 --- /dev/null +++ b/website/.env.sample @@ -0,0 +1,21 @@ +# These environment variables are required so that the Firebase Admin SDK (server side) connects to the local emulator. +GCLOUD_PROJECT="social-income-staging" +FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 +FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 + +# These environment variables are required so that the Firebase JS SDK (client side) connects to the local emulator. +NEXT_PUBLIC_FIREBASE_API_KEY="demo-key" +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="social-income-staging" +NEXT_PUBLIC_FIREBASE_PROJECT_ID="social-income-staging" +NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_URL="http://localhost:9099" +NEXT_PUBLIC_FIREBASE_FIRESTORE_EMULATOR_HOST="localhost" +NEXT_PUBLIC_FIREBASE_FIRESTORE_EMULATOR_PORT="8080" +NEXT_PUBLIC_FIREBASE_FUNCTIONS_EMULATOR_HOST="localhost" +NEXT_PUBLIC_FIREBASE_FUNCTIONS_EMULATOR_PORT="5001" + +BASE_URL="http://localhost:3001" + +MAILCHIMP_LIST_ID="11223344" +MAILCHIMP_API_KEY="ABC*************" +MAILCHIMP_SERVER="us11" + diff --git a/website/src/app/[lang]/[region]/(website)/(home)/page.tsx b/website/src/app/[lang]/[region]/(website)/(home)/page.tsx index 7ff0476ff..670dbeb81 100644 --- a/website/src/app/[lang]/[region]/(website)/(home)/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/(home)/page.tsx @@ -6,6 +6,7 @@ import { Sdg } from '@/app/[lang]/[region]/(website)/(home)/(sections)/sdg'; import { SwissSection } from '@/app/[lang]/[region]/(website)/(home)/(sections)/swiss-section'; import { ThreeApproaches } from '@/app/[lang]/[region]/(website)/(home)/(sections)/three-approaches'; import { WhatWouldChange } from '@/app/[lang]/[region]/(website)/(home)/(sections)/what-would-change'; +import NewsletterPopup from '@/components/newsletter-popup/newsletter-popup'; import { Translator } from '@socialincome/shared/src/utils/i18n'; import { Video } from './(sections)/video'; @@ -30,6 +31,7 @@ export default async function Page({ params: { lang, region } }: DefaultPageProp + ); } diff --git a/website/src/app/[lang]/[region]/(website)/how-to-reduce-income-inequality/page.tsx b/website/src/app/[lang]/[region]/(website)/how-to-reduce-income-inequality/page.tsx index 83f50f9c7..c73281d2e 100644 --- a/website/src/app/[lang]/[region]/(website)/how-to-reduce-income-inequality/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/how-to-reduce-income-inequality/page.tsx @@ -1,4 +1,5 @@ import { DefaultPageProps } from '@/app/[lang]/[region]'; +import NewsletterPopup from '@/components/newsletter-popup/newsletter-popup'; import { getMetadata } from '@/metadata'; import { Translator } from '@socialincome/shared/src/utils/i18n'; import { BaseContainer, Button, Typography } from '@socialincome/ui'; @@ -45,11 +46,12 @@ export default async function Page({ params: { lang, region } }: DefaultPageProp {translator.t('difference')} - + + ); } diff --git a/website/src/app/[lang]/[region]/(website)/me/hooks.ts b/website/src/app/[lang]/[region]/(website)/me/hooks.ts index c83af9f9b..4869af5a6 100644 --- a/website/src/app/[lang]/[region]/(website)/me/hooks.ts +++ b/website/src/app/[lang]/[region]/(website)/me/hooks.ts @@ -84,7 +84,7 @@ export const useDonationCertificates = () => { }; // Mailchimp -export const useMailchimpSubscription = () => { +export const useNewsletterSubscription = () => { const api = useApi(); const { data: status, @@ -92,9 +92,9 @@ export const useMailchimpSubscription = () => { isRefetching, error, } = useQuery({ - queryKey: ['me', 'mailchimp'], + queryKey: ['me', 'newsletter'], queryFn: async () => { - const response = await api.get('/api/mailchimp/subscription'); + const response = await api.get('/api/newsletter/subscription'); return (await response.json()).status; }, staleTime: 3600000, // 1 hour @@ -102,13 +102,13 @@ export const useMailchimpSubscription = () => { return { status, loading: isLoading || isRefetching, error }; }; -export const useUpsertMailchimpSubscription = () => { +export const useUpsertNewsletterSubscription = () => { const api = useApi(); const queryClient = useQueryClient(); return async (status: Status) => { - const response = await api.post('/api/mailchimp/subscription', { status }); - await queryClient.invalidateQueries({ queryKey: ['me', 'mailchimp'] }); + const response = await api.post('/api/newsletter/subscription', { status }); + await queryClient.invalidateQueries({ queryKey: ['me', 'newsletter'] }); return response; }; }; diff --git a/website/src/app/[lang]/[region]/(website)/me/layout.tsx b/website/src/app/[lang]/[region]/(website)/me/layout.tsx index 387a14c68..93f5b786e 100644 --- a/website/src/app/[lang]/[region]/(website)/me/layout.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/layout.tsx @@ -1,6 +1,5 @@ import { DefaultLayoutProps } from '@/app/[lang]/[region]'; import { LayoutClient } from '@/app/[lang]/[region]/(website)/me/layout-client'; -import { ApiProvider } from '@/components/providers/api-provider'; import { UserContextProvider } from '@/components/providers/user-context-provider'; import { getMetadata } from '@/metadata'; import { Translator } from '@socialincome/shared/src/utils/i18n'; @@ -17,25 +16,23 @@ export default async function Layout({ children, params }: PropsWithChildren - - - {children} - - + + {children} + ); diff --git a/website/src/app/[lang]/[region]/(website)/me/personal-info/personal-info-form.tsx b/website/src/app/[lang]/[region]/(website)/me/personal-info/personal-info-form.tsx index f633214c6..fe3fee63e 100644 --- a/website/src/app/[lang]/[region]/(website)/me/personal-info/personal-info-form.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/personal-info/personal-info-form.tsx @@ -2,8 +2,8 @@ import { DefaultParams } from '@/app/[lang]/[region]'; import { - useMailchimpSubscription, - useUpsertMailchimpSubscription, + useNewsletterSubscription, + useUpsertNewsletterSubscription, useUser, } from '@/app/[lang]/[region]/(website)/me/hooks'; import { useTranslator } from '@/hooks/useTranslator'; @@ -60,8 +60,8 @@ export function PersonalInfoForm({ lang, translations }: PersonalInfoFormProps) const queryClient = useQueryClient(); const commonTranslator = useTranslator(lang, 'common'); const countryTranslator = useTranslator(lang, 'countries'); - const { status, loading } = useMailchimpSubscription(); - const upsertMailchimpSubscription = useUpsertMailchimpSubscription(); + const { status, loading } = useNewsletterSubscription(); + const upsertMailchimpSubscription = useUpsertNewsletterSubscription(); const formSchema = z.object({ firstname: z.string(), diff --git a/website/src/app/[lang]/[region]/(website)/updates/page.tsx b/website/src/app/[lang]/[region]/(website)/newsletter/page.tsx similarity index 96% rename from website/src/app/[lang]/[region]/(website)/updates/page.tsx rename to website/src/app/[lang]/[region]/(website)/newsletter/page.tsx index d145d1653..0f90ac73e 100644 --- a/website/src/app/[lang]/[region]/(website)/updates/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/newsletter/page.tsx @@ -1,5 +1,5 @@ import { DefaultPageProps } from '@/app/[lang]/[region]'; -import { SubscriptionInfoForm } from '@/app/[lang]/[region]/(website)/updates/subscription-info-form'; +import { SubscriptionInfoForm } from '@/app/[lang]/[region]/(website)/newsletter/subscription-info-form'; import { Translator } from '@socialincome/shared/src/utils/i18n'; import { Alert, AlertDescription, AlertTitle } from '@socialincome/ui'; diff --git a/website/src/app/[lang]/[region]/(website)/updates/subscription-info-form.tsx b/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx similarity index 93% rename from website/src/app/[lang]/[region]/(website)/updates/subscription-info-form.tsx rename to website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx index 8ed2ecb2b..e15b46c95 100644 --- a/website/src/app/[lang]/[region]/(website)/updates/subscription-info-form.tsx +++ b/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx @@ -1,9 +1,10 @@ 'use client'; import { DefaultParams } from '@/app/[lang]/[region]'; +import { useApi } from '@/hooks/useApi'; import { useTranslator } from '@/hooks/useTranslator'; import { zodResolver } from '@hookform/resolvers/zod'; -import { MailchimpSubscriptionData } from '@socialincome/shared/src/mailchimp/MailchimpAPI'; +import { NewsletterSubscriptionData } from '@socialincome/shared/src/mailchimp/MailchimpAPI'; import { COUNTRY_CODES } from '@socialincome/shared/src/types/country'; import { Button, @@ -41,6 +42,7 @@ type PersonalInfoFormProps = { export function SubscriptionInfoForm({ lang, translations }: PersonalInfoFormProps) { const commonTranslator = useTranslator(lang, 'common'); const countryTranslator = useTranslator(lang, 'countries'); + const api = useApi(); const formSchema = z.object({ firstname: z.string(), @@ -63,7 +65,7 @@ export function SubscriptionInfoForm({ lang, translations }: PersonalInfoFormPro }); const onSubmit = async (values: FormSchema) => { - const data: MailchimpSubscriptionData = { + const data: NewsletterSubscriptionData = { firstname: values.firstname, lastname: values.lastname, email: values.email, @@ -71,8 +73,8 @@ export function SubscriptionInfoForm({ lang, translations }: PersonalInfoFormPro language: values.language, status: 'subscribed', }; - // Call the API to change Mailchimp subscription - fetch('/api/mailchimp/subscription/public', { method: 'POST', body: JSON.stringify(data) }).then((response) => { + + api.post('/api/newsletter/subscription/public', data).then((response) => { if (response.status === 200) { toast.success(translations.toastMessage); } else { diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect.tsx index 303c6f5c7..360967e44 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useI18n } from '@/app/context-providers'; +import { useI18n } from '@/components/providers/context-providers'; import { WebsiteCurrency } from '@/i18n'; import { redirect } from 'next/navigation'; import { useEffect } from 'react'; diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/page.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/page.tsx index c56d9fa8a..50a4c0deb 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useI18n } from '@/app/context-providers'; +import { useI18n } from '@/components/providers/context-providers'; import { redirect } from 'next/navigation'; import { useEffect } from 'react'; diff --git a/website/src/app/[lang]/[region]/(website)/what-is-poverty/page.tsx b/website/src/app/[lang]/[region]/(website)/what-is-poverty/page.tsx index 8549a388a..cef715ad1 100644 --- a/website/src/app/[lang]/[region]/(website)/what-is-poverty/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/what-is-poverty/page.tsx @@ -70,7 +70,7 @@ export default async function Page({ params: { lang, region } }: DefaultPageProp {translator.t('difference')} - + diff --git a/website/src/app/[lang]/[region]/(website)/world-poverty-statistics-2024/page.tsx b/website/src/app/[lang]/[region]/(website)/world-poverty-statistics-2024/page.tsx index 977aadd1e..a04c127df 100644 --- a/website/src/app/[lang]/[region]/(website)/world-poverty-statistics-2024/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/world-poverty-statistics-2024/page.tsx @@ -1,4 +1,5 @@ import { DefaultPageProps } from '@/app/[lang]/[region]'; +import NewsletterPopup from '@/components/newsletter-popup/newsletter-popup'; import { getMetadata } from '@/metadata'; import { Translator } from '@socialincome/shared/src/utils/i18n'; import { BaseContainer, Button, Typography } from '@socialincome/ui'; @@ -7,6 +8,7 @@ import Link from 'next/link'; export async function generateMetadata({ params }: DefaultPageProps) { return getMetadata(params.lang, 'website-poverty-statistics-2024'); } + export default async function Page({ params: { lang, region } }: DefaultPageProps) { const translator = await Translator.getInstance({ language: lang, @@ -45,11 +47,12 @@ export default async function Page({ params: { lang, region } }: DefaultPageProp {translator.t('difference')} - + + ); } diff --git a/website/src/app/[lang]/[region]/donate/individual/donation-form.tsx b/website/src/app/[lang]/[region]/donate/individual/donation-form.tsx index 153070b96..b16ca3252 100644 --- a/website/src/app/[lang]/[region]/donate/individual/donation-form.tsx +++ b/website/src/app/[lang]/[region]/donate/individual/donation-form.tsx @@ -2,7 +2,7 @@ import { DefaultParams } from '@/app/[lang]/[region]'; import { CreateCheckoutSessionData } from '@/app/api/stripe/checkout-session/create/route'; -import { useI18n } from '@/app/context-providers'; +import { useI18n } from '@/components/providers/context-providers'; import { CurrencySelector } from '@/components/ui/currency-selector'; import { useTranslator } from '@/hooks/useTranslator'; import { websiteCurrencies, WebsiteLanguage } from '@/i18n'; diff --git a/website/src/app/[lang]/[region]/donate/one-time/one-time-donation-form.tsx b/website/src/app/[lang]/[region]/donate/one-time/one-time-donation-form.tsx index ae147a0e7..6d24f20a1 100644 --- a/website/src/app/[lang]/[region]/donate/one-time/one-time-donation-form.tsx +++ b/website/src/app/[lang]/[region]/donate/one-time/one-time-donation-form.tsx @@ -2,7 +2,7 @@ import { DefaultParams } from '@/app/[lang]/[region]'; import { CreateCheckoutSessionData } from '@/app/api/stripe/checkout-session/create/route'; -import { useI18n } from '@/app/context-providers'; +import { useI18n } from '@/components/providers/context-providers'; import { CurrencySelector } from '@/components/ui/currency-selector'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button, Form, FormControl, FormField, FormItem, Input } from '@socialincome/ui'; diff --git a/website/src/app/api/mailchimp/subscription/public/route.ts b/website/src/app/api/mailchimp/subscription/public/route.ts deleted file mode 100644 index c6d62b862..000000000 --- a/website/src/app/api/mailchimp/subscription/public/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MailchimpAPI, MailchimpSubscriptionData } from '@socialincome/shared/src/mailchimp/MailchimpAPI'; - -type MailchimpSubscriptionRequest = { json(): Promise } & Request; - -export async function POST(request: MailchimpSubscriptionRequest) { - const data = await request.json(); - const mailchimpAPI = new MailchimpAPI(process.env.MAILCHIMP_API_KEY!, process.env.MAILCHIMP_SERVER!); - try { - await mailchimpAPI.upsertSubscription(data, process.env.MAILCHIMP_LIST_ID!); - return new Response(null, { status: 200 }); - } catch (e) { - console.error(e); - return new Response(null, { status: 405 }); - } -} diff --git a/website/src/app/api/newsletter/subscription/public/route.ts b/website/src/app/api/newsletter/subscription/public/route.ts new file mode 100644 index 000000000..57c7f2911 --- /dev/null +++ b/website/src/app/api/newsletter/subscription/public/route.ts @@ -0,0 +1,16 @@ +import { MailchimpAPI, NewsletterSubscriptionData } from '@socialincome/shared/src/mailchimp/MailchimpAPI'; + +export type CreateNewsletterSubscription = Omit; +type CreateNewsletterSubscriptionReqeust = { json(): Promise } & Request; + +export async function POST(request: CreateNewsletterSubscriptionReqeust) { + const data = await request.json(); + const mailchimpAPI = new MailchimpAPI(process.env.MAILCHIMP_API_KEY!, process.env.MAILCHIMP_SERVER!); + try { + await mailchimpAPI.createSubscription({ ...data, status: 'subscribed' }, process.env.MAILCHIMP_LIST_ID!); + return new Response(null, { status: 200 }); + } catch (e: any) { + console.error(e); + return new Response(null, { status: 405, statusText: e }); + } +} diff --git a/website/src/app/api/mailchimp/subscription/route.ts b/website/src/app/api/newsletter/subscription/route.ts similarity index 80% rename from website/src/app/api/mailchimp/subscription/route.ts rename to website/src/app/api/newsletter/subscription/route.ts index 675edfe19..8d80c4817 100644 --- a/website/src/app/api/mailchimp/subscription/route.ts +++ b/website/src/app/api/newsletter/subscription/route.ts @@ -1,5 +1,4 @@ import { authorizeRequest, handleApiError } from '@/app/api/auth'; -import { Body } from '@mailchimp/mailchimp_marketing'; import { MailchimpAPI } from '@socialincome/shared/src/mailchimp/MailchimpAPI'; import { NextResponse } from 'next/server'; @@ -20,9 +19,8 @@ export async function GET(request: Request) { /** * Upsert Mailchimp subscription */ -export type MailchimpSubscriptionUpdate = { status: 'subscribed' | 'unsubscribed' } & Body; -type MailchimpSubscriptionUpdateRequest = { json(): Promise } & Request; -export async function POST(request: MailchimpSubscriptionUpdateRequest) { +type NewsletterSubscriptionUpdateRequest = { json(): Promise<{ status: 'subscribed' | 'unsubscribed' }> } & Request; +export async function POST(request: NewsletterSubscriptionUpdateRequest) { try { const userDoc = await authorizeRequest(request); const data = await request.json(); diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx index 28438dcba..12ff93745 100644 --- a/website/src/app/layout.tsx +++ b/website/src/app/layout.tsx @@ -1,4 +1,4 @@ -import { ContextProviders } from '@/app/context-providers'; +import { ContextProviders } from '@/components/providers/context-providers'; import { getMetadata } from '@/metadata'; import { PropsWithChildren } from 'react'; import './globals.css'; diff --git a/website/src/components/footer/footer-client.tsx b/website/src/components/footer/footer-client.tsx index 0d6c1c7f1..00422e45a 100644 --- a/website/src/components/footer/footer-client.tsx +++ b/website/src/components/footer/footer-client.tsx @@ -1,7 +1,7 @@ 'use client'; import { DefaultParams } from '@/app/[lang]/[region]'; -import { useI18n } from '@/app/context-providers'; +import { useI18n } from '@/components/providers/context-providers'; import { WebsiteLanguage, WebsiteRegion } from '@/i18n'; import { GlobeAltIcon, LanguageIcon } from '@heroicons/react/24/solid'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@socialincome/ui'; diff --git a/website/src/components/i18n-dialog.tsx b/website/src/components/i18n-dialog.tsx index 93eb3aa46..0f0e62176 100644 --- a/website/src/components/i18n-dialog.tsx +++ b/website/src/components/i18n-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useI18n } from '@/app/context-providers'; +import { useI18n } from '@/components/providers/context-providers'; import { WebsiteCurrency, WebsiteLanguage, WebsiteRegion } from '@/i18n'; import { CurrencyDollarIcon } from '@heroicons/react/24/outline'; import { GlobeEuropeAfricaIcon, LanguageIcon } from '@heroicons/react/24/solid'; diff --git a/website/src/components/newsletter-popup/newsletter-popup-client.tsx b/website/src/components/newsletter-popup/newsletter-popup-client.tsx new file mode 100644 index 000000000..50a7b33f6 --- /dev/null +++ b/website/src/components/newsletter-popup/newsletter-popup-client.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { CreateNewsletterSubscription } from '@/app/api/newsletter/subscription/public/route'; +import { NewsletterPopupProps } from '@/components/newsletter-popup/newsletter-popup'; +import { useApi } from '@/hooks/useApi'; +import { XMarkIcon } from '@heroicons/react/24/solid'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LanguageCode } from '@socialincome/shared/src/types/language'; +import { Button, Form, FormControl, FormField, FormItem, FormMessage, Input, Typography } from '@socialincome/ui'; +import classNames from 'classnames'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import toast, { Toast } from 'react-hot-toast'; +import * as z from 'zod'; + +type NewsletterPopupTranslations = { + informationLabel: string; + toastSuccess: string; + toastFailure: string; + emailPlaceholder: string; + buttonAddSubscriber: string; +}; + +type NewsletterPopupToastProps = { + lang: LanguageCode; + translations: NewsletterPopupTranslations; + t: Toast; + onClose: () => void; +}; + +const NewsletterPopupToast = ({ lang, translations, t, onClose }: NewsletterPopupToastProps) => { + const api = useApi(); + const formSchema = z.object({ email: z.string().email() }); + type FormSchema = z.infer; + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { email: '' }, + }); + + const onSubmit = async (values: FormSchema) => { + const body: CreateNewsletterSubscription = { email: values.email, language: lang as any }; + api.post('/api/newsletter/subscription/public', body).then((response) => { + if (response.status === 200) { + toast.dismiss(t.id); + toast.success(translations.toastSuccess); + } else { + toast.error(translations.toastFailure); + } + }); + }; + + return ( +
+ + {translations.informationLabel} +
+ + ( + + + + + + + )} + /> + + + +
+ ); +}; + +type NewsletterPopupClientProps = { + translations: NewsletterPopupTranslations; +} & NewsletterPopupProps; + +export const NewsletterPopupClient = ({ delay, lang, translations }: NewsletterPopupClientProps) => { + useEffect(() => { + toast.dismiss(); + const timeout = setTimeout(() => { + toast.custom( + (t) => ( + toast.dismiss(t.id)} /> + ), + { + position: 'bottom-right', + duration: Infinity, + }, + ); + }, delay); + return () => { + clearTimeout(timeout); + }; + }, []); + + return null; +}; diff --git a/website/src/components/newsletter-popup/newsletter-popup.tsx b/website/src/components/newsletter-popup/newsletter-popup.tsx new file mode 100644 index 000000000..a2719de60 --- /dev/null +++ b/website/src/components/newsletter-popup/newsletter-popup.tsx @@ -0,0 +1,28 @@ +import { DefaultParams } from '@/app/[lang]/[region]'; +import { Translator } from '@socialincome/shared/src/utils/i18n'; +import { NewsletterPopupClient } from './newsletter-popup-client'; + +export type NewsletterPopupProps = { + delay?: number; // Delay in milliseconds until the popup is shown +} & DefaultParams; + +export default async function NewsletterPopup({ lang, region, delay = 0 }: NewsletterPopupProps) { + const translator = await Translator.getInstance({ + language: lang, + namespaces: ['website-newsletter'], + }); + return ( + + ); +} diff --git a/website/src/components/providers/api-provider.tsx b/website/src/components/providers/api-provider.tsx index 32f35582f..12830cd34 100644 --- a/website/src/components/providers/api-provider.tsx +++ b/website/src/components/providers/api-provider.tsx @@ -8,7 +8,7 @@ export const ApiProviderContext = createContext(undefined export function ApiProvider({ children }: { children: React.ReactNode }) { const { data: authUser } = useFirebaseUser(); - const [idToken, setIdToken] = useState(); + const [idToken, setIdToken] = useState(''); useEffect(() => { if (authUser) { @@ -16,8 +16,5 @@ export function ApiProvider({ children }: { children: React.ReactNode }) { } }, [authUser, setIdToken]); - if (!authUser || !idToken) { - return; - } return {children}; } diff --git a/website/src/app/context-providers.tsx b/website/src/components/providers/context-providers.tsx similarity index 97% rename from website/src/app/context-providers.tsx rename to website/src/components/providers/context-providers.tsx index e8c1c5d7d..a278cb819 100644 --- a/website/src/app/context-providers.tsx +++ b/website/src/components/providers/context-providers.tsx @@ -1,6 +1,7 @@ 'use client'; import { CURRENCY_COOKIE, LANGUAGE_COOKIE, REGION_COOKIE } from '@/app/[lang]/[region]'; +import { ApiProvider } from '@/components/providers/api-provider'; import { FacebookTracking } from '@/components/tracking/facebook-tracking'; import { GoogleTagManager } from '@/components/tracking/google-tag-manager'; import { LinkedInTracking } from '@/components/tracking/linkedin-tracking'; @@ -211,9 +212,11 @@ export function ContextProviders({ children }: PropsWithChildren) { return ( - - {children} - + + + {children} + + ); diff --git a/website/src/components/ui/currency-selector.tsx b/website/src/components/ui/currency-selector.tsx index 384602731..4b5c9e90a 100644 --- a/website/src/components/ui/currency-selector.tsx +++ b/website/src/components/ui/currency-selector.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useI18n } from '@/app/context-providers'; import { getFlagComponentByCurrency } from '@/components/country-flags'; +import { useI18n } from '@/components/providers/context-providers'; import { WebsiteCurrency } from '@/i18n'; import { isValidCurrency } from '@socialincome/shared/src/types/currency'; import { diff --git a/website/src/hooks/useApi.ts b/website/src/hooks/useApi.ts index f01d4de42..c482b3c3c 100644 --- a/website/src/hooks/useApi.ts +++ b/website/src/hooks/useApi.ts @@ -24,7 +24,7 @@ export class ApiClient { let url: URL; if (path.startsWith('/')) { url = new URL(path, window.location.origin); - url.searchParams.append('firebaseAuthToken', this.token); + if (this.token) url.searchParams.append('firebaseAuthToken', this.token); } else { url = new URL(path); }