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:
76
resources/js/components/forum/MentionList.jsx
Normal file
76
resources/js/components/forum/MentionList.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Dropdown list rendered by TipTap's mention suggestion.
|
||||
* Receives `items` (user objects) and keyboard nav commands via ref.
|
||||
*/
|
||||
const MentionList = forwardRef(function MentionList({ items, command }, ref) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
// Reset selection when items change
|
||||
useEffect(() => setSelectedIndex(0), [items])
|
||||
|
||||
// Expose keyboard handler to TipTap suggestion plugin
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
setSelectedIndex((i) => (i + items.length - 1) % items.length)
|
||||
return true
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
setSelectedIndex((i) => (i + 1) % items.length)
|
||||
return true
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
selectItem(selectedIndex)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}))
|
||||
|
||||
const selectItem = (index) => {
|
||||
const item = items[index]
|
||||
if (item) {
|
||||
command({ id: item.username, label: item.username })
|
||||
}
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<div className="rounded-xl border border-white/[0.08] bg-nova-800 p-3 shadow-xl backdrop-blur">
|
||||
<p className="text-xs text-zinc-500">No users found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-[200px] overflow-hidden rounded-xl border border-white/[0.08] bg-nova-800 shadow-xl backdrop-blur">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => selectItem(index)}
|
||||
className={[
|
||||
'flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors',
|
||||
index === selectedIndex
|
||||
? 'bg-sky-600/20 text-white'
|
||||
: 'text-zinc-300 hover:bg-white/[0.04]',
|
||||
].join(' ')}
|
||||
>
|
||||
<img
|
||||
src={item.avatar_url}
|
||||
alt=""
|
||||
className="h-6 w-6 rounded-full border border-white/10 object-cover"
|
||||
/>
|
||||
<span className="truncate font-medium">@{item.username}</span>
|
||||
{item.name && item.name !== item.username && (
|
||||
<span className="truncate text-xs text-zinc-500">{item.name}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default MentionList
|
||||
Reference in New Issue
Block a user