Add study enrollment manager, profile settings, and email verification flow

This commit is contained in:
nmemmert
2026-05-21 11:46:49 -04:00
parent 67f3caf40b
commit 376088a52b
8 changed files with 1702 additions and 135 deletions
+23 -1
View File
@@ -69,6 +69,12 @@ export interface AdminStats {
totalSubmissions: number
totalQuestions: number
}
studyEnrollment: {
totalUsers: number
enrolledUsers: number
totalEnrollments: number
enrollmentsByStudy: Array<{ slug: string; title: string; count: number }>
}
}
interface AdminAsset {
@@ -2214,6 +2220,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
.slice(0, 5)
const approvedCount = questions.filter(q => q.isApproved).length
const answeredCount = questions.filter(q => !!q.answer?.trim()).length
const topEnrollment = stats?.studyEnrollment?.enrollmentsByStudy?.[0]
return (
<section className="admin-panel-section">
<div className="admin-panel-head">
@@ -2257,6 +2264,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<div className="admin-dashboard-card-sub">{contactSubmissions.length} total contact submissions</div>
<button type="button" className="admin-dashboard-card-action" onClick={() => navigateTo('subscribers')}>View List </button>
</div>
<div className="admin-dashboard-card">
<div className="admin-dashboard-card-value">{stats?.studyEnrollment?.totalEnrollments ?? 0}</div>
<div className="admin-dashboard-card-label">Study Enrollments</div>
<div className="admin-dashboard-card-sub">
{stats?.studyEnrollment?.enrolledUsers ?? 0} of {stats?.studyEnrollment?.totalUsers ?? 0} students enrolled
{topEnrollment ? ` · Top: ${topEnrollment.title} (${topEnrollment.count})` : ''}
</div>
</div>
<div className="admin-dashboard-card">
<div className="admin-dashboard-card-value">{downloadStats['titus-study'] ?? 0}</div>
<div className="admin-dashboard-card-label">Titus Study Downloads</div>
@@ -3422,7 +3437,10 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<strong>Lesson {section.reference || section.id}</strong>
<p>{section.title || 'Untitled section'}</p>
</div>
<span className="admin-collapsible-hint">Expand to edit lesson</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<a href={`/study/${study.slug}/${section.id}`} target="_blank" rel="noopener noreferrer" className="btn-admin-secondary" onClick={e => e.stopPropagation()}>Preview</a>
<span className="admin-collapsible-hint">Expand to edit lesson</span>
</div>
</summary>
<div className="admin-collapsible-body">
<div className="admin-array-row">
@@ -3475,6 +3493,10 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<label htmlFor={`study-section-greek-${study.id}-${section.id}`}>Greek Words</label>
<textarea id={`study-section-greek-${study.id}-${section.id}`} rows={4} value={(section.greekNotes ?? []).join('\n')} placeholder="One note per line" onChange={e => updateStudySection(study.id, section.id, 'greekNotes', e.target.value.split('\n').map(line => line.trim()).filter(Boolean))} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-focus-${study.id}-${section.id}`}>Focus Question</label>
<input id={`study-section-focus-${study.id}-${section.id}`} type="text" value={section.focusQuestion ?? ''} placeholder="Shown on the lesson card (defaults to first study question)" onChange={e => updateStudySection(study.id, section.id, 'focusQuestion', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-questions-${study.id}-${section.id}`}>Study Questions</label>
<textarea id={`study-section-questions-${study.id}-${section.id}`} rows={5} value={(section.studyQuestions ?? []).join('\n')} placeholder="One question per line" onChange={e => updateStudySection(study.id, section.id, 'studyQuestions', e.target.value.split('\n').map(line => line.trim()).filter(Boolean))} />
+20 -6
View File
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react'
import type { ReactElement } from 'react'
import { Link, NavLink, Routes, Route, useLocation, useNavigate, useParams } from 'react-router-dom'
import AdminPage from './AdminPage'
import QASection from './components/QASection'
import ContactForm from './components/ContactForm'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage, StudySignupPage } from './colossiansStudy'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage, StudySignupPage, StudyAccountPage } from './colossiansStudy'
import { SpotifyIcon } from './icons'
import type { SiteContent, ArchivedSeries, StudyProgram } from './content'
import { DEFAULTS } from './content'
@@ -467,6 +468,7 @@ function SiteHeader({ content }: { content: SiteContent }) {
<NavLink to="/episodes" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Episodes</NavLink>
<NavLink to="/questions" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Q&amp;A</NavLink>
<NavLink to="/study" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Studies</NavLink>
<NavLink to="/study/account" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>My Account</NavLink>
<NavLink to="/resources" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Downloads</NavLink>
<NavLink to="/about" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>About</NavLink>
<NavLink to="/contact" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Contact</NavLink>
@@ -1930,6 +1932,16 @@ function PreviewPage() {
return <LandingPage content={content} />
}
function StudyRouteFrame({ content, child }: { content: SiteContent; child: ReactElement }) {
return (
<>
<SiteHeader content={content} />
{child}
<SiteFooter content={content} />
</>
)
}
export default function App() {
const [content, setContent] = useState<SiteContent>(DEFAULTS)
const navigate = useNavigate()
@@ -2004,11 +2016,13 @@ export default function App() {
<Routes>
<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} />} />
<Route path="/study" element={<StudyRouteFrame content={content} child={<StudyLandingPage content={content} />} />} />
<Route path="/studys" element={<StudyRouteFrame content={content} child={<StudyLandingPage content={content} />} />} />
<Route path="/study/signup" element={<StudyRouteFrame content={content} child={<StudySignupPage />} />} />
<Route path="/study/account" element={<StudyRouteFrame content={content} child={<StudyAccountPage />} />} />
<Route path="/study/:studySlug" element={<StudyRouteFrame content={content} child={<ColossiansStudyIndexPage content={content} />} />} />
<Route path="/study/:studySlug/notes" element={<StudyRouteFrame content={content} child={<ColossiansStudyNotesPage content={content} />} />} />
<Route path="/study/:studySlug/:sectionId" element={<StudyRouteFrame content={content} child={<ColossiansStudySectionPage content={content} />} />} />
<Route path="/episodes" element={<EpisodesPage content={content} />} />
<Route path="/episodes/:id" element={<EpisodeDetailPage content={content} />} />
<Route path="/resources" element={<ResourcesPage content={content} />} />
+743 -45
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -9,6 +9,7 @@ export type ColossiansStudySection = {
commentary: string
greekNotes: string[]
studyQuestions: string[]
focusQuestion?: string
releasedAt?: string
}