#21 完了
This commit is contained in:
@@ -2,6 +2,12 @@ import React, { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { API_BASE_URL } from '../config'
|
||||
import TagSearchBox from './TagSearchBox'
|
||||
|
||||
type Tag = { id: number
|
||||
name: string
|
||||
category: string
|
||||
count?: number }
|
||||
|
||||
|
||||
const TagSearch: React.FC = () => {
|
||||
@@ -9,11 +15,68 @@ const TagSearch: React.FC = () => {
|
||||
const location = useLocation ()
|
||||
|
||||
const [search, setSearch] = useState ('')
|
||||
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||
const [activeIndex, setActiveIndex] = useState (-1)
|
||||
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
||||
|
||||
const whenChanged = e => {
|
||||
setSearch (e.target.value)
|
||||
|
||||
const q: string = e.target.value.split (' ').at (-1)
|
||||
if (!(q))
|
||||
{
|
||||
setSuggestions ([])
|
||||
return
|
||||
}
|
||||
void (axios.get (`${ API_BASE_URL }/tags/autocomplete`, { params: { q } })
|
||||
.then (res => {
|
||||
setSuggestions (res.data)
|
||||
if (suggestions.length)
|
||||
setSuggestionsVsbl (true)
|
||||
}))
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && search.length > 0)
|
||||
navigate (`/posts?${ (new URLSearchParams ({ tags: search })).toString () }`,
|
||||
{ replace: true })
|
||||
switch (e.key)
|
||||
{
|
||||
case 'ArrowDown':
|
||||
e.preventDefault ()
|
||||
setActiveIndex (i => Math.min (i + 1, suggestions.length - 1))
|
||||
setSuggestionsVsbl (true)
|
||||
break
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault ()
|
||||
setActiveIndex (i => Math.max (i - 1, -1))
|
||||
setSuggestionsVsbl (true)
|
||||
break
|
||||
|
||||
case 'Enter':
|
||||
if (activeIndex < 0)
|
||||
break
|
||||
e.preventDefault ()
|
||||
const selected = suggestions[activeIndex]
|
||||
selected && handleTagSelect (selected)
|
||||
break
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault ()
|
||||
setSuggestionsVsbl (false)
|
||||
break
|
||||
}
|
||||
if (e.key === 'Enter' && search.length && (!(suggestionsVsbl) || activeIndex < 0))
|
||||
{
|
||||
navigate (`/posts?${ (new URLSearchParams ({ tags: search })).toString () }`)
|
||||
setSuggestionsVsbl (false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTagSelect = (tag: Tag) => {
|
||||
const parts = search.split (' ')
|
||||
parts[parts.length - 1] = tag.name
|
||||
setSearch (parts.join (' ') + ' ')
|
||||
setSuggestions ([])
|
||||
setActiveIndex (-1)
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
@@ -23,15 +86,19 @@ const TagSearch: React.FC = () => {
|
||||
}, [location.search])
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="タグ検索..."
|
||||
value={search}
|
||||
onChange={e => setSearch (e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full px-3 py-2 mb-4 border rounded"
|
||||
/>
|
||||
)
|
||||
<div className="relative w-full mb-4">
|
||||
<input type="text"
|
||||
placeholder="タグ検索..."
|
||||
value={search}
|
||||
onChange={whenChanged}
|
||||
onFocus={() => setSuggestionsVsbl (true)}
|
||||
onBlur={() => setSuggestionsVsbl (false)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full px-3 py-2 border rounded border-gray-600 bg-gray-800 text-white" />
|
||||
<TagSearchBox suggestions={suggestionsVsbl && suggestions.length && suggestions}
|
||||
activeIndex={activeIndex}
|
||||
onSelect={handleTagSelect} />
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TagSearch
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { API_BASE_URL } from '../config'
|
||||
|
||||
type Tag = { id: number
|
||||
name: string
|
||||
category: string
|
||||
count?: number }
|
||||
|
||||
type Props = { suggestions: Tag[]
|
||||
activeIndex: number
|
||||
onSelect: (tag: Tag) => void }
|
||||
|
||||
|
||||
const TagSearchBox: React.FC = (props: Props) => {
|
||||
const { suggestions, activeIndex, onSelect } = props
|
||||
|
||||
if (!(suggestions.length))
|
||||
return null
|
||||
|
||||
const navigate = useNavigate ()
|
||||
const location = useLocation ()
|
||||
|
||||
return (
|
||||
<ul className="absolute left-0 right-0 z-50 w-full bg-gray-800 border border-gray-600 rounded shadow">
|
||||
{suggestions.map ((tag, i) => (
|
||||
<li key={tag.id}
|
||||
className={`px-3 py-2 cursor-pointer ${
|
||||
i === activeIndex ? 'bg-blue-600 text-white' : 'hover:bg-gray-700' }`}
|
||||
onMouseDown={() => onSelect (tag)}
|
||||
>
|
||||
{tag.name}
|
||||
{<span className="ml-2 text-sm text-gray-400">{tag.count}</span>}
|
||||
</li>))}
|
||||
</ul>)
|
||||
}
|
||||
|
||||
|
||||
export default TagSearchBox
|
||||
Reference in New Issue
Block a user