Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | 26x 26x 26x 26x 26x 26x 7x 7x 7x 7x 1x 1x 6x 6x 6x 1x 3x 1x 2x 1x 1x 1x 26x 1x 25x 1x 24x 7x | /**
* NewsletterSubscribeWidget (Story 10.7 — AC4)
*
* Footer widget for subscribing anonymous visitors to the BATbern newsletter.
* Shows success / already-subscribed / error inline states.
*/
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import axios from 'axios';
import { Input } from '@/components/public/ui/input';
import { Button } from '@/components/public/ui/button';
import { useNewsletterSubscribe } from '@/hooks/useNewsletter/useNewsletter';
import { useTurnstile } from '@/hooks/useTurnstile';
type WidgetState = 'idle' | 'submitting' | 'success' | 'already-subscribed' | 'error';
export function NewsletterSubscribeWidget() {
const { t } = useTranslation('events');
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
const [widgetState, setWidgetState] = useState<WidgetState>('idle');
const subscribeMutation = useNewsletterSubscribe();
const { getToken, resetWidget, widgetRef } = useTurnstile();
function validateEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setEmailError('');
if (!validateEmail(email)) {
setEmailError(t('newsletter.widget.emailInvalid', 'Please enter a valid email address.'));
return;
}
setWidgetState('submitting');
getToken().then((turnstileToken) => {
subscribeMutation.mutate(
{ request: { email }, turnstileToken },
{
onSuccess: () => {
setWidgetState('success');
},
onError: (error) => {
if (axios.isAxiosError(error) && error.response?.status === 409) {
setWidgetState('already-subscribed');
} else if (
axios.isAxiosError(error) &&
error.response?.status === 403 &&
(error.response.data?.error === 'turnstile_required' ||
error.response.data?.error === 'turnstile_failed')
) {
resetWidget();
setWidgetState('error');
} else {
setWidgetState('error');
}
},
}
);
});
}
if (widgetState === 'success') {
return (
<div className="py-4 text-center text-sm text-green-700 dark:text-green-400">
{t('newsletter.widget.success')}
</div>
);
}
if (widgetState === 'already-subscribed') {
return (
<div className="py-4 text-center text-sm text-muted-foreground">
{t('newsletter.widget.alreadySubscribed')}
</div>
);
}
return (
<div className="py-4">
<p className="mb-3 text-sm font-medium">{t('newsletter.widget.title')}</p>
{/* Invisible Turnstile widget container (Story 10.31, AC7) */}
<div ref={widgetRef} />
<form onSubmit={handleSubmit} noValidate className="flex flex-col gap-2 sm:flex-row">
<div className="flex-1">
<Input
type="email"
placeholder={t('newsletter.widget.placeholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-label={t('newsletter.widget.placeholder')}
disabled={widgetState === 'submitting'}
/>
{emailError && <p className="mt-1 text-xs text-destructive">{emailError}</p>}
</div>
<Button type="submit" disabled={widgetState === 'submitting'} size="sm">
{t('newsletter.widget.button')}
</Button>
</form>
{widgetState === 'error' && (
<p className="mt-2 text-xs text-destructive">{t('newsletter.widget.error')}</p>
)}
</div>
);
}
|