Shop Newsletter Editorial
Suscripción editorial de gran tamaño con campo subrayado, consentimiento y validación en línea.
newsletterformecommercecta
Vista Previa en Vivo
Letters from the studio
One email a month: new pieces before anyone else, the people who make them, and 10% off your first order.
Uso
Copia y pega este código directamente en tu componente React:
tsx
import { ShopNewsletterEditorial } from 'react-component-kit-ui/sections';
<ShopNewsletterEditorial title="Letters from the *studio*" variant="accent" />Instalación Manual
Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/sections/ShopNewsletterEditorial.tsx + ShopNewsletterEditorial.css:
ShopNewsletterEditorial.tsx
'use client';
import { useEffect, useRef } from 'react';
import './ShopNewsletterEditorial.css';
/** class:list → className */
function cx(...args: unknown[]): string {
const out: string[] = [];
const walk = (v: unknown) => {
if (!v) return;
if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
else if (Array.isArray(v)) v.forEach(walk);
else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
};
args.forEach(walk);
return out.join(' ');
}
/**
* ShopNewsletterEditorial — Oversized editorial sign-up: large serif statement on one side,
* underline-style email field, consent note and inline success/error messages on the other.
*
* @param {string} title - Statement (use *asterisks* for italics). Default 'Letters from the *studio*'.
* @param {string} text - Supporting copy.
* @param {string} action - Form action. With '#' the form validates and confirms locally. Default '#'.
* @param {string} buttonLabel - Submit label. Default 'Subscribe'.
* @param {string} consent - Small print under the field.
* @param {string} successText - Message after a valid submit.
* @param {string} variant - Style (plain|inverted|accent). Default 'plain'.
* @param {string} size - Type scale (sm|md|lg). Default 'md'.
* @param {string} id - Base id. Default 'shop-news'.
* @param {string} className - Extra class.
*
* Accessibility: labelled email input with autocomplete, aria-invalid + described error,
* messages in a polite live region.
*/
export interface ShopNewsletterEditorialProps {
title?: string;
text?: string;
action?: string;
buttonLabel?: string;
consent?: string;
successText?: string;
variant?: 'plain' | 'inverted' | 'accent';
size?: 'sm' | 'md' | 'lg';
id?: string;
className?: string;
}
export function ShopNewsletterEditorial({
title = 'Letters from the *studio*',
text = 'One email a month: new pieces before anyone else, the people who make them, and 10% off your first order.',
action = '#',
buttonLabel = 'Subscribe',
consent = 'By subscribing you accept our privacy policy. Unsubscribe in one click.',
successText = 'Thank you — your first letter is on its way.',
variant = 'plain',
size = 'md',
id = 'shop-news',
className = '',
}: ShopNewsletterEditorialProps) {
const parts = title.split(/\*(.+?)\*/g);
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
document.querySelectorAll<HTMLFormElement>('form[data-news]:not([data-bound])').forEach((form) => {
form.dataset.bound = '';
const input = form.querySelector<HTMLInputElement>('input[type="email"]')!;
const msg = form.querySelector<HTMLElement>('[data-msg]')!;
form.addEventListener('submit', (e) => {
const local = form.getAttribute('action') === '#';
if (!input.checkValidity()) {
e.preventDefault();
input.setAttribute('aria-invalid', 'true');
msg.dataset.state = 'error';
msg.textContent = 'Please enter a valid email address.';
input.focus();
return;
}
input.removeAttribute('aria-invalid');
if (local) {
e.preventDefault();
msg.dataset.state = 'ok';
msg.textContent = form.dataset.success || 'Subscribed.';
form.reset();
}
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className="sne-cq" data-rk-b34b0e="">
<section className={cx(['sne', `sne--${variant}`, `sne--${size}`, className])} aria-labelledby={`${id}-title`} data-rk-b34b0e="">
<h2 className="sne__title" id={`${id}-title`} data-rk-b34b0e="">{parts.map((p, i) => (i % 2 ? <em data-rk-b34b0e="">{p}</em> : p))}</h2>
<div className="sne__side" data-rk-b34b0e="">
{text && <p className="sne__text" data-rk-b34b0e="">{text}</p>}
<form className="sne__form" action={action} method="post" noValidate data-news data-success={successText} data-rk-b34b0e="">
<label className="sne__label" htmlFor={`${id}-email`} data-rk-b34b0e="">Email address</label>
<div className="sne__field" data-rk-b34b0e="">
<input id={`${id}-email`} name="email" type="email" required autoComplete="email" placeholder="you@example.com" aria-describedby={`${id}-msg`} data-rk-b34b0e="" />
<button type="submit" data-rk-b34b0e="">{buttonLabel} <span aria-hidden="true" data-rk-b34b0e="">→</span></button>
</div>
<p className="sne__msg" id={`${id}-msg`} aria-live="polite" data-msg data-rk-b34b0e=""></p>
{consent && <p className="sne__consent" data-rk-b34b0e="">{consent}</p>}
</form>
</div>
</section>
</div>
</>
);
}
export default ShopNewsletterEditorial;ShopNewsletterEditorial.css
/* ShopNewsletterEditorial — ported from astro-component-kit (sections/ShopNewsletterEditorial.astro) */
.sne-cq:where([data-rk-b34b0e]) { container-type: inline-size; }
.sne:where([data-rk-b34b0e]) {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
gap: clamp(1.5rem, 5vw, 5rem);
align-items: end;
padding: clamp(2.5rem, 6vw, 5rem) clamp(1rem, 3vw, 2.5rem);
color: var(--c-text-1, #fff);
}
.sne--inverted:where([data-rk-b34b0e]) { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
.sne--accent:where([data-rk-b34b0e]) { background: var(--shop-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
.sne__title:where([data-rk-b34b0e]) { margin: 0; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: clamp(2.6rem, 7vw, 6.5rem); font-weight: 400; letter-spacing: -0.045em; line-height: 0.92; text-wrap: balance; }
.sne--sm:where([data-rk-b34b0e]) .sne__title:where([data-rk-b34b0e]) { font-size: clamp(2rem, 5vw, 4rem); }
.sne--lg:where([data-rk-b34b0e]) .sne__title:where([data-rk-b34b0e]) { font-size: clamp(3rem, 9vw, 8.5rem); }
.sne__title:where([data-rk-b34b0e]) em:where([data-rk-b34b0e]) { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.sne--accent:where([data-rk-b34b0e]) .sne__title:where([data-rk-b34b0e]) em:where([data-rk-b34b0e]) { color: inherit; text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 0.12em; }
.sne__text:where([data-rk-b34b0e]) { margin: 0 0 var(--sp-6, 1.5rem); max-width: 26rem; line-height: 1.65; opacity: 0.8; }
.sne__label:where([data-rk-b34b0e]) { display: block; margin-bottom: var(--sp-2, 0.5rem); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.75; }
.sne__field:where([data-rk-b34b0e]) { display: flex; align-items: stretch; border-bottom: 1.5px solid currentColor; }
.sne__field:where([data-rk-b34b0e]) input:where([data-rk-b34b0e]) { flex: 1; min-width: 0; min-height: 56px; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; font-size: 1.15rem; outline: none; }
.sne__field:where([data-rk-b34b0e]) input:where([data-rk-b34b0e])::placeholder { color: currentColor; opacity: 0.45; }
.sne__field:where([data-rk-b34b0e]) input[aria-invalid='true']:where([data-rk-b34b0e]) { color: var(--c-danger, #f87171); }
.sne__field:where([data-rk-b34b0e]) button:where([data-rk-b34b0e]) { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0 0 0 var(--sp-4, 1rem); border: 0; background: none; color: inherit; font: inherit; font-size: 0.85rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer; }
.sne__field:where([data-rk-b34b0e]) button:hover:where([data-rk-b34b0e]) { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.sne--accent:where([data-rk-b34b0e]) .sne__field:where([data-rk-b34b0e]) button:hover:where([data-rk-b34b0e]) { color: inherit; text-decoration: underline; }
.sne__field:focus-within:where([data-rk-b34b0e]) { border-bottom-width: 2.5px; }
.sne__field:where([data-rk-b34b0e]) button:focus-visible:where([data-rk-b34b0e]) { outline: 2px solid currentColor; outline-offset: 2px; }
.sne__msg:where([data-rk-b34b0e]) { min-height: 1.4em; margin: var(--sp-2, 0.5rem) 0 0; font-size: 0.85rem; }
.sne__msg[data-state='error']:where([data-rk-b34b0e]) { color: var(--c-danger, #f87171); }
.sne__msg[data-state='ok']:where([data-rk-b34b0e]) { color: var(--c-success, #34d399); }
.sne--accent:where([data-rk-b34b0e]) .sne__msg:where([data-rk-b34b0e]), .sne--inverted:where([data-rk-b34b0e]) .sne__msg:where([data-rk-b34b0e]) { color: inherit; font-weight: 600; }
.sne__consent:where([data-rk-b34b0e]) { margin: var(--sp-2, 0.5rem) 0 0; font-size: 0.75rem; opacity: 0.6; }
@container (max-width: 860px) { .sne:where([data-rk-b34b0e]) { grid-template-columns: 1fr; } }