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,177 @@
import React, { useState, useEffect, useCallback } from 'react'
import { usePage } from '@inertiajs/react'
import ProfileHero from '../../Components/Profile/ProfileHero'
import ProfileStatsRow from '../../Components/Profile/ProfileStatsRow'
import ProfileTabs from '../../Components/Profile/ProfileTabs'
import TabArtworks from '../../Components/Profile/tabs/TabArtworks'
import TabAbout from '../../Components/Profile/tabs/TabAbout'
import TabStats from '../../Components/Profile/tabs/TabStats'
import TabFavourites from '../../Components/Profile/tabs/TabFavourites'
import TabCollections from '../../Components/Profile/tabs/TabCollections'
import TabActivity from '../../Components/Profile/tabs/TabActivity'
import TabPosts from '../../Components/Profile/tabs/TabPosts'
const VALID_TABS = ['artworks', 'posts', 'collections', 'about', 'stats', 'favourites', 'activity']
function getInitialTab() {
try {
const sp = new URLSearchParams(window.location.search)
const t = sp.get('tab')
return VALID_TABS.includes(t) ? t : 'artworks'
} catch {
return 'artworks'
}
}
/**
* ProfileShow Inertia page for /@username
*
* Props injected by ProfileController::renderUserProfile()
*/
export default function ProfileShow() {
const { props } = usePage()
const {
user,
profile,
artworks,
featuredArtworks,
favourites,
stats,
socialLinks,
followerCount,
recentFollowers,
viewerIsFollowing,
heroBgUrl,
profileComments,
countryName,
isOwner,
auth,
} = props
const [activeTab, setActiveTab] = useState(getInitialTab)
const handleTabChange = useCallback((tab) => {
if (!VALID_TABS.includes(tab)) return
setActiveTab(tab)
// Update URL query param without full navigation
try {
const url = new URL(window.location.href)
if (tab === 'artworks') {
url.searchParams.delete('tab')
} else {
url.searchParams.set('tab', tab)
}
window.history.pushState({}, '', url.toString())
} catch (_) {}
}, [])
// Handle browser back/forward
useEffect(() => {
const onPop = () => setActiveTab(getInitialTab())
window.addEventListener('popstate', onPop)
return () => window.removeEventListener('popstate', onPop)
}, [])
const isLoggedIn = !!(auth?.user)
// Normalise artwork list (SSR may send cursor-paginated object)
const artworkList = Array.isArray(artworks)
? artworks
: (artworks?.data ?? [])
const artworkNextCursor = artworks?.next_cursor ?? null
// Normalise social links (may be object keyed by platform, or array)
const socialLinksObj = Array.isArray(socialLinks)
? socialLinks.reduce((acc, l) => { acc[l.platform] = l; return acc }, {})
: (socialLinks ?? {})
return (
<div className="min-h-screen pb-16">
{/* Hero section */}
<ProfileHero
user={user}
profile={profile}
isOwner={isOwner}
viewerIsFollowing={viewerIsFollowing}
followerCount={followerCount}
heroBgUrl={heroBgUrl}
countryName={countryName}
/>
{/* Stats pills row */}
<ProfileStatsRow
stats={stats}
followerCount={followerCount}
onTabChange={handleTabChange}
/>
{/* Sticky tabs */}
<ProfileTabs
activeTab={activeTab}
onTabChange={handleTabChange}
/>
{/* Tab content area */}
<div className="max-w-6xl mx-auto px-4">
{activeTab === 'artworks' && (
<TabArtworks
artworks={{ data: artworkList, next_cursor: artworkNextCursor }}
featuredArtworks={featuredArtworks}
username={user.username || user.name}
isActive
/>
)}
{activeTab === 'posts' && (
<TabPosts
username={user.username || user.name}
isOwner={isOwner}
authUser={auth?.user ?? null}
user={user}
profile={profile}
stats={stats}
followerCount={followerCount}
recentFollowers={recentFollowers}
socialLinks={socialLinksObj}
countryName={countryName}
onTabChange={handleTabChange}
/>
)}
{activeTab === 'collections' && (
<TabCollections collections={[]} />
)}
{activeTab === 'about' && (
<TabAbout
user={user}
profile={profile}
socialLinks={socialLinksObj}
countryName={countryName}
followerCount={followerCount}
/>
)}
{activeTab === 'stats' && (
<TabStats
stats={stats}
followerCount={followerCount}
/>
)}
{activeTab === 'favourites' && (
<TabFavourites
favourites={favourites}
isOwner={isOwner}
username={user.username || user.name}
/>
)}
{activeTab === 'activity' && (
<TabActivity
profileComments={profileComments}
user={user}
isOwner={isOwner}
isLoggedIn={isLoggedIn}
/>
)}
</div>
</div>
)
}