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
77 lines
2.3 KiB
JavaScript
77 lines
2.3 KiB
JavaScript
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
|