diff --git a/app/email/unsubscribe/EmailUnsubscribeForm.tsx b/app/email/unsubscribe/EmailUnsubscribeForm.tsx new file mode 100644 index 000000000..22239d695 --- /dev/null +++ b/app/email/unsubscribe/EmailUnsubscribeForm.tsx @@ -0,0 +1,131 @@ +'use client'; + +import Link from 'next/link'; +import { CircleCheck, LoaderCircle, TriangleAlert } from 'lucide-react'; +import { useState } from 'react'; +import { Button } from '@/components/ui/Button'; +import { Logo } from '@/components/ui/Logo'; +import { EmailSubscriptionService } from '@/services/emailSubscription.service'; + +type SubmissionState = 'idle' | 'submitting' | 'success' | 'error'; + +interface EmailUnsubscribeFormProps { + code: string; +} + +export function EmailUnsubscribeForm({ code }: Readonly) { + const [submissionState, setSubmissionState] = useState('idle'); + const [errorMessage, setErrorMessage] = useState(''); + const hasCode = code.length > 0; + + const handleUnsubscribe = async () => { + if (!hasCode || submissionState === 'submitting') { + return; + } + + setSubmissionState('submitting'); + setErrorMessage(''); + + try { + await EmailSubscriptionService.unsubscribe(code); + setSubmissionState('success'); + } catch (error) { + setErrorMessage( + error instanceof Error + ? error.message + : 'Unable to unsubscribe right now. Please try again.' + ); + setSubmissionState('error'); + } + }; + + const isSuccess = submissionState === 'success'; + const isSubmitting = submissionState === 'submitting'; + + return ( +
+
+ ); +} diff --git a/app/email/unsubscribe/page.tsx b/app/email/unsubscribe/page.tsx new file mode 100644 index 000000000..41b526f47 --- /dev/null +++ b/app/email/unsubscribe/page.tsx @@ -0,0 +1,26 @@ +import { Metadata } from 'next'; +import { EmailUnsubscribeForm } from './EmailUnsubscribeForm'; + +export const metadata: Metadata = { + title: 'Unsubscribe from emails', + description: 'Manage your ResearchHub email subscription.', + robots: { + index: false, + follow: false, + }, +}; + +interface EmailUnsubscribePageProps { + searchParams: Promise<{ + code?: string | string[]; + }>; +} + +export default async function EmailUnsubscribePage({ + searchParams, +}: Readonly) { + const { code } = await searchParams; + const unsubscribeCode = typeof code === 'string' ? code : ''; + + return ; +} diff --git a/services/emailSubscription.service.ts b/services/emailSubscription.service.ts new file mode 100644 index 000000000..0431678cd --- /dev/null +++ b/services/emailSubscription.service.ts @@ -0,0 +1,41 @@ +import { ApiClient } from './client'; +import { ApiError } from './types'; + +const DEFAULT_ERROR_MESSAGE = 'Unable to unsubscribe right now. Please try again.'; + +export class EmailSubscriptionService { + private static readonly BASE_PATH = '/api/email'; + + static async unsubscribe(code: string): Promise { + const query = new URLSearchParams({ code }); + + try { + await ApiClient.post(`${this.BASE_PATH}/unsubscribe?${query.toString()}`); + } catch (error) { + throw new Error(this.getErrorMessage(error)); + } + } + + private static getErrorMessage(error: unknown): string { + const responseBody = error instanceof ApiError ? error.errors : undefined; + + if (!responseBody || typeof responseBody !== 'object') { + return DEFAULT_ERROR_MESSAGE; + } + + const body = responseBody as { + detail?: unknown; + code?: unknown; + }; + + if (typeof body.detail === 'string') { + return body.detail; + } + + if (Array.isArray(body.code) && body.code.length > 0 && typeof body.code[0] === 'string') { + return body.code[0]; + } + + return DEFAULT_ERROR_MESSAGE; + } +}