| 
				
				
					
				
				
				 | 
			
			 | 
			@@ -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 |