diff --git a/package-lock.json b/package-lock.json index fda67d5..3a08d42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "2.10.8", + "version": "2.10.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "2.10.8", + "version": "2.10.9", "dependencies": { "@aws-sdk/client-s3": "^3.1135.0", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 9954c68..36f3582 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.10.8", + "version": "2.10.9", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.css b/src/App.css index b88ebaa..10ea056 100644 --- a/src/App.css +++ b/src/App.css @@ -4156,6 +4156,338 @@ border-left: 3px solid rgba(111, 207, 151, 0.7); } +/* ── My Account ── */ +.account-page { + padding: clamp(1.5rem, 5vw, 3rem) 1.25rem 4rem; + background: + radial-gradient(ellipse at top, rgba(201, 168, 76, 0.1) 0%, transparent 55%), + var(--brand-black); +} + +.account-shell { + max-width: 1040px; + margin: 0 auto; +} + +.account-header { + display: flex; + align-items: center; + gap: 1.25rem; + flex-wrap: wrap; + padding: 1.5rem; + margin-bottom: 1.25rem; + border: 1px solid rgba(201, 168, 76, 0.24); + border-radius: 18px; + background: linear-gradient(180deg, rgba(26, 26, 22, 0.95), rgba(16, 16, 14, 0.95)); +} + +.account-avatar { + width: 84px; + height: 84px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(201, 168, 76, 0.5); + flex-shrink: 0; +} + +.account-avatar--initial { + display: flex; + align-items: center; + justify-content: center; + background: rgba(201, 168, 76, 0.14); + color: var(--brand-gold); + font-family: var(--brand-font-heading); + font-size: 2.2rem; + font-weight: 700; +} + +.account-header-body { flex: 1; min-width: 200px; } + +.account-header h1 { + margin: 0.15rem 0 0.25rem; + font-family: var(--brand-font-heading); + font-size: clamp(1.7rem, 4vw, 2.3rem); + color: var(--brand-warm-white); + overflow-wrap: anywhere; +} + +.account-header .eyebrow { margin: 0; } + +.account-email { + margin: 0; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.6rem; + color: var(--brand-muted); + font-size: 0.95rem; + overflow-wrap: anywhere; +} + +.account-header-actions { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; +} + +.account-chip { + display: inline-block; + padding: 0.15rem 0.6rem; + border-radius: 999px; + border: 1px solid; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.04em; +} + +.account-chip--ok { color: #6fcf97; border-color: rgba(111, 207, 151, 0.45); background: rgba(111, 207, 151, 0.1); } +.account-chip--warn { color: #e0b861; border-color: rgba(224, 184, 97, 0.45); background: rgba(224, 184, 97, 0.1); } + +.account-notice { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1.25rem; + padding: 0.85rem 1.1rem; + border: 1px solid rgba(201, 168, 76, 0.3); + border-radius: 12px; + background: rgba(201, 168, 76, 0.07); + color: #c9bb91; + font-size: 0.9rem; +} + +.account-notice strong { color: var(--brand-gold); } + +.account-notice-actions { display: flex; align-items: center; gap: 0.75rem; } +.account-notice-msg { font-size: 0.82rem; } + +.account-layout { + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); + gap: 1.25rem; + align-items: start; +} + +.account-main, +.account-side { + display: flex; + flex-direction: column; + gap: 1.25rem; + min-width: 0; +} + +.account-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; +} + +.account-stat { + padding: 1rem; + border: 1px solid rgba(201, 168, 76, 0.2); + border-radius: 14px; + background: rgba(17, 17, 15, 0.84); +} + +.account-stat strong { + display: block; + font-family: var(--brand-font-heading); + font-size: 1.7rem; + line-height: 1.1; + color: var(--brand-warm-white); +} + +.account-stat .account-stat-date { font-size: 1.05rem; padding-top: 0.45rem; } + +.account-stat span { + display: block; + margin-top: 0.3rem; + font-size: 0.78rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--brand-muted); +} + +.account-card { + padding: 1.25rem 1.35rem; + border: 1px solid rgba(201, 168, 76, 0.2); + border-radius: 16px; + background: rgba(17, 17, 15, 0.84); +} + +.account-card--danger { border-color: rgba(229, 115, 115, 0.3); } +.account-card--danger h2 { color: #e57373; } + +.account-card-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.9rem; +} + +.account-card-head h2 { + margin: 0; + font-family: var(--brand-font-heading); + font-size: 1.25rem; + color: #e8d9b5; +} + +.account-link { + background: none; + border: none; + padding: 0; + color: var(--brand-gold); + font: inherit; + font-size: 0.85rem; + text-decoration: underline; + cursor: pointer; +} + +.account-muted { + margin: 0 0 0.75rem; + color: var(--brand-muted); + font-size: 0.92rem; + line-height: 1.55; +} + +.account-progress + .account-progress { margin-top: 1rem; } + +.account-progress-row { + display: flex; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 0.4rem; + font-size: 0.9rem; + color: var(--brand-muted); +} + +.account-progress-row a { + color: var(--brand-warm-white); + text-decoration: none; + font-weight: 600; +} + +.account-progress-row a:hover { text-decoration: underline; } + +.account-cert { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.6rem; + padding: 0.6rem 0; + border-top: 1px solid rgba(201, 168, 76, 0.12); + font-size: 0.92rem; +} + +.account-cert:first-of-type { border-top: none; } +.account-cert small { color: var(--brand-muted); } +.account-cert-actions { display: flex; gap: 0.5rem; } +.account-cert-actions .btn-secondary { font-size: 0.78rem; padding: 0.3rem 0.8rem; } + +.account-menu { + list-style: none; + margin: 0; + padding: 0; +} + +.account-menu li + li { border-top: 1px solid rgba(201, 168, 76, 0.12); } + +.account-menu button { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.8rem 0.25rem; + background: none; + border: none; + color: inherit; + text-align: left; + cursor: pointer; + font-family: var(--brand-font-body); +} + +.account-menu button:hover strong { color: var(--brand-gold); } +.account-menu button:disabled { opacity: 0.6; cursor: default; } + +.account-menu strong { + display: block; + color: var(--brand-warm-white); + font-weight: 600; + font-size: 0.98rem; + transition: color 0.15s ease; +} + +.account-menu small { + display: block; + margin-top: 0.1rem; + color: var(--brand-muted); + font-size: 0.82rem; + overflow-wrap: anywhere; +} + +.account-menu button > span:last-child { color: var(--brand-gold); font-size: 1.2rem; } + +.account-callout { + margin: 0.75rem 0 0; + padding: 0.7rem 0.9rem; + border: 1px solid rgba(201, 168, 76, 0.25); + border-radius: 10px; + background: rgba(201, 168, 76, 0.07); + color: #b9aa86; + font-size: 0.85rem; + line-height: 1.5; +} + +.account-callout strong { color: var(--brand-gold); } + +.account-callout--danger { + border-color: rgba(224, 92, 92, 0.3); + background: rgba(224, 92, 92, 0.07); + color: #e07a7a; +} + +.account-callout--danger strong { color: #e07a7a; } + +.account-session { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(201, 168, 76, 0.12); +} + +.account-session .btn-secondary { width: 100%; justify-content: center; font-size: 0.82rem; } + +.account-danger-step { + padding: 0.9rem; + border: 1px solid rgba(229, 115, 115, 0.35); + border-radius: 10px; + background: rgba(43, 17, 17, 0.6); +} + +.account-danger-step p { margin: 0 0 0.75rem; color: #e0cfcf; } + +.account-danger-input { + width: 100%; + margin-bottom: 0.8rem; + padding: 0.55rem 0.7rem; + border: 1px solid rgba(229, 115, 115, 0.5); + border-radius: 8px; + background: #140f0f; + color: var(--brand-warm-white); +} + +.account-danger-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; } + +@media (max-width: 900px) { + .account-layout { grid-template-columns: 1fr; } + .account-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + /* ── Study hub (guest landing) ── */ .study-hub-count { margin: 1.25rem 0 0; diff --git a/src/studyPages.tsx b/src/studyPages.tsx index 32071a4..2a7348c 100644 --- a/src/studyPages.tsx +++ b/src/studyPages.tsx @@ -3193,670 +3193,687 @@ export function StudyAccountPage() { if (!auth.checked) { return ( -
-

Loading...

+
+
+

Loading...

+
) } if (!auth.authenticated) { return ( -
-
-

My Account

-

Sign In Required

-

Please sign in to manage your account.

- Sign In +
+
+
+

My Account

+

Sign In Required

+

Please sign in to manage your account.

+
+ Sign In +
+
) } - const divider =
+ const enrolledProgress = overview?.studies.filter(item => item.enrolled && item.totalLessons > 0) ?? [] + const memberSince = overview?.stats.memberSince && !isNaN(new Date(overview.stats.memberSince).getTime()) + ? new Date(overview.stats.memberSince).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + : '—' + const initial = (auth.displayName?.slice(0, 1) || auth.username.slice(0, 1)).toUpperCase() return ( -
-
-

Student Account

-

My Account

-
+
+
+
{auth.avatarUrl ? ( - Your avatar + Your avatar ) : ( -
- {auth.displayName?.slice(0, 1).toUpperCase() || auth.username.slice(0, 1).toUpperCase()} -
+ )} -
-

Signed in as

-

{auth.displayName || auth.username}

-

{auth.username}

+
+

Student Account

+

{auth.displayName || auth.username.split('@')[0]}

+

+ {auth.username} + {auth.emailVerified === false + ? Unverified + : Verified} +

-
+
+ + +
+
{auth.emailVerified === false && ( -
- Verify your email. We sent a link to {auth.username} — click it to confirm this address. -
- {resendVerifyMessage && {resendVerifyMessage}} -
)} - {overview?.stats && ( - <> -
-
-

{overview.stats.noteCount}

-

Notes saved

-
-
-

{overview.stats.currentStreak ?? 0} 🔥

-

Day streak

-
-
-

{overview.stats.longestStreak ?? 0}

-

Longest streak

-
-
-

{overview.stats.memberSince && !isNaN(new Date(overview.stats.memberSince).getTime()) ? new Date(overview.stats.memberSince).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}

-

Member since

-
-
- {overview.studies.filter(s => s.enrolled && s.totalLessons > 0).length > 0 && ( -
-

Study Progress

- {overview.studies.filter(s => s.enrolled && s.totalLessons > 0).map(s => { - const pct = Math.round((s.completedLessons / s.totalLessons) * 100) - return ( -
-
- {s.title} - {s.completedLessons}/{s.totalLessons} lessons · {pct}% -
-
-
-
-
- ) - })} -
- )} - {overview.stats.noteLimit > 0 && ( -
-
- Note Usage - {overview.stats.noteCount}/{overview.stats.noteLimit} notes · up to {overview.stats.noteCharLimit.toLocaleString()} characters each +
+
+ {loadingOverview && !overview &&

Loading your progress…

} + {overview?.stats && ( + <> +
+
{overview.stats.noteCount}Notes saved
+
{overview.stats.currentStreak ?? 0} 🔥Day streak
+
{overview.stats.longestStreak ?? 0}Longest streak
+
{memberSince}Member since
-
-
-
-
- )} - {overview.certificates.length > 0 && ( -
-

Certificates

- {overview.certificates.map(cert => ( -
- 🏅 {cert.studyTitle} — {new Date(cert.issuedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} -
- View - -
-
- ))} -
- )} - {divider} - - )} -
-

Quick Actions

-

Open the settings you need without scrolling through the whole page.

-
- - - - - - - +
+
+

Study Progress

+ +
+ {enrolledProgress.length === 0 ? ( +

You aren't enrolled in a study yet. Browse the studies →

+ ) : enrolledProgress.map(item => { + const pct = Math.round((item.completedLessons / item.totalLessons) * 100) + return ( +
+
+ {item.title} + {item.completedLessons}/{item.totalLessons} lessons · {pct}% +
+
+
+
+
+ ) + })} +
+ + {overview.certificates.length > 0 && ( +
+

Certificates

+ {overview.certificates.map(cert => ( +
+ 🏅 {cert.studyTitle} — {new Date(cert.issuedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} +
+ View + +
+
+ ))} +
+ )} + + {overview.stats.noteLimit > 0 && ( +
+
+

Note Usage

+ +
+
+ {overview.stats.noteCount} of {overview.stats.noteLimit} notes + up to {overview.stats.noteCharLimit.toLocaleString()} characters each +
+
+
+
+
+ )} + + )}
- {!auth.totpEnabled && ( -
- Recommended: Enable two-factor authentication to protect your study notes and account. -
- )} - {auth.totpEnabled && auth.totpRecoveryCodesRemaining !== undefined && auth.totpRecoveryCodesRemaining <= 2 && ( -
- Warning: You only have {auth.totpRecoveryCodesRemaining} recovery code{auth.totpRecoveryCodesRemaining === 1 ? '' : 's'} left.{' '} - -
- )} -
- {accountModal !== 'none' && ( -
-
- - {accountModal === 'enrollments' && ( +
+ )} + {deleteStep === 'warn2' && ( +
+

Type DELETE to confirm permanent deletion:

+ setDeleteConfirmInput(e.target.value)} placeholder="DELETE" aria-label="Type DELETE to confirm" /> + {deleteMessage &&

{deleteMessage}

} +
+ +
- {emailTokenInput && ( -
-

Have a verification token?

- setEmailTokenInput(e.target.value)} placeholder="Paste token here" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '0.75rem' }} /> - -
- )} - {emailMessage &&

{emailMessage}

} - +
)} - {accountModal === 'profile' && ( - <> -

Profile Settings

-

Update your display name and avatar in one place.

- -

Pick a preset avatar or upload your own image.

-
- {[ - { key: 'dog', label: 'Dog', emoji: '🐶' }, - { key: 'cat', label: 'Cat', emoji: '🐱' }, - { key: 'rabbit', label: 'Rabbit', emoji: '🐰' }, - { key: 'fox', label: 'Fox', emoji: '🦊' }, - { key: 'panda', label: 'Panda', emoji: '🐼' }, - { key: 'lion', label: 'Lion', emoji: '🦁' }, - ].map(item => { - const svg = `${item.emoji}` - const url = `data:image/svg+xml;utf8,${encodeURIComponent(svg)}` - return ( - - ) - })} -
-
- - { - const file = e.target.files?.[0] - if (file) { - void handleAvatarUpload(file) - } - }} - style={{ color: '#f0ead8' }} - /> - {avatarUploadBusy &&

Uploading avatar…

} - {avatarUploadMessage &&

{avatarUploadMessage}

} -
- {(avatarUrlInput || auth.avatarUrl) && ( -
- Avatar preview -

Preview of your selected avatar. Save your profile to keep it.

-
- )} - - setDisplayNameInput(e.target.value)} - placeholder="How your name appears in emails" - style={{ width: '100%', maxWidth: '420px', marginBottom: '0.75rem', padding: '0.5rem', border: '1px solid #3a3320', borderRadius: '4px', background: '#14130f', color: '#f0ead8' }} - /> -
- -
- {profileMessage &&

{profileMessage}

} - - )} - {accountModal === 'preferences' && ( - <> -

Preferences

-

Adjust how you receive study reminders and newsletter updates.

-
- - {studyRemindersEnabled && ( -
-
-

Reminder frequency

-
+ + +
+ + {accountModal !== 'none' && ( +
+
+ + {accountModal === 'enrollments' && ( + <> +

Manage Enrollments

+

Enroll or leave studies from one place. Your active study list is shown below.

+ {loadingOverview ? ( +

Loading study access...

+ ) : overview?.studies?.length ? ( +
+ {overview.studies.map(studyInfo => ( +
+
+
+ +

{studyInfo.title}

+ {!studyInfo.enrolled && studyInfo.status === 'planned' ?

Not open for enrollment yet

:

{studyInfo.completedLessons} of {studyInfo.totalLessons} lessons completed · {studyInfo.noteCount} notes

} +
+ {!studyInfo.enrolled && studyInfo.status === 'planned' ? ( + Coming soon + ) : ( - -
-

- {reminderFrequency === 'weekly' - ? 'Receive one weekly email summarizing all new lessons.' - : 'Receive an email each time a new lesson is released.'} -

-
-
-

- Pause reminders - {remindersPausedUntil && new Date(remindersPausedUntil) > new Date() - ? ` · paused until ${new Date(remindersPausedUntil).toLocaleDateString()}` - : ''} -

-
- {[7, 14, 30].map(days => ( - - ))} - {remindersPausedUntil && new Date(remindersPausedUntil) > new Date() && ( - - )} -
+ )}
- )} - -
- - {prefMessage &&

{prefMessage}

} - - )} - {accountModal === 'export' && ( - <> -

Export Notes

-

Download all saved notes in a single document.

- - {exportMessage &&

{exportMessage}

} - - )} - {accountModal === 'choose2fa' && ( - <> -

Choose Your 2FA Method

-

- Pick how you'd like to verify your identity each time you sign in. -

-
- - -
- {twoFaMessage &&

{twoFaMessage}

} - - )} - - {accountModal === 'setup2fa-email' && ( - <> -

Verify Your Email for 2FA

-

- We sent a 6-digit code to your email. Enter it below to activate email-based two-factor authentication. -

- - setTwoFaCode(e.target.value.replace(/\D/g, '').slice(0, 6))} - placeholder="000000" - autoFocus - style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem', letterSpacing: '0.3em', fontSize: '1.3rem', textAlign: 'center' }} - /> - {twoFaMessage &&

{twoFaMessage}

} -
- - -
- - )} - - {accountModal === 'setup2fa' && ( - <> -

Enable Two-Factor Authentication

-

- Scan the QR code below with your authenticator app (Google Authenticator, Authy, 1Password, etc.), then enter the 6-digit code to confirm. -

- {twoFaQr ? ( -
- 2FA QR code { (e.target as HTMLImageElement).style.display = 'none' }} - /> -

Can't scan? Enter this key manually:

-
- ) : ( -

Enter this key manually in your authenticator app:

- )} - {twoFaSecret && ( -
- {twoFaSecret} -
- )} - - setTwoFaCode(e.target.value.replace(/\D/g, '').slice(0, 6))} - placeholder="000000" - style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem', letterSpacing: '0.3em', fontSize: '1.3rem', textAlign: 'center' }} - /> - {twoFaMessage &&

{twoFaMessage}

} -
- -
- - )} - - {accountModal === 'recoveryCodes' && ( - <> -

Save Your Recovery Codes

-

- Store these codes somewhere safe. Each one can be used once if you ever lose access to your authenticator app. You won't be able to see these again. -

-
- {twoFaRecoveryCodes.map(code => ( - {code} ))}
-
- - + ) : ( +

No study access information is available right now.

+ )} + {enrollMessage &&

{enrollMessage}

} + + )} + {accountModal === 'changeEmail' && ( + <> +

Change Email

+

Update the email address used for sign in and course notifications.

+ + + + setEmailNew(e.target.value)} placeholder="you@example.com" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> + + setEmailCurrentPassword(e.target.value)} placeholder="Current password" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> +
+ +
+ {emailTokenInput && ( +
+

Have a verification token?

+ setEmailTokenInput(e.target.value)} placeholder="Paste token here" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '0.75rem' }} /> +
- - )} - - {accountModal === 'disable2fa' && ( - <> -

Disable Two-Factor Authentication

-

- Enter your password to confirm. This will remove {auth.twoFaMethod === 'email' ? 'email code' : 'authenticator app'} 2FA from your account. -

- + )} + {emailMessage &&

{emailMessage}

} + + )} + {accountModal === 'profile' && ( + <> +

Profile Settings

+

Update your display name and avatar in one place.

+ +

Pick a preset avatar or upload your own image.

+
+ {[ + { key: 'dog', label: 'Dog', emoji: '🐶' }, + { key: 'cat', label: 'Cat', emoji: '🐱' }, + { key: 'rabbit', label: 'Rabbit', emoji: '🐰' }, + { key: 'fox', label: 'Fox', emoji: '🦊' }, + { key: 'panda', label: 'Panda', emoji: '🐼' }, + { key: 'lion', label: 'Lion', emoji: '🦁' }, + ].map(item => { + const svg = `${item.emoji}` + const url = `data:image/svg+xml;utf8,${encodeURIComponent(svg)}` + return ( + + ) + })} +
+
+ setDisablePassword(e.target.value)} - placeholder="Your password" - style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} + type="file" + accept="image/*" + onChange={e => { + const file = e.target.files?.[0] + if (file) { + void handleAvatarUpload(file) + } + }} + style={{ color: '#f0ead8' }} /> - {twoFaMessage &&

{twoFaMessage}

} -
- - + {avatarUploadBusy &&

Uploading avatar…

} + {avatarUploadMessage &&

{avatarUploadMessage}

} +
+ {(avatarUrlInput || auth.avatarUrl) && ( +
+ Avatar preview +

Preview of your selected avatar. Save your profile to keep it.

- - )} - - {accountModal === 'changePassword' && ( - <> -

Change Password

-

Use a strong password to keep your study materials and notes secure.

- - setPwCurrent(e.target.value)} placeholder="Current password" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> - - setPwNew(e.target.value)} placeholder="New password" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> - - setPwConfirm(e.target.value)} placeholder="Confirm new password" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> -
- -
- {pwMessage &&

{pwMessage}

} - {pwSuccess &&

Password changed successfully.

} - - )} -
-
- )} - - {divider} - -
-

Sign Out

-
- - -
- {signOutAllMessage &&

{signOutAllMessage}

} -
- - {divider} - -
-

Delete Account

- {deleteStep === 'idle' && ( - <> -

Permanently remove your account and all saved notes. This cannot be undone.

- - - )} - {deleteStep === 'warn1' && ( -
-

Final warning: export your notes first.

-
- - - -
-
- )} - {deleteStep === 'warn2' && ( -
-

Type DELETE to confirm permanent deletion:

- setDeleteConfirmInput(e.target.value)} placeholder="DELETE" style={{ marginBottom: '0.8rem', padding: '0.5rem', border: '1px solid #8a3b3b', borderRadius: '4px', background: '#140f0f', color: '#f0ead8' }} /> - {deleteMessage &&

{deleteMessage}

} -
- +
+ {profileMessage &&

{profileMessage}

} + + )} + {accountModal === 'preferences' && ( + <> +

Preferences

+

Adjust how you receive study reminders and newsletter updates.

+
+ + {studyRemindersEnabled && ( +
+
+

Reminder frequency

+
+ + +
+

+ {reminderFrequency === 'weekly' + ? 'Receive one weekly email summarizing all new lessons.' + : 'Receive an email each time a new lesson is released.'} +

+
+
+

+ Pause reminders + {remindersPausedUntil && new Date(remindersPausedUntil) > new Date() + ? ` · paused until ${new Date(remindersPausedUntil).toLocaleDateString()}` + : ''} +

+
+ {[7, 14, 30].map(days => ( + + ))} + {remindersPausedUntil && new Date(remindersPausedUntil) > new Date() && ( + + )} +
+
+
+ )} + +
+ + {prefMessage &&

{prefMessage}

} + + )} + {accountModal === 'export' && ( + <> +

Export Notes

+

Download all saved notes in a single document.

+ - -
-
- )} - + {exportMessage &&

{exportMessage}

} + + )} + {accountModal === 'choose2fa' && ( + <> +

Choose Your 2FA Method

+

+ Pick how you'd like to verify your identity each time you sign in. +

+
+ + +
+ {twoFaMessage &&

{twoFaMessage}

} + + )} -
- Back to Studies + {accountModal === 'setup2fa-email' && ( + <> +

Verify Your Email for 2FA

+

+ We sent a 6-digit code to your email. Enter it below to activate email-based two-factor authentication. +

+ + setTwoFaCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + placeholder="000000" + autoFocus + style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem', letterSpacing: '0.3em', fontSize: '1.3rem', textAlign: 'center' }} + /> + {twoFaMessage &&

{twoFaMessage}

} +
+ + +
+ + )} + + {accountModal === 'setup2fa' && ( + <> +

Enable Two-Factor Authentication

+

+ Scan the QR code below with your authenticator app (Google Authenticator, Authy, 1Password, etc.), then enter the 6-digit code to confirm. +

+ {twoFaQr ? ( +
+ 2FA QR code { (e.target as HTMLImageElement).style.display = 'none' }} + /> +

Can't scan? Enter this key manually:

+
+ ) : ( +

Enter this key manually in your authenticator app:

+ )} + {twoFaSecret && ( +
+ {twoFaSecret} +
+ )} + + setTwoFaCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + placeholder="000000" + style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem', letterSpacing: '0.3em', fontSize: '1.3rem', textAlign: 'center' }} + /> + {twoFaMessage &&

{twoFaMessage}

} +
+ +
+ + )} + + {accountModal === 'recoveryCodes' && ( + <> +

Save Your Recovery Codes

+

+ Store these codes somewhere safe. Each one can be used once if you ever lose access to your authenticator app. You won't be able to see these again. +

+
+ {twoFaRecoveryCodes.map(code => ( + {code} + ))} +
+
+ + +
+ + )} + + {accountModal === 'disable2fa' && ( + <> +

Disable Two-Factor Authentication

+

+ Enter your password to confirm. This will remove {auth.twoFaMethod === 'email' ? 'email code' : 'authenticator app'} 2FA from your account. +

+ + setDisablePassword(e.target.value)} + placeholder="Your password" + style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} + /> + {twoFaMessage &&

{twoFaMessage}

} +
+ + +
+ + )} + + {accountModal === 'changePassword' && ( + <> +

Change Password

+

Use a strong password to keep your study materials and notes secure.

+ + setPwCurrent(e.target.value)} placeholder="Current password" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> + + setPwNew(e.target.value)} placeholder="New password" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> + + setPwConfirm(e.target.value)} placeholder="Confirm new password" style={{ width: '100%', padding: '0.75rem', borderRadius: '8px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8', marginBottom: '1rem' }} /> +
+ +
+ {pwMessage &&

{pwMessage}

} + {pwSuccess &&

Password changed successfully.

} + + )} +
+ )} + +

← Back to Studies

)