Add dedicated subscribe page and seed all JSON defaults on startup

This commit is contained in:
nmemmert
2026-04-13 12:06:48 -04:00
parent 0e9d112c44
commit 36594d5e16
3 changed files with 142 additions and 7 deletions
+16 -7
View File
@@ -1,17 +1,26 @@
#!/bin/sh
set -e
DATA_FILE=/app/data/admin-content.json
SEED_FILE=/app/data-seed/admin-content.json
DATA_DIR=/app/data
SEED_DIR=/app/data-seed
# Ensure the data directory exists (in case the volume was not mounted)
mkdir -p /app/data
mkdir -p "$DATA_DIR"
# Only seed from image defaults when no live data file exists yet.
# Seed each default JSON file only when missing.
# This runs on first boot or a fresh volume, but never overwrites existing data.
if [ ! -f "$DATA_FILE" ] && [ -f "$SEED_FILE" ]; then
echo "[siteforge] No admin-content.json found. Seeding from image defaults..."
cp "$SEED_FILE" "$DATA_FILE"
if [ -d "$SEED_DIR" ]; then
for seed_file in "$SEED_DIR"/*.json; do
[ -f "$seed_file" ] || continue
base_name=$(basename "$seed_file")
target_file="$DATA_DIR/$base_name"
if [ ! -f "$target_file" ]; then
echo "[siteforge] No $base_name found. Seeding from image defaults..."
cp "$seed_file" "$target_file"
fi
done
fi
exec node server.js
+14
View File
@@ -774,6 +774,20 @@
padding: 0;
}
.subscribe-card {
max-width: 560px;
}
.subscribe-form {
margin-top: 1rem;
text-align: left;
}
.subscribe-form .btn-secondary {
width: 100%;
justify-content: center;
}
.thanks-countdown {
color: #d4b87a !important;
letter-spacing: 0.06em;
+112
View File
@@ -1127,6 +1127,116 @@ function ThankYouPage() {
)
}
function SubscribePage() {
const navigate = useNavigate()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [honey, setHoney] = useState('')
const [status, setStatus] = useState<'idle' | 'submitting' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setStatus('submitting')
setErrorMsg('')
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
email,
messageType: 'general',
message: 'Newsletter signup from subscribe page',
subscribe: true,
_honey: honey,
}),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
setErrorMsg((data as { message?: string }).message ?? 'Something went wrong. Please try again.')
setStatus('error')
return
}
navigate('/subscribe/thanks')
} catch {
setErrorMsg('Could not connect. Please try again later.')
setStatus('error')
}
}
return (
<main className="thanks-page" aria-label="Subscribe">
<div className="thanks-card subscribe-card">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>Subscribe for Updates</h1>
<p>
Get ministry updates and new episode announcements by email.
</p>
<form className="contact-form subscribe-form" onSubmit={handleSubmit} noValidate>
<input
type="text"
className="contact-honeypot"
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
value={honey}
onChange={e => setHoney(e.target.value)}
/>
<label>
Name
<input
type="text"
name="name"
required
autoComplete="name"
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<label>
Email
<input
type="email"
name="email"
required
autoComplete="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</label>
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
</button>
<Link to="/" className="btn-secondary">Back to Site</Link>
</form>
</div>
</main>
)
}
function SubscribeThankYouPage() {
return (
<main className="thanks-page" aria-label="Subscription confirmed">
<div className="thanks-card">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>You are subscribed</h1>
<p>
Thank you for subscribing. You will receive future ministry updates and episode announcements.
</p>
<Link to="/" className="btn-primary">Back to Site</Link>
</div>
</main>
)
}
function LegalPage({ title, body }: { title: string; body: string[] }) {
return (
<main className="thanks-page" aria-label={title}>
@@ -1252,6 +1362,8 @@ export default function App() {
<Routes>
<Route path="/" element={<LandingPage content={content} />} />
<Route path="/thanks" element={<ThankYouPage />} />
<Route path="/subscribe" element={<SubscribePage />} />
<Route path="/subscribe/thanks" element={<SubscribeThankYouPage />} />
<Route path="/admin" element={<AdminShell content={content} onSave={setContent} />} />
<Route
path="/privacy"