This commit is contained in:
@@ -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/>}/>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }> =>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=""> </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
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user