Browse Source

#61

feature/061
みてるぞ 1 week ago
parent
commit
f8e4da6fcb
11 changed files with 249 additions and 19 deletions
  1. +2
    -0
      frontend/src/App.tsx
  2. +2
    -11
      frontend/src/components/TagDetailSidebar.tsx
  3. +1
    -1
      frontend/src/components/TopNav.tsx
  4. +10
    -0
      frontend/src/consts.ts
  5. +1
    -1
      frontend/src/lib/posts.ts
  6. +1
    -0
      frontend/src/lib/queryKeys.ts
  7. +16
    -0
      frontend/src/lib/tags.ts
  8. +5
    -5
      frontend/src/lib/utils.ts
  9. +1
    -1
      frontend/src/pages/posts/PostSearchPage.tsx
  10. +201
    -0
      frontend/src/pages/tags/TagListPage.tsx
  11. +9
    -0
      frontend/src/types.ts

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

@@ -19,6 +19,7 @@ import PostNewPage from '@/pages/posts/PostNewPage'
import PostSearchPage from '@/pages/posts/PostSearchPage'
import ServiceUnavailable from '@/pages/ServiceUnavailable'
import SettingPage from '@/pages/users/SettingPage'
import TagListPage from '@/pages/tags/TagListPage'
import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
import WikiDiffPage from '@/pages/wiki/WikiDiffPage'
import WikiEditPage from '@/pages/wiki/WikiEditPage'
@@ -46,6 +47,7 @@ const RouteTransitionWrapper = ({ user, setUser }: {
<Route path="/posts/search" element={<PostSearchPage/>}/>
<Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/>
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
<Route path="/tags" element={<TagListPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="/wiki" element={<WikiSearchPage/>}/>
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>


+ 2
- 11
frontend/src/components/TagDetailSidebar.tsx View File

@@ -17,7 +17,7 @@ import SectionTitle from '@/components/common/SectionTitle'
import SubsectionTitle from '@/components/common/SubsectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent'
import { toast } from '@/components/ui/use-toast'
import { CATEGORIES } from '@/consts'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api'
import { dateString, originalCreatedAtString } from '@/lib/utils'

@@ -255,15 +255,6 @@ export default (({ post }: Props) => {
}
}

const categoryNames: Record<Category, string> = {
deerjikist: 'ニジラー',
meme: '原作・ネタ元・ミーム等',
character: 'キャラクター',
general: '一般',
material: '素材',
meta: 'メタタグ',
nico: 'ニコニコタグ' }

useEffect (() => {
if (!(post))
return
@@ -317,7 +308,7 @@ export default (({ post }: Props) => {
<motion.div key={post?.id ?? 0} layout>
{CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && (
<motion.div layout className="my-3" key={cat}>
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
<SubsectionTitle>{CATEGORY_NAMES[cat]}</SubsectionTitle>

<motion.ul layout>
<AnimatePresence initial={false}>


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

@@ -74,7 +74,7 @@ export default (({ user }: Props) => {
{ name: '履歴', to: '/posts/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
{ name: 'タグ', to: '/tags', subMenu: [
{ name: 'タグ一覧', to: '/tags', visible: false },
{ name: 'タグ一覧', to: '/tags', visible: true },
{ name: '別名タグ', to: '/tags/aliases', visible: false },
{ name: '上位タグ', to: '/tags/implications', visible: false },
{ name: 'ニコニコ連携', to: '/tags/nico' },


+ 10
- 0
frontend/src/consts.ts View File

@@ -13,6 +13,16 @@ export const CATEGORIES = [
'nico',
] as const

export const CATEGORY_NAMES: Record<Category, string> = {
deerjikist: 'ニジラー',
meme: '原作・ネタ元・ミーム等',
character: 'キャラクター',
general: '一般',
material: '素材',
meta: 'メタタグ',
nico: 'ニコニコタグ',
} as const

export const FETCH_POSTS_ORDER_FIELDS = [
'title',
'url',


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

@@ -5,7 +5,7 @@ import type { FetchPostsParams, Post, PostTagChange } from '@/types'

export const fetchPosts = async (
{ url, title, tags, match, createdFrom, createdTo, updatedFrom, updatedTo,
originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams
originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams,
): Promise<{
posts: Post[]
count: number }> =>


+ 1
- 0
frontend/src/lib/queryKeys.ts View File

@@ -10,6 +10,7 @@ export const postsKeys = {

export const tagsKeys = {
root: ['tags'] as const,
index: (p) => ['tags', 'index', p] as const,
show: (name: string) => ['tags', name] as const }

export const wikiKeys = {


+ 16
- 0
frontend/src/lib/tags.ts View File

@@ -3,6 +3,22 @@ import { apiGet } from '@/lib/api'
import type { Tag } from '@/types'


export const fetchTags = async (
{ name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
updatedFrom, updatedTo },
): Promise<{ tags: Tag[]
count: number }> =>
await apiGet ('/tags', { params: {
...(name && { name }),
...(category && { category }),
...(postCountGTE && { post_count_gte: postCountGTE }),
...(postCountLTE && { post_count_lte: postCountLTE }),
...(createdFrom && { created_from: createdFrom }),
...(createdTo && { created_to: createdTo }),
...(updatedFrom && { updated_from: updatedFrom }),
...(updatedTo && { updated_to: updatedTo }) } })


export const fetchTagByName = async (name: string): Promise<Tag | null> => {
try
{


+ 5
- 5
frontend/src/lib/utils.ts View File

@@ -47,16 +47,16 @@ export const originalCreatedAtString = (

if (from.getHours () === 0 && before.getHours () === 0)
{
if (Math.abs (diff - 86_400_000) < 60_000)
if (Math.abs (diff - 86_400_000 /* 1 日 */) < 60_000)
return dateString (from, 'hour') + ' (時刻不詳)'

if (from.getDate () === 1 && before.getDate () === 1)
{
if (2_419_200_000 /* 28 日 */ <= diff && diff < 2_764_800_000 /* 32 日 */)
if (2_332_800_000 /* 27 日 */ < diff && diff < 2_764_800_000 /* 32 日 */)
return dateString (from, 'day') + ' (日不詳)'

if (from.getMonth () === 0 && before.getMonth () === 0
&& (31_536_000_000 /* 365 日 */ <= diff
&& (31_449_600_000 /* 364 日 */ <= diff
&& diff < 31_708_800_000 /* 367 日 */))
return dateString (from, 'month') + ' (月日不詳)'
}
@@ -65,9 +65,9 @@ export const originalCreatedAtString = (
}

const rtn = ([from ? `${ dateString (from, 'second') }` : '',
'',
'',
before ? `${ dateString (new Date (before.getTime () - 60_000), 'second') }` : '']
.filter (Boolean)
.join (' '))
return rtn === '' ? '年月日不詳' : rtn
return rtn === '' ? '年月日不詳' : rtn
}

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

@@ -358,7 +358,7 @@ export default (() => {
</thead>
<tbody>
{results.map (row => (
<tr key={row.id} className={'even:bg-gray-100 dark:even:bg-gray-700'}>
<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


+ 201
- 0
frontend/src/pages/tags/TagListPage.tsx View File

@@ -0,0 +1,201 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, 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 PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { tagsKeys } from '@/lib/queryKeys'

import type { FC } from 'react'

import type { Category, FetchTagsOrder } from '@/types'


export default (() => {
const location = useLocation ()
const query = useMemo (() => new URLSearchParams (location.search), [location.search])

const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)

const qName = query.get ('name') ?? ''
const qCategory = (query.get ('category') || null) as Category | null
const qPostCountGTE = Number (query.get ('post_count_gte') ?? 1)
const qPostCountLTE = Number (query.get ('post_count_lte') ?? (-1))
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') || 'post_count:desc') as FetchTagsOrder

const [name, setName] = useState ('')
const [category, setCategory] = useState<Category | null> (null)
const [postCountGTE, setPostCountGTE] = useState (1)
const [postCountLTE, setPostCountLTE] = useState (-1)
const [createdFrom, setCreatedFrom] = useState<string | null> (null)
const [createdTo, setCreatedTo] = useState<string | null> (null)
const [updatedFrom, setUpdatedFrom] = useState<string | null> (null)
const [updatedTo, setUpdatedTo] = useState<string | null> (null)

const keys = { name: qName, category: qCategory }
const { data, isLoading: loading } = useQuery ({
queryKey: tagsKeys.index (keys),
queryFn: () => fetchTags (keys) })
const results = data?.tags ?? []

useEffect (() => {
setName (qName)
setCategory (qCategory)
setPostCountGTE (qPostCountGTE)
setPostCountLTE (qPostCountLTE)
setCreatedFrom (qCreatedFrom)
setCreatedTo (qCreatedTo)
setUpdatedFrom (qUpdatedFrom)
setUpdatedTo (qUpdatedTo)
}, [])

const handleSearch = () => {
;
}

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={name}
onChange={e => setName (e.target.value)}
className="w-full border p-2 rounded"/>
</div>

{/* カテゴリ */}
<div>
<Label>カテゴリ</Label>
<select
value={category ?? ''}
onChange={e => setCategory(e.target.value || null)}
className="w-full border p-2 rounded">
<option value="">&nbsp;</option>
{CATEGORIES.map (cat => (
<option key={cat} value={cat}>
{CATEGORY_NAMES[cat]}
</option>))}
</select>
</div>

{/* 広場の投稿数 */}
<div>
<Label>広場の投稿数</Label>
<input
type="number"
min="0"
value={postCountGTE < 0 ? '' : String (postCountGTE)}
onChange={e => setPostCountGTE (Number (e.target.value || (-1)))}
className="border rounded p-2"/>
<span className="mx-1">〜</span>
<input
type="number"
min="0"
value={postCountLTE < 0 ? '' : String (postCountLTE)}
onChange={e => setPostCountLTE (Number (e.target.value || (-1)))}
className="border rounded p-2"/>
</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-72"/>
<col className="w-48"/>
<col className="w-16"/>
<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">
<SortHeader by="name" label="タグ"/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader by="category" label="カテゴリ"/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader by="post_count" label="件数"/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader by="created_at" label="最初の記載日時"/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader by="updated_at" label="更新日時"/>
</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">
<TagLink tag={row} withCount={false}/>
</td>
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
<td className="p-2">{dateString (row.postCount)}</td>
<td className="p-2">{dateString (row.createdAt)}</td>
<td className="p-2">{dateString (row.updatedAt)}</td>
</tr>))}
</tbody>
</table>
</div>
</div>) : '結果ないよ(笑)')}
</MainArea>)
}) satisfies FC

+ 9
- 0
frontend/src/types.ts View File

@@ -26,6 +26,15 @@ export type FetchPostsParams = {
limit: number
order: FetchPostsOrder }

export type FetchTagsOrder = `${ FetchTagsOrderField }:${ 'asc' | 'desc' }`

export type FetchTagsOrderField =
| 'name'
| 'category'
| 'post_count'
| 'create_at'
| 'updated_at'

export type Menu = MenuItem[]

export type MenuItem = {


Loading…
Cancel
Save