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;
|
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 {
|
.subscribe-card {
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,7 @@ import { Link, NavLink, Routes, Route, useLocation, useNavigate, useParams } fro
|
|||||||
import AdminPage from './AdminPage'
|
import AdminPage from './AdminPage'
|
||||||
import QASection from './components/QASection'
|
import QASection from './components/QASection'
|
||||||
import ContactForm from './components/ContactForm'
|
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 { SpotifyIcon } from './icons'
|
||||||
import type { SiteContent, ArchivedSeries, StudyProgram } from './content'
|
import type { SiteContent, ArchivedSeries, StudyProgram } from './content'
|
||||||
import { DEFAULTS } from './content'
|
import { DEFAULTS } from './content'
|
||||||
@@ -1931,6 +1931,7 @@ export default function App() {
|
|||||||
<Route path="/" element={<LandingPage content={content} />} />
|
<Route path="/" element={<LandingPage content={content} />} />
|
||||||
<Route path="/start-here" element={<StartHerePage content={content} />} />
|
<Route path="/start-here" element={<StartHerePage content={content} />} />
|
||||||
<Route path="/study" element={<StudyLandingPage 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" element={<ColossiansStudyIndexPage content={content} />} />
|
||||||
<Route path="/study/:studySlug/notes" element={<ColossiansStudyNotesPage content={content} />} />
|
<Route path="/study/:studySlug/notes" element={<ColossiansStudyNotesPage content={content} />} />
|
||||||
<Route path="/study/:studySlug/:sectionId" element={<ColossiansStudySectionPage content={content} />} />
|
<Route path="/study/:studySlug/:sectionId" element={<ColossiansStudySectionPage content={content} />} />
|
||||||
|
|||||||
+130
-2
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState, useCallback } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
import type { SiteContent, StudyProgram, StudySection } from './content'
|
import type { SiteContent, StudyProgram, StudySection } from './content'
|
||||||
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './colossiansStudyData'
|
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './colossiansStudyData'
|
||||||
|
|
||||||
@@ -119,6 +119,7 @@ export function StudyLandingPage({ content }: Props) {
|
|||||||
<div className="study-course-hero-actions">
|
<div className="study-course-hero-actions">
|
||||||
{firstActiveStudy && <Link to={`/study/${firstActiveStudy.slug}`} className="btn-primary">Start Learning</Link>}
|
{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-tracks" className="btn-secondary">Browse Tracks</Link>
|
||||||
|
<Link to="/study/signup" className="btn-start-here">New here? Create a free account →</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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) {
|
export function ColossiansStudyIndexPage({ content }: Props) {
|
||||||
const { studySlug } = useParams<{ studySlug?: string }>()
|
const { studySlug } = useParams<{ studySlug?: string }>()
|
||||||
const studies = getStudies(content)
|
const studies = getStudies(content)
|
||||||
|
|||||||
Reference in New Issue
Block a user