|
|
@@ -5,16 +5,20 @@ import { useLocation, useNavigate } from 'react-router-dom' |
|
|
|
|
|
|
|
|
import PrefetchLink from '@/components/PrefetchLink' |
|
|
import PrefetchLink from '@/components/PrefetchLink' |
|
|
import TagLink from '@/components/TagLink' |
|
|
import TagLink from '@/components/TagLink' |
|
|
|
|
|
import TagSearchBox from '@/components/TagSearchBox' |
|
|
import DateTimeField from '@/components/common/DateTimeField' |
|
|
import DateTimeField from '@/components/common/DateTimeField' |
|
|
import Label from '@/components/common/Label' |
|
|
import Label from '@/components/common/Label' |
|
|
import PageTitle from '@/components/common/PageTitle' |
|
|
import PageTitle from '@/components/common/PageTitle' |
|
|
import Pagination from '@/components/common/Pagination' |
|
|
import Pagination from '@/components/common/Pagination' |
|
|
import MainArea from '@/components/layout/MainArea' |
|
|
import MainArea from '@/components/layout/MainArea' |
|
|
import { SITE_TITLE } from '@/config' |
|
|
import { SITE_TITLE } from '@/config' |
|
|
|
|
|
import { apiGet } from '@/lib/api' |
|
|
import { fetchPosts } from '@/lib/posts' |
|
|
import { fetchPosts } from '@/lib/posts' |
|
|
import { postsKeys } from '@/lib/queryKeys' |
|
|
import { postsKeys } from '@/lib/queryKeys' |
|
|
|
|
|
|
|
|
import type { FC, FormEvent } from 'react' |
|
|
|
|
|
|
|
|
import type { FC, ChangeEvent, FormEvent, KeyboardEvent } from 'react' |
|
|
|
|
|
|
|
|
|
|
|
import type { Tag } from '@/types' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const setIf = (qs: URLSearchParams, k: string, v: string | null) => { |
|
|
const setIf = (qs: URLSearchParams, k: string, v: string | null) => { |
|
|
@@ -46,11 +50,14 @@ export default (() => { |
|
|
const qUpdatedFrom = query.get ('updated_from') |
|
|
const qUpdatedFrom = query.get ('updated_from') |
|
|
const qUpdatedTo = query.get ('updated_to') |
|
|
const qUpdatedTo = query.get ('updated_to') |
|
|
|
|
|
|
|
|
|
|
|
const [activeIndex, setActiveIndex] = useState (-1) |
|
|
const [createdFrom, setCreatedFrom] = useState (qCreatedFrom) |
|
|
const [createdFrom, setCreatedFrom] = useState (qCreatedFrom) |
|
|
const [createdTo, setCreatedTo] = useState (qCreatedTo) |
|
|
const [createdTo, setCreatedTo] = useState (qCreatedTo) |
|
|
const [matchType, setMatchType] = useState (qMatch ?? 'all') |
|
|
const [matchType, setMatchType] = useState (qMatch ?? 'all') |
|
|
const [originalCreatedFrom, setOriginalCreatedFrom] = useState (qOriginalCreatedFrom) |
|
|
const [originalCreatedFrom, setOriginalCreatedFrom] = useState (qOriginalCreatedFrom) |
|
|
const [originalCreatedTo, setOriginalCreatedTo] = useState (qOriginalCreatedTo) |
|
|
const [originalCreatedTo, setOriginalCreatedTo] = useState (qOriginalCreatedTo) |
|
|
|
|
|
const [suggestions, setSuggestions] = useState<Tag[]> ([]) |
|
|
|
|
|
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false) |
|
|
const [tagsStr, setTagsStr] = useState (qTags) |
|
|
const [tagsStr, setTagsStr] = useState (qTags) |
|
|
const [title, setTitle] = useState (qTitle ?? '') |
|
|
const [title, setTitle] = useState (qTitle ?? '') |
|
|
const [updatedFrom, setUpdatedFrom] = useState (qUpdatedFrom) |
|
|
const [updatedFrom, setUpdatedFrom] = useState (qUpdatedFrom) |
|
|
@@ -88,6 +95,58 @@ export default (() => { |
|
|
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) |
|
|
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) |
|
|
}, [location.search]) |
|
|
}, [location.search]) |
|
|
|
|
|
|
|
|
|
|
|
// TODO: TagSearch からのコピペのため,共通化を考へる. |
|
|
|
|
|
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => { |
|
|
|
|
|
setTagsStr (ev.target.value) |
|
|
|
|
|
|
|
|
|
|
|
const q = ev.target.value.trim ().split (' ').at (-1) |
|
|
|
|
|
if (!(q)) |
|
|
|
|
|
{ |
|
|
|
|
|
setSuggestions ([]) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q } }) |
|
|
|
|
|
setSuggestions (data.filter (t => t.postCount > 0)) |
|
|
|
|
|
if (suggestions.length > 0) |
|
|
|
|
|
setSuggestionsVsbl (true) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// TODO: TagSearch からのコピペのため,共通化を考へる. |
|
|
|
|
|
const handleKeyDown = (ev: KeyboardEvent<HTMLInputElement>) => { |
|
|
|
|
|
switch (ev.key) |
|
|
|
|
|
{ |
|
|
|
|
|
case 'ArrowDown': |
|
|
|
|
|
ev.preventDefault () |
|
|
|
|
|
setActiveIndex (i => Math.min (i + 1, suggestions.length - 1)) |
|
|
|
|
|
setSuggestionsVsbl (true) |
|
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
|
|
case 'ArrowUp': |
|
|
|
|
|
ev.preventDefault () |
|
|
|
|
|
setActiveIndex (i => Math.max (i - 1, -1)) |
|
|
|
|
|
setSuggestionsVsbl (true) |
|
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
|
|
case 'Enter': |
|
|
|
|
|
if (activeIndex < 0) |
|
|
|
|
|
break |
|
|
|
|
|
ev.preventDefault () |
|
|
|
|
|
const selected = suggestions[activeIndex] |
|
|
|
|
|
selected && handleTagSelect (selected) |
|
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
|
|
case 'Escape': |
|
|
|
|
|
ev.preventDefault () |
|
|
|
|
|
setSuggestionsVsbl (false) |
|
|
|
|
|
break |
|
|
|
|
|
} |
|
|
|
|
|
if (ev.key === 'Enter' && (!(suggestionsVsbl) || activeIndex < 0)) |
|
|
|
|
|
{ |
|
|
|
|
|
setSuggestionsVsbl (false) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const search = async () => { |
|
|
const search = async () => { |
|
|
const qs = new URLSearchParams () |
|
|
const qs = new URLSearchParams () |
|
|
setIf (qs, 'tags', tagsStr) |
|
|
setIf (qs, 'tags', tagsStr) |
|
|
@@ -104,6 +163,15 @@ export default (() => { |
|
|
navigate (`${ location.pathname }?${ qs.toString () }`) |
|
|
navigate (`${ location.pathname }?${ qs.toString () }`) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// TODO: TagSearch からのコピペのため,共通化を考へる. |
|
|
|
|
|
const handleTagSelect = (tag: Tag) => { |
|
|
|
|
|
const parts = tagsStr.split (' ') |
|
|
|
|
|
parts[parts.length - 1] = tag.name |
|
|
|
|
|
setTagsStr (parts.join (' ') + ' ') |
|
|
|
|
|
setSuggestions ([]) |
|
|
|
|
|
setActiveIndex (-1) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const handleSearch = (e: FormEvent) => { |
|
|
const handleSearch = (e: FormEvent) => { |
|
|
e.preventDefault () |
|
|
e.preventDefault () |
|
|
search () |
|
|
search () |
|
|
@@ -140,13 +208,21 @@ export default (() => { |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
{/* タグ */} |
|
|
{/* タグ */} |
|
|
<div> |
|
|
|
|
|
|
|
|
<div className="relative"> |
|
|
<Label>タグ</Label> |
|
|
<Label>タグ</Label> |
|
|
<input |
|
|
<input |
|
|
type="text" |
|
|
type="text" |
|
|
value={tagsStr} |
|
|
value={tagsStr} |
|
|
onChange={e => setTagsStr (e.target.value)} |
|
|
|
|
|
|
|
|
onChange={whenChanged} |
|
|
|
|
|
onFocus={() => setSuggestionsVsbl (true)} |
|
|
|
|
|
onBlur={() => setSuggestionsVsbl (false)} |
|
|
|
|
|
onKeyDown={handleKeyDown} |
|
|
className="w-full border p-2 rounded"/> |
|
|
className="w-full border p-2 rounded"/> |
|
|
|
|
|
<TagSearchBox |
|
|
|
|
|
suggestions={ |
|
|
|
|
|
suggestionsVsbl && suggestions.length > 0 ? suggestions : [] as Tag[]} |
|
|
|
|
|
activeIndex={activeIndex} |
|
|
|
|
|
onSelect={handleTagSelect}/> |
|
|
<fieldset className="w-full my-2"> |
|
|
<fieldset className="w-full my-2"> |
|
|
<label>検索区分:</label> |
|
|
<label>検索区分:</label> |
|
|
<label className="mx-2"> |
|
|
<label className="mx-2"> |
|
|
|