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:
2026-03-03 09:48:31 +01:00
parent 1266f81d35
commit dc51d65440
178 changed files with 14308 additions and 665 deletions

View File

@@ -0,0 +1,154 @@
import React, { useState, useEffect, useCallback } from 'react'
import { usePage } from '@inertiajs/react'
import axios from 'axios'
import PostCard from '../../Components/Feed/PostCard'
import PostCardSkeleton from '../../Components/Feed/PostCardSkeleton'
const FILTER_OPTIONS = [
{ value: 'all', label: 'All' },
{ value: 'shares', label: 'Artwork Shares' },
{ value: 'uploads', label: 'New Uploads' },
{ value: 'text', label: 'Text Posts' },
]
function EmptyFollowingState() {
return (
<div className="flex flex-col items-center justify-center py-24 text-center">
<div className="w-20 h-20 rounded-2xl bg-white/5 flex items-center justify-center mb-5 text-slate-600">
<i className="fa-solid fa-users text-3xl" />
</div>
<h2 className="text-lg font-semibold text-white/80 mb-2">Nothing here yet</h2>
<p className="text-slate-500 text-sm max-w-sm leading-relaxed">
Follow some creators to see their posts here. Discover amazing artwork on{' '}
<a href="/discover/trending" className="text-sky-400 hover:underline">Trending</a>.
</p>
</div>
)
}
export default function FollowingFeed() {
const { props } = usePage()
const { auth } = props
const authUser = auth?.user ?? null
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(false)
const [loaded, setLoaded] = useState(false)
const [filter, setFilter] = useState('all')
const fetchFeed = useCallback(async (p = 1, f = filter) => {
setLoading(true)
try {
const { data } = await axios.get('/api/posts/following', {
params: { page: p, filter: f },
})
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)
}
}, [filter])
useEffect(() => {
fetchFeed(1, filter)
}, [filter])
const handleFilterChange = (f) => {
if (f === filter) return
setFilter(f)
setPosts([])
setLoaded(false)
setPage(1)
}
const handleDeleted = useCallback((postId) => {
setPosts((prev) => prev.filter((p) => p.id !== postId))
}, [])
return (
<div className="min-h-screen bg-[#080f1e]">
{/* ── Page header ────────────────────────────────────────────────────── */}
<div className="max-w-2xl mx-auto px-4 pt-8 pb-4">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-xl font-bold text-white">
<i className="fa-solid fa-users-rays mr-2 text-sky-400 opacity-80" />
Following Feed
</h1>
<p className="text-sm text-slate-500 mt-0.5">Posts from creators you follow</p>
</div>
<a
href="/discover/trending"
className="text-xs text-sky-400 hover:text-sky-300 transition-colors"
>
Discover creators
</a>
</div>
{/* Filter chips */}
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-hide">
{FILTER_OPTIONS.map((f) => (
<button
key={f.value}
onClick={() => handleFilterChange(f.value)}
className={`px-3.5 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all border ${
filter === f.value
? 'bg-sky-600/20 border-sky-500/40 text-sky-300'
: 'bg-white/[0.03] border-white/[0.06] text-slate-400 hover:text-white hover:border-white/10'
}`}
>
{f.label}
</button>
))}
</div>
</div>
{/* ── Feed ────────────────────────────────────────────────────────────── */}
<div className="max-w-2xl mx-auto px-4 pb-16 space-y-4">
{/* Loading skeletons */}
{!loaded && loading && (
<>
<PostCardSkeleton />
<PostCardSkeleton />
<PostCardSkeleton />
</>
)}
{/* Empty */}
{loaded && !loading && posts.length === 0 && <EmptyFollowingState />}
{/* Posts */}
{posts.map((post) => (
<PostCard
key={post.id}
post={post}
isLoggedIn={!!authUser}
viewerUsername={authUser?.username ?? null}
onDelete={handleDeleted}
/>
))}
{/* 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'}
</button>
</div>
)}
</div>
</div>
)
}