Files
website/client/src/routes/public/Newsletter.jsx
2026-06-26 21:51:27 -05:00

137 lines
5.1 KiB
JavaScript

import { useState } from 'react'
import { Link } from 'react-router-dom'
import PublicLayout from '../../components/PublicLayout.jsx'
import PageHeader from '../../components/PageHeader.jsx'
import { Loading, ErrorState, EmptyState } from '../../components/PageState.jsx'
import { useAsync } from '../../lib/useAsync.js'
import { monthTile } from '../../lib/format.js'
import { api } from '../../api/client.js'
import { useSite } from '../../contexts/SiteContext.jsx'
export default function Newsletter() {
const { loading, error, data } = useAsync(() => api.posts('newsletter'))
const issues = data || []
return (
<PublicLayout section="website">
<div className="shell-mid page-body">
<PageHeader
eyebrow="Long-form"
title="Monthly Newsletter"
lead="A fuller monthly summary for players who want the whole picture."
/>
<SubscribeBox />
<section style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{loading && <Loading />}
{error && <ErrorState message="Could not load newsletter issues right now." />}
{!loading && !error && issues.length === 0 && <EmptyState>No issues published yet.</EmptyState>}
{issues.map((i) => {
const tile = monthTile(i.published_at || i.created_at)
return (
<Link
key={i.id}
to={`/site/newsletter/${i.slug || i.id}`}
className="panel"
style={{ display: 'flex', gap: 20, alignItems: 'center', padding: '22px 24px', textDecoration: 'none' }}
>
<div
className="display"
style={{
flex: 'none',
width: 64,
height: 64,
borderRadius: 8,
border: '1px solid var(--line)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(11,22,48,0.5)',
}}
>
<span style={{ color: 'var(--accent)', fontSize: '0.66rem', letterSpacing: '0.1em' }}>{tile.mon}</span>
<span style={{ color: 'var(--head)', fontSize: '1.4rem', lineHeight: 1 }}>{tile.num}</span>
</div>
<div style={{ flex: 1 }}>
<h3 className="display" style={{ margin: '0 0 4px', fontSize: '1.2rem', color: 'var(--head)' }}>
{i.title}
</h3>
<p className="muted" style={{ margin: 0, fontSize: '0.98rem' }}>
{i.excerpt || ''}
</p>
</div>
<span className="sans" style={{ flex: 'none', color: 'var(--accent)', fontSize: '0.84rem' }}>
Read
</span>
</Link>
)
})}
</section>
</div>
</PublicLayout>
)
}
function SubscribeBox() {
const { contactEmail } = useSite()
const [email, setEmail] = useState('')
const [status, setStatus] = useState(null) // null | 'sending' | 'done' | 'mailto' | 'error'
async function onSubmit(e) {
e.preventDefault()
if (!email) return
setStatus('sending')
try {
const res = await api.contact({ email, message: `Newsletter subscription request from ${email}` })
setStatus(res && res.fallback === 'mailto' ? 'mailto' : 'done')
} catch {
setStatus('error')
}
}
return (
<section
style={{
display: 'flex',
alignItems: 'center',
gap: 14,
flexWrap: 'wrap',
padding: '22px 24px',
border: '1px solid var(--line)',
borderRadius: 10,
background: 'rgba(19,36,60,0.4)',
marginBottom: 34,
}}
>
<span style={{ color: '#dbe2ea', fontSize: '1.02rem', flex: 1, minWidth: 220 }}>
Get each issue in your inbox the day it ships.
</span>
{status === 'done' && <span className="muted sans" style={{ fontSize: '0.9rem' }}>Thanks we'll be in touch.</span>}
{status === 'mailto' && (
<a className="link-accent sans" href={`mailto:${contactEmail}?subject=Newsletter%20subscribe`}>
Email us to subscribe →
</a>
)}
{status !== 'done' && status !== 'mailto' && (
<form onSubmit={onSubmit} style={{ display: 'flex', gap: 10, flex: 'none', flexWrap: 'wrap' }}>
<input
type="email"
required
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input"
style={{ borderRadius: 999, minWidth: 200, width: 'auto' }}
/>
<button type="submit" className="btn btn-primary btn-sq" style={{ borderRadius: 999 }} disabled={status === 'sending'}>
{status === 'sending' ? 'Sending' : 'Subscribe'}
</button>
</form>
)}
{status === 'error' && <span className="sans" style={{ color: '#d98b84', fontSize: '0.85rem' }}>Something went wrong.</span>}
</section>
)
}