Compare commits
5
Commits
b708371cc5
...
899e715067
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
899e715067 | ||
|
|
669cd6555c | ||
|
|
52568db82b | ||
|
|
b8410da5f4 | ||
|
|
640d0c1c18 |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"version": "2.10.0",
|
||||
"version": "2.10.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "siteforge",
|
||||
"version": "2.10.0",
|
||||
"version": "2.10.9",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1135.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.10.5",
|
||||
"version": "2.10.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+838
@@ -4033,6 +4033,844 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ── Study index: progress, chapters, lesson badges ── */
|
||||
.study-course-meta { margin-bottom: 1.25rem; }
|
||||
|
||||
.study-progress {
|
||||
max-width: 460px;
|
||||
margin: 0 0 1.75rem;
|
||||
}
|
||||
|
||||
.study-progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.45rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--brand-muted);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.study-progress-label strong {
|
||||
color: var(--brand-warm-white);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.study-progress-track {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(201, 168, 76, 0.16);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.study-progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #c9a84c, #e0c67a);
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.study-chapter + .study-chapter { margin-top: 1.25rem; }
|
||||
|
||||
.study-chapter > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.study-chapter > summary::-webkit-details-marker { display: none; }
|
||||
|
||||
.study-chapter > summary h3::before {
|
||||
content: '▸';
|
||||
display: inline-block;
|
||||
margin-right: 0.55rem;
|
||||
color: var(--brand-gold);
|
||||
font-size: 0.9em;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.study-chapter[open] > summary h3::before { transform: rotate(90deg); }
|
||||
|
||||
/* Upcoming lessons stay compact until they release */
|
||||
.study-module-row--coming {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0.7rem 1.05rem;
|
||||
}
|
||||
|
||||
.study-module-row--coming .study-module-row-right,
|
||||
.study-module-row--coming .study-module-row-left > p { display: none; }
|
||||
|
||||
.study-module-row--coming .study-module-row-left h3 { margin: 0.1rem 0 0; font-size: 1.05rem; }
|
||||
|
||||
|
||||
.study-chapter-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin: 0 0 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(201, 168, 76, 0.2);
|
||||
}
|
||||
|
||||
.study-chapter-head h3 {
|
||||
margin: 0;
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.3rem;
|
||||
color: #e8d9b5;
|
||||
}
|
||||
|
||||
.study-chapter-head span {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brand-muted);
|
||||
}
|
||||
|
||||
.study-lesson-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.lesson-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.18rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.lesson-badge--new { background: rgba(76, 175, 80, 0.16); color: #7fd484; border-color: rgba(76, 175, 80, 0.4); }
|
||||
.lesson-badge--coming { background: rgba(255, 183, 77, 0.14); color: #f0b45c; border-color: rgba(255, 183, 77, 0.4); }
|
||||
.lesson-badge--enroll { background: rgba(201, 168, 76, 0.12); color: var(--brand-gold); border-color: rgba(201, 168, 76, 0.4); }
|
||||
.lesson-badge--locked { background: rgba(255, 255, 255, 0.05); color: var(--brand-muted); border-color: rgba(255, 255, 255, 0.14); }
|
||||
.lesson-badge--done { background: rgba(111, 207, 151, 0.12); color: #6fcf97; border-color: rgba(111, 207, 151, 0.4); }
|
||||
|
||||
.study-module-row--done {
|
||||
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;
|
||||
font-size: 0.9rem;
|
||||
color: var(--brand-muted);
|
||||
}
|
||||
|
||||
.study-hub-steps {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.study-hub-step {
|
||||
padding: 1.4rem 1.4rem 1.5rem;
|
||||
border: 1px solid rgba(201, 168, 76, 0.22);
|
||||
border-radius: 14px;
|
||||
background: rgba(17, 17, 15, 0.84);
|
||||
}
|
||||
|
||||
.study-hub-step-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.3rem;
|
||||
height: 2.3rem;
|
||||
margin-bottom: 0.9rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(201, 168, 76, 0.6);
|
||||
background: rgba(201, 168, 76, 0.14);
|
||||
color: var(--brand-gold);
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.study-hub-step h3 {
|
||||
margin: 0 0 0.45rem;
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.3rem;
|
||||
color: var(--brand-warm-white);
|
||||
}
|
||||
|
||||
.study-hub-step p {
|
||||
margin: 0;
|
||||
color: #d6c7a6;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.study-track-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.study-track-wrap { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
|
||||
.study-track-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 1.4rem;
|
||||
border: 1px solid rgba(201, 168, 76, 0.24);
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(26, 26, 22, 0.95), rgba(16, 16, 14, 0.95));
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
a.study-track-card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: rgba(201, 168, 76, 0.55);
|
||||
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.study-track-card--planned {
|
||||
border-style: dashed;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.study-track-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.study-track-status {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brand-gold);
|
||||
}
|
||||
|
||||
.study-track-status--complete { color: #6fcf97; }
|
||||
.study-track-status--planned { color: var(--brand-muted); }
|
||||
|
||||
.study-difficulty {
|
||||
padding: 0.18rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.study-difficulty--beginner { color: #7fd484; border-color: rgba(76, 175, 80, 0.45); background: rgba(76, 175, 80, 0.12); }
|
||||
.study-difficulty--intermediate { color: #f0b45c; border-color: rgba(245, 124, 0, 0.45); background: rgba(245, 124, 0, 0.12); }
|
||||
.study-difficulty--advanced { color: #ef8a82; border-color: rgba(211, 47, 47, 0.5); background: rgba(211, 47, 47, 0.12); }
|
||||
|
||||
.study-track-title {
|
||||
margin: 0.2rem 0 0;
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.2;
|
||||
color: var(--brand-warm-white);
|
||||
}
|
||||
|
||||
.study-track-desc {
|
||||
margin: 0;
|
||||
color: #d6c7a6;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.study-track-facts {
|
||||
list-style: none;
|
||||
margin: 0.4rem 0 0;
|
||||
padding: 0.75rem 0 0;
|
||||
border-top: 1px solid rgba(201, 168, 76, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--brand-muted);
|
||||
}
|
||||
|
||||
.study-track-cta {
|
||||
margin-top: 0.5rem;
|
||||
color: var(--brand-gold);
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.study-track-listen {
|
||||
margin: 0;
|
||||
padding: 0 0.4rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--brand-muted);
|
||||
}
|
||||
|
||||
.study-hub-ask {
|
||||
max-width: 640px;
|
||||
margin: 1rem auto 2rem;
|
||||
padding: clamp(1.75rem, 4vw, 2.5rem);
|
||||
text-align: center;
|
||||
border: 1px solid rgba(201, 168, 76, 0.24);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
radial-gradient(ellipse at top, rgba(201, 168, 76, 0.1), transparent 70%),
|
||||
rgba(17, 17, 15, 0.9);
|
||||
}
|
||||
|
||||
.study-hub-ask h2 {
|
||||
margin: 0.4rem 0 0.75rem;
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.8rem;
|
||||
color: var(--brand-warm-white);
|
||||
}
|
||||
|
||||
.study-hub-ask p:not(.eyebrow) {
|
||||
margin: 0 auto 1.5rem;
|
||||
max-width: 480px;
|
||||
color: #b9aa86;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.study-hub-ask-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.study-hub-steps { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
a.study-track-card:hover { transform: none; }
|
||||
}
|
||||
|
||||
/* ── Sign in / Create account (split layout) ── */
|
||||
.auth-page {
|
||||
min-height: 70vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: clamp(2rem, 6vw, 4rem) 1.25rem;
|
||||
background:
|
||||
radial-gradient(ellipse at top, rgba(201, 168, 76, 0.12) 0%, transparent 55%),
|
||||
var(--brand-black);
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
width: min(1000px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
|
||||
align-items: start;
|
||||
gap: clamp(1.5rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
.auth-shell--single {
|
||||
grid-template-columns: minmax(0, 520px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-aside {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.auth-aside-title {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: clamp(1.7rem, 3.4vw, 2.3rem);
|
||||
line-height: 1.15;
|
||||
color: var(--brand-warm-white);
|
||||
margin: 0.4rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.auth-benefits {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.1rem;
|
||||
}
|
||||
|
||||
.auth-benefits li {
|
||||
display: flex;
|
||||
gap: 0.9rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.auth-benefits li > span {
|
||||
flex-shrink: 0;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(201, 168, 76, 0.12);
|
||||
border: 1px solid rgba(201, 168, 76, 0.35);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.auth-benefits strong {
|
||||
display: block;
|
||||
color: var(--brand-warm-white);
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.auth-benefits p {
|
||||
margin: 0;
|
||||
color: #b9aa86;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: #121212;
|
||||
border: 1px solid rgba(201, 168, 76, 0.24);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
|
||||
padding: clamp(1.5rem, 4vw, 2.25rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.auth-card h1 {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: clamp(1.8rem, 4vw, 2.3rem);
|
||||
color: var(--brand-warm-white);
|
||||
margin: 0.2rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.auth-card p {
|
||||
font-family: var(--brand-font-body);
|
||||
color: var(--brand-muted);
|
||||
}
|
||||
|
||||
.auth-card-eyebrow { margin: 0; }
|
||||
|
||||
.auth-card--center { text-align: center; }
|
||||
|
||||
.auth-card--center .study-signup-actions { justify-content: center; }
|
||||
|
||||
.auth-card .study-signup-form { margin-top: 0.5rem; }
|
||||
|
||||
.auth-card .study-signup-actions { margin-top: 0.75rem; }
|
||||
|
||||
.auth-card .study-signup-actions .btn-primary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-password-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.auth-password-wrap input {
|
||||
width: 100%;
|
||||
padding-right: 4.5rem;
|
||||
}
|
||||
|
||||
.auth-password-toggle {
|
||||
position: absolute;
|
||||
right: 0.35rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--brand-gold);
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.4rem 0.6rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-switch {
|
||||
margin: 1.1rem 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--brand-gold);
|
||||
font: inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-footer-links {
|
||||
margin: 1.5rem 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.auth-footer-links a {
|
||||
color: var(--brand-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-footer-links a:hover {
|
||||
color: var(--brand-warm-white);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.auth-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Form first on phones; the benefits follow below it */
|
||||
.auth-aside {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Study Signup Page ── */
|
||||
.study-signup-page-card {
|
||||
max-width: 680px;
|
||||
|
||||
+367
-338
@@ -911,8 +911,8 @@ export function StudyLandingPage({ content }: Props) {
|
||||
<span style={{ color: '#7a7060', fontSize: '0.9rem' }}>{study.completedLessons}/{study.totalLessons} lessons completed</span>
|
||||
<span style={{ color: '#7a7060', fontSize: '0.9rem' }}>{study.noteCount} notes</span>
|
||||
</div>
|
||||
<div style={{ marginTop: '0.75rem', height: '10px', width: '100%', background: '#27231b', borderRadius: '999px', overflow: 'hidden' }}>
|
||||
<div style={{ width: `${progress}%`, height: '100%', background: '#e0c070' }} />
|
||||
<div className="study-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={progress} style={{ marginTop: '0.75rem' }}>
|
||||
<div className="study-progress-fill" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="study-module-row-right" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
@@ -942,6 +942,54 @@ export function StudyLandingPage({ content }: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
const availableCount = activeStudies.length + completeStudies.length
|
||||
|
||||
const renderTrackCard = (study: StudyProgram, variant: 'active' | 'complete' | 'planned') => {
|
||||
const releasedCount = study.sections.filter(isSectionReleased).length
|
||||
const nextLabel = getNextReleaseLabel(study)
|
||||
const book = variant === 'complete' ? (content.finishedBooks ?? []).find(item => item.studySlug === study.slug) : undefined
|
||||
const body = (
|
||||
<>
|
||||
<div className="study-track-top">
|
||||
<span className={`study-track-status study-track-status--${variant}`}>
|
||||
{variant === 'active' ? 'In progress · start anytime' : variant === 'complete' ? 'Complete · self-paced' : 'Coming soon'}
|
||||
</span>
|
||||
{study.difficulty && <span className={`study-difficulty study-difficulty--${study.difficulty}`}>{study.difficulty}</span>}
|
||||
</div>
|
||||
<h3 className="study-track-title">{study.title}</h3>
|
||||
<p className="study-track-desc">{study.description}</p>
|
||||
<ul className="study-track-facts">
|
||||
{variant === 'planned' ? (
|
||||
<li>
|
||||
{study.startsAt && new Date(study.startsAt).getTime() > Date.now()
|
||||
? `Starts ${new Date(study.startsAt).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}`
|
||||
: 'Preparing lesson structure and media'}
|
||||
</li>
|
||||
) : (
|
||||
<>
|
||||
<li>{variant === 'complete' ? `All ${study.sections.length} lessons available` : releasedCount > 0 ? `${releasedCount} of ${study.sections.length} lessons available` : 'Lessons will be published soon'}</li>
|
||||
{variant === 'active' && nextLabel && <li>Next lesson: {nextLabel}</li>}
|
||||
{study.estimatedHours ? <li>About {study.estimatedHours} hours</li> : null}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
{variant !== 'planned' && <span className="study-track-cta">View lessons →</span>}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<div className="study-track-wrap" key={study.id}>
|
||||
{variant === 'planned'
|
||||
? <article className="study-track-card study-track-card--planned" aria-disabled="true">{body}</article>
|
||||
: <Link to={`/study/${study.slug}`} className="study-track-card">{body}</Link>}
|
||||
{book && (
|
||||
<p className="study-track-listen">
|
||||
Just want to listen? <Link to={`/finished/${book.id}`}>Open the audio playlist — no account needed →</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="study-index-page" aria-label="Study hub">
|
||||
<section className="section-study-course-hero">
|
||||
@@ -950,16 +998,22 @@ export function StudyLandingPage({ content }: Props) {
|
||||
<h1>Study at Your Own Pace</h1>
|
||||
<p className="study-course-hero-copy">Pick a study track, move lesson by lesson on your own schedule, and keep personal notes as you grow through each passage.</p>
|
||||
<div className="study-course-meta">
|
||||
<span>{activeStudies.length + completeStudies.length} {activeStudies.length + completeStudies.length === 1 ? 'study' : 'studies'} available{completeStudies.length > 0 ? ` (${completeStudies.length} finished, start anytime)` : ''}{plannedStudies.length > 0 ? ` · ${plannedStudies.length} coming soon` : ''}</span>
|
||||
<span>Self-paced flow</span>
|
||||
<span>Self-paced</span>
|
||||
<span>Personal notes</span>
|
||||
<span>Audio + commentary</span>
|
||||
<span>Free account</span>
|
||||
</div>
|
||||
<div className="study-course-hero-actions">
|
||||
{firstActiveStudyFirstReleasedSection && <Link to={`/study/${firstActiveStudy.slug}/${firstActiveStudyFirstReleasedSection.id}`} className="btn-primary">Start Learning</Link>}
|
||||
<Link to="/study/signup" className="btn-primary">Create Account or Login</Link>
|
||||
<Link to="/study/account" className="btn-secondary">My Account</Link>
|
||||
{firstActiveStudyFirstReleasedSection
|
||||
? <Link to={`/study/${firstActiveStudy.slug}/${firstActiveStudyFirstReleasedSection.id}`} className="btn-primary">Start Learning</Link>
|
||||
: <a href="#study-tracks" className="btn-primary">Browse Studies</a>}
|
||||
<Link to="/study/signup" className="btn-secondary">Create Free Account or Sign In</Link>
|
||||
</div>
|
||||
<p className="study-hub-count">
|
||||
{availableCount} {availableCount === 1 ? 'study' : 'studies'} available
|
||||
{completeStudies.length > 0 ? ` (${completeStudies.length} finished, start anytime)` : ''}
|
||||
{plannedStudies.length > 0 ? ` · ${plannedStudies.length} coming soon` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -969,63 +1023,33 @@ export function StudyLandingPage({ content }: Props) {
|
||||
<p>How It Works</p>
|
||||
<h2>A Simple Self-Paced Rhythm</h2>
|
||||
</div>
|
||||
<div className="study-hub-flow-grid">
|
||||
<article className="study-hub-flow-card">
|
||||
<p className="study-module-lesson">Step 1</p>
|
||||
<h3>Choose Your Track</h3>
|
||||
<p>Start with any active study and begin at lesson one, or jump back in where you left off.</p>
|
||||
</article>
|
||||
<article className="study-hub-flow-card">
|
||||
<p className="study-module-lesson">Step 2</p>
|
||||
<h3>Work Each Lesson</h3>
|
||||
<p>Read the text, listen to audio, review commentary, and process key Greek word notes.</p>
|
||||
</article>
|
||||
<article className="study-hub-flow-card">
|
||||
<p className="study-module-lesson">Step 3</p>
|
||||
<h3>Save Notes and Continue</h3>
|
||||
<p>Keep personal notes per lesson and build your own study archive over time.</p>
|
||||
</article>
|
||||
</div>
|
||||
<ol className="study-hub-steps">
|
||||
{[
|
||||
{ title: 'Choose Your Track', body: 'Start with any active study and begin at lesson one, or jump back in where you left off.' },
|
||||
{ title: 'Work Each Lesson', body: 'Read the text, listen to audio, review commentary, and process key Greek word notes.' },
|
||||
{ title: 'Save Notes and Continue', body: 'Keep personal notes per lesson and build your own study archive over time.' },
|
||||
].map((step, index) => (
|
||||
<li className="study-hub-step" key={step.title}>
|
||||
<span className="study-hub-step-number" aria-hidden="true">{index + 1}</span>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{activeStudies.length > 0 && (
|
||||
<section id="study-tracks" className="section-study-module" aria-label="Available studies">
|
||||
<div className="section-inner">
|
||||
<div className="study-module-header">
|
||||
<p>Available Now</p>
|
||||
<h2>Current Study Tracks</h2>
|
||||
</div>
|
||||
<div className="study-module-list">
|
||||
{activeStudies.map(study => (
|
||||
(() => {
|
||||
const releasedCount = study.sections.filter(isSectionReleased).length
|
||||
return (
|
||||
<Link key={study.id} to={`/study/${study.slug}`} className="study-module-row">
|
||||
<div className="study-module-row-left">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<p className="study-module-lesson">Start Anytime</p>
|
||||
{study.difficulty && <span style={{ backgroundColor: study.difficulty === 'advanced' ? '#d32f2f' : (study.difficulty === 'intermediate' ? '#f57c00' : '#388e3c'), color: '#fff', padding: '0.25rem 0.75rem', borderRadius: '16px', fontSize: '0.75rem', fontWeight: '600', textTransform: 'capitalize' }}>{study.difficulty}</span>}
|
||||
</div>
|
||||
<h3>{study.title}</h3>
|
||||
<p>{study.description}</p>
|
||||
{study.estimatedHours && <p style={{ fontSize: '0.875rem', color: '#666', marginTop: '0.5rem' }}>⏱ {study.estimatedHours} hours</p>}
|
||||
</div>
|
||||
<div className="study-module-row-right">
|
||||
<p className="study-module-focus">Track Snapshot</p>
|
||||
<p>{releasedCount > 0 ? `${releasedCount} lessons available now` : 'Lessons will be published soon.'}</p>
|
||||
<p>{(() => {
|
||||
const next = getNextReleaseLabel(study)
|
||||
if (next) return `Next lesson: ${next}`
|
||||
return study.sections.length > 0 ? 'New lessons release on a regular schedule.' : 'Pacing details coming with first lesson release.'
|
||||
})()}</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()
|
||||
))}
|
||||
</div>
|
||||
<div className="study-track-grid">{activeStudies.map(study => renderTrackCard(study, 'active'))}</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{completeStudies.length > 0 && (
|
||||
<section className="section-study-module" aria-label="Finished studies" id="finished-studies">
|
||||
@@ -1034,32 +1058,7 @@ export function StudyLandingPage({ content }: Props) {
|
||||
<p>Study On Your Own Time</p>
|
||||
<h2>Finished Studies</h2>
|
||||
</div>
|
||||
<div className="study-module-list">
|
||||
{completeStudies.map(study => {
|
||||
const book = (content.finishedBooks ?? []).find(item => item.studySlug === study.slug)
|
||||
return (
|
||||
<div key={study.id}>
|
||||
<Link to={`/study/${study.slug}`} className="study-module-row">
|
||||
<div className="study-module-row-left">
|
||||
<p className="study-module-lesson">Complete · Start Anytime</p>
|
||||
<h3>{study.title}</h3>
|
||||
<p>{study.description}</p>
|
||||
</div>
|
||||
<div className="study-module-row-right">
|
||||
<p className="study-module-focus">Self-Paced</p>
|
||||
<p>All {study.sections.length} lessons available now</p>
|
||||
<p>No schedule — go as fast or slow as you like.</p>
|
||||
</div>
|
||||
</Link>
|
||||
{book && (
|
||||
<p style={{ margin: '0.4rem 0 0', fontSize: '0.9rem' }}>
|
||||
Just want to listen? <Link to={`/finished/${book.id}`}>Open the audio playlist — no account needed →</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="study-track-grid">{completeStudies.map(study => renderTrackCard(study, 'complete'))}</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -1071,41 +1070,24 @@ export function StudyLandingPage({ content }: Props) {
|
||||
<p>Coming Soon</p>
|
||||
<h2>Next Study Tracks</h2>
|
||||
</div>
|
||||
<div className="study-module-list">
|
||||
{plannedStudies.map(study => (
|
||||
<article key={study.id} className="study-module-row study-module-row--disabled" aria-disabled="true">
|
||||
<div className="study-module-row-left">
|
||||
<p className="study-module-lesson">Planned</p>
|
||||
<h3>{study.title}</h3>
|
||||
<p>{study.description}</p>
|
||||
</div>
|
||||
<div className="study-module-row-right">
|
||||
<p className="study-module-focus">Status</p>
|
||||
<p>{study.startsAt && new Date(study.startsAt).getTime() > Date.now()
|
||||
? `Starts ${new Date(study.startsAt).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}`
|
||||
: 'Preparing lesson structure and media.'}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div className="study-track-grid">{plannedStudies.map(study => renderTrackCard(study, 'planned'))}</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="section-study-module" aria-label="Q&A">
|
||||
<div className="section-inner" style={{ textAlign: 'center', padding: '3rem 1rem' }}>
|
||||
<div className="section-inner">
|
||||
<div className="study-hub-ask">
|
||||
<p className="eyebrow">Have a question from the Word?</p>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 700, margin: '0.5rem 0 0.75rem', color: '#f0e9cc' }}>Ask Nate a Question</h2>
|
||||
<p style={{ color: '#8a7f5a', maxWidth: '480px', margin: '0 auto 1.5rem', fontSize: '0.95rem' }}>
|
||||
Submit a Bible or theology question and it may be answered in a future episode or the Q&A archive.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<a href="/questions" className="btn-primary">Browse Q&A Archive →</a>
|
||||
<a href="/contact" className="btn-secondary">Submit a Question</a>
|
||||
<h2>Ask Nate a Question</h2>
|
||||
<p>Submit a Bible or theology question and it may be answered in a future episode or the Q&A archive.</p>
|
||||
<div className="study-hub-ask-actions">
|
||||
<Link to="/questions" className="btn-primary">Browse Q&A Archive →</Link>
|
||||
<Link to="/contact" className="btn-secondary">Submit a Question</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1130,6 +1112,7 @@ export function StudySignupPage() {
|
||||
const [resendStatus, setResendStatus] = useState<'idle' | 'sending' | 'sent'>('idle')
|
||||
// Forgot-password state
|
||||
const [forgotSent, setForgotSent] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
readJson<StudyAuthStatusResponse>('/api/study-auth/status')
|
||||
@@ -1215,10 +1198,38 @@ export function StudySignupPage() {
|
||||
}
|
||||
}, [email])
|
||||
|
||||
const showAside = !done && mode !== 'forgot' && !totpPendingToken
|
||||
|
||||
return (
|
||||
<main className="thanks-page" aria-label={mode === 'signup' ? 'Create account' : mode === 'forgot' ? 'Reset password' : 'Sign in'}>
|
||||
<div className="thanks-card study-signup-page-card">
|
||||
<main className="auth-page" aria-label={mode === 'signup' ? 'Create account' : mode === 'forgot' ? 'Reset password' : 'Sign in'}>
|
||||
<div className={`auth-shell${showAside ? '' : ' auth-shell--single'}`}>
|
||||
{showAside && (
|
||||
<aside className="auth-aside" aria-label={mode === 'signup' ? 'Why create an account' : 'Welcome back'}>
|
||||
{mode === 'signup' ? (
|
||||
<>
|
||||
<p className="eyebrow">Free Student Account</p>
|
||||
<h2 className="auth-aside-title">Make every lesson your own.</h2>
|
||||
<ul className="auth-benefits">
|
||||
<li><span aria-hidden="true">📝</span><div><strong>Notes on every lesson</strong><p>Up to 500 notes across all tracks, each holding about 2,000 words.</p></div></li>
|
||||
<li><span aria-hidden="true">✅</span><div><strong>Track your progress</strong><p>Pick up exactly where you left off, on any device.</p></div></li>
|
||||
<li><span aria-hidden="true">🔒</span><div><strong>Private to you</strong><p>Your notes are tied to your account. Nobody else can read them.</p></div></li>
|
||||
<li><span aria-hidden="true">⚡</span><div><strong>Free, no ads</strong><p>Just an email and password. You stay signed in for 30 days.</p></div></li>
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="eyebrow">Free Student Account</p>
|
||||
<h2 className="auth-aside-title">Pick up where you left off.</h2>
|
||||
<ul className="auth-benefits">
|
||||
<li><span aria-hidden="true">📝</span><div><strong>Your notes</strong><p>Every lesson note you've saved, waiting for you.</p></div></li>
|
||||
<li><span aria-hidden="true">✅</span><div><strong>Your progress</strong><p>Resume your studies right where you stopped.</p></div></li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
<div className="auth-card">
|
||||
<p className="eyebrow auth-card-eyebrow">{mode === 'signup' ? 'Create account' : mode === 'forgot' ? 'Account recovery' : 'Sign in'}</p>
|
||||
<h1>{mode === 'signup' ? 'Create Your Account' : mode === 'forgot' ? 'Reset Your Password' : 'Welcome Back'}</h1>
|
||||
|
||||
{done ? (
|
||||
@@ -1317,7 +1328,7 @@ export function StudySignupPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="study-signup-form">
|
||||
<form className="study-signup-form" noValidate onSubmit={e => { e.preventDefault(); void submit() }}>
|
||||
<label htmlFor="signup-email">Email</label>
|
||||
<input
|
||||
id="signup-email"
|
||||
@@ -1337,9 +1348,10 @@ export function StudySignupPage() {
|
||||
/>
|
||||
{fieldErrors.email && <p className="study-signup-field-error">{fieldErrors.email}</p>}
|
||||
<label htmlFor="signup-password">Password</label>
|
||||
<div className="auth-password-wrap">
|
||||
<input
|
||||
id="signup-password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => { setPassword(e.target.value); if (fieldErrors.password) setFieldErrors(prev => ({ ...prev, password: undefined })) }}
|
||||
onBlur={() => {
|
||||
@@ -1349,10 +1361,13 @@ export function StudySignupPage() {
|
||||
setFieldErrors(prev => ({ ...prev, password: undefined }))
|
||||
}
|
||||
}}
|
||||
placeholder="At least 8 characters"
|
||||
placeholder={mode === 'signup' ? 'At least 8 characters' : 'Your password'}
|
||||
autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}
|
||||
onKeyDown={e => e.key === 'Enter' && submit()}
|
||||
/>
|
||||
<button type="button" className="auth-password-toggle" aria-pressed={showPassword} onClick={() => setShowPassword(v => !v)}>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
{fieldErrors.password && <p className="study-signup-field-error">{fieldErrors.password}</p>}
|
||||
{mode === 'login' && (
|
||||
<button type="button" onClick={() => { setMode('forgot'); setMessage(''); setForgotSent(false) }} style={{ background: 'none', border: 'none', color: '#c9a84c', cursor: 'pointer', fontSize: '0.85rem', textDecoration: 'underline', padding: 0, textAlign: 'left', marginTop: '0.5rem', alignSelf: 'flex-start' }}>
|
||||
@@ -1369,56 +1384,26 @@ export function StudySignupPage() {
|
||||
<span>Send me updates from Verse by Verse with Nate. I can unsubscribe anytime.</span>
|
||||
</label>
|
||||
)}
|
||||
{message && <p className="study-signup-error">{message}</p>}
|
||||
{message && <p className="study-signup-error" role="alert">{message}</p>}
|
||||
<div className="study-signup-actions">
|
||||
<button type="button" className="btn-primary" disabled={busy} onClick={submit}>
|
||||
<button type="submit" className="btn-primary" disabled={busy}>
|
||||
{busy ? (mode === 'signup' ? 'Creating...' : 'Signing in...') : (mode === 'signup' ? 'Create Account' : 'Sign In')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="study-signup-actions" style={{ marginTop: '1rem' }}>
|
||||
{mode === 'login' && (
|
||||
<button type="button" className="btn-secondary" onClick={() => { setMode('signup'); setMessage('') }}>
|
||||
Don't have an account? Sign up here
|
||||
<p className="auth-switch">
|
||||
{mode === 'login' ? "Don't have an account? " : 'Already have an account? '}
|
||||
<button type="button" className="auth-link" onClick={() => { setMode(mode === 'login' ? 'signup' : 'login'); setMessage('') }}>
|
||||
{mode === 'login' ? 'Sign up' : 'Sign in'}
|
||||
</button>
|
||||
)}
|
||||
{mode === 'signup' && (
|
||||
<button type="button" className="btn-secondary" onClick={() => { setMode('login'); setMessage('') }}>
|
||||
Already have an account? Sign in instead
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{mode === 'signup' && (
|
||||
<div className="study-signup-why-grid" style={{ marginTop: '2rem' }}>
|
||||
<article className="study-signup-why-card">
|
||||
<p className="study-signup-why-icon" aria-hidden="true">📝</p>
|
||||
<h3>Notes on Every Lesson</h3>
|
||||
<p>Save your own notes per lesson — up to 500 notes across all tracks.</p>
|
||||
</article>
|
||||
<article className="study-signup-why-card">
|
||||
<p className="study-signup-why-icon" aria-hidden="true">📖</p>
|
||||
<h3>Space to Go Deep</h3>
|
||||
<p>Each note holds ~2,000 words so you can write as much as you need.</p>
|
||||
</article>
|
||||
<article className="study-signup-why-card">
|
||||
<p className="study-signup-why-icon" aria-hidden="true">🔒</p>
|
||||
<h3>Private to You</h3>
|
||||
<p>Your notes are tied to your account. Nobody else can read them.</p>
|
||||
</article>
|
||||
<article className="study-signup-why-card">
|
||||
<p className="study-signup-why-icon" aria-hidden="true">⚡</p>
|
||||
<h3>Free Forever</h3>
|
||||
<p>Just an email and password. No charge, no ads. Stay signed in 30 days.</p>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
|
||||
<Link to="/study/account" className="btn-secondary">My Account</Link>
|
||||
<Link to="/study" className="btn-secondary">Back to Studies</Link>
|
||||
<p className="auth-footer-links">
|
||||
<Link to="/study">← Back to Studies</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1636,7 +1621,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
||||
// Resume at the first released lesson the student hasn't finished yet
|
||||
const resumeSection = releasedSections.find(item => !studyProgress.completedSectionIds.includes(item.id)) ?? firstReleasedSection
|
||||
const hasStarted = enrolled && studyProgress.completedSectionIds.length > 0
|
||||
const populatedChapterCount = chapterSummaries.filter(chapter => chapter.lessonCount > 0).length
|
||||
const resumeChapter = resumeSection?.chapter ?? chapterSummaries.find(chapter => chapter.lessonCount > 0)?.chapter
|
||||
const completedCount = sections.filter(item => studyProgress.completedSectionIds.includes(item.id)).length
|
||||
const chaptersWithLessons = chapterSummaries.filter(chapter => chapter.lessonCount > 0)
|
||||
|
||||
async function enrollInStudy() {
|
||||
if (!study) return
|
||||
@@ -1667,16 +1654,22 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
||||
<h1>{study.title}</h1>
|
||||
<p className="study-course-hero-copy">{study.description}</p>
|
||||
<div className="study-course-meta">
|
||||
<span>{releasedSections.length} lessons available now</span>
|
||||
<span>{chapterSummaries.length} chapters</span>
|
||||
<span>{populatedChapterCount} chapters with lessons</span>
|
||||
{enrolled && !progressLoading ? (
|
||||
<span>{studyProgress.completedSectionIds.length} lessons completed</span>
|
||||
) : null}
|
||||
<span>{releasedSections.length} of {sections.length} lessons available</span>
|
||||
<span>Text + commentary + discussion</span>
|
||||
<span>Student notes enabled</span>
|
||||
{isSelfPaced && <span>Finished study — go at your own pace</span>}
|
||||
</div>
|
||||
{enrolled && !progressLoading && sections.length > 0 && (
|
||||
<div className="study-progress" role="group" aria-label="Your progress">
|
||||
<div className="study-progress-label">
|
||||
<span>Your progress</span>
|
||||
<strong>{completedCount} of {sections.length} lessons completed</strong>
|
||||
</div>
|
||||
<div className="study-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={sections.length} aria-valuenow={completedCount}>
|
||||
<div className="study-progress-fill" style={{ width: `${Math.round((completedCount / sections.length) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="study-course-hero-actions">
|
||||
{enrolled && resumeSection && <Link to={`/study/${study.slug}/${resumeSection.id}`} className="btn-primary">{hasStarted ? 'Resume Where You Left Off' : 'Start Class'}</Link>}
|
||||
{!auth.authenticated && <Link to="/study/signup" className="btn-primary">Sign In to Enroll</Link>}
|
||||
@@ -1685,10 +1678,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
||||
{enrolling ? 'Enrolling...' : 'Enroll in This Study'}
|
||||
</button>
|
||||
)}
|
||||
{auth.authenticated && <Link to="/study/account" className="btn-secondary">My Account</Link>}
|
||||
{enrolled && <Link to={`/study/${study.slug}/notes`} className="btn-secondary">My Notes</Link>}
|
||||
{enrolled && <Link to={`/study/${study.slug}/community`} className="btn-secondary">Community</Link>}
|
||||
<Link to="/study" className="btn-secondary">Back to Studies</Link>
|
||||
{auth.authenticated && <Link to="/study/account" className="btn-secondary">My Account</Link>}
|
||||
</div>
|
||||
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
|
||||
</div>
|
||||
@@ -1734,30 +1726,45 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
||||
<p>Course Lessons</p>
|
||||
<h2>{study.title}</h2>
|
||||
</div>
|
||||
{chaptersWithLessons.map(({ chapter }) => {
|
||||
const chapterSections = sections.filter(item => item.chapter === chapter)
|
||||
const chapterDone = chapterSections.filter(item => studyProgress.completedSectionIds.includes(item.id)).length
|
||||
return (
|
||||
<details className="study-chapter" key={chapter} open={chaptersWithLessons.length === 1 || chapter === resumeChapter}>
|
||||
<summary className="study-chapter-head">
|
||||
<h3>Chapter {chapter}</h3>
|
||||
<span>
|
||||
{enrolled && !progressLoading
|
||||
? `${chapterDone} of ${chapterSections.length} completed`
|
||||
: `${chapterSections.length} ${chapterSections.length === 1 ? 'lesson' : 'lessons'}`}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="study-module-list">
|
||||
{sections.map((section, sectionIdx) => {
|
||||
const lessonNumber = getLessonNumber(sections, section.id)
|
||||
{chapterSections.map(section => {
|
||||
const sectionIdx = sections.indexOf(section)
|
||||
const lessonNumber = sectionIdx + 1
|
||||
const isNew = isNewLesson(section)
|
||||
const coming = isComingSoon(section)
|
||||
const prevSection = sectionIdx > 0 ? sections[sectionIdx - 1] : null
|
||||
const prevCompleted = !prevSection || studyProgress.completedSectionIds.includes(prevSection.id)
|
||||
const lockedByProgress = enrolled && !isSelfPaced && !coming && sectionIdx > 0 && !prevCompleted && !studyProgress.completedSectionIds.includes(section.id)
|
||||
const completed = studyProgress.completedSectionIds.includes(section.id)
|
||||
const lockedByProgress = enrolled && !isSelfPaced && !coming && sectionIdx > 0 && !prevCompleted && !completed
|
||||
const isDisabled = coming || !enrolled || lockedByProgress
|
||||
|
||||
return (
|
||||
<Link key={section.id} to={isDisabled ? '#' : `/study/${study.slug}/${section.id}`} className={`study-module-row${isDisabled ? ' study-module-row--disabled' : ''}`} onClick={e => isDisabled && e.preventDefault()}>
|
||||
<Link key={section.id} to={isDisabled ? '#' : `/study/${study.slug}/${section.id}`} className={`study-module-row${isDisabled ? ' study-module-row--disabled' : ''}${coming ? ' study-module-row--coming' : ''}${completed && enrolled ? ' study-module-row--done' : ''}`} aria-disabled={isDisabled || undefined} onClick={e => isDisabled && e.preventDefault()}>
|
||||
<div className="study-module-row-left">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
||||
<div className="study-lesson-meta">
|
||||
<p className="study-module-lesson">Lesson {lessonNumber}</p>
|
||||
{isNew && <span style={{ backgroundColor: '#4caf50', color: '#fff', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>New</span>}
|
||||
{coming && <span style={{ backgroundColor: '#ffb74d', color: '#333', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Coming {getReleaseDateDisplay(section)}</span>}
|
||||
{!coming && !enrolled && <span style={{ backgroundColor: '#ef5350', color: '#fff', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Enroll to open</span>}
|
||||
{lockedByProgress && <span style={{ backgroundColor: '#5a5440', color: '#f0e9cc', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Complete previous lesson</span>}
|
||||
{enrolled && !lockedByProgress && studyProgress.completedSectionIds.includes(section.id) && (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.3rem', backgroundColor: '#1a3a2a', color: '#6fcf97', padding: '0.2rem 0.55rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>
|
||||
{isNew && <span className="lesson-badge lesson-badge--new">New</span>}
|
||||
{coming && <span className="lesson-badge lesson-badge--coming">Coming {getReleaseDateDisplay(section)}</span>}
|
||||
{!coming && !enrolled && <span className="lesson-badge lesson-badge--enroll">Enroll to open</span>}
|
||||
{lockedByProgress && <span className="lesson-badge lesson-badge--locked">Finish the previous lesson first</span>}
|
||||
{enrolled && !lockedByProgress && completed && (
|
||||
<span className="lesson-badge lesson-badge--done">
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
|
||||
<circle cx="6.5" cy="6.5" r="6" stroke="#6fcf97" strokeWidth="1.2" />
|
||||
<path d="M3.5 6.5l2 2 3.5-3.5" stroke="#6fcf97" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="6.5" cy="6.5" r="6" stroke="currentColor" strokeWidth="1.2" />
|
||||
<path d="M3.5 6.5l2 2 3.5-3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
Completed
|
||||
</span>
|
||||
@@ -1775,6 +1782,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -2166,15 +2176,16 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
||||
|
||||
if (!auth.authenticated) {
|
||||
return (
|
||||
<main className="thanks-page" aria-label="Sign in required">
|
||||
<div className="thanks-card">
|
||||
<main className="auth-page" aria-label="Sign in required">
|
||||
<div className="auth-shell auth-shell--single">
|
||||
<div className="auth-card auth-card--center">
|
||||
<p className="eyebrow">{study.title}</p>
|
||||
<h1>Sign In to Continue</h1>
|
||||
<p>Create a free account or sign in to access lessons and save your personal notes.</p>
|
||||
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
|
||||
<p>Create a free account or sign in to open lessons and save your personal notes.</p>
|
||||
<div className="study-signup-actions">
|
||||
<Link to="/study/signup" className="btn-primary">Create Account or Sign In</Link>
|
||||
<Link to="/study/account" className="btn-secondary">My Account</Link>
|
||||
<Link to={`/study/${study.slug}`} className="btn-secondary">Back to Lessons</Link>
|
||||
</div>
|
||||
<p className="auth-footer-links"><Link to={`/study/${study.slug}`}>← Back to lessons</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -2183,19 +2194,20 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
||||
|
||||
if (!isEnrolled) {
|
||||
return (
|
||||
<main className="thanks-page" aria-label="Enrollment required">
|
||||
<div className="thanks-card">
|
||||
<main className="auth-page" aria-label="Enrollment required">
|
||||
<div className="auth-shell auth-shell--single">
|
||||
<div className="auth-card auth-card--center">
|
||||
<p className="eyebrow">{study.title}</p>
|
||||
<h1>Enrollment Required</h1>
|
||||
<p>You need to enroll in this study before entering lessons.</p>
|
||||
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
|
||||
<h1>Enroll to Begin</h1>
|
||||
<p>Enroll in this study to open its lessons and keep your notes and progress.</p>
|
||||
<div className="study-signup-actions">
|
||||
<button type="button" className="btn-primary" disabled={enrollBusy} onClick={enrollInCurrentStudy}>
|
||||
{enrollBusy ? 'Enrolling...' : `Enroll in ${study.title}`}
|
||||
</button>
|
||||
<Link to="/study/account" className="btn-secondary">My Account</Link>
|
||||
<Link to={`/study/${study.slug}`} className="btn-secondary">Back to Study</Link>
|
||||
</div>
|
||||
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
|
||||
{enrollMessage && <p className="study-note-status" role="status">{enrollMessage}</p>}
|
||||
<p className="auth-footer-links"><Link to={`/study/${study.slug}`}>← Back to study</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
@@ -3181,164 +3193,233 @@ export function StudyAccountPage() {
|
||||
|
||||
if (!auth.checked) {
|
||||
return (
|
||||
<main className="thanks-page" aria-label="Loading">
|
||||
<div className="thanks-card"><p>Loading...</p></div>
|
||||
<main className="auth-page" aria-label="Loading">
|
||||
<div className="auth-shell auth-shell--single">
|
||||
<div className="auth-card auth-card--center"><p>Loading...</p></div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (!auth.authenticated) {
|
||||
return (
|
||||
<main className="thanks-page" aria-label="Sign in required">
|
||||
<div className="thanks-card">
|
||||
<main className="auth-page" aria-label="Sign in required">
|
||||
<div className="auth-shell auth-shell--single">
|
||||
<div className="auth-card auth-card--center">
|
||||
<p className="eyebrow">My Account</p>
|
||||
<h1>Sign In Required</h1>
|
||||
<p>Please sign in to manage your account.</p>
|
||||
<Link to="/study/signup" className="btn-primary" style={{ marginTop: '1rem', display: 'inline-block' }}>Sign In</Link>
|
||||
<div className="study-signup-actions">
|
||||
<Link to="/study/signup" className="btn-primary">Sign In</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const divider = <hr style={{ margin: '0 0 2rem', border: 'none', borderTop: '1px solid #2a2518' }} />
|
||||
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 (
|
||||
<main className="thanks-page" aria-label="My account">
|
||||
<div className="thanks-card" style={{ maxWidth: '760px', textAlign: 'left' }}>
|
||||
<p className="eyebrow">Student Account</p>
|
||||
<h1>My Account</h1>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
|
||||
<main className="account-page" aria-label="My account">
|
||||
<div className="account-shell">
|
||||
<header className="account-header">
|
||||
{auth.avatarUrl ? (
|
||||
<img src={auth.avatarUrl} alt="Your avatar" width={64} height={64} style={{ borderRadius: '50%', border: '1px solid #2a2518' }} />
|
||||
<img className="account-avatar" src={auth.avatarUrl} alt="Your avatar" width={84} height={84} />
|
||||
) : (
|
||||
<div style={{ width: 64, height: 64, borderRadius: '50%', background: '#2a2518', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#f0ead8', fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
{auth.displayName?.slice(0, 1).toUpperCase() || auth.username.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<div className="account-avatar account-avatar--initial" aria-hidden="true">{initial}</div>
|
||||
)}
|
||||
<div>
|
||||
<p style={{ margin: 0, fontSize: '0.95rem' }}>Signed in as</p>
|
||||
<p style={{ margin: '0.25rem 0 0', fontWeight: 700 }}>{auth.displayName || auth.username}</p>
|
||||
<p style={{ margin: '0.25rem 0 0', fontSize: '0.9rem', color: '#7a7060' }}>{auth.username}</p>
|
||||
<div className="account-header-body">
|
||||
<p className="eyebrow">Student Account</p>
|
||||
<h1>{auth.displayName || auth.username.split('@')[0]}</h1>
|
||||
<p className="account-email">
|
||||
{auth.username}
|
||||
{auth.emailVerified === false
|
||||
? <span className="account-chip account-chip--warn">Unverified</span>
|
||||
: <span className="account-chip account-chip--ok">Verified</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="account-header-actions">
|
||||
<button type="button" className="btn-secondary" onClick={() => setAccountModal('profile')}>Edit Profile</button>
|
||||
<button type="button" className="btn-secondary" disabled={signOutBusy} onClick={handleSignOut}>
|
||||
{signOutBusy ? 'Signing out...' : 'Sign Out'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{auth.emailVerified === false && (
|
||||
<div style={{ marginBottom: '1.5rem', background: 'rgba(201,168,76,0.07)', border: '1px solid rgba(201,168,76,0.2)', borderRadius: '8px', padding: '0.75rem 1rem', fontSize: '0.85rem', color: '#a89a6a', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.75rem' }}>
|
||||
<span><strong style={{ color: '#c9a84c' }}>Verify your email.</strong> We sent a link to {auth.username} — click it to confirm this address.</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
{resendVerifyMessage && <span style={{ fontSize: '0.8rem' }}>{resendVerifyMessage}</span>}
|
||||
<button type="button" className="btn-secondary" style={{ fontSize: '0.82rem', padding: '0.3rem 0.75rem' }} disabled={resendVerifyBusy} onClick={handleResendVerification}>
|
||||
<div className="account-notice" role="status">
|
||||
<span><strong>Verify your email.</strong> We sent a link to {auth.username} — click it to confirm this address.</span>
|
||||
<div className="account-notice-actions">
|
||||
{resendVerifyMessage && <span className="account-notice-msg">{resendVerifyMessage}</span>}
|
||||
<button type="button" className="btn-secondary" disabled={resendVerifyBusy} onClick={handleResendVerification}>
|
||||
{resendVerifyBusy ? 'Sending…' : 'Resend Email'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="account-layout">
|
||||
<div className="account-main">
|
||||
{loadingOverview && !overview && <p className="account-muted">Loading your progress…</p>}
|
||||
{overview?.stats && (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', marginBottom: '1.5rem' }}>
|
||||
<div style={{ flex: '1 1 130px', background: '#1a1a16', borderRadius: '8px', padding: '1rem', border: '1px solid #2a2518' }}>
|
||||
<p style={{ fontSize: '1.6rem', fontWeight: 700, margin: 0 }}>{overview.stats.noteCount}</p>
|
||||
<p style={{ margin: 0, fontSize: '0.85rem', color: '#7a7060' }}>Notes saved</p>
|
||||
<div className="account-stats">
|
||||
<div className="account-stat"><strong>{overview.stats.noteCount}</strong><span>Notes saved</span></div>
|
||||
<div className="account-stat"><strong>{overview.stats.currentStreak ?? 0} 🔥</strong><span>Day streak</span></div>
|
||||
<div className="account-stat"><strong>{overview.stats.longestStreak ?? 0}</strong><span>Longest streak</span></div>
|
||||
<div className="account-stat"><strong className="account-stat-date">{memberSince}</strong><span>Member since</span></div>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 130px', background: '#1a1a16', borderRadius: '8px', padding: '1rem', border: '1px solid #2a2518' }}>
|
||||
<p style={{ fontSize: '1.6rem', fontWeight: 700, margin: 0 }}>{overview.stats.currentStreak ?? 0} 🔥</p>
|
||||
<p style={{ margin: 0, fontSize: '0.85rem', color: '#7a7060' }}>Day streak</p>
|
||||
|
||||
<section className="account-card" aria-label="Study progress">
|
||||
<div className="account-card-head">
|
||||
<h2>Study Progress</h2>
|
||||
<button type="button" className="account-link" onClick={() => setAccountModal('enrollments')}>Manage enrollments</button>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 130px', background: '#1a1a16', borderRadius: '8px', padding: '1rem', border: '1px solid #2a2518' }}>
|
||||
<p style={{ fontSize: '1.6rem', fontWeight: 700, margin: 0 }}>{overview.stats.longestStreak ?? 0}</p>
|
||||
<p style={{ margin: 0, fontSize: '0.85rem', color: '#7a7060' }}>Longest streak</p>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 130px', background: '#1a1a16', borderRadius: '8px', padding: '1rem', border: '1px solid #2a2518' }}>
|
||||
<p style={{ fontSize: '1rem', fontWeight: 600, margin: 0 }}>{overview.stats.memberSince && !isNaN(new Date(overview.stats.memberSince).getTime()) ? new Date(overview.stats.memberSince).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}</p>
|
||||
<p style={{ margin: 0, fontSize: '0.85rem', color: '#7a7060' }}>Member since</p>
|
||||
</div>
|
||||
</div>
|
||||
{overview.studies.filter(s => s.enrolled && s.totalLessons > 0).length > 0 && (
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.75rem', fontSize: '0.85rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: '#7a7060' }}>Study Progress</p>
|
||||
{overview.studies.filter(s => s.enrolled && s.totalLessons > 0).map(s => {
|
||||
const pct = Math.round((s.completedLessons / s.totalLessons) * 100)
|
||||
{enrolledProgress.length === 0 ? (
|
||||
<p className="account-muted">You aren't enrolled in a study yet. <Link to="/study">Browse the studies →</Link></p>
|
||||
) : enrolledProgress.map(item => {
|
||||
const pct = Math.round((item.completedLessons / item.totalLessons) * 100)
|
||||
return (
|
||||
<div key={s.slug} style={{ marginBottom: '0.85rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.88rem', marginBottom: '0.3rem' }}>
|
||||
<span>{s.title}</span>
|
||||
<span style={{ color: '#7a7060' }}>{s.completedLessons}/{s.totalLessons} lessons · {pct}%</span>
|
||||
<div className="account-progress" key={item.slug}>
|
||||
<div className="account-progress-row">
|
||||
<Link to={`/study/${item.slug}`}>{item.title}</Link>
|
||||
<span>{item.completedLessons}/{item.totalLessons} lessons · {pct}%</span>
|
||||
</div>
|
||||
<div style={{ height: '6px', background: '#2a2518', borderRadius: '3px', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${pct}%`, background: pct === 100 ? '#4a9a4a' : '#c9a84c', borderRadius: '3px', transition: 'width 0.4s ease' }} />
|
||||
<div className="study-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={pct}>
|
||||
<div className="study-progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{overview.stats.noteLimit > 0 && (
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', marginBottom: '0.3rem', color: '#7a7060' }}>
|
||||
<span style={{ textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600 }}>Note Usage</span>
|
||||
<span>{overview.stats.noteCount}/{overview.stats.noteLimit} notes · up to {overview.stats.noteCharLimit.toLocaleString()} characters each</span>
|
||||
</div>
|
||||
<div style={{ height: '6px', background: '#2a2518', borderRadius: '3px', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${Math.min(100, Math.round((overview.stats.noteCount / overview.stats.noteLimit) * 100))}%`, background: '#c9a84c', borderRadius: '3px', transition: 'width 0.4s ease' }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{overview.certificates.length > 0 && (
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.75rem', fontSize: '0.85rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: '#7a7060' }}>Certificates</p>
|
||||
<section className="account-card" aria-label="Certificates">
|
||||
<div className="account-card-head"><h2>Certificates</h2></div>
|
||||
{overview.certificates.map(cert => (
|
||||
<div key={cert.studySlug} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.5rem', marginBottom: '0.6rem', fontSize: '0.88rem' }}>
|
||||
<span>🏅 {cert.studyTitle} <span style={{ color: '#7a7060', fontSize: '0.82rem' }}>— {new Date(cert.issuedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</span></span>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<a href={cert.url} target="_blank" rel="noopener noreferrer" className="btn-admin-ghost" style={{ fontSize: '0.78rem', padding: '0.2rem 0.6rem' }}>View</a>
|
||||
<button type="button" className="btn-admin-ghost" style={{ fontSize: '0.78rem', padding: '0.2rem 0.6rem' }} onClick={() => { navigator.clipboard?.writeText(cert.url).catch(() => {}) }}>Copy Link</button>
|
||||
<div className="account-cert" key={cert.studySlug}>
|
||||
<span>🏅 {cert.studyTitle} <small>— {new Date(cert.issuedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</small></span>
|
||||
<div className="account-cert-actions">
|
||||
<a href={cert.url} target="_blank" rel="noopener noreferrer" className="btn-secondary">View</a>
|
||||
<button type="button" className="btn-secondary" onClick={() => { navigator.clipboard?.writeText(cert.url).catch(() => {}) }}>Copy link</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{divider}
|
||||
</>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ marginTop: 0 }}>Quick Actions</h2>
|
||||
<p style={{ fontSize: '0.92rem' }}>Open the settings you need without scrolling through the whole page.</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginTop: '1rem' }}>
|
||||
<button type="button" className="btn-primary" onClick={() => setAccountModal('profile')}>Edit Profile</button>
|
||||
<button type="button" className="btn-primary" onClick={() => setAccountModal('preferences')}>Preferences</button>
|
||||
<button type="button" className="btn-primary" onClick={() => setAccountModal('export')}>Export Notes</button>
|
||||
<button type="button" className="btn-primary" onClick={() => setAccountModal('enrollments')}>Manage Enrollments</button>
|
||||
<button type="button" className="btn-secondary" onClick={() => setAccountModal('changeEmail')}>Change Email</button>
|
||||
<button type="button" className="btn-secondary" onClick={() => setAccountModal('changePassword')}>Change Password</button>
|
||||
<button type="button" className={auth.totpEnabled ? 'btn-secondary' : 'btn-primary'} onClick={() => {
|
||||
{overview.stats.noteLimit > 0 && (
|
||||
<section className="account-card" aria-label="Note usage">
|
||||
<div className="account-card-head">
|
||||
<h2>Note Usage</h2>
|
||||
<button type="button" className="account-link" onClick={() => setAccountModal('export')}>Export notes</button>
|
||||
</div>
|
||||
<div className="account-progress-row">
|
||||
<span>{overview.stats.noteCount} of {overview.stats.noteLimit} notes</span>
|
||||
<span>up to {overview.stats.noteCharLimit.toLocaleString()} characters each</span>
|
||||
</div>
|
||||
<div className="study-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={overview.stats.noteLimit} aria-valuenow={overview.stats.noteCount}>
|
||||
<div className="study-progress-fill" style={{ width: `${Math.min(100, Math.round((overview.stats.noteCount / overview.stats.noteLimit) * 100))}%` }} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<aside className="account-side">
|
||||
<section className="account-card" aria-label="Settings">
|
||||
<div className="account-card-head"><h2>Settings</h2></div>
|
||||
<ul className="account-menu">
|
||||
<li><button type="button" onClick={() => setAccountModal('profile')}><span><strong>Profile</strong><small>Name and avatar</small></span><span aria-hidden="true">›</span></button></li>
|
||||
<li><button type="button" onClick={() => setAccountModal('preferences')}><span><strong>Preferences</strong><small>Email updates and reminders</small></span><span aria-hidden="true">›</span></button></li>
|
||||
<li><button type="button" onClick={() => setAccountModal('enrollments')}><span><strong>Enrollments</strong><small>Join or leave studies</small></span><span aria-hidden="true">›</span></button></li>
|
||||
<li><button type="button" onClick={() => setAccountModal('export')}><span><strong>Export notes</strong><small>Download everything you've written</small></span><span aria-hidden="true">›</span></button></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="account-card" aria-label="Security">
|
||||
<div className="account-card-head"><h2>Security</h2></div>
|
||||
<ul className="account-menu">
|
||||
<li><button type="button" onClick={() => setAccountModal('changeEmail')}><span><strong>Email address</strong><small>{auth.username}</small></span><span aria-hidden="true">›</span></button></li>
|
||||
<li><button type="button" onClick={() => setAccountModal('changePassword')}><span><strong>Password</strong><small>Change your password</small></span><span aria-hidden="true">›</span></button></li>
|
||||
<li>
|
||||
<button type="button" disabled={twoFaBusy} onClick={() => {
|
||||
if (auth.totpEnabled) { setAccountModal('disable2fa'); return }
|
||||
setAccountModal('choose2fa')
|
||||
}} disabled={twoFaBusy}>
|
||||
{auth.totpEnabled
|
||||
? `2FA: ${auth.twoFaMethod === 'email' ? 'Email ✓' : 'App ✓'}`
|
||||
: '🔐 Enable Two-Factor Auth'}
|
||||
}}>
|
||||
<span>
|
||||
<strong>Two-factor authentication</strong>
|
||||
<small>{auth.totpEnabled ? `On — ${auth.twoFaMethod === 'email' ? 'email code' : 'authenticator app'}` : 'Off — recommended'}</small>
|
||||
</span>
|
||||
<span className={`account-chip ${auth.totpEnabled ? 'account-chip--ok' : 'account-chip--warn'}`}>{auth.totpEnabled ? 'On' : 'Off'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{!auth.totpEnabled && (
|
||||
<div style={{ marginTop: '1rem', background: 'rgba(201,168,76,0.07)', border: '1px solid rgba(201,168,76,0.2)', borderRadius: '8px', padding: '0.75rem 1rem', fontSize: '0.85rem', color: '#a89a6a' }}>
|
||||
<strong style={{ color: '#c9a84c' }}>Recommended:</strong> Enable two-factor authentication to protect your study notes and account.
|
||||
</div>
|
||||
<p className="account-callout">
|
||||
<strong>Recommended:</strong> turn on two-factor authentication to protect your study notes and account.
|
||||
</p>
|
||||
)}
|
||||
{auth.totpEnabled && auth.totpRecoveryCodesRemaining !== undefined && auth.totpRecoveryCodesRemaining <= 2 && (
|
||||
<div style={{ marginTop: '1rem', background: 'rgba(224,92,92,0.07)', border: '1px solid rgba(224,92,92,0.2)', borderRadius: '8px', padding: '0.75rem 1rem', fontSize: '0.85rem', color: '#e07a7a' }}>
|
||||
<strong>Warning:</strong> You only have {auth.totpRecoveryCodesRemaining} recovery code{auth.totpRecoveryCodesRemaining === 1 ? '' : 's'} left.{' '}
|
||||
<button type="button" style={{ background: 'none', border: 'none', color: '#c9a84c', cursor: 'pointer', textDecoration: 'underline', fontSize: 'inherit' }} onClick={async () => {
|
||||
<p className="account-callout account-callout--danger">
|
||||
<strong>Warning:</strong> you only have {auth.totpRecoveryCodesRemaining} recovery code{auth.totpRecoveryCodesRemaining === 1 ? '' : 's'} left.{' '}
|
||||
<button type="button" className="account-link" onClick={async () => {
|
||||
try {
|
||||
const data = await readJson<{ ok: boolean; recoveryCodes: string[] }>('/api/study-auth/totp-regen-recovery', { method: 'POST' })
|
||||
setTwoFaRecoveryCodes(data.recoveryCodes)
|
||||
setAccountModal('recoveryCodes')
|
||||
} catch { /* ignore */ }
|
||||
}}>Generate new codes →</button>
|
||||
</p>
|
||||
)}
|
||||
<div className="account-session">
|
||||
<button type="button" className="btn-secondary" disabled={signOutAllBusy} onClick={handleSignOutAllDevices}>
|
||||
{signOutAllBusy ? 'Signing out other devices…' : 'Sign out of all other devices'}
|
||||
</button>
|
||||
{signOutAllMessage && <p className="study-note-status" role="status">{signOutAllMessage}</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="account-card account-card--danger" aria-label="Delete account">
|
||||
<div className="account-card-head"><h2>Delete Account</h2></div>
|
||||
{deleteStep === 'idle' && (
|
||||
<>
|
||||
<p className="account-muted">Permanently remove your account and all saved notes. This cannot be undone.</p>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => setDeleteStep('warn1')}>Delete My Account</button>
|
||||
</>
|
||||
)}
|
||||
{deleteStep === 'warn1' && (
|
||||
<div className="account-danger-step">
|
||||
<p><strong>Final warning: export your notes first.</strong></p>
|
||||
<div className="account-danger-actions">
|
||||
<button type="button" className="btn-primary" onClick={handleExport} disabled={exporting}>Export Notes First</button>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => setDeleteStep('warn2')}>Continue to Delete</button>
|
||||
<button type="button" className="btn-secondary" onClick={() => setDeleteStep('idle')}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{deleteStep === 'warn2' && (
|
||||
<div className="account-danger-step">
|
||||
<p>Type <strong>DELETE</strong> to confirm permanent deletion:</p>
|
||||
<input type="text" className="account-danger-input" value={deleteConfirmInput} onChange={e => setDeleteConfirmInput(e.target.value)} placeholder="DELETE" aria-label="Type DELETE to confirm" />
|
||||
{deleteMessage && <p className="study-signup-error" role="alert">{deleteMessage}</p>}
|
||||
<div className="account-danger-actions">
|
||||
<button type="button" className="btn-admin-remove" disabled={deleteBusy || deleteConfirmInput.trim() !== 'DELETE'} onClick={handleDeleteAccount}>
|
||||
{deleteBusy ? 'Deleting...' : 'Permanently Delete My Account'}
|
||||
</button>
|
||||
<button type="button" className="btn-secondary" onClick={() => { setDeleteStep('idle'); setDeleteConfirmInput('') }}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{accountModal !== 'none' && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(5, 5, 5, 0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '1.25rem' }}>
|
||||
@@ -3792,59 +3873,7 @@ export function StudyAccountPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{divider}
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ marginTop: 0 }}>Sign Out</h2>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<button type="button" className="btn-secondary" disabled={signOutBusy} onClick={handleSignOut}>
|
||||
{signOutBusy ? 'Signing out...' : 'Sign Out'}
|
||||
</button>
|
||||
<button type="button" className="btn-secondary" disabled={signOutAllBusy} onClick={handleSignOutAllDevices}>
|
||||
{signOutAllBusy ? 'Signing out other devices…' : 'Sign Out of All Other Devices'}
|
||||
</button>
|
||||
</div>
|
||||
{signOutAllMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{signOutAllMessage}</p>}
|
||||
</section>
|
||||
|
||||
{divider}
|
||||
|
||||
<section>
|
||||
<h2 style={{ marginTop: 0, color: '#e57373' }}>Delete Account</h2>
|
||||
{deleteStep === 'idle' && (
|
||||
<>
|
||||
<p style={{ fontSize: '0.9rem' }}>Permanently remove your account and all saved notes. This cannot be undone.</p>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => setDeleteStep('warn1')}>Delete My Account</button>
|
||||
</>
|
||||
)}
|
||||
{deleteStep === 'warn1' && (
|
||||
<div style={{ background: '#2d2414', border: '1px solid #6b4b1f', borderRadius: '6px', padding: '1rem', marginBottom: '1rem' }}>
|
||||
<p style={{ fontWeight: 600 }}>Final warning: export your notes first.</p>
|
||||
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn-primary" onClick={handleExport} disabled={exporting}>Export Notes First</button>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => setDeleteStep('warn2')}>Continue to Delete</button>
|
||||
<button type="button" className="btn-secondary" onClick={() => setDeleteStep('idle')}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{deleteStep === 'warn2' && (
|
||||
<div style={{ background: '#2b1111', border: '1px solid #8a3b3b', borderRadius: '6px', padding: '1rem' }}>
|
||||
<p>Type <strong>DELETE</strong> to confirm permanent deletion:</p>
|
||||
<input type="text" value={deleteConfirmInput} onChange={e => setDeleteConfirmInput(e.target.value)} placeholder="DELETE" style={{ marginBottom: '0.8rem', padding: '0.5rem', border: '1px solid #8a3b3b', borderRadius: '4px', background: '#140f0f', color: '#f0ead8' }} />
|
||||
{deleteMessage && <p style={{ color: '#ef9a9a', fontSize: '0.875rem' }}>{deleteMessage}</p>}
|
||||
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn-admin-remove" disabled={deleteBusy || deleteConfirmInput.trim() !== 'DELETE'} onClick={handleDeleteAccount}>
|
||||
{deleteBusy ? 'Deleting...' : 'Permanently Delete My Account'}
|
||||
</button>
|
||||
<button type="button" className="btn-secondary" onClick={() => { setDeleteStep('idle'); setDeleteConfirmInput('') }}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div style={{ marginTop: '2rem' }}>
|
||||
<Link to="/study" className="btn-secondary">Back to Studies</Link>
|
||||
</div>
|
||||
<p className="auth-footer-links"><Link to="/study">← Back to Studies</Link></p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user