import { useQuery } from '@tanstack/react-query' import { motion } from 'framer-motion' import { useEffect, useMemo, useState } from 'react' import { Helmet } from 'react-helmet-async' import { useLocation, useNavigate } from 'react-router-dom' import PrefetchLink from '@/components/PrefetchLink' import TagLink from '@/components/TagLink' import TagSearchBox from '@/components/TagSearchBox' import DateTimeField from '@/components/common/DateTimeField' import Label from '@/components/common/Label' import PageTitle from '@/components/common/PageTitle' import Pagination from '@/components/common/Pagination' import MainArea from '@/components/layout/MainArea' import { SITE_TITLE } from '@/config' import { apiGet } from '@/lib/api' import { fetchPosts } from '@/lib/posts' import { postsKeys } from '@/lib/queryKeys' import { dateString, originalCreatedAtString } from '@/lib/utils' import type { FC, ChangeEvent, FormEvent, KeyboardEvent } from 'react' import type { FetchPostsOrder, FetchPostsOrderField, FetchPostsParams, Tag } from '@/types' const setIf = (qs: URLSearchParams, k: string, v: string | null) => { const t = v?.trim () if (t) qs.set (k, t) } export default (() => { const location = useLocation () const navigate = useNavigate () const query = useMemo (() => new URLSearchParams (location.search), [location.search]) const page = Number (query.get ('page') ?? 1) const limit = Number (query.get ('limit') ?? 20) const qURL = query.get ('url') ?? '' const qTitle = query.get ('title') ?? '' const qTags = query.get ('tags') ?? '' const qMatch: 'all' | 'any' = query.get ('match') === 'any' ? 'any' : 'all' const qOriginalCreatedFrom = query.get ('original_created_from') ?? '' const qOriginalCreatedTo = query.get ('original_created_to') ?? '' const qCreatedFrom = query.get ('created_from') ?? '' const qCreatedTo = query.get ('created_to') ?? '' const qUpdatedFrom = query.get ('updated_from') ?? '' const qUpdatedTo = query.get ('updated_to') ?? '' const order = (query.get ('order') || 'original_created_at:desc') as FetchPostsOrder const [activeIndex, setActiveIndex] = useState (-1) const [createdFrom, setCreatedFrom] = useState (null) const [createdTo, setCreatedTo] = useState (null) const [matchType, setMatchType] = useState<'all' | 'any'> ('all') const [originalCreatedFrom, setOriginalCreatedFrom] = useState (null) const [originalCreatedTo, setOriginalCreatedTo] = useState (null) const [suggestions, setSuggestions] = useState ([]) const [suggestionsVsbl, setSuggestionsVsbl] = useState (false) const [tagsStr, setTagsStr] = useState ('') const [title, setTitle] = useState ('') const [updatedFrom, setUpdatedFrom] = useState (null) const [updatedTo, setUpdatedTo] = useState (null) const [url, setURL] = useState ('') const keys: FetchPostsParams = { tags: qTags, match: qMatch, page, limit, url: qURL, title: qTitle, originalCreatedFrom: qOriginalCreatedFrom, originalCreatedTo: qOriginalCreatedTo, createdFrom: qCreatedFrom, createdTo: qCreatedTo, updatedFrom: qUpdatedFrom, updatedTo: qUpdatedTo, order } const { data, isLoading: loading } = useQuery ({ queryKey: postsKeys.index (keys), queryFn: () => fetchPosts (keys) }) const results = data?.posts ?? [] const totalPages = data ? Math.ceil (data.count / limit) : 0 useEffect (() => { setURL (qURL ?? '') setTitle (qTitle ?? '') setTagsStr (qTags ?? '') setMatchType (qMatch ?? 'all') setOriginalCreatedFrom (qOriginalCreatedFrom) setOriginalCreatedTo (qOriginalCreatedTo) setCreatedFrom (qCreatedFrom) setCreatedTo (qCreatedTo) setUpdatedFrom (qUpdatedFrom) setUpdatedTo (qUpdatedTo) document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) }, [location.search]) const SortHeader = ({ by, label }: { by: FetchPostsOrderField; label: string }) => { const [fld, dir] = order.split (':') const qs = new URLSearchParams (location.search) const nextDir = (by === fld) ? (dir === 'asc' ? 'desc' : 'asc') : (['title', 'url'].includes (by) ? 'asc' : 'desc') qs.set ('order', `${ by }:${ nextDir }`) qs.set ('page', '1') return ( {label} {by === fld && (dir === 'asc' ? ' ▲' : ' ▼')} ) } // TODO: TagSearch からのコピペのため,共通化を考へる. const whenChanged = async (ev: ChangeEvent) => { setTagsStr (ev.target.value) const q = ev.target.value.trim ().split (' ').at (-1) if (!(q)) { setSuggestions ([]) return } const data = await apiGet ('/tags/autocomplete', { params: { q } }) setSuggestions (data.filter (t => t.postCount > 0)) if (suggestions.length > 0) setSuggestionsVsbl (true) } // TODO: TagSearch からのコピペのため,共通化を考へる. const handleKeyDown = (ev: KeyboardEvent) => { 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 qs = new URLSearchParams () setIf (qs, 'tags', tagsStr) setIf (qs, 'url', url) setIf (qs, 'title', title) setIf (qs, 'original_created_from', originalCreatedFrom) setIf (qs, 'original_created_to', originalCreatedTo) setIf (qs, 'created_from', createdFrom) setIf (qs, 'created_to', createdTo) setIf (qs, 'updated_from', updatedFrom) setIf (qs, 'updated_to', updatedTo) qs.set ('match', matchType) qs.set ('page', String ('1')) qs.set ('order', order) 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) => { e.preventDefault () search () } return ( 広場検索 | {SITE_TITLE}
広場検索
{/* タイトル */}
setTitle (e.target.value)} className="w-full border p-2 rounded"/>
{/* URL */}
setURL (e.target.value)} className="w-full border p-2 rounded"/>
{/* タグ */}
setSuggestionsVsbl (true)} onBlur={() => setSuggestionsVsbl (false)} onKeyDown={handleKeyDown} className="w-full border p-2 rounded"/> 0 ? suggestions : [] as Tag[]} activeIndex={activeIndex} onSelect={handleTagSelect}/>
{/* オリジナルの投稿日時 */}
{/* 投稿日時 */}
{/* 更新日時 */}
{/* 検索 */}
{loading ? 'Loading...' : (results.length > 0 ? (
{results.map (row => ( ))}
投稿 タグ
{row.title {row.title} {row.url} {row.tags.map (t => ( ))} {originalCreatedAtString (row.originalCreatedFrom, row.originalCreatedBefore)} {dateString (row.createdAt)} {dateString (row.updatedAt)}
) : '結果ないよ(笑)')}
) }) satisfies FC