Browse Source

#206

feature/206
みてるぞ 1 week ago
parent
commit
498f215538
4 changed files with 170 additions and 2 deletions
  1. +2
    -0
      frontend/src/App.tsx
  2. +2
    -1
      frontend/src/components/TopNav.tsx
  3. +1
    -1
      frontend/src/lib/prefetchers.ts
  4. +165
    -0
      frontend/src/pages/posts/PostSearchPage.tsx

+ 2
- 0
frontend/src/App.tsx View File

@@ -16,6 +16,7 @@ import PostDetailPage from '@/pages/posts/PostDetailPage'
import PostHistoryPage from '@/pages/posts/PostHistoryPage' import PostHistoryPage from '@/pages/posts/PostHistoryPage'
import PostListPage from '@/pages/posts/PostListPage' import PostListPage from '@/pages/posts/PostListPage'
import PostNewPage from '@/pages/posts/PostNewPage' import PostNewPage from '@/pages/posts/PostNewPage'
import PostSearchPage from '@/pages/posts/PostSearchPage'
import ServiceUnavailable from '@/pages/ServiceUnavailable' import ServiceUnavailable from '@/pages/ServiceUnavailable'
import SettingPage from '@/pages/users/SettingPage' import SettingPage from '@/pages/users/SettingPage'
import WikiDetailPage from '@/pages/wiki/WikiDetailPage' import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
@@ -42,6 +43,7 @@ const RouteTransitionWrapper = ({ user, setUser }: {
<Route path="/" element={<Navigate to="/posts" replace/>}/> <Route path="/" element={<Navigate to="/posts" replace/>}/>
<Route path="/posts" element={<PostListPage/>}/> <Route path="/posts" element={<PostListPage/>}/>
<Route path="/posts/new" element={<PostNewPage user={user}/>}/> <Route path="/posts/new" element={<PostNewPage user={user}/>}/>
<Route path="/posts/search" element={<PostSearchPage/>}/>
<Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/> <Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/>
<Route path="/posts/changes" element={<PostHistoryPage/>}/> <Route path="/posts/changes" element={<PostHistoryPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/> <Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>


+ 2
- 1
frontend/src/components/TopNav.tsx View File

@@ -69,8 +69,9 @@ export default (({ user }: Props) => {
const menu: Menu = [ const menu: Menu = [
{ name: '広場', to: '/posts', subMenu: [ { name: '広場', to: '/posts', subMenu: [
{ name: '一覧', to: '/posts' }, { name: '一覧', to: '/posts' },
{ name: '検索', to: '/posts/search' },
{ name: '投稿追加', to: '/posts/new' }, { name: '投稿追加', to: '/posts/new' },
{ name: '耕作履歴', to: '/posts/changes' },
{ name: '履歴', to: '/posts/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] }, { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
{ name: 'タグ', to: '/tags', subMenu: [ { name: 'タグ', to: '/tags', subMenu: [
{ name: 'タグ一覧', to: '/tags', visible: false }, { name: 'タグ一覧', to: '/tags', visible: false },


+ 1
- 1
frontend/src/lib/prefetchers.ts View File

@@ -104,7 +104,7 @@ const prefetchPostChanges: Prefetcher = async (qc, url) => {


export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [ export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
{ test: u => u.pathname === '/' || u.pathname === '/posts', run: prefetchPostsIndex }, { test: u => u.pathname === '/' || u.pathname === '/posts', run: prefetchPostsIndex },
{ test: u => (!(['/posts/new', '/posts/changes'].includes (u.pathname))
{ test: u => (!(['/posts/new', '/posts/changes', '/posts/search'].includes (u.pathname))
&& Boolean (mPost (u.pathname))), && Boolean (mPost (u.pathname))),
run: prefetchPostShow }, run: prefetchPostShow },
{ test: u => u.pathname === '/posts/changes', run: prefetchPostChanges }, { test: u => u.pathname === '/posts/changes', run: prefetchPostChanges },


+ 165
- 0
frontend/src/pages/posts/PostSearchPage.tsx View File

@@ -0,0 +1,165 @@
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'

import DateTimeField from '@/components/common/DateTimeField'
import Label from '@/components/common/Label'
import SectionTitle from '@/components/common/SectionTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { apiGet } from '@/lib/api'

import type { FC, FormEvent } from 'react'

import type { Post } from '@/types'


export default (() => {
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 [results, setResults] = useState<Post[]> ([])

const location = useLocation ()
const query = new URLSearchParams (location.search)
const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)

const search = async () => {
const tags = tagsStr.split (' ').filter (e => e !== '')
setResults (await apiGet ('/posts', { params: {
url, title, tags: tags.join (' '), match: matchType,
created_from: createdFrom, created_to: createdTo,
updated_from: updatedFrom, updated_to: updatedTo,
original_created_from: originalCreatedFrom,
original_created_to: originalCreatedTo,
page, limit } }))
}

const handleSearch = (e: FormEvent) => {
e.preventDefault ()
search ()
}

return (
<MainArea>
<Helmet>
<title>広場検索 | {SITE_TITLE}</title>
</Helmet>

<div className="max-w-xl">
<SectionTitle>広場検索</SectionTitle>
<form onSubmit={handleSearch} className="space-y-2">
{/* URL */}
<div>
<Label>URL</Label>
<input
type="url"
value={url}
onChange={e => setURL (e.target.value)}
className="w-full border p-2 rounded"/>
</div>

{/* タイトル */}
<div>
<Label>タイトル</Label>
<input
type="text"
value={title}
onChange={e => setTitle (e.target.value)}
className="w-full border p-2 rounded"/>
</div>

{/* タグ */}
<div>
<Label>タグ</Label>
<input
type="text"
value={tagsStr}
onChange={e => setTagsStr (e.target.value)}
className="w-full border p-2 rounded"/>
<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={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>
<Label>オリジナルの投稿日時</Label>
<DateTimeField
value={originalCreatedFrom ?? undefined}
onChange={setOriginalCreatedFrom}/>
<span className="mx-1">〜</span>
<DateTimeField
value={originalCreatedTo ?? undefined}
onChange={setOriginalCreatedTo}/>
</div>

{/* 検索 */}
<div className="py-3">
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded">
検索
</button>
</div>
</form>
</div>

<div className="mt-4">
<table className="table-auto w-full border-collapse">
<thead>
<tr>
</tr>
</thead>
</table>
</div>
</MainArea>)
}) satisfies FC

Loading…
Cancel
Save