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,74 @@
import { ReactRenderer } from '@tiptap/react'
import tippy from 'tippy.js'
import MentionList from './MentionList'
/**
* TipTap suggestion configuration for @mentions.
* Fetches users from /api/search/users?q=... as the user types.
*/
export default {
items: async ({ query }) => {
if (!query || query.length < 2) return []
try {
const res = await fetch(`/api/search/users?q=${encodeURIComponent(query)}&per_page=6`)
if (!res.ok) return []
const json = await res.json()
return json.data ?? []
} catch {
return []
}
},
render: () => {
let component
let popup
return {
onStart: (props) => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
})
if (!props.clientRect) return
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
theme: 'mention',
arrow: false,
offset: [0, 8],
})
},
onUpdate: (props) => {
component?.updateProps(props)
if (!props.clientRect) return
popup?.[0]?.setProps({
getReferenceClientRect: props.clientRect,
})
},
onKeyDown: (props) => {
if (props.event.key === 'Escape') {
popup?.[0]?.hide()
return true
}
return component?.ref?.onKeyDown(props) ?? false
},
onExit: () => {
popup?.[0]?.destroy()
component?.destroy()
},
}
},
}