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))} />