Browse Source

#206

feature/206
みてるぞ 1 week ago
parent
commit
3e5eb4687b
5 changed files with 135 additions and 52 deletions
  1. +18
    -12
      frontend/src/lib/posts.ts
  2. +12
    -2
      frontend/src/lib/queryKeys.ts
  3. +1
    -1
      frontend/src/pages/posts/PostListPage.tsx
  4. +101
    -35
      frontend/src/pages/posts/PostSearchPage.tsx
  5. +3
    -2
      frontend/src/types.ts

+ 18
- 12
frontend/src/lib/posts.ts View File

@@ -4,22 +4,28 @@ import type { Post, PostTagChange } from '@/types'




export const fetchPosts = async ( export const fetchPosts = async (
{ tags, match, page, limit, cursor }: {
tags: string
match: 'any' | 'all'
page?: number
limit?: number
cursor?: string }
{ url, title, tags, match, created_from, created_to, updated_from,
updated_to, original_created_from, original_created_to, page, limit }: {
url?: string
title?: string
tags?: string
match?: 'all' | 'any'
created_from?: string
created_to?: string
updated_from?: string
updated_to?: string
original_created_from?: string
original_created_to?: string
page?: number
limit?: number },
): Promise<{ ): Promise<{
posts: Post[] posts: Post[]
count: number
nextCursor: string }> =>
count: number }> =>
await apiGet ('/posts', { params: { await apiGet ('/posts', { params: {
tags,
match,
url, title, tags, match, created_from, created_to, updated_from, updated_to,
original_created_from, original_created_to,
...(page && { page }), ...(page && { page }),
...(limit && { limit }),
...(cursor && { cursor }) } })
...(limit && { limit }) } })




export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`) export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`)


+ 12
- 2
frontend/src/lib/queryKeys.ts View File

@@ -1,7 +1,17 @@
export const postsKeys = { export const postsKeys = {
root: ['posts'] as const, root: ['posts'] as const,
index: (p: { tags: string; match: 'any' | 'all'; page: number; limit: number }) =>
['posts', 'index', p] as const,
index: (p: { url?: string
title?: string
tags?: string
match?: 'all' | 'any'
created_from?: string
created_to?: string
updated_from?: string
updated_to?: string
original_created_from?: string
original_created_to?: string
page?: number
limit?: number }) => ['posts', 'index', p] as const,
show: (id: string) => ['posts', id] as const, show: (id: string) => ['posts', id] as const,
related: (id: string) => ['related', id] as const, related: (id: string) => ['related', id] as const,
changes: (p: { id?: string; page: number; limit: number }) => changes: (p: { id?: string; page: number; limit: number }) =>


+ 1
- 1
frontend/src/pages/posts/PostListPage.tsx View File

@@ -39,7 +39,7 @@ export default (() => {
queryKey: postsKeys.index ({ tags: tagsKey, match, page, limit }), queryKey: postsKeys.index ({ tags: tagsKey, match, page, limit }),
queryFn: () => fetchPosts ({ tags: tagsKey, match, page, limit }) }) queryFn: () => fetchPosts ({ tags: tagsKey, match, page, limit }) })
const posts = data?.posts ?? [] const posts = data?.posts ?? []
const cursor = data?.nextCursor ?? ''
const cursor = ''
const totalPages = data ? Math.ceil (data.count / limit) : 0 const totalPages = data ? Math.ceil (data.count / limit) : 0


useLayoutEffect (() => { useLayoutEffect (() => {


+ 101
- 35
frontend/src/pages/posts/PostSearchPage.tsx View File

@@ -2,12 +2,15 @@ import { useState } from 'react'
import { Helmet } from 'react-helmet-async' import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'


import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink'
import DateTimeField from '@/components/common/DateTimeField' import DateTimeField from '@/components/common/DateTimeField'
import Label from '@/components/common/Label' import Label from '@/components/common/Label'
import SectionTitle from '@/components/common/SectionTitle'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { apiGet } from '@/lib/api'
import { fetchPosts } from '@/lib/posts'


import type { FC, FormEvent } from 'react' import type { FC, FormEvent } from 'react'


@@ -15,17 +18,18 @@ import type { Post } from '@/types'




export default (() => { export default (() => {
const [createdFrom, setCreatedFrom] = useState<string | null> (null)
const [createdTo, setCreatedTo] = useState<string | null> (null)
const [createdFrom, setCreatedFrom] = useState<string | undefined> ()
const [createdTo, setCreatedTo] = useState<string | undefined> ()
const [matchType, setMatchType] = useState<'all' | 'any'> ('all') const [matchType, setMatchType] = useState<'all' | 'any'> ('all')
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null)
const [originalCreatedTo, setOriginalCreatedTo] = useState<string | null> (null)
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | undefined> ()
const [originalCreatedTo, setOriginalCreatedTo] = useState<string | undefined> ()
const [tagsStr, setTagsStr] = useState ('') const [tagsStr, setTagsStr] = useState ('')
const [title, setTitle] = useState ('') const [title, setTitle] = useState ('')
const [updatedFrom, setUpdatedFrom] = useState<string | null> (null)
const [updatedTo, setUpdatedTo] = useState<string | null> (null)
const [updatedFrom, setUpdatedFrom] = useState<string | undefined> ()
const [updatedTo, setUpdatedTo] = useState<string | undefined> ()
const [url, setURL] = useState ('') const [url, setURL] = useState ('')
const [results, setResults] = useState<Post[]> ([]) const [results, setResults] = useState<Post[]> ([])
const [totalPages, setTotalPages] = useState (0)


const location = useLocation () const location = useLocation ()
const query = new URLSearchParams (location.search) const query = new URLSearchParams (location.search)
@@ -34,13 +38,15 @@ export default (() => {


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


const handleSearch = (e: FormEvent) => { const handleSearch = (e: FormEvent) => {
@@ -55,18 +61,9 @@ export default (() => {
</Helmet> </Helmet>


<div className="max-w-xl"> <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>
<PageTitle>広場検索</PageTitle>


<form onSubmit={handleSearch} className="space-y-2">
{/* タイトル */} {/* タイトル */}
<div> <div>
<Label>タイトル</Label> <Label>タイトル</Label>
@@ -77,6 +74,16 @@ export default (() => {
className="w-full border p-2 rounded"/> className="w-full border p-2 rounded"/>
</div> </div>


{/* 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> <div>
<Label>タグ</Label> <Label>タグ</Label>
@@ -111,11 +118,11 @@ export default (() => {
<Label>投稿日時</Label> <Label>投稿日時</Label>
<DateTimeField <DateTimeField
value={createdFrom ?? undefined} value={createdFrom ?? undefined}
onChange={setCreatedFrom}/>
onChange={isoUTC => setCreatedFrom (isoUTC ?? undefined)}/>
<span className="mx-1">〜</span> <span className="mx-1">〜</span>
<DateTimeField <DateTimeField
value={createdTo ?? undefined} value={createdTo ?? undefined}
onChange={setCreatedTo}/>
onChange={isoUTC => setCreatedTo (isoUTC ?? undefined)}/>
</div> </div>


{/* 更新日時 */} {/* 更新日時 */}
@@ -123,11 +130,11 @@ export default (() => {
<Label>更新日時</Label> <Label>更新日時</Label>
<DateTimeField <DateTimeField
value={updatedFrom ?? undefined} value={updatedFrom ?? undefined}
onChange={setUpdatedFrom}/>
onChange={isoUTC => setUpdatedFrom (isoUTC ?? undefined)}/>
<span className="mx-1">〜</span> <span className="mx-1">〜</span>
<DateTimeField <DateTimeField
value={updatedTo ?? undefined} value={updatedTo ?? undefined}
onChange={setUpdatedTo}/>
onChange={isoUTC => setUpdatedTo (isoUTC ?? undefined)}/>
</div> </div>


{/* オリジナルの投稿日時 */} {/* オリジナルの投稿日時 */}
@@ -135,11 +142,11 @@ export default (() => {
<Label>オリジナルの投稿日時</Label> <Label>オリジナルの投稿日時</Label>
<DateTimeField <DateTimeField
value={originalCreatedFrom ?? undefined} value={originalCreatedFrom ?? undefined}
onChange={setOriginalCreatedFrom}/>
onChange={isoUTC => setOriginalCreatedFrom (isoUTC ?? undefined)}/>
<span className="mx-1">〜</span> <span className="mx-1">〜</span>
<DateTimeField <DateTimeField
value={originalCreatedTo ?? undefined} value={originalCreatedTo ?? undefined}
onChange={setOriginalCreatedTo}/>
onChange={isoUTC => setOriginalCreatedTo (isoUTC ?? undefined)}/>
</div> </div>


{/* 検索 */} {/* 検索 */}
@@ -153,13 +160,72 @@ export default (() => {
</form> </form>
</div> </div>


<div className="mt-4">
<table className="table-auto w-full border-collapse">
<thead>
<tr>
</tr>
</thead>
</table>
</div>
{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-44"/>
<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">タイトル</th>
<th className="p-2 text-left whitespace-nowrap">URL</th>
<th className="p-2 text-left whitespace-nowrap">タグ</th>
<th className="p-2 text-left whitespace-nowrap">オリジナルの投稿日時</th>
<th className="p-2 text-left whitespace-nowrap">投稿日時</th>
<th className="p-2 text-left whitespace-nowrap">更新日時</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}>
<img src={row.thumbnail || row.thumbnailBase || undefined}
alt={row.title || row.url}
title={row.title || row.url || undefined}
className="w-8"/>
</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">
{row.originalCreatedFrom} 〜 {row.originalCreatedBefore}
</td>
<td className="p-2">{row.createdAt}</td>
<td className="p-2">{row.updatedAt}</td>
</tr>))}
</tbody>
</table>
</div>

<Pagination page={page} totalPages={totalPages}/>
</div>)}
</MainArea>) </MainArea>)
}) satisfies FC }) satisfies FC

+ 3
- 2
frontend/src/types.ts View File

@@ -25,9 +25,10 @@ export type Post = {
tags: Tag[] tags: Tag[]
viewed: boolean viewed: boolean
related: Post[] related: Post[]
createdAt: string
originalCreatedFrom: string | null originalCreatedFrom: string | null
originalCreatedBefore: string | null }
originalCreatedBefore: string | null
createdAt: string
updatedAt: string }


export type PostTagChange = { export type PostTagChange = {
post: Post post: Post


Loading…
Cancel
Save