|
@@ -2,6 +2,12 @@ import React, { useEffect, useState } from 'react' |
|
|
import axios from 'axios' |
|
|
import axios from 'axios' |
|
|
import { Link, useNavigate, useLocation } from 'react-router-dom' |
|
|
import { Link, useNavigate, useLocation } from 'react-router-dom' |
|
|
import { API_BASE_URL } from '../config' |
|
|
import { API_BASE_URL } from '../config' |
|
|
|
|
|
import TagSearchBox from './TagSearchBox' |
|
|
|
|
|
|
|
|
|
|
|
type Tag = { id: number |
|
|
|
|
|
name: string |
|
|
|
|
|
category: string |
|
|
|
|
|
count?: number } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const TagSearch: React.FC = () => { |
|
|
const TagSearch: React.FC = () => { |
|
@@ -9,11 +15,68 @@ const TagSearch: React.FC = () => { |
|
|
const location = useLocation () |
|
|
const location = useLocation () |
|
|
|
|
|
|
|
|
const [search, setSearch] = useState ('') |
|
|
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>) => { |
|
|
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 (() => { |
|
|
useEffect (() => { |
|
@@ -23,15 +86,19 @@ const TagSearch: React.FC = () => { |
|
|
}, [location.search]) |
|
|
}, [location.search]) |
|
|
|
|
|
|
|
|
return ( |
|
|
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 |
|
|
export default TagSearch |