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:
1 parent
874dd0c89b
commit
49e3a80c2c
5 files changed
+261
-135
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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) })
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user