Files
SkinbaseNova/resources/js/components/profile/tabs/TabPosts.jsx
Gregor Klevze dc51d65440 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
2026-03-03 09:48:31 +01:00

159 lines
5.0 KiB
JavaScript

import React, { useState, useCallback } from 'react'
import axios from 'axios'
import PostCard from '../../Feed/PostCard'
import PostComposer from '../../Feed/PostComposer'
import PostCardSkeleton from '../../Feed/PostCardSkeleton'
import FeedSidebar from '../../Feed/FeedSidebar'
function EmptyPostsState({ isOwner, username }) {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-4 text-slate-600">
<i className="fa-regular fa-newspaper text-2xl" />
</div>
<p className="text-slate-400 font-medium mb-1">No posts yet</p>
{isOwner ? (
<p className="text-slate-600 text-sm max-w-xs">
Share your thoughts or showcase your artworks. Your first post is a tap away.
</p>
) : (
<p className="text-slate-600 text-sm">@{username} hasn't posted anything yet.</p>
)}
</div>
)
}
/**
* TabPosts
* Profile Posts tab — shows the user's post feed with optional composer (for owner).
*
* Props:
* username string
* isOwner boolean
* authUser object|null { id, username, name, avatar }
* user object full user from ProfileController
* profile object
* stats object|null
* followerCount number
* recentFollowers array
* socialLinks object
* countryName string|null
* onTabChange function(tab)
*/
export default function TabPosts({
username,
isOwner,
authUser,
user,
profile,
stats,
followerCount,
recentFollowers,
socialLinks,
countryName,
onTabChange,
}) {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(false)
const [loaded, setLoaded] = useState(false)
// Fetch on mount
React.useEffect(() => {
fetchFeed(1)
}, [username])
const fetchFeed = async (p = 1) => {
setLoading(true)
try {
const { data } = await axios.get(`/api/posts/profile/${username}`, { params: { page: p } })
setPosts((prev) => p === 1 ? data.data : [...prev, ...data.data])
setHasMore(data.meta.current_page < data.meta.last_page)
setPage(p)
} catch {
//
} finally {
setLoading(false)
setLoaded(true)
}
}
const handlePosted = useCallback((newPost) => {
setPosts((prev) => [newPost, ...prev])
}, [])
const handleDeleted = useCallback((postId) => {
setPosts((prev) => prev.filter((p) => p.id !== postId))
}, [])
return (
<div className="flex gap-6 py-4 items-start">
{/* ── Main feed column ──────────────────────────────────────────────── */}
<div className="flex-1 min-w-0 space-y-4">
{/* Composer (owner only) */}
{isOwner && authUser && (
<PostComposer user={authUser} onPosted={handlePosted} />
)}
{/* Skeletons while loading */}
{!loaded && loading && (
<div className="space-y-4">
{[1, 2, 3].map((i) => <PostCardSkeleton key={i} />)}
</div>
)}
{/* Empty state */}
{loaded && !loading && posts.length === 0 && (
<EmptyPostsState isOwner={isOwner} username={username} />
)}
{/* Post list */}
{posts.length > 0 && (
<div className="space-y-4">
{posts.map((post) => (
<PostCard
key={post.id}
post={post}
isLoggedIn={!!authUser}
viewerUsername={authUser?.username ?? null}
onDelete={handleDeleted}
/>
))}
</div>
)}
{/* Load more */}
{loaded && hasMore && (
<div className="flex justify-center py-4">
<button
onClick={() => fetchFeed(page + 1)}
disabled={loading}
className="px-6 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 text-sm transition-colors disabled:opacity-50"
>
{loading ? (
<><i className="fa-solid fa-spinner fa-spin mr-2" />Loading</>
) : 'Load more posts'}
</button>
</div>
)}
</div>
{/* ── Sidebar ───────────────────────────────────────────────────────── */}
<aside className="w-72 xl:w-80 shrink-0 hidden lg:block sticky top-20 self-start">
<FeedSidebar
user={user}
profile={profile}
stats={stats}
followerCount={followerCount}
recentFollowers={recentFollowers}
socialLinks={socialLinks}
countryName={countryName}
isLoggedIn={!!authUser}
onTabChange={onTabChange}
/>
</aside>
</div>
)
}