From 498f215538ed25c7bcb546b7e83ebb40d7a2ef74 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Mon, 23 Feb 2026 18:52:09 +0900 Subject: [PATCH] #206 --- frontend/src/App.tsx | 2 + frontend/src/components/TopNav.tsx | 3 +- frontend/src/lib/prefetchers.ts | 2 +- frontend/src/pages/posts/PostSearchPage.tsx | 165 ++++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/posts/PostSearchPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 98fa8ce..f7c9545 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ import PostDetailPage from '@/pages/posts/PostDetailPage' import PostHistoryPage from '@/pages/posts/PostHistoryPage' import PostListPage from '@/pages/posts/PostListPage' import PostNewPage from '@/pages/posts/PostNewPage' +import PostSearchPage from '@/pages/posts/PostSearchPage' import ServiceUnavailable from '@/pages/ServiceUnavailable' import SettingPage from '@/pages/users/SettingPage' import WikiDetailPage from '@/pages/wiki/WikiDetailPage' @@ -42,6 +43,7 @@ const RouteTransitionWrapper = ({ user, setUser }: { }/> }/> }/> + }/> }/> }/> }/> diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 144f517..06c30cf 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -69,8 +69,9 @@ export default (({ user }: Props) => { const menu: Menu = [ { name: '広場', to: '/posts', subMenu: [ { name: '一覧', to: '/posts' }, + { name: '検索', to: '/posts/search' }, { name: '投稿追加', to: '/posts/new' }, - { name: '耕作履歴', to: '/posts/changes' }, + { name: '履歴', to: '/posts/changes' }, { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] }, { name: 'タグ', to: '/tags', subMenu: [ { name: 'タグ一覧', to: '/tags', visible: false }, diff --git a/frontend/src/lib/prefetchers.ts b/frontend/src/lib/prefetchers.ts index d61291e..956f250 100644 --- a/frontend/src/lib/prefetchers.ts +++ b/frontend/src/lib/prefetchers.ts @@ -104,7 +104,7 @@ const prefetchPostChanges: Prefetcher = async (qc, url) => { export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [ { 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))), run: prefetchPostShow }, { test: u => u.pathname === '/posts/changes', run: prefetchPostChanges }, diff --git a/frontend/src/pages/posts/PostSearchPage.tsx b/frontend/src/pages/posts/PostSearchPage.tsx new file mode 100644 index 0000000..cb5067e --- /dev/null +++ b/frontend/src/pages/posts/PostSearchPage.tsx @@ -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 (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 [results, setResults] = useState ([]) + + 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 ( + + + 広場検索 | {SITE_TITLE} + + +
+ 広場検索 +
+ {/* URL */} +
+ + setURL (e.target.value)} + className="w-full border p-2 rounded"/> +
+ + {/* タイトル */} +
+ + setTitle (e.target.value)} + className="w-full border p-2 rounded"/> +
+ + {/* タグ */} +
+ + setTagsStr (e.target.value)} + className="w-full border p-2 rounded"/> +
+ + + +
+
+ + {/* 投稿日時 */} +
+ + + + +
+ + {/* 更新日時 */} +
+ + + + +
+ + {/* オリジナルの投稿日時 */} +
+ + + + +
+ + {/* 検索 */} +
+ +
+
+
+ +
+ + + + + +
+
+
) +}) satisfies FC