161 lines
5.9 KiB
JavaScript
161 lines
5.9 KiB
JavaScript
import React, { useState } from 'react'
|
|
import FollowButton from './FollowButton'
|
|
import LikeButton from './LikeButton'
|
|
import BookmarkButton from './BookmarkButton'
|
|
import CommentForm from './CommentForm'
|
|
import CommentList from './CommentList'
|
|
|
|
export default function StorySocialPanel({ story, creator, initialState, initialComments, isAuthenticated = false }) {
|
|
const [state, setState] = useState({
|
|
liked: Boolean(initialState?.liked),
|
|
bookmarked: Boolean(initialState?.bookmarked),
|
|
likesCount: Number(initialState?.likes_count || 0),
|
|
commentsCount: Number(initialState?.comments_count || 0),
|
|
bookmarksCount: Number(initialState?.bookmarks_count || 0),
|
|
})
|
|
const [comments, setComments] = useState(Array.isArray(initialComments) ? initialComments : [])
|
|
const csrfToken = typeof document !== 'undefined'
|
|
? document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
: null
|
|
|
|
const postJson = async (url, method = 'POST', body = null) => {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken || '',
|
|
},
|
|
credentials: 'same-origin',
|
|
body: body ? JSON.stringify(body) : null,
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const payload = await response.json().catch(() => ({}))
|
|
throw new Error(payload?.message || 'Request failed.')
|
|
}
|
|
|
|
return response.json()
|
|
}
|
|
|
|
const refreshCounts = (nextComments) => {
|
|
const countReplies = (items) => items.reduce((sum, item) => sum + 1 + countReplies(item.replies || []), 0)
|
|
return countReplies(nextComments)
|
|
}
|
|
|
|
const insertReply = (items, parentId, newComment) => items.map((item) => {
|
|
if (item.id === parentId) {
|
|
return { ...item, replies: [...(item.replies || []), newComment] }
|
|
}
|
|
|
|
if (Array.isArray(item.replies) && item.replies.length > 0) {
|
|
return { ...item, replies: insertReply(item.replies, parentId, newComment) }
|
|
}
|
|
|
|
return item
|
|
})
|
|
|
|
const deleteCommentRecursive = (items, commentId) => items
|
|
.filter((item) => item.id !== commentId)
|
|
.map((item) => ({
|
|
...item,
|
|
replies: Array.isArray(item.replies) ? deleteCommentRecursive(item.replies, commentId) : [],
|
|
}))
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<LikeButton
|
|
active={state.liked}
|
|
count={state.likesCount}
|
|
onToggle={async () => {
|
|
if (!isAuthenticated) {
|
|
window.location.href = '/login'
|
|
return
|
|
}
|
|
|
|
const payload = await postJson(`/api/stories/${story.id}/like`, 'POST', { state: !state.liked })
|
|
setState((current) => ({
|
|
...current,
|
|
liked: Boolean(payload?.liked),
|
|
likesCount: Number(payload?.likes_count || 0),
|
|
}))
|
|
}}
|
|
/>
|
|
|
|
<BookmarkButton
|
|
active={state.bookmarked}
|
|
count={state.bookmarksCount}
|
|
onToggle={async () => {
|
|
if (!isAuthenticated) {
|
|
window.location.href = '/login'
|
|
return
|
|
}
|
|
|
|
const payload = await postJson(`/api/stories/${story.id}/bookmark`, 'POST', { state: !state.bookmarked })
|
|
setState((current) => ({
|
|
...current,
|
|
bookmarked: Boolean(payload?.bookmarked),
|
|
bookmarksCount: Number(payload?.bookmarks_count || 0),
|
|
}))
|
|
}}
|
|
/>
|
|
|
|
{creator?.username ? (
|
|
<FollowButton
|
|
username={creator.username}
|
|
initialFollowing={Boolean(initialState?.is_following_creator)}
|
|
initialCount={Number(creator.followers_count || 0)}
|
|
className="min-w-[11rem]"
|
|
/>
|
|
) : null}
|
|
</div>
|
|
|
|
<section className="rounded-2xl border border-white/[0.08] bg-white/[0.04] p-5">
|
|
<div className="mb-4 flex items-center justify-between gap-3">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white">Discussion</h2>
|
|
<p className="text-sm text-white/40">{state.commentsCount.toLocaleString()} comments on this story</p>
|
|
</div>
|
|
</div>
|
|
|
|
{isAuthenticated ? (
|
|
<div className="mb-5">
|
|
<CommentForm
|
|
placeholder="Add to the story discussion…"
|
|
submitLabel="Post Comment"
|
|
onSubmit={async (content) => {
|
|
const payload = await postJson(`/api/stories/${story.id}/comments`, 'POST', { content })
|
|
const nextComments = [payload.data, ...comments]
|
|
setComments(nextComments)
|
|
setState((current) => ({ ...current, commentsCount: refreshCounts(nextComments) }))
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<p className="mb-5 text-sm text-white/45">
|
|
<a href="/login" className="text-sky-300 hover:text-sky-200">Sign in</a> to join the discussion.
|
|
</p>
|
|
)}
|
|
|
|
<CommentList
|
|
comments={comments}
|
|
canReply={isAuthenticated}
|
|
emptyMessage="No comments yet. Start the discussion."
|
|
onReply={async (parentId, content) => {
|
|
const payload = await postJson(`/api/stories/${story.id}/comments`, 'POST', { content, parent_id: parentId })
|
|
const nextComments = insertReply(comments, parentId, payload.data)
|
|
setComments(nextComments)
|
|
setState((current) => ({ ...current, commentsCount: refreshCounts(nextComments) }))
|
|
}}
|
|
onDelete={async (commentId) => {
|
|
await postJson(`/api/stories/${story.id}/comments/${commentId}`, 'DELETE')
|
|
const nextComments = deleteCommentRecursive(comments, commentId)
|
|
setComments(nextComments)
|
|
setState((current) => ({ ...current, commentsCount: refreshCounts(nextComments) }))
|
|
}}
|
|
/>
|
|
</section>
|
|
</div>
|
|
)
|
|
} |