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
106 lines
3.8 KiB
JavaScript
106 lines
3.8 KiB
JavaScript
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'
|
|
|
|
export default function SavedFeed() {
|
|
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 fetchFeed = useCallback(async (p = 1) => {
|
|
setLoading(true)
|
|
try {
|
|
const { data } = await axios.get('/api/posts/saved', { 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)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => { fetchFeed(1) }, [])
|
|
|
|
const handleDeleted = useCallback((id) => setPosts((prev) => prev.filter((p) => p.id !== id)), [])
|
|
|
|
// When a post is unsaved, remove it from the list too
|
|
const handleUnsaved = useCallback((id) => setPosts((prev) => prev.filter((p) => p.id !== id)), [])
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#080f1e]">
|
|
<div className="max-w-2xl mx-auto px-4 pt-8 pb-16">
|
|
{/* Header */}
|
|
<div className="mb-6">
|
|
<h1 className="text-xl font-bold text-white">
|
|
<i className="fa-solid fa-bookmark mr-2 text-amber-400 opacity-80" />
|
|
Saved Posts
|
|
</h1>
|
|
<p className="text-sm text-slate-500 mt-0.5">Posts you've bookmarked</p>
|
|
</div>
|
|
|
|
{/* Feed */}
|
|
<div className="space-y-4">
|
|
{!loaded && loading && (
|
|
<>{Array.from({ length: 3 }).map((_, i) => <PostCardSkeleton key={i} />)}</>
|
|
)}
|
|
|
|
{loaded && !loading && posts.length === 0 && (
|
|
<div className="flex flex-col items-center justify-center py-24 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-solid fa-bookmark text-2xl" />
|
|
</div>
|
|
<h2 className="text-lg font-semibold text-white/80 mb-2">Nothing saved yet</h2>
|
|
<p className="text-slate-500 text-sm max-w-xs leading-relaxed">
|
|
Bookmark posts to read later. Look for the{' '}
|
|
<i className="fa-regular fa-bookmark text-amber-400" /> icon on any post.
|
|
</p>
|
|
<a
|
|
href="/feed/trending"
|
|
className="mt-4 text-sm text-sky-400 hover:text-sky-300 transition-colors"
|
|
>
|
|
Browse trending posts →
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{posts.map((post) => (
|
|
<PostCard
|
|
key={post.id}
|
|
post={post}
|
|
isLoggedIn={!!authUser}
|
|
viewerUsername={authUser?.username ?? null}
|
|
onDelete={handleDeleted}
|
|
onUnsaved={handleUnsaved}
|
|
/>
|
|
))}
|
|
|
|
{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>
|
|
</div>
|
|
)
|
|
}
|