Files
SkinbaseNova/resources/js/components/social/StorySocialPanel.jsx
2026-03-20 21:17:26 +01:00

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>
)
}