Discussion Questions (one per line)
diff --git a/src/App.css b/src/App.css
index 8a862ef..5d3f188 100644
--- a/src/App.css
+++ b/src/App.css
@@ -2668,17 +2668,20 @@
.note-anchor-btn {
position: absolute;
- top: 1rem;
- right: 1rem;
+ top: 0.85rem;
+ right: 0.85rem;
background: transparent;
border: 1px solid #3a3828;
border-radius: 6px;
color: #5a5440;
- padding: 0.2rem 0.55rem;
- font-size: 0.9rem;
+ padding: 0.22rem 0.65rem;
+ font-size: 0.78rem;
+ font-family: var(--brand-font-body);
+ font-weight: 500;
+ letter-spacing: 0.02em;
cursor: pointer;
transition: color 150ms, border-color 150ms, background 150ms;
- line-height: 1;
+ line-height: 1.4;
z-index: 1;
}
@@ -2699,6 +2702,184 @@
border-color: rgba(224, 192, 112, 0.5);
}
+/* ── Progress bar ────────────────────────────────────────────────── */
+.study-lesson-progress-bar {
+ height: 3px;
+ background: rgba(201, 168, 76, 0.12);
+ border-radius: 2px;
+ margin: 0.35rem 0 0.1rem;
+ overflow: hidden;
+}
+
+.study-lesson-progress-fill {
+ height: 100%;
+ background: #c9a84c;
+ border-radius: 2px;
+ transition: width 0.4s ease;
+ min-width: 3px;
+}
+
+/* ── Sidebar lesson navigator ────────────────────────────────────── */
+.study-lesson-nav-progress {
+ font-size: 0.72rem;
+ font-weight: 400;
+ color: #5a5440;
+ margin-left: 0.4rem;
+ letter-spacing: 0.02em;
+}
+
+.study-lesson-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ margin-top: 0.25rem;
+ max-height: 320px;
+ overflow-y: auto;
+}
+
+.study-lesson-nav-item {
+ display: flex;
+ align-items: center;
+ gap: 0.55rem;
+ padding: 0.42rem 0.55rem;
+ border-radius: 7px;
+ border: 1px solid transparent;
+ text-decoration: none;
+ transition: background 120ms, border-color 120ms;
+ cursor: pointer;
+}
+
+.study-lesson-nav-item:hover {
+ background: rgba(201, 168, 76, 0.06);
+}
+
+.study-lesson-nav-item--current {
+ background: rgba(201, 168, 76, 0.1);
+ border-color: rgba(201, 168, 76, 0.2);
+}
+
+.study-lesson-nav-item--locked {
+ opacity: 0.45;
+ cursor: default;
+ pointer-events: none;
+}
+
+.study-lesson-nav-icon {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.72rem;
+ font-weight: 700;
+ flex-shrink: 0;
+ background: rgba(255, 255, 255, 0.05);
+ color: #5a5440;
+}
+
+.study-lesson-nav-item--done .study-lesson-nav-icon {
+ background: rgba(201, 168, 76, 0.18);
+ color: #c9a84c;
+}
+
+.study-lesson-nav-item--current .study-lesson-nav-icon {
+ background: #c9a84c;
+ color: #0e0d0b;
+}
+
+.study-lesson-nav-label {
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: #7a7060;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+
+.study-lesson-nav-item--current .study-lesson-nav-label {
+ color: #e2d4a8;
+}
+
+.study-lesson-nav-item--done .study-lesson-nav-label {
+ color: #5a5040;
+}
+
+.study-lesson-nav-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.25rem;
+}
+
+/* ── Notes count badge ────────────────────────────────────────────── */
+.study-notes-count-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 18px;
+ height: 18px;
+ border-radius: 9px;
+ background: rgba(201, 168, 76, 0.18);
+ color: #c9a84c;
+ font-size: 0.7rem;
+ font-weight: 600;
+ padding: 0 5px;
+ margin-left: 0.4rem;
+ font-family: var(--brand-font-body);
+ vertical-align: middle;
+}
+
+/* ── Inline reflection question answers ─────────────────────────── */
+.study-detail-list--questions {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+ margin-top: 0.25rem;
+}
+
+.study-question-item {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.study-question-text {
+ color: var(--brand-muted);
+ font-size: 0.93rem;
+ line-height: 1.6;
+ margin: 0;
+}
+
+.study-question-answer {
+ width: 100%;
+ box-sizing: border-box;
+ background: rgba(201, 168, 76, 0.04);
+ border: 1px solid rgba(201, 168, 76, 0.15);
+ border-radius: 8px;
+ padding: 0.65rem 0.85rem;
+ font-size: 0.88rem;
+ font-family: var(--brand-font-body);
+ color: #f0ead8;
+ resize: vertical;
+ min-height: 72px;
+ line-height: 1.6;
+ transition: border-color 150ms;
+}
+
+.study-question-answer:focus {
+ outline: none;
+ border-color: rgba(201, 168, 76, 0.4);
+}
+
+.study-question-answer::placeholder {
+ color: #3a3020;
+}
+
/* ── Sidebar anchor list ─────────────────────────────────────────── */
.note-anchor-list {
display: flex;
@@ -12369,3 +12550,81 @@
.admin-save-ok { color: #6ebd7c; font-size: 0.85rem; margin: 0; }
.admin-save-error { color: #e05c5c; font-size: 0.85rem; margin: 0; }
.admin-field-hint { font-size: 0.78rem; color: var(--admin-muted, #a0957e); margin: 0; }
+
+/* ── Community feed ──────────────────────────────────────────────────────── */
+.community-feed { display: flex; flex-direction: column; gap: 0; }
+
+.community-filter-bar { display: flex; gap: 0.4rem; flex-wrap: nowrap; overflow-x: auto; padding-bottom: 0.25rem; margin-bottom: 1.25rem; scrollbar-width: none; }
+.community-filter-bar::-webkit-scrollbar { display: none; }
+.community-filter-chip { flex-shrink: 0; padding: 0.3rem 0.85rem; border-radius: 999px; border: 1px solid var(--brand-border, #2a2518); background: transparent; color: var(--brand-muted, #b0a48c); font-size: 0.82rem; font-weight: 500; cursor: pointer; white-space: nowrap; transition: background 0.15s, color 0.15s, border-color 0.15s; }
+.community-filter-chip:hover { background: rgba(201,168,76,0.08); color: var(--brand-gold, #c9a84c); border-color: var(--brand-gold, #c9a84c); }
+.community-filter-chip--active { background: rgba(201,168,76,0.12); color: var(--brand-gold, #c9a84c); border-color: var(--brand-gold, #c9a84c); font-weight: 600; }
+
+/* Avatar */
+.community-avatar { border-radius: 50%; object-fit: cover; flex-shrink: 0; display: block; }
+.community-avatar--initials { background: #2a2518; display: grid; place-items: center; color: var(--brand-warm-white, #f0ead8); font-weight: 700; font-family: var(--brand-font-body, sans-serif); line-height: 1; }
+
+/* Compose card */
+.community-compose-card { display: flex; gap: 0.85rem; padding: 1.1rem 1.25rem; background: var(--brand-card-bg, #11100d); border: 1px solid var(--brand-border, #2a2518); border-radius: 14px; margin-bottom: 1.25rem; }
+.community-compose-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.6rem; }
+.community-compose-tag { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: var(--brand-muted, #b0a48c); }
+.community-compose-tag strong { color: var(--brand-gold, #c9a84c); }
+.community-compose-tag-clear { background: none; border: none; color: var(--brand-muted, #b0a48c); cursor: pointer; font-size: 1rem; line-height: 1; padding: 0 0.1rem; }
+.community-compose-textarea { width: 100%; padding: 0.7rem 0; background: transparent; border: none; border-bottom: 1px solid var(--brand-border, #2a2518); color: var(--brand-warm-white, #f0ead8); font-family: var(--brand-font-body, sans-serif); font-size: 0.975rem; resize: none; overflow: hidden; outline: none; box-sizing: border-box; transition: border-color 0.15s; }
+.community-compose-textarea:focus { border-bottom-color: var(--brand-gold, #c9a84c); }
+.community-compose-textarea::placeholder { color: var(--brand-muted, #b0a48c); }
+.community-compose-footer { display: flex; justify-content: flex-end; }
+.community-post-btn { font-size: 0.875rem; padding: 0.4rem 1.1rem; border-radius: 999px; }
+
+/* Status */
+.community-status-msg { color: var(--brand-muted, #b0a48c); font-size: 0.9rem; padding: 0.75rem 0; margin: 0; }
+.community-status-msg--error { color: #e07060; }
+
+/* Post list */
+.community-posts-list { display: flex; flex-direction: column; }
+
+/* Post card */
+.community-post-card { display: flex; gap: 0.75rem; padding: 1rem 0; border-bottom: 1px solid var(--brand-border, #2a2518); opacity: 1; transition: opacity 300ms; }
+.community-post-card--pending { opacity: 0.55; }
+.community-post-card:last-child { border-bottom: none; }
+.community-post-left { display: flex; flex-direction: column; align-items: center; gap: 0; padding-top: 0.15rem; }
+.community-thread-line { flex: 1; width: 2px; background: var(--brand-border, #2a2518); margin-top: 0.35rem; border-radius: 1px; min-height: 1rem; }
+.community-post-body { flex: 1; min-width: 0; }
+.community-post-header { display: flex; align-items: baseline; gap: 0.55rem; flex-wrap: wrap; margin-bottom: 0.25rem; }
+.community-post-author { font-weight: 700; font-size: 0.925rem; color: var(--brand-warm-white, #f0ead8); }
+.community-post-time { font-size: 0.8rem; color: var(--brand-muted, #b0a48c); }
+.community-lesson-chip { font-size: 0.73rem; font-weight: 500; padding: 0.15rem 0.55rem; border-radius: 999px; border: 1px solid var(--brand-border, #2a2518); background: transparent; color: var(--brand-muted, #b0a48c); cursor: pointer; transition: color 0.15s, border-color 0.15s; white-space: nowrap; }
+.community-lesson-chip:hover { color: var(--brand-gold, #c9a84c); border-color: var(--brand-gold, #c9a84c); }
+.community-post-text { font-size: 0.95rem; line-height: 1.6; color: var(--brand-warm-white, #f0ead8); white-space: pre-wrap; margin: 0 0 0.6rem; }
+
+/* Engagement bar */
+.community-engagement-bar { display: flex; align-items: center; gap: 0.15rem; margin-bottom: 0.25rem; }
+.community-action-btn { display: flex; align-items: center; gap: 0.3rem; background: none; border: none; color: var(--brand-muted, #b0a48c); cursor: pointer; padding: 0.3rem 0.6rem; border-radius: 999px; font-size: 0.82rem; font-weight: 500; transition: color 0.15s, background 0.15s; line-height: 1; }
+.community-action-btn:hover { color: var(--brand-gold, #c9a84c); background: rgba(201,168,76,0.08); }
+.community-action-btn--liked .community-action-icon { fill: #e05c5c; color: #e05c5c; }
+.community-action-btn--liked { color: #e05c5c; }
+.community-action-btn--liked:hover { background: rgba(224,92,92,0.08); color: #e05c5c; }
+.community-action-btn--active { color: var(--brand-gold, #c9a84c); }
+.community-action-icon { width: 17px; height: 17px; fill: currentColor; flex-shrink: 0; }
+
+/* Reply compose */
+.community-reply-compose { display: flex; gap: 0.65rem; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--brand-border, #2a2518); align-items: flex-start; }
+
+/* Replies thread */
+.community-replies { display: flex; flex-direction: column; gap: 0; margin-top: 0.5rem; padding-top: 0.5rem; }
+.community-reply { display: flex; gap: 0.6rem; padding: 0.65rem 0; border-top: 1px solid color-mix(in srgb, var(--brand-border, #2a2518) 60%, transparent); }
+.community-reply:first-child { border-top: none; }
+.community-reply-body { flex: 1; min-width: 0; }
+
+@media (max-width: 540px) {
+ .community-compose-card { padding: 0.9rem 1rem; }
+ .community-post-card { gap: 0.55rem; padding: 0.85rem 0; }
+}
+
+/* ── Community sidebar shortcut ─────────────────────────────────────────── */
+.study-class-side-block--community { padding: 0; border: 1px solid rgba(201,168,76,0.2); border-radius: 10px; overflow: hidden; }
+.study-community-shortcut { display: flex; align-items: center; gap: 0.6rem; padding: 0.75rem 1rem; color: var(--brand-gold, #c9a84c); font-size: 0.9rem; font-weight: 600; text-decoration: none; background: rgba(201,168,76,0.05); transition: background 0.15s; }
+.study-community-shortcut:hover { background: rgba(201,168,76,0.12); }
+.study-community-shortcut-icon { width: 16px; height: 16px; fill: currentColor; flex-shrink: 0; }
+.study-community-shortcut-arrow { margin-left: auto; opacity: 0.6; transition: transform 0.15s; }
+.study-community-shortcut:hover .study-community-shortcut-arrow { transform: translateX(3px); opacity: 1; }
diff --git a/src/App.tsx b/src/App.tsx
index 7c32a91..7904cca 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -716,6 +716,11 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|| isLikelySpotifyEpisodeUrl(link.url)
|| (link.discussionQuestions ?? []).length > 0
|| link.showNotes
+ || link.showNotesSummary
+ || (link.showNotesThemes ?? []).length > 0
+ || (link.showNotesScripture ?? []).length > 0
+ || (link.showNotesQuotes ?? []).length > 0
+ || (link.showNotesResources ?? []).length > 0
)
const dest = (!hasRichContent && link.url)
? { external: link.url }
@@ -1500,10 +1505,45 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
)}
- {episode.showNotes && (
+ {(episode.showNotesSummary || (episode.showNotesThemes ?? []).length > 0 || (episode.showNotesScripture ?? []).length > 0 || (episode.showNotesQuotes ?? []).length > 0 || (episode.showNotesResources ?? []).length > 0 || episode.showNotes) && (
Show Notes
-
{episode.showNotes}
+ {episode.showNotesSummary
+ ?
{episode.showNotesSummary}
+ : episode.showNotes && !(episode.showNotesThemes ?? []).length &&
{episode.showNotes}
+ }
+ {(episode.showNotesThemes ?? []).length > 0 && (
+
+
Key Themes
+
+ {episode.showNotesThemes!.map((t, i) => {t} )}
+
+
+ )}
+ {(episode.showNotesScripture ?? []).length > 0 && (
+
+
Scripture References
+
+ {episode.showNotesScripture!.map((s, i) => {s} )}
+
+
+ )}
+ {(episode.showNotesQuotes ?? []).length > 0 && (
+
+
Key Quotes
+ {episode.showNotesQuotes!.map((q, i) => (
+
{q}
+ ))}
+
+ )}
+ {(episode.showNotesResources ?? []).length > 0 && (
+
+
Resources
+
+ {episode.showNotesResources!.map((r, i) => {r} )}
+
+
+ )}
)}
diff --git a/src/colossiansStudy.tsx b/src/colossiansStudy.tsx
index 31e5cb7..83e3d0c 100644
--- a/src/colossiansStudy.tsx
+++ b/src/colossiansStudy.tsx
@@ -44,6 +44,8 @@ type StudyAccountOverview = {
displayName: string
subscribeNewsletter: boolean
studyRemindersEnabled: boolean
+ reminderFrequency?: string
+ remindersPausedUntil?: string | null
avatarUrl?: string
}
stats: {
@@ -69,6 +71,7 @@ type StudyNotesMap = Record
type StudyProgress = {
completedSectionIds: string[]
+ bookmarkedSectionIds: string[]
}
type StudyCommunityReply = {
@@ -88,6 +91,8 @@ type StudyCommunityPost = {
message: string
createdAt: string
replies: StudyCommunityReply[]
+ likeCount?: number
+ likedByMe?: boolean
pending?: boolean
}
@@ -225,10 +230,31 @@ function getDisplayName(auth: StudyAuthState) {
return auth.displayName?.trim() || auth.username.split('@')[0] || 'student'
}
-function formatCommunityDate(value: string) {
+function formatRelativeTime(value: string) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return ''
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
+ const diffMs = Date.now() - date.getTime()
+ const diffSec = Math.floor(diffMs / 1000)
+ if (diffSec < 60) return 'just now'
+ const diffMin = Math.floor(diffSec / 60)
+ if (diffMin < 60) return `${diffMin}m ago`
+ const diffHr = Math.floor(diffMin / 60)
+ if (diffHr < 24) return `${diffHr}h ago`
+ const diffDay = Math.floor(diffHr / 24)
+ if (diffDay < 7) return `${diffDay}d ago`
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
+}
+
+function UserAvatar({ name, url, size = 36 }: { name?: string; url?: string; size?: number }) {
+ if (url) {
+ return
+ }
+ const initial = (name || 'S').slice(0, 1).toUpperCase()
+ return (
+
+ {initial}
+
+ )
}
function CommunityBoard({
@@ -245,7 +271,6 @@ function CommunityBoard({
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
- const [message, setMessage] = useState('')
const [replyDrafts, setReplyDrafts] = useState>({})
const [replyingTo, setReplyingTo] = useState(null)
const [submitting, setSubmitting] = useState(false)
@@ -262,40 +287,25 @@ function CommunityBoard({
useEffect(() => {
let cancelled = false
-
async function load() {
- if (!canParticipate) {
- setPosts([])
- setLoading(false)
- return
- }
-
- setLoading(true)
- setError('')
+ if (!canParticipate) { setPosts([]); setLoading(false); return }
+ setLoading(true); setError('')
try {
const data = await readJson<{ posts?: StudyCommunityPost[] }>('/api/study-community?' + new URLSearchParams({ studySlug: study.slug }).toString())
- if (cancelled) return
- setPosts(Array.isArray(data.posts) ? data.posts : [])
+ if (!cancelled) setPosts(Array.isArray(data.posts) ? data.posts : [])
} catch (err) {
- if (cancelled) return
- setError(err instanceof Error ? err.message : 'Unable to load the community right now.')
+ if (!cancelled) setError(err instanceof Error ? err.message : 'Unable to load the community right now.')
} finally {
- if (cancelled) return
- setLoading(false)
+ if (!cancelled) setLoading(false)
}
}
-
void load()
- return () => {
- cancelled = true
- }
+ return () => { cancelled = true }
}, [canParticipate, study.slug])
async function submitPost() {
if (!newPost.trim() || !canParticipate) return
- setSubmitting(true)
- setMessage('')
- setError('')
+ setSubmitting(true); setError('')
const optimisticId = `pending-${Date.now()}`
const optimisticPost: StudyCommunityPost = {
id: optimisticId,
@@ -306,6 +316,8 @@ function CommunityBoard({
message: newPost,
createdAt: new Date().toISOString(),
replies: [],
+ likeCount: 0,
+ likedByMe: false,
pending: true,
}
setPosts(prev => [optimisticPost, ...prev])
@@ -318,9 +330,8 @@ function CommunityBoard({
body: JSON.stringify({ studySlug: study.slug, sectionId: activeSection, message: postedMessage }),
})
setPosts(prev => data.post
- ? prev.map(p => p.id === optimisticId ? { ...data.post!, pending: false } : p)
+ ? prev.map(p => p.id === optimisticId ? { ...data.post!, likeCount: 0, likedByMe: false, pending: false } : p)
: prev.filter(p => p.id !== optimisticId))
- setMessage('')
} catch (err) {
setPosts(prev => prev.filter(p => p.id !== optimisticId))
setNewPost(postedMessage)
@@ -333,8 +344,7 @@ function CommunityBoard({
async function submitReply(postId: string) {
const reply = replyDrafts[postId]?.trim()
if (!reply || !canParticipate) return
- setSubmitting(true)
- setError('')
+ setSubmitting(true); setError('')
try {
const data = await readJson<{ reply?: StudyCommunityReply }>(`/api/study-community/posts/${encodeURIComponent(postId)}/replies`, {
method: 'POST',
@@ -346,7 +356,6 @@ function CommunityBoard({
}
setReplyDrafts(prev => ({ ...prev, [postId]: '' }))
setReplyingTo(null)
- setMessage('Reply posted.')
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to post your reply.')
} finally {
@@ -354,34 +363,41 @@ function CommunityBoard({
}
}
+ async function toggleLike(postId: string) {
+ if (!canParticipate) return
+ setPosts(prev => prev.map(p => p.id === postId
+ ? { ...p, likedByMe: !p.likedByMe, likeCount: (p.likeCount ?? 0) + (p.likedByMe ? -1 : 1) }
+ : p))
+ try {
+ await readJson(`/api/study-community/posts/${encodeURIComponent(postId)}/like`, {
+ method: 'POST',
+ })
+ } catch {
+ setPosts(prev => prev.map(p => p.id === postId
+ ? { ...p, likedByMe: !p.likedByMe, likeCount: (p.likeCount ?? 0) + (p.likedByMe ? -1 : 1) }
+ : p))
+ }
+ }
+
return (
-
+
{/* Section filter tabs */}
{releasedSections.length > 1 && canParticipate && (
-
-
setActiveSection('')}
- >All lessons
+
+ setActiveSection('')}>All
{releasedSections.map(s => (
- setActiveSection(s.id)}
- >{s.title}
+ setActiveSection(s.id)}>{s.title}
))}
)}
- {!auth.checked &&
Checking your account status...
}
+ {!auth.checked &&
Checking your account status...
}
{auth.checked && !auth.authenticated && (
Sign in to join the study community.
Create Account
- My Account
+ Sign In
)}
@@ -395,112 +411,131 @@ function CommunityBoard({
)}
{canParticipate && (
-
- {activeSection && (
-
- Posting to: {releasedSections.find(s => s.id === activeSection)?.title ?? 'this lesson'}
-
- )}
-