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

@@ -196,6 +196,7 @@ export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStats
const [reporting, setReporting] = useState(false)
const [reported, setReported] = useState(false)
const [reportOpen, setReportOpen] = useState(false)
const isLoggedIn = artwork?.viewer != null
useEffect(() => {
setFavorited(Boolean(artwork?.viewer?.is_favorited))
}, [artwork?.id, artwork?.viewer?.is_favorited])
@@ -311,7 +312,7 @@ export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStats
</div>
{/* Share pill */}
<ArtworkShareButton artwork={artwork} shareUrl={shareUrl} size="default" />
<ArtworkShareButton artwork={artwork} shareUrl={shareUrl} size="default" isLoggedIn={isLoggedIn} />
{/* Report pill */}
<button
@@ -362,7 +363,7 @@ export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStats
</button>
{/* Share */}
<ArtworkShareButton artwork={artwork} shareUrl={shareUrl} size="small" />
<ArtworkShareButton artwork={artwork} shareUrl={shareUrl} size="small" isLoggedIn={isLoggedIn} />
{/* Report */}
<button

View File

@@ -20,7 +20,7 @@ function ShareIcon() {
* shareUrl canonical URL to share
* size 'default' | 'small' (for mobile bar)
*/
export default function ArtworkShareButton({ artwork, shareUrl, size = 'default' }) {
export default function ArtworkShareButton({ artwork, shareUrl, size = 'default', isLoggedIn = false }) {
const [modalOpen, setModalOpen] = useState(false)
const openModal = useCallback(
@@ -69,6 +69,7 @@ export default function ArtworkShareButton({ artwork, shareUrl, size = 'default'
onClose={closeModal}
artwork={artwork}
shareUrl={shareUrl}
isLoggedIn={isLoggedIn}
/>
</Suspense>
)}

View File

@@ -1,7 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import ShareToast from '../ui/ShareToast'
// Lazy-load the Feed share modal so artwork pages don't bundle the feed layer unless needed
const FeedShareArtworkModal = lazy(() => import('../Feed/ShareArtworkModal'))
/* ── Platform share URLs ─────────────────────────────────────────────────── */
function facebookUrl(url) {
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`
@@ -107,13 +110,14 @@ function trackShare(artworkId, platform) {
* artwork artwork object (id, title, description, thumbs, canonical_url, …)
* shareUrl canonical share URL
*/
export default function ArtworkShareModal({ open, onClose, artwork, shareUrl }) {
export default function ArtworkShareModal({ open, onClose, artwork, shareUrl, isLoggedIn = false }) {
const backdropRef = useRef(null)
const [linkCopied, setLinkCopied] = useState(false)
const [embedCopied, setEmbedCopied] = useState(false)
const [showEmbed, setShowEmbed] = useState(false)
const [toastVisible, setToastVisible] = useState(false)
const [toastMessage, setToastMessage] = useState('')
const [profileShareOpen, setProfileShareOpen] = useState(false)
const url = shareUrl || artwork?.canonical_url || (typeof window !== 'undefined' ? window.location.href : '#')
const title = artwork?.title || 'Artwork'
@@ -213,6 +217,12 @@ export default function ArtworkShareModal({ open, onClose, artwork, shareUrl })
onClick: () => { window.location.href = emailUrl(url, title); trackShare(artwork?.id, 'email') },
className: 'border-white/[0.08] bg-white/[0.04] text-white/70 hover:border-white/[0.15] hover:bg-white/[0.07] hover:text-white',
},
...(isLoggedIn ? [{
label: 'My Profile',
icon: <i className="fa-solid fa-share-nodes h-5 w-5 text-[1.1rem]" />,
onClick: () => setProfileShareOpen(true),
className: 'border-sky-500/30 bg-sky-500/10 text-sky-400 hover:border-sky-400/50 hover:bg-sky-500/20',
}] : []),
]
return createPortal(
@@ -330,6 +340,26 @@ export default function ArtworkShareModal({ open, onClose, artwork, shareUrl })
visible={toastVisible}
onHide={() => setToastVisible(false)}
/>
{/* Share to Profile (Feed) modal — lazy loaded */}
{profileShareOpen && (
<Suspense fallback={null}>
<FeedShareArtworkModal
isOpen={profileShareOpen}
onClose={() => setProfileShareOpen(false)}
preselectedArtwork={artwork?.id ? {
id: artwork.id,
title: artwork.title,
thumb_url: artwork.thumbs?.md?.url ?? artwork.thumbs?.lg?.url ?? null,
user: artwork.user ?? null,
} : null}
onShared={() => {
setProfileShareOpen(false)
showToast('Shared to your profile!')
}}
/>
</Suspense>
)}
</>,
document.body,
)