Add /study/signup page with auth form, benefits, and start-here style link

This commit is contained in:
nmemmert
2026-05-12 13:28:32 -04:00
parent 29ef1abb74
commit e60c1fb8f9
3 changed files with 211 additions and 3 deletions
+79
View File
@@ -1891,6 +1891,85 @@
padding: 0;
}
/* ── Study Signup Page ── */
.study-signup-page-card {
max-width: 680px;
}
.study-signup-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1rem;
}
.study-signup-form label {
font-family: var(--brand-font-body);
font-size: 0.82rem;
font-weight: 600;
color: var(--brand-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-top: 0.25rem;
}
.study-signup-form input {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(201, 168, 76, 0.25);
border-radius: 8px;
padding: 0.7rem 0.9rem;
font-family: var(--brand-font-body);
font-size: 1rem;
color: var(--brand-warm-white);
outline: none;
transition: border-color 180ms;
}
.study-signup-form input:focus {
border-color: rgba(201, 168, 76, 0.6);
}
.study-signup-error {
font-size: 0.9rem !important;
color: #e57373 !important;
margin: 0.25rem 0 0 !important;
}
.study-signup-success {
font-size: 1.05rem !important;
color: #81c784 !important;
margin: 0.5rem 0 1rem !important;
}
.study-signup-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-top: 0.75rem;
}
.study-signup-switch {
font-size: 0.88rem !important;
color: var(--brand-muted) !important;
margin: 0.5rem 0 0 !important;
}
.study-signup-toggle {
background: none;
border: none;
cursor: pointer;
padding: 0;
font-family: var(--brand-font-body);
font-size: 0.88rem;
color: rgba(201, 168, 76, 0.85);
text-decoration: underline;
text-underline-offset: 2px;
}
.study-signup-toggle:hover {
color: var(--brand-gold);
}
.subscribe-card {
max-width: 560px;
}
+2 -1
View File
@@ -3,7 +3,7 @@ import { Link, NavLink, Routes, Route, useLocation, useNavigate, useParams } fro
import AdminPage from './AdminPage'
import QASection from './components/QASection'
import ContactForm from './components/ContactForm'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage } from './colossiansStudy'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage, StudySignupPage } from './colossiansStudy'
import { SpotifyIcon } from './icons'
import type { SiteContent, ArchivedSeries, StudyProgram } from './content'
import { DEFAULTS } from './content'
@@ -1931,6 +1931,7 @@ export default function App() {
<Route path="/" element={<LandingPage content={content} />} />
<Route path="/start-here" element={<StartHerePage content={content} />} />
<Route path="/study" element={<StudyLandingPage content={content} />} />
<Route path="/study/signup" element={<StudySignupPage />} />
<Route path="/study/:studySlug" element={<ColossiansStudyIndexPage content={content} />} />
<Route path="/study/:studySlug/notes" element={<ColossiansStudyNotesPage content={content} />} />
<Route path="/study/:studySlug/:sectionId" element={<ColossiansStudySectionPage content={content} />} />
+130 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { useEffect, useMemo, useState, useCallback } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import type { SiteContent, StudyProgram, StudySection } from './content'
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './colossiansStudyData'
@@ -119,6 +119,7 @@ export function StudyLandingPage({ content }: Props) {
<div className="study-course-hero-actions">
{firstActiveStudy && <Link to={`/study/${firstActiveStudy.slug}`} className="btn-primary">Start Learning</Link>}
<Link to="#study-tracks" className="btn-secondary">Browse Tracks</Link>
<Link to="/study/signup" className="btn-start-here">New here? Create a free account </Link>
</div>
</div>
</section>
@@ -238,6 +239,133 @@ export function StudyLandingPage({ content }: Props) {
)
}
export function StudySignupPage() {
const navigate = useNavigate()
const [mode, setMode] = useState<'signup' | 'login'>('signup')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [done, setDone] = useState(false)
const [loggedInAs, setLoggedInAs] = useState('')
useEffect(() => {
readJson<{ authenticated: boolean; username: string }>('/api/study-auth/status')
.then(data => {
if (data.authenticated) {
setDone(true)
setLoggedInAs(data.username ?? '')
}
})
.catch(() => {})
}, [])
const submit = useCallback(async () => {
if (!username.trim() || !password.trim()) {
setMessage('Please enter a username and password.')
return
}
setBusy(true)
setMessage('')
try {
const data = await readJson<{ username: string }>(`/api/study-auth/${mode}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
setDone(true)
setLoggedInAs(data.username ?? username.trim().toLowerCase())
} catch (err) {
setMessage(err instanceof Error ? err.message : 'Something went wrong. Please try again.')
} finally {
setBusy(false)
}
}, [mode, username, password])
return (
<main className="thanks-page" aria-label={mode === 'signup' ? 'Create account' : 'Sign in'}>
<div className="thanks-card study-signup-page-card">
<p className="eyebrow">Free Student Account</p>
<h1>{mode === 'signup' ? 'Create Your Account' : 'Welcome Back'}</h1>
{done ? (
<>
<p className="study-signup-success">You're signed in as <strong>{loggedInAs}</strong>. Your notes are ready on every lesson.</p>
<div className="study-signup-actions">
<button type="button" className="btn-primary" onClick={() => navigate('/study')}>Go to Study Hub</button>
<Link to="/study" className="btn-secondary">Browse Tracks</Link>
</div>
</>
) : (
<>
<div className="study-signup-form">
<label htmlFor="signup-username">Username</label>
<input
id="signup-username"
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="yourname"
autoComplete={mode === 'signup' ? 'username' : 'username'}
autoFocus
/>
<label htmlFor="signup-password">Password</label>
<input
id="signup-password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="At least 8 characters"
autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}
onKeyDown={e => e.key === 'Enter' && submit()}
/>
{message && <p className="study-signup-error">{message}</p>}
<div className="study-signup-actions">
<button type="button" className="btn-primary" disabled={busy} onClick={submit}>
{busy ? (mode === 'signup' ? 'Creating...' : 'Signing in...') : (mode === 'signup' ? 'Create Account' : 'Sign In')}
</button>
</div>
<p className="study-signup-switch">
{mode === 'signup'
? <><span>Already have an account? </span><button type="button" className="study-signup-toggle" onClick={() => { setMode('login'); setMessage('') }}>Sign in instead</button></>
: <><span>New here? </span><button type="button" className="study-signup-toggle" onClick={() => { setMode('signup'); setMessage('') }}>Create a free account</button></>
}
</p>
</div>
<div className="study-signup-why-grid" style={{ marginTop: '2rem' }}>
<article className="study-signup-why-card">
<p className="study-signup-why-icon" aria-hidden="true">📝</p>
<h3>Notes on Every Lesson</h3>
<p>Save your own notes per lesson — up to 500 notes across all tracks.</p>
</article>
<article className="study-signup-why-card">
<p className="study-signup-why-icon" aria-hidden="true">📖</p>
<h3>Space to Go Deep</h3>
<p>Each note holds ~2,000 words so you can write as much as you need.</p>
</article>
<article className="study-signup-why-card">
<p className="study-signup-why-icon" aria-hidden="true">🔒</p>
<h3>Private to You</h3>
<p>Your notes are tied to your account. Nobody else can read them.</p>
</article>
<article className="study-signup-why-card">
<p className="study-signup-why-icon" aria-hidden="true">⚡</p>
<h3>No Email Required</h3>
<p>Just a username and password. Free forever. Stay signed in 30 days.</p>
</article>
</div>
</>
)}
<div style={{ marginTop: '1.5rem' }}>
<Link to="/study" className="btn-secondary">Back to Studies</Link>
</div>
</div>
</main>
)
}
export function ColossiansStudyIndexPage({ content }: Props) {
const { studySlug } = useParams<{ studySlug?: string }>()
const studies = getStudies(content)