Add study enrollment manager, profile settings, and email verification flow
This commit is contained in:
+23
-1
@@ -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
@@ -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&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
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ export type ColossiansStudySection = {
|
||||
commentary: string
|
||||
greekNotes: string[]
|
||||
studyQuestions: string[]
|
||||
focusQuestion?: string
|
||||
releasedAt?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user