7b15cb2c5a
#99 #99 #99 #99 #99 #99 #99 #99 #99 #99 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #303
340 lines
10 KiB
TypeScript
340 lines
10 KiB
TypeScript
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 Label from '@/components/common/Label'
|
||
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 { fetchPosts } from '@/lib/posts'
|
||
import { postsKeys } from '@/lib/queryKeys'
|
||
import { dateString, 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)
|
||
}
|
||
|
||
|
||
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 [createdFrom, setCreatedFrom] = useState<string | null> (null)
|
||
const [createdTo, setCreatedTo] = useState<string | null> (null)
|
||
const [matchType, setMatchType] = useState<'all' | 'any'> ('all')
|
||
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null)
|
||
const [originalCreatedTo, setOriginalCreatedTo] = useState<string | null> (null)
|
||
const [tagsStr, setTagsStr] = useState ('')
|
||
const [title, setTitle] = useState ('')
|
||
const [updatedFrom, setUpdatedFrom] = useState<string | null> (null)
|
||
const [updatedTo, setUpdatedTo] = useState<string | null> (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 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)
|
||
navigate (`${ location.pathname }?${ qs.toString () }`)
|
||
}
|
||
|
||
const handleSearch = (e: FormEvent) => {
|
||
e.preventDefault ()
|
||
search ()
|
||
}
|
||
|
||
const defaultDirection = { title: 'asc',
|
||
url: 'asc',
|
||
original_created_at: 'desc',
|
||
created_at: 'desc',
|
||
updated_at: 'desc' } as const
|
||
|
||
return (
|
||
<MainArea>
|
||
<Helmet>
|
||
<title>広場検索 | {SITE_TITLE}</title>
|
||
</Helmet>
|
||
|
||
<div className="max-w-xl">
|
||
<PageTitle>広場検索</PageTitle>
|
||
|
||
<form onSubmit={handleSearch} className="space-y-2">
|
||
{/* タイトル */}
|
||
<div>
|
||
<Label>タイトル</Label>
|
||
<input
|
||
type="text"
|
||
value={title}
|
||
onChange={e => setTitle (e.target.value)}
|
||
className="w-full border p-2 rounded"/>
|
||
</div>
|
||
|
||
{/* URL */}
|
||
<div>
|
||
<Label>URL</Label>
|
||
<input
|
||
type="text"
|
||
value={url}
|
||
onChange={e => setURL (e.target.value)}
|
||
className="w-full border p-2 rounded"/>
|
||
</div>
|
||
|
||
{/* タグ */}
|
||
<div>
|
||
<Label>タグ</Label>
|
||
<TagInput
|
||
value={tagsStr}
|
||
setValue={setTagsStr}/>
|
||
<fieldset className="w-full my-2">
|
||
<label>検索区分:</label>
|
||
<label className="mx-2">
|
||
<input
|
||
type="radio"
|
||
name="match-type"
|
||
checked={matchType === 'all'}
|
||
onChange={() => setMatchType ('all')}/>
|
||
AND
|
||
</label>
|
||
<label className="mx-2">
|
||
<input
|
||
type="radio"
|
||
name="match-type"
|
||
checked={matchType === 'any'}
|
||
onChange={() => setMatchType ('any')}/>
|
||
OR
|
||
</label>
|
||
</fieldset>
|
||
</div>
|
||
|
||
{/* オリジナルの投稿日時 */}
|
||
<div>
|
||
<Label>オリジナルの投稿日時</Label>
|
||
<DateTimeField
|
||
value={originalCreatedFrom ?? undefined}
|
||
onChange={setOriginalCreatedFrom}/>
|
||
<span className="mx-1">〜</span>
|
||
<DateTimeField
|
||
value={originalCreatedTo ?? undefined}
|
||
onChange={setOriginalCreatedTo}/>
|
||
</div>
|
||
|
||
{/* 投稿日時 */}
|
||
<div>
|
||
<Label>投稿日時</Label>
|
||
<DateTimeField
|
||
value={createdFrom ?? undefined}
|
||
onChange={setCreatedFrom}/>
|
||
<span className="mx-1">〜</span>
|
||
<DateTimeField
|
||
value={createdTo ?? undefined}
|
||
onChange={setCreatedTo}/>
|
||
</div>
|
||
|
||
{/* 更新日時 */}
|
||
<div>
|
||
<Label>更新日時</Label>
|
||
<DateTimeField
|
||
value={updatedFrom ?? undefined}
|
||
onChange={setUpdatedFrom}/>
|
||
<span className="mx-1">〜</span>
|
||
<DateTimeField
|
||
value={updatedTo ?? undefined}
|
||
onChange={setUpdatedTo}/>
|
||
</div>
|
||
|
||
{/* 検索 */}
|
||
<div className="py-3">
|
||
<button
|
||
type="submit"
|
||
className="bg-blue-500 text-white px-4 py-2 rounded">
|
||
検索
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{loading ? 'Loading...' : (results.length > 0 ? (
|
||
<div className="mt-4">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full min-w-[1200px] table-fixed border-collapse">
|
||
<colgroup>
|
||
<col className="w-14"/>
|
||
<col className="w-72"/>
|
||
<col className="w-80"/>
|
||
<col className="w-[24rem]"/>
|
||
<col className="w-60"/>
|
||
<col className="w-44"/>
|
||
<col className="w-44"/>
|
||
</colgroup>
|
||
|
||
<thead className="border-b-2 border-black dark:border-white">
|
||
<tr>
|
||
<th className="p-2 text-left whitespace-nowrap">投稿</th>
|
||
<th className="p-2 text-left whitespace-nowrap">
|
||
<SortHeader<FetchPostsOrderField>
|
||
by="title"
|
||
label="タイトル"
|
||
currentOrder={order}
|
||
defaultDirection={defaultDirection}/>
|
||
</th>
|
||
<th className="p-2 text-left whitespace-nowrap">
|
||
<SortHeader<FetchPostsOrderField>
|
||
by="url"
|
||
label="URL"
|
||
currentOrder={order}
|
||
defaultDirection={defaultDirection}/>
|
||
</th>
|
||
<th className="p-2 text-left whitespace-nowrap">タグ</th>
|
||
<th className="p-2 text-left whitespace-nowrap">
|
||
<SortHeader<FetchPostsOrderField>
|
||
by="original_created_at"
|
||
label="オリジナルの投稿日時"
|
||
currentOrder={order}
|
||
defaultDirection={defaultDirection}/>
|
||
</th>
|
||
<th className="p-2 text-left whitespace-nowrap">
|
||
<SortHeader<FetchPostsOrderField>
|
||
by="created_at"
|
||
label="投稿日時"
|
||
currentOrder={order}
|
||
defaultDirection={defaultDirection}/>
|
||
</th>
|
||
<th className="p-2 text-left whitespace-nowrap">
|
||
<SortHeader<FetchPostsOrderField>
|
||
by="updated_at"
|
||
label="更新日時"
|
||
currentOrder={order}
|
||
defaultDirection={defaultDirection}/>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{results.map (row => (
|
||
<tr key={row.id} className="even:bg-gray-100 dark:even:bg-gray-700">
|
||
<td className="p-2">
|
||
<PrefetchLink to={`/posts/${ row.id }`} title={row.title}>
|
||
<motion.div
|
||
layoutId={`page-${ row.id }`}
|
||
transition={{ type: 'spring',
|
||
stiffness: 500,
|
||
damping: 40,
|
||
mass: .5 }}>
|
||
<img src={row.thumbnail || row.thumbnailBase || undefined}
|
||
alt={row.title || row.url}
|
||
title={row.title || row.url || undefined}
|
||
className="w-8"/>
|
||
</motion.div>
|
||
</PrefetchLink>
|
||
</td>
|
||
<td className="p-2 truncate">
|
||
<PrefetchLink to={`/posts/${ row.id }`} title={row.title}>
|
||
{row.title}
|
||
</PrefetchLink>
|
||
</td>
|
||
<td className="p-2 truncate">
|
||
<a href={row.url}
|
||
title={row.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer nofollow">
|
||
{row.url}
|
||
</a>
|
||
</td>
|
||
<td className="p-2">
|
||
{row.tags.map (t => (
|
||
<span key={t.id} className="mr-2">
|
||
<TagLink tag={t} withWiki={false} withCount={false}/>
|
||
</span>))}
|
||
</td>
|
||
<td className="p-2">
|
||
{originalCreatedAtString (row.originalCreatedFrom,
|
||
row.originalCreatedBefore)}
|
||
</td>
|
||
<td className="p-2">{dateString (row.createdAt)}</td>
|
||
<td className="p-2">{dateString (row.updatedAt)}</td>
|
||
</tr>))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<Pagination page={page} totalPages={totalPages}/>
|
||
</div>) : '結果ないよ(笑)')}
|
||
</MainArea>)
|
||
}) satisfies FC
|