feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
Forum: - TipTap WYSIWYG editor with full toolbar - @emoji-mart/react emoji picker (consistent with tweets) - @mention autocomplete with user search API - Fix PHP 8.4 parse errors in Blade templates - Fix thread data display (paginator items) - Align forum page widths to max-w-5xl Discover: - Extract shared _nav.blade.php partial - Add missing nav links to for-you page - Add Following link for authenticated users Feed/Posts: - Post model, controllers, policies, migrations - Feed page components (PostComposer, FeedCard, etc) - Post reactions, comments, saves, reports, sharing - Scheduled publishing support - Link preview controller Profile: - Profile page components (ProfileHero, ProfileTabs) - Profile API controller Uploads: - Upload wizard enhancements - Scheduled publish picker - Studio status bar and readiness checklist
This commit is contained in:
139
resources/js/components/Feed/PostActions.jsx
Normal file
139
resources/js/components/Feed/PostActions.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, { useState } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
/**
|
||||
* PostActions: Like toggle, Comment toggle, Share menu, Report
|
||||
*/
|
||||
export default function PostActions({
|
||||
post,
|
||||
isLoggedIn,
|
||||
onCommentToggle,
|
||||
onReactionChange,
|
||||
}) {
|
||||
const [liked, setLiked] = useState(post.viewer_liked ?? false)
|
||||
const [likeCount, setLikeCount] = useState(post.reactions_count ?? 0)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [shareMsg, setShareMsg] = useState(null)
|
||||
|
||||
const handleLike = async () => {
|
||||
if (!isLoggedIn) {
|
||||
window.location.href = '/login'
|
||||
return
|
||||
}
|
||||
if (busy) return
|
||||
setBusy(true)
|
||||
try {
|
||||
if (liked) {
|
||||
await axios.delete(`/api/posts/${post.id}/reactions/like`)
|
||||
setLiked(false)
|
||||
setLikeCount((c) => Math.max(0, c - 1))
|
||||
onReactionChange?.({ liked: false, count: Math.max(0, likeCount - 1) })
|
||||
} else {
|
||||
await axios.post(`/api/posts/${post.id}/reactions`, { reaction: 'like' })
|
||||
setLiked(true)
|
||||
setLikeCount((c) => c + 1)
|
||||
onReactionChange?.({ liked: true, count: likeCount + 1 })
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const url = `${window.location.origin}/@${post.author.username}?tab=posts&post=${post.id}`
|
||||
navigator.clipboard?.writeText(url)
|
||||
setShareMsg('Link copied!')
|
||||
setTimeout(() => setShareMsg(null), 2000)
|
||||
setMenuOpen(false)
|
||||
}
|
||||
|
||||
const handleReport = async () => {
|
||||
setMenuOpen(false)
|
||||
const reason = window.prompt('Why are you reporting this post? (required)')
|
||||
if (!reason?.trim()) return
|
||||
try {
|
||||
await axios.post(`/api/posts/${post.id}/report`, { reason: reason.trim() })
|
||||
alert('Report submitted. Thank you!')
|
||||
} catch (err) {
|
||||
if (err.response?.data?.message) {
|
||||
alert(err.response.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-slate-400 relative">
|
||||
{/* Like */}
|
||||
<button
|
||||
onClick={handleLike}
|
||||
disabled={busy}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors ${
|
||||
liked
|
||||
? 'text-sky-400 bg-sky-500/10 hover:bg-sky-500/20'
|
||||
: 'hover:bg-white/5 hover:text-slate-200'
|
||||
}`}
|
||||
title={liked ? 'Unlike' : 'Like'}
|
||||
aria-label={liked ? 'Unlike this post' : 'Like this post'}
|
||||
>
|
||||
<i className={`fa-${liked ? 'solid' : 'regular'} fa-heart fa-fw text-xs`} />
|
||||
<span className="tabular-nums">{likeCount > 0 && likeCount}</span>
|
||||
</button>
|
||||
|
||||
{/* Comment toggle */}
|
||||
<button
|
||||
onClick={onCommentToggle}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm hover:bg-white/5 hover:text-slate-200 transition-colors"
|
||||
title="Comment"
|
||||
aria-label="Show comments"
|
||||
>
|
||||
<i className="fa-regular fa-comment fa-fw text-xs" />
|
||||
<span className="tabular-nums">{post.comments_count > 0 && post.comments_count}</span>
|
||||
</button>
|
||||
|
||||
{/* Share / More menu */}
|
||||
<div className="relative ml-auto">
|
||||
<button
|
||||
onClick={() => setMenuOpen((v) => !v)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm hover:bg-white/5 hover:text-slate-200 transition-colors"
|
||||
aria-label="More options"
|
||||
>
|
||||
<i className="fa-solid fa-ellipsis-h fa-fw text-xs" />
|
||||
</button>
|
||||
|
||||
{menuOpen && (
|
||||
<div
|
||||
className="absolute right-0 bottom-full mb-1 w-44 rounded-xl border border-white/10 bg-[#10192e] shadow-2xl z-50 overflow-hidden"
|
||||
onBlur={() => setMenuOpen(false)}
|
||||
>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="w-full text-left px-4 py-2.5 text-sm text-slate-300 hover:bg-white/5 flex items-center gap-2"
|
||||
>
|
||||
<i className="fa-solid fa-link fa-fw opacity-60" />
|
||||
Copy link
|
||||
</button>
|
||||
{isLoggedIn && (
|
||||
<button
|
||||
onClick={handleReport}
|
||||
className="w-full text-left px-4 py-2.5 text-sm text-rose-400 hover:bg-white/5 flex items-center gap-2"
|
||||
>
|
||||
<i className="fa-solid fa-flag fa-fw opacity-60" />
|
||||
Report post
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Share feedback toast */}
|
||||
{shareMsg && (
|
||||
<span className="absolute -top-8 right-0 text-xs bg-slate-800 text-white px-2 py-1 rounded shadow">
|
||||
{shareMsg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user