diff --git a/src/App.css b/src/App.css index 29179d0..4ba061e 100644 --- a/src/App.css +++ b/src/App.css @@ -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; } diff --git a/src/App.tsx b/src/App.tsx index 3f42155..bb93bbd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/colossiansStudy.tsx b/src/colossiansStudy.tsx index d9322f3..4d3a861 100644 --- a/src/colossiansStudy.tsx +++ b/src/colossiansStudy.tsx @@ -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) {
{firstActiveStudy && Start Learning} Browse Tracks + New here? Create a free account →
@@ -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 ( +
+
+

Free Student Account

+

{mode === 'signup' ? 'Create Your Account' : 'Welcome Back'}

+ + {done ? ( + <> +

You're signed in as {loggedInAs}. Your notes are ready on every lesson.

+
+ + Browse Tracks +
+ + ) : ( + <> +
+ + setUsername(e.target.value)} + placeholder="yourname" + autoComplete={mode === 'signup' ? 'username' : 'username'} + autoFocus + /> + + setPassword(e.target.value)} + placeholder="At least 8 characters" + autoComplete={mode === 'signup' ? 'new-password' : 'current-password'} + onKeyDown={e => e.key === 'Enter' && submit()} + /> + {message &&

{message}

} +
+ +
+

+ {mode === 'signup' + ? <>Already have an account? + : <>New here? + } +

+
+ +
+
+ +

Notes on Every Lesson

+

Save your own notes per lesson — up to 500 notes across all tracks.

+
+
+ +

Space to Go Deep

+

Each note holds ~2,000 words so you can write as much as you need.

+
+
+ +

Private to You

+

Your notes are tied to your account. Nobody else can read them.

+
+
+ +

No Email Required

+

Just a username and password. Free forever. Stay signed in 30 days.

+
+
+ + )} + +
+ Back to Studies +
+
+
+ ) +} + export function ColossiansStudyIndexPage({ content }: Props) { const { studySlug } = useParams<{ studySlug?: string }>() const studies = getStudies(content)