Redesign community section with social media feed UX; v1.2.6

- Twitter/X-style post cards: avatar thread line, author + relative timestamp
- Heart like button with optimistic toggle and like count per post
- Comment button with reply count; inline threaded replies always visible
- Compose card with user avatar, borderless textarea, pill Post button
- Horizontal scrollable lesson filter chips (All + per-lesson)
- New POST /api/study-community/posts/:postId/like endpoint (toggle)
- GET community posts now returns likedByMe + likeCount per post
- sanitizeStudyCommunityPosts preserves likedBy array on disk
- Relative timestamps (just now, Xm ago, Xh ago, Xd ago, then date)
- UserAvatar helper renders img or initials fallback consistently

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-03 16:34:28 -04:00
1 parent 874dd0c89b
commit 49e3a80c2c
5 files changed
+261 -135

No files matched your search

+2
View File
@@ -1523,6 +1523,7 @@ export function sanitizeStudyCommunityPosts(value) {
? item.replies.map(sanitizeStudyCommunityReply).filter(Boolean).slice(0, 50)
: []
if (!studySlug || !message) return null
const likedBy = Array.isArray(item.likedBy) ? item.likedBy.filter(id => typeof id === 'string' && id.trim()).slice(0, 1000) : []
return {
id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
studySlug,
@@ -1532,6 +1533,7 @@ export function sanitizeStudyCommunityPosts(value) {
message,
createdAt: typeof item.createdAt === 'string' ? item.createdAt : new Date().toISOString(),
replies,
likedBy,
}
})
.filter(Boolean)
+22
View File
@@ -363,9 +363,12 @@ export function register(app) {
.slice(0, 50)
.map(post => {
const author = findStudyUserById(post.authorUserId)
const likedBy = Array.isArray(post.likedBy) ? post.likedBy : []
return {
...post,
authorAvatarUrl: getStudyAvatarUrl(author || post.authorName || ''),
likedByMe: likedBy.includes(user.id),
likeCount: likedBy.length,
replies: Array.isArray(post.replies)
? post.replies.map(reply => {
const replyAuthor = findStudyUserById(reply.authorUserId)
@@ -452,4 +455,23 @@ export function register(app) {
res.json({ ok: true, reply })
})
app.post('/api/study-community/posts/:postId/like', requireStudyAuth, (req, res) => {
const user = req.studyUser
const postId = typeof req.params.postId === 'string' ? req.params.postId.trim() : ''
const post = state.studyCommunityPosts.find(item => item.id === postId)
if (!post) { res.status(404).json({ message: 'Post not found.' }); return }
if (!isStudyUserEnrolled(user, post.studySlug)) {
res.status(403).json({ message: 'Please enroll in this study to like posts.' }); return
}
if (!Array.isArray(post.likedBy)) post.likedBy = []
const idx = post.likedBy.indexOf(user.id)
if (idx === -1) {
post.likedBy.push(user.id)
} else {
post.likedBy.splice(idx, 1)
}
queueStudyCommunityWrite()
res.json({ ok: true, likeCount: post.likedBy.length, likedByMe: post.likedBy.includes(user.id) })
})
}