From 554a150377134c6a70ed14b1421ce8731aaac232 Mon Sep 17 00:00:00 2001 From: Inna Abdrakhmanova Date: Sat, 25 Jan 2025 12:15:26 +0100 Subject: [PATCH] Website: Implement glowing cursor effect (#1009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GlowHoverContainer component * Add usage example to the main page (TODO: remove the example before merge) * Remove unnecessary comments * Prettified Code! * Add missing wrapperClassName prop to BaseContainerProps * wip fix * Prettified Code! * wip fix 2 * Prettified Code! * wip fix 3 * wip fix 4 * Prettified Code! * Remove temporary GlowHoverContainer example from the main page * Refactor newsletter popup form, extract it into a separate component, create NewsletterGlowContainer and place it on the Campaign page * Add translations * Separate GlowHoverContainer from BaseContainer * Refactor BaseContainer after resolving conflicts with the main branch * Clean up the mess caused by experiments * Prettified Code! * Apply CodeRabbitAI refactoring suggestions * Clean mess caused by experiments * Prettified Code! * Remove unused prop * Add types for NewsletterForm component * Prettified Code! * Add types for NewsletterForm component * Prettified Code! * Tune types for NewsletterForm component * Tune types for NewsletterForm component * Tune types for NewsletterGlowContainer component * Prettified Code! * Tune types for NewsletterGlowContainer component * Rename NewsletterForm props to avoid collisions * Rollback unnecessary change * Rollback unnecessary change --------- Co-authored-by: IPogorelova Co-authored-by: Sandino Scheidegger Co-authored-by: ssandino Co-authored-by: Michael Kündig --- shared/locales/de/website-newsletter.json | 3 + shared/locales/en/website-newsletter.json | 3 + shared/locales/fr/website-newsletter.json | 3 + shared/locales/it/website-newsletter.json | 3 + .../components/containers/base-container.tsx | 9 +- .../containers/glow-hover-container.tsx | 23 ++ ui/src/components/containers/index.ts | 1 + .../use-glow-hover/glow-hover-effect.ts | 266 ++++++++++++++++++ ui/src/components/use-glow-hover/index.ts | 4 + .../use-glow-hover/linear-animation.ts | 38 +++ ui/src/components/use-glow-hover/presets.ts | 11 + .../use-glow-hover/use-glow-hover.ts | 16 ++ ui/src/index.ts | 1 + .../(website)/campaign/[campaign]/page.tsx | 15 +- .../newsletter-form/newsletter-form.tsx | 71 +++++ .../newsletter-glow-container.tsx | 32 +++ .../newsletter-popup-client.tsx | 51 +--- 17 files changed, 499 insertions(+), 51 deletions(-) create mode 100644 ui/src/components/containers/glow-hover-container.tsx create mode 100644 ui/src/components/use-glow-hover/glow-hover-effect.ts create mode 100644 ui/src/components/use-glow-hover/index.ts create mode 100644 ui/src/components/use-glow-hover/linear-animation.ts create mode 100644 ui/src/components/use-glow-hover/presets.ts create mode 100644 ui/src/components/use-glow-hover/use-glow-hover.ts create mode 100644 website/src/components/newsletter-form/newsletter-form.tsx create mode 100644 website/src/components/newsletter-glow-container/newsletter-glow-container.tsx diff --git a/shared/locales/de/website-newsletter.json b/shared/locales/de/website-newsletter.json index 4f34b0bc8..aa1f15ad7 100644 --- a/shared/locales/de/website-newsletter.json +++ b/shared/locales/de/website-newsletter.json @@ -25,5 +25,8 @@ "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": "Newsletter abonnieren" + }, + "campaign": { + "information-label": "Erhalte Updates von diesem Fundraiser und von Social Income." } } diff --git a/shared/locales/en/website-newsletter.json b/shared/locales/en/website-newsletter.json index f9fff5505..1c87032fa 100644 --- a/shared/locales/en/website-newsletter.json +++ b/shared/locales/en/website-newsletter.json @@ -25,5 +25,8 @@ "toast-failure": "Something went wrong. Try again or contact us at hello@socialincome.org", "email-placeholder": "Your email", "button-subscribe": "Subscribe now" + }, + "campaign": { + "information-label": "Get updates from the fundraiser and Social Income." } } diff --git a/shared/locales/fr/website-newsletter.json b/shared/locales/fr/website-newsletter.json index bc2f4bd37..62b4f616b 100644 --- a/shared/locales/fr/website-newsletter.json +++ b/shared/locales/fr/website-newsletter.json @@ -25,5 +25,8 @@ "toast-failure": "Oups! Ça n’a pas marché. Essaie encore une fois ou prend contact avec nous (hello@socialincome.org)", "email-placeholder": "Ton adresse E-Mail", "button-subscribe": "S'abonner" + }, + "campaign": { + "information-label": "Reçois des mises à jour du collecteur de fonds et de Social Income." } } diff --git a/shared/locales/it/website-newsletter.json b/shared/locales/it/website-newsletter.json index 08b582b01..b4be453c1 100644 --- a/shared/locales/it/website-newsletter.json +++ b/shared/locales/it/website-newsletter.json @@ -25,5 +25,8 @@ "toast-failure": "Qualcosa è andato storto. Prova di nuovo o contattaci a hello@socialincome.org", "email-placeholder": "La tua email", "button-subscribe": "Iscriviti ora" + }, + "campaign": { + "information-label": "Ricevi aggiornamenti dal fundraiser e da Social Income." } } diff --git a/ui/src/components/containers/base-container.tsx b/ui/src/components/containers/base-container.tsx index bef37fae1..157505838 100644 --- a/ui/src/components/containers/base-container.tsx +++ b/ui/src/components/containers/base-container.tsx @@ -1,13 +1,16 @@ import React from 'react'; +import { twMerge } from 'tailwind-merge'; -type BaseContainerProps = { +export type BaseContainerProps = { + wrapperClassName?: string; + wrapperRef?: React.Ref; baseClassNames?: string; } & React.HTMLAttributes; export const BaseContainer = React.forwardRef( - ({ children, className, baseClassNames, ...props }, ref) => { + ({ children, className, baseClassNames, wrapperClassName, wrapperRef, ...props }, ref) => { return ( -
+
{children} diff --git a/ui/src/components/containers/glow-hover-container.tsx b/ui/src/components/containers/glow-hover-container.tsx new file mode 100644 index 000000000..22d474457 --- /dev/null +++ b/ui/src/components/containers/glow-hover-container.tsx @@ -0,0 +1,23 @@ +'use client'; + +import React from 'react'; +import { twMerge } from 'tailwind-merge'; +import { useGlowHover } from '../use-glow-hover'; +import { BaseContainer } from './base-container'; + +import { BaseContainerProps } from './base-container'; + +export const GlowHoverContainer = React.forwardRef>( + ({ className, ...props }, ref) => { + const refCard = useGlowHover({ lightColor: '#CEFF00' }); + + return ( + } + /> + ); + }, +); diff --git a/ui/src/components/containers/index.ts b/ui/src/components/containers/index.ts index 359f778af..99e805dfa 100644 --- a/ui/src/components/containers/index.ts +++ b/ui/src/components/containers/index.ts @@ -1 +1,2 @@ export * from './base-container'; +export * from './glow-hover-container'; diff --git a/ui/src/components/use-glow-hover/glow-hover-effect.ts b/ui/src/components/use-glow-hover/glow-hover-effect.ts new file mode 100644 index 000000000..5c1870507 --- /dev/null +++ b/ui/src/components/use-glow-hover/glow-hover-effect.ts @@ -0,0 +1,266 @@ +'use client'; + +import { linearAnimation } from './linear-animation'; +import { presets } from './presets'; + +export type GlowHoverOptions = { + hoverBg?: string; + lightSize?: number; + lightSizeEnterAnimationTime?: number; + lightSizeLeaveAnimationTime?: number; + isElementMovable?: boolean; + customStaticBg?: string; + enableBurst?: boolean; +} & ( + | { + preset: keyof typeof presets; + lightColor?: string; + } + | { + preset?: undefined; + lightColor: string; + } +); + +type Coords = { + x: number; + y: number; +}; + +const BURST_TIME = 300; + +export function parseColor(colorToParse: string) { + const div = document.createElement('div'); + div.style.color = colorToParse; + div.style.position = 'absolute'; + div.style.display = 'none'; + document.body.appendChild(div); + const colorFromEl = getComputedStyle(div).color; + document.body.removeChild(div); + const parsedColor = colorFromEl.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/i); + if (parsedColor) { + const alpha = typeof parsedColor[4] === 'undefined' ? 1 : parsedColor[4]; + return [parsedColor[1], parsedColor[2], parsedColor[3], alpha]; + } else { + console.error(`Color ${colorToParse} could not be parsed.`); + return [0, 0, 0, 0]; + } +} + +export const glowHoverEffect = (el: HTMLElement, { preset, ...options }: GlowHoverOptions) => { + if (!el) { + return () => {}; + } + + const lightColor = options.lightColor ?? '#CEFF00'; + const lightSize = options.lightSize ?? 130; + const lightSizeEnterAnimationTime = options.lightSizeEnterAnimationTime ?? 100; + const lightSizeLeaveAnimationTime = options.lightSizeLeaveAnimationTime ?? 50; + const isElementMovable = options.isElementMovable ?? false; + const customStaticBg = options.customStaticBg ?? null; + + const enableBurst = options.enableBurst ?? false; + + const getResolvedHoverBg = () => getComputedStyle(el).backgroundColor; + + let resolvedHoverBg = getResolvedHoverBg(); + + // default bg (if not defined) is rgba(0, 0, 0, 0) which is bugged in gradients in Safari + // so we use transparent lightColor instead + const parsedLightColor = parseColor(lightColor); + const parsedLightColorRGBString = parsedLightColor.slice(0, 3).join(','); + const resolvedGradientBg = `rgba(${parsedLightColorRGBString},0)`; + + let isMouseInside = false; + let currentLightSize = 0; + let blownSize = 0; + let lightSizeEnterAnimationId: number | undefined = undefined; + let lightSizeLeaveAnimationId: number | undefined = undefined; + let blownSizeIncreaseAnimationId: number | undefined = undefined; + let blownSizeDecreaseAnimationId: number | undefined = undefined; + let lastMousePos: Coords; + const defaultBox = el.getBoundingClientRect(); + let lastElPos: Coords = { x: defaultBox.left, y: defaultBox.top }; + + const updateGlowEffect = () => { + if (!lastMousePos) { + return; + } + const gradientXPos = lastMousePos.x - lastElPos.x; + const gradientYPos = lastMousePos.y - lastElPos.y; + // we do not use transparent color here because of dirty gradient in Safari (more info: https://stackoverflow.com/questions/38391457/linear-gradient-to-transparent-bug-in-latest-safari) + const gradient = `radial-gradient(circle at ${gradientXPos}px ${gradientYPos}px, ${lightColor} 0%, ${resolvedGradientBg} calc(${ + blownSize * 2.5 + }% + ${currentLightSize}px)) no-repeat`; + + // we duplicate resolvedHoverBg layer here because of transition "blinking" without it + el.style.background = `${gradient} border-box border-box ${resolvedHoverBg}`; + }; + + const updateEffectWithPosition = () => { + if (isMouseInside) { + const curBox = el.getBoundingClientRect(); + lastElPos = { x: curBox.left, y: curBox.top }; + updateGlowEffect(); + } + }; + + const onMouseEnter = (e: MouseEvent) => { + resolvedHoverBg = getResolvedHoverBg(); + lastMousePos = { x: e.clientX, y: e.clientY }; + const curBox = el.getBoundingClientRect(); + lastElPos = { x: curBox.left, y: curBox.top }; + isMouseInside = true; + if (lightSizeEnterAnimationId !== undefined) { + window.cancelAnimationFrame(lightSizeEnterAnimationId); + } + if (lightSizeLeaveAnimationId !== undefined) { + window.cancelAnimationFrame(lightSizeLeaveAnimationId); + } + + // animate currentLightSize from 0 to lightSize + linearAnimation({ + onProgress: (progress) => { + currentLightSize = lightSize * progress; + updateGlowEffect(); + }, + time: lightSizeEnterAnimationTime, + initialProgress: currentLightSize / lightSize, + onIdUpdate: (newId) => (lightSizeEnterAnimationId = newId), + }); + }; + + const onMouseMove = (e: MouseEvent) => { + lastMousePos = { x: e.clientX, y: e.clientY }; + if (isElementMovable) { + updateEffectWithPosition(); + } else { + updateGlowEffect(); + } + }; + + const onMouseLeave = () => { + isMouseInside = false; + if (lightSizeEnterAnimationId !== undefined) { + window.cancelAnimationFrame(lightSizeEnterAnimationId); + } + if (lightSizeLeaveAnimationId !== undefined) { + window.cancelAnimationFrame(lightSizeLeaveAnimationId); + } + if (blownSizeIncreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeIncreaseAnimationId); + } + if (blownSizeDecreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeDecreaseAnimationId); + } + + // animate currentLightSize from lightSize to 0 + linearAnimation({ + onProgress: (progress) => { + currentLightSize = lightSize * (1 - progress); + blownSize = Math.min(blownSize, (1 - progress) * 100); + + if (progress < 1) { + updateGlowEffect(); + } else { + el.style.background = customStaticBg ? customStaticBg : ''; + } + }, + time: lightSizeLeaveAnimationTime, + initialProgress: 1 - currentLightSize / lightSize, + onIdUpdate: (newId) => (lightSizeLeaveAnimationId = newId), + }); + }; + + const onMouseDown = (e: MouseEvent) => { + lastMousePos = { x: e.clientX, y: e.clientY }; + const curBox = el.getBoundingClientRect(); + lastElPos = { x: curBox.left, y: curBox.top }; + if (blownSizeIncreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeIncreaseAnimationId); + } + if (blownSizeDecreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeDecreaseAnimationId); + } + + // animate blownSize from 0 to 100 + linearAnimation({ + onProgress: (progress) => { + blownSize = 100 * progress; + updateGlowEffect(); + }, + time: BURST_TIME, + initialProgress: blownSize / 100, + onIdUpdate: (newId) => (blownSizeIncreaseAnimationId = newId), + }); + }; + + const onMouseUp = (e: MouseEvent) => { + lastMousePos = { x: e.clientX, y: e.clientY }; + const curBox = el.getBoundingClientRect(); + lastElPos = { x: curBox.left, y: curBox.top }; + if (blownSizeIncreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeIncreaseAnimationId); + } + if (blownSizeDecreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeDecreaseAnimationId); + } + + // animate blownSize from 100 to 0 + linearAnimation({ + onProgress: (progress) => { + blownSize = (1 - progress) * 100; + updateGlowEffect(); + }, + time: BURST_TIME, + initialProgress: 1 - blownSize / 100, + onIdUpdate: (newId) => (blownSizeDecreaseAnimationId = newId), + }); + }; + + document.addEventListener('scroll', updateEffectWithPosition); + window.addEventListener('resize', updateEffectWithPosition); + el.addEventListener('mouseenter', onMouseEnter); + el.addEventListener('mousemove', onMouseMove); + el.addEventListener('mouseleave', onMouseLeave); + if (enableBurst) { + el.addEventListener('mousedown', onMouseDown); + el.addEventListener('mouseup', onMouseUp); + } + + let resizeObserver: ResizeObserver; + if (window.ResizeObserver) { + resizeObserver = new ResizeObserver(updateEffectWithPosition); + resizeObserver.observe(el); + } + + return () => { + if (lightSizeEnterAnimationId !== undefined) { + window.cancelAnimationFrame(lightSizeEnterAnimationId); + } + if (lightSizeLeaveAnimationId !== undefined) { + window.cancelAnimationFrame(lightSizeLeaveAnimationId); + } + if (blownSizeIncreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeIncreaseAnimationId); + } + if (blownSizeDecreaseAnimationId !== undefined) { + window.cancelAnimationFrame(blownSizeDecreaseAnimationId); + } + + document.removeEventListener('scroll', updateEffectWithPosition); + window.removeEventListener('resize', updateEffectWithPosition); + el.removeEventListener('mouseenter', onMouseEnter); + el.removeEventListener('mousemove', onMouseMove); + el.removeEventListener('mouseleave', onMouseLeave); + if (enableBurst) { + el.removeEventListener('mousedown', onMouseDown); + el.removeEventListener('mouseup', onMouseUp); + } + + if (resizeObserver) { + resizeObserver.unobserve(el); + resizeObserver.disconnect(); + } + }; +}; diff --git a/ui/src/components/use-glow-hover/index.ts b/ui/src/components/use-glow-hover/index.ts new file mode 100644 index 000000000..7818a6ed8 --- /dev/null +++ b/ui/src/components/use-glow-hover/index.ts @@ -0,0 +1,4 @@ +export { glowHoverEffect } from './glow-hover-effect'; +export type { GlowHoverOptions } from './glow-hover-effect'; +export { useGlowHover } from './use-glow-hover'; +export type { GlowHoverHookOptions } from './use-glow-hover'; diff --git a/ui/src/components/use-glow-hover/linear-animation.ts b/ui/src/components/use-glow-hover/linear-animation.ts new file mode 100644 index 000000000..4f2106f89 --- /dev/null +++ b/ui/src/components/use-glow-hover/linear-animation.ts @@ -0,0 +1,38 @@ +'use client'; + +interface LinearAnimationParams { + onProgress: (progress: number) => void; + onIdUpdate?: (id: number | undefined) => void; + time: number; + initialProgress?: number; +} + +export const linearAnimation = ({ + onProgress, + onIdUpdate = () => {}, + time, + initialProgress = 0, +}: LinearAnimationParams) => { + if (time === 0) { + onProgress(1); + onIdUpdate(undefined); + return; + } + + let start: number | undefined = undefined; + const step = (timestamp: number) => { + if (start === undefined) start = timestamp; + const progress = Math.min((timestamp - start) / time + initialProgress, 1); + + onProgress(progress); + + if (progress < 1) { + const id = window.requestAnimationFrame(step); + onIdUpdate(id); + } else { + onIdUpdate(undefined); + } + }; + const id = window.requestAnimationFrame(step); + onIdUpdate(id); +}; diff --git a/ui/src/components/use-glow-hover/presets.ts b/ui/src/components/use-glow-hover/presets.ts new file mode 100644 index 000000000..a49047d95 --- /dev/null +++ b/ui/src/components/use-glow-hover/presets.ts @@ -0,0 +1,11 @@ +export const presets = { + default: { + lightColor: '#CEFF00', + lightSize: 130, + lightSizeEnterAnimationTime: 100, + lightSizeLeaveAnimationTime: 50, + isElementMovable: false, + customStaticBg: null, + enableBurst: false, + }, +}; diff --git a/ui/src/components/use-glow-hover/use-glow-hover.ts b/ui/src/components/use-glow-hover/use-glow-hover.ts new file mode 100644 index 000000000..e86636125 --- /dev/null +++ b/ui/src/components/use-glow-hover/use-glow-hover.ts @@ -0,0 +1,16 @@ +'use client'; + +import { useEffect, useRef } from 'react'; + +import { glowHoverEffect, GlowHoverOptions } from './glow-hover-effect'; + +export type GlowHoverHookOptions = GlowHoverOptions & { disabled?: boolean }; +export const useGlowHover = ({ disabled = false, ...options }: GlowHoverHookOptions) => { + const ref = useRef(null); + + useEffect( + () => (!disabled && ref.current ? glowHoverEffect(ref.current, options) : () => {}), + [disabled, ...Object.values(options)], + ); + return ref; +}; diff --git a/ui/src/index.ts b/ui/src/index.ts index 7c691c3a5..cabca62b0 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -24,6 +24,7 @@ export * from './components/table'; export * from './components/tabs'; export * from './components/tooltip'; export * from './components/typography'; +export * from './components/use-glow-hover'; export * from './icons/donate'; export * from './icons/si'; 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 423756918..2798280f8 100644 --- a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx @@ -28,6 +28,8 @@ import { import { Progress } from '@socialincome/ui/src/components/progress'; import Link from 'next/link'; +import NewsletterGlowContainer from '@/components/newsletter-glow-container/newsletter-glow-container'; + export type CampaignPageProps = { params: { country: WebsiteRegion; @@ -64,7 +66,7 @@ export async function generateMetadata({ params }: CampaignPageProps) { export default async function Page({ params }: CampaignPageProps) { const translator = await Translator.getInstance({ language: params.lang, - namespaces: ['website-campaign', 'website-donate', 'website-videos', 'website-faq'], + namespaces: ['website-campaign', 'website-donate', 'website-videos', 'website-faq', 'website-newsletter'], }); const campaignDoc = await firestoreAdmin.collection(CAMPAIGN_FIRESTORE_PATH).doc(params.campaign).get(); @@ -269,6 +271,17 @@ export default async function Page({ params }: CampaignPageProps) {
)} +
diff --git a/website/src/components/newsletter-form/newsletter-form.tsx b/website/src/components/newsletter-form/newsletter-form.tsx new file mode 100644 index 000000000..143ee85ce --- /dev/null +++ b/website/src/components/newsletter-form/newsletter-form.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { CreateNewsletterSubscription } from '@/app/api/newsletter/subscription/public/route'; +import { NewsletterPopupTranslations } from '@/components/newsletter-popup/newsletter-popup-client'; +import { useApi } from '@/hooks/useApi'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LanguageCode } from '@socialincome/shared/src/types/language'; +import { Button, Form, FormControl, FormField, FormItem, FormMessage, Input } from '@socialincome/ui'; +import { useForm } from 'react-hook-form'; +import toast, { Toast } from 'react-hot-toast'; +import * as z from 'zod'; + +type NewsletterFormProps = { + lang: LanguageCode; + t?: Toast; + translations: NewsletterPopupTranslations; +}; + +const NewsletterForm = ({ t, lang, translations }: NewsletterFormProps) => { + 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 === 'de' ? 'de' : 'en', + }; + api.post('/api/newsletter/subscription/public', body).then((response) => { + if (response.status === 200) { + if (t && t.id) { + toast.dismiss(t.id); + } + toast.success(translations.toastSuccess); + } else { + toast.error(translations.toastFailure); + } + }); + }; + + return ( +
+ + ( + + + + + + + )} + /> + + + + ); +}; + +export default NewsletterForm; diff --git a/website/src/components/newsletter-glow-container/newsletter-glow-container.tsx b/website/src/components/newsletter-glow-container/newsletter-glow-container.tsx new file mode 100644 index 000000000..f23e87ef2 --- /dev/null +++ b/website/src/components/newsletter-glow-container/newsletter-glow-container.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { LanguageCode } from '@socialincome/shared/src/types/language'; +import { GlowHoverContainer, Typography } from '@socialincome/ui'; + +import NewsletterForm from '@/components/newsletter-form/newsletter-form'; +import { NewsletterPopupTranslations } from '@/components/newsletter-popup/newsletter-popup-client'; + +type NewsletterGlowContainerProps = { + title: string; + lang: LanguageCode; + formTranslations: NewsletterPopupTranslations; +}; + +const NewsletterGlowContainer = ({ title, lang, formTranslations }: NewsletterGlowContainerProps) => { + return ( + +
+
+ + {title} + +
+
+ +
+
+
+ ); +}; + +export default NewsletterGlowContainer; diff --git a/website/src/components/newsletter-popup/newsletter-popup-client.tsx b/website/src/components/newsletter-popup/newsletter-popup-client.tsx index c4c0e96ac..7a3fa91ca 100644 --- a/website/src/components/newsletter-popup/newsletter-popup-client.tsx +++ b/website/src/components/newsletter-popup/newsletter-popup-client.tsx @@ -1,19 +1,15 @@ 'use client'; -import { CreateNewsletterSubscription } from '@/app/api/newsletter/subscription/public/route'; +import NewsletterForm from '@/components/newsletter-form/newsletter-form'; 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 { 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 = { +export type NewsletterPopupTranslations = { informationLabel: string; toastSuccess: string; toastFailure: string; @@ -29,29 +25,6 @@ type NewsletterPopupToastProps = { }; 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 === 'de' ? 'de' : 'en', - }; - 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} -
- - ( - - - - - - - )} - /> - - - +
); };