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 SortHeader from '@/components/SortHeader' import TagLink from '@/components/TagLink' import DateTimeField from '@/components/common/DateTimeField' import FormField from '@/components/common/FormField' import PageTitle from '@/components/common/PageTitle' import Pagination from '@/components/common/Pagination' import TagInput from '@/components/common/TagInput' import MainArea from '@/components/layout/MainArea' import { SITE_TITLE } from '@/config' import { clientAnimationTransition, clientScrollBehaviour } from '@/lib/clientAnimation' import { fetchPosts } from '@/lib/posts' import { postsKeys } from '@/lib/queryKeys' import { DEFAULT_POST_LIST_LIMIT, DEFAULT_POST_LIST_ORDER, getClientListLimit, getClientListOrder, LIST_LIMIT_OPTIONS, setClientListSettings, } from '@/lib/settings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts' import { dateString, inputClass, originalCreatedAtString } from '@/lib/utils' import type { FC, FormEvent } from 'react' import type { FetchPostsOrder, FetchPostsOrderField, FetchPostsParams } from '@/types' const setIf = (qs: URLSearchParams, k: string, v: string | null) => { const t = v?.trim () if (t) qs.set (k, t) } const parseLimit = (value: string | null): 20 | 50 | 100 | null => { const n = Number (value) return n === 20 || n === 50 || n === 100 ? n : null } const parseOrder = (value: string | null): FetchPostsOrder | null => { switch (value) { case 'title:asc': case 'title:desc': case 'url:asc': case 'url:desc': case 'original_created_at:asc': case 'original_created_at:desc': case 'created_at:asc': case 'created_at:desc': case 'updated_at:asc': case 'updated_at:desc': return value default: return null } } const PostSearchPage: FC = () => { const behaviourSettings = useClientBehaviourSettings () const animationMode = behaviourSettings.animation ?? 'normal' const thumbnailTransition = clientAnimationTransition ( animationMode, { normal: { type: 'spring', stiffness: 500, damping: 40, mass: .5 }, reduced: { duration: .08, ease: 'linear' as const } }, ) const location = useLocation () const navigate = useNavigate () const query = useMemo (() => new URLSearchParams (location.search), [location.search]) const page = Number (query.get ('page') ?? 1) const limit = ( parseLimit (query.get ('limit')) ?? getClientListLimit ('postSearch') ?? DEFAULT_POST_LIST_LIMIT ) 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 = ( parseOrder (query.get ('order')) ?? getClientListOrder ('postSearch') ?? DEFAULT_POST_LIST_ORDER ) 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 [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 (() => { setClientListSettings ('postSearch', { limit, order }) }, [limit, order]) 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: clientScrollBehaviour (animationMode) }) }, [animationMode, location.search, qCreatedFrom, qCreatedTo, qMatch, qOriginalCreatedFrom, qOriginalCreatedTo, qTags, qTitle, qUpdatedFrom, qUpdatedTo, qURL]) 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', '1') qs.set ('order', order) qs.set ('limit', String (limit)) navigate (`${ location.pathname }?${ qs.toString () }`) } const updateListQuery = (next: { limit?: 20 | 50 | 100 order?: FetchPostsOrder }) => { const qs = new URLSearchParams (location.search) qs.set ('limit', String (next.limit ?? limit)) qs.set ('order', next.order ?? order) qs.set ('page', '1') navigate (`${ location.pathname }?${ qs.toString () }`) } const updatePage = (nextPage: number) => { const qs = new URLSearchParams (location.search) qs.set ('page', String (Math.max (1, nextPage))) navigate (`${ location.pathname }?${ qs.toString () }`) } const handleSearch = (e: FormEvent) => { e.preventDefault () search () } useKeyboardShortcuts (useMemo (() => ({ ...(page > 1 ? { 'pagination.previous': () => { updatePage (page - 1) }, } : { }), ...(totalPages > 0 && page < totalPages ? { 'pagination.next': () => { updatePage (Math.min (page + 1, totalPages)) }, } : { }), }), [page, totalPages, location.search])) const defaultDirection = { title: 'asc', url: 'asc', original_created_at: 'desc', created_at: 'desc', updated_at: 'desc' } as const return ( 広場検索 | {SITE_TITLE}
広場検索
{/* タイトル */} {({ invalid }) => ( setTitle (e.target.value)} className={inputClass (invalid)}/>)} {/* URL */} {({ invalid }) => ( setURL (e.target.value)} className={inputClass (invalid)}/>)} {/* タグ */} {() => ( )}
{/* オリジナルの投稿日時 */} {() => ( <> )} {/* 投稿日時 */} {() => ( <> )} {/* 更新日時 */} {() => ( <> )} {/* 検索 */}
{loading ? 'Loading...' : (results.length > 0 ? (
{results.map (row => ( ))}
投稿 by="title" label="タイトル" currentOrder={order} defaultDirection={defaultDirection}/> by="url" label="URL" currentOrder={order} defaultDirection={defaultDirection}/> タグ by="original_created_at" label="オリジナルの投稿日時" currentOrder={order} defaultDirection={defaultDirection}/> by="created_at" label="投稿日時" currentOrder={order} defaultDirection={defaultDirection}/> by="updated_at" label="更新日時" currentOrder={order} defaultDirection={defaultDirection}/>
{row.title {row.title} {row.url} {row.tags.map (t => ( ))} {originalCreatedAtString (row.originalCreatedFrom, row.originalCreatedBefore)} {dateString (row.createdAt)} {dateString (row.updatedAt)}
) : '結果ないよ(笑)')}
) } export default PostSearchPage