Add /study/signup page with auth form, benefits, and start-here style link
This commit is contained in:
+79
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user