Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions src/components/Newsletter/SubscribeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function SubscribeForm(): React.JSX.Element {
const {t, locale} = useI18n();
const [email, setEmail] = useState('');
const [status, setStatus] = useState<Status>('idle');
const [consentVisible, setConsentVisible] = useState(false);

const handleSubmit = useCallback(
(event: React.FormEvent) => {
Expand Down Expand Up @@ -44,18 +45,21 @@ export default function SubscribeForm(): React.JSX.Element {
// list — the form must not answer "is this person subscribed?".
if (status === 'sent') {
return (
<div className={styles.panel} role="status">
<p className={styles.successTitle}>{t('newsletter.checkInbox')}</p>
<p className={styles.note}>{t('newsletter.confirmHint')}</p>
<div className={styles.bar} role="status">
<div className={styles.text}>
<p className={styles.title}>{t('newsletter.checkInbox')}</p>
<p className={styles.note}>{t('newsletter.confirmHint')}</p>
</div>
</div>
);
}

return (
<form className={styles.panel} onSubmit={handleSubmit}>
<p className={styles.eyebrow}>{t('newsletter.eyebrow')}</p>
<h2 className={styles.title}>{t('newsletter.title')}</h2>
<p className={styles.note}>{t('newsletter.description')}</p>
<form className={styles.bar} onSubmit={handleSubmit}>
<div className={styles.text}>
<p className={styles.title}>{t('newsletter.title')}</p>
<p className={styles.note}>{t('newsletter.description')}</p>
</div>

<div className={styles.row}>
<label className={styles.srOnly} htmlFor="newsletter-email">
Expand All @@ -73,10 +77,16 @@ export default function SubscribeForm(): React.JSX.Element {
setEmail(event.target.value);
if (status === 'invalid' || status === 'failed') setStatus('idle');
}}
onFocus={() => setConsentVisible(true)}
aria-invalid={status === 'invalid'}
aria-describedby="newsletter-consent"
required
/>
<button type="submit" className="button primary" disabled={status === 'sending'}>
<button
type="submit"
className={`button button--primary button--sm ${styles.submit}`}
disabled={status === 'sending'}
>
{status === 'sending' ? t('newsletter.sending') : t('newsletter.subscribe')}
</button>
</div>
Expand All @@ -92,7 +102,18 @@ export default function SubscribeForm(): React.JSX.Element {
</p>
) : null}

<p className={styles.consent}>{t('newsletter.consent')}</p>
{/*
The strip stays one line until the visitor engages with it, but the
consent text must be readable before they can submit — so it unfolds on
the first focus of the field, and screen readers reach it either way
through aria-describedby.
*/}
<p
id="newsletter-consent"
className={consentVisible ? styles.consent : styles.srOnly}
>
{t('newsletter.consent')}
</p>
</form>
);
}
104 changes: 69 additions & 35 deletions src/components/Newsletter/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,48 +1,62 @@
.panel {
/*
* A single slim strip, not a card: the home page already stacks two full-width
* panels (docs, support) and a third heavy block between them read as an
* interruption. Same border, radius family and surface as those panels so it
* sits in the rhythm instead of announcing itself.
*/
.bar {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 1.25rem 1.5rem;
border: 1px solid var(--ifm-color-emphasis-200);
flex-wrap: wrap;
align-items: center;
gap: 0.6rem 1.5rem;
padding: 0.85rem 1.25rem;
border: 1px solid var(--card-border);
border-radius: 14px;
background: var(--ifm-background-surface-color);
background: rgba(255, 255, 255, 0.02);
}

.eyebrow {
margin: 0;
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.6;
.text {
flex: 1 1 17rem;
min-width: 0;
}

.title {
margin: 0;
font-size: 1.15rem;
font-size: 0.95rem;
font-weight: 600;
line-height: 1.3;
}

.note {
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
margin: 0.15rem 0 0;
font-size: 0.78rem;
line-height: 1.4;
opacity: 0.65;
}

.row {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
margin-top: 0.6rem;
align-items: center;
gap: 0.5rem;
margin-left: auto;
}

.input {
flex: 1 1 16rem;
flex: 1 1 13rem;
min-width: 0;
padding: 0.55rem 0.8rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 9px;
background: var(--ifm-background-color);
padding: 0.4rem 0.7rem;
border: 1px solid var(--card-border);
border-radius: 8px;
background: transparent;
color: inherit;
font-size: 0.95rem;
/* iOS zooms the page in when a focused field is under 16px. */
font-size: 1rem;
}

@media (min-width: 601px) {
.input {
font-size: 0.875rem;
}
}

.input:focus {
Expand All @@ -54,22 +68,26 @@
border-color: var(--ifm-color-danger);
}

.error {
margin: 0.4rem 0 0;
font-size: 0.85rem;
color: var(--ifm-color-danger);
.submit {
white-space: nowrap;
}

.successTitle {
/* Full-width lines below the strip, so they never widen the row itself. */
.error,
.consent {
flex: 1 0 100%;
margin: 0;
font-weight: 600;
line-height: 1.45;
}

.consent {
margin: 0.7rem 0 0;
.error {
font-size: 0.78rem;
opacity: 0.65;
line-height: 1.5;
color: var(--ifm-color-danger);
}

.consent {
font-size: 0.7rem;
opacity: 0.55;
}

.srOnly {
Expand All @@ -83,3 +101,19 @@
white-space: nowrap;
border: 0;
}

@media (max-width: 700px) {
.row {
width: 100%;
margin-left: 0;
}
}

:global(html[data-theme='light']) .bar {
background: var(--light-surface);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
}

:global(html[data-theme='light']) .input {
background: var(--light-surface-strong, #fff);
}
1 change: 0 additions & 1 deletion src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export const de: TranslationDict = {
error: 'Gespeicherte Berechnungen konnten nicht geladen werden. Bitte erneut versuchen.',
},
newsletter: {
eyebrow: 'Auf dem Laufenden bleiben',
title: 'Neue Tools, direkt per E-Mail',
description:
'Eine gelegentliche Nachricht, wenn ein neues Ingenieur-Tool erscheint oder ein bestehendes erweitert wird. Höchstens einmal im Monat.',
Expand Down
1 change: 0 additions & 1 deletion src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export const en = {
error: 'Unable to load your saved calculations. Please try again.',
},
newsletter: {
eyebrow: 'Stay updated',
title: 'New tools, in your inbox',
description:
'An occasional note when a new engineering utility ships or an existing one gains a feature. No more than once a month.',
Expand Down
1 change: 0 additions & 1 deletion src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export const es: TranslationDict = {
error: 'No se pudieron cargar tus cálculos guardados. Inténtalo de nuevo.',
},
newsletter: {
eyebrow: 'Mantente al día',
title: 'Nuevas herramientas, en tu correo',
description:
'Un aviso ocasional cuando se publica una nueva utilidad de ingeniería o una existente gana una función. No más de una vez al mes.',
Expand Down
1 change: 0 additions & 1 deletion src/i18n/locales/et.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export const et: TranslationDict = {
error: 'Salvestatud arvutusi ei õnnestunud laadida. Palun proovi uuesti.',
},
newsletter: {
eyebrow: 'Ole kursis',
title: 'Uued tööriistad sinu postkasti',
description:
'Harv kiri, kui ilmub uus inseneritööriist või olemasolev saab uue võimaluse. Mitte sagedamini kui kord kuus.',
Expand Down
1 change: 0 additions & 1 deletion src/i18n/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export const ru: TranslationDict = {
error: 'Не удалось загрузить сохранённые расчёты. Попробуйте снова.',
},
newsletter: {
eyebrow: 'Будьте в курсе',
title: 'Новые инструменты — на вашу почту',
description:
'Редкое письмо, когда выходит новая инженерная утилита или у существующей появляется функция. Не чаще раза в месяц.',
Expand Down
1 change: 0 additions & 1 deletion src/i18n/locales/ua.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export const ua: TranslationDict = {
error: 'Не вдалося завантажити збережені розрахунки. Спробуйте знову.',
},
newsletter: {
eyebrow: 'Будьте в курсі',
title: 'Нові інструменти — на вашу пошту',
description:
'Рідкий лист, коли виходить нова інженерна утиліта або наявна отримує функцію. Не частіше разу на місяць.',
Expand Down
4 changes: 1 addition & 3 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,7 @@ export default function Home(): ReactNode {

<DocsSection sectionRef={docsSection.ref} paused={docsSection.paused} />

<section style={{maxWidth: 720, margin: '0 auto', padding: '2.5rem 1rem 0'}}>
<SubscribeForm />
</section>
<SubscribeForm />
<SupportSection />
</main>
</Layout>
Expand Down
Loading