This commit is contained in:
@@ -75,7 +75,7 @@ class PostsController < ApplicationController
|
||||
else
|
||||
"posts.#{ order[0] }"
|
||||
end
|
||||
posts = q.order(Arel.sql("#{ sort_sql } #{ order[1] }, id #{ order[1] }"))
|
||||
posts = q.order(Arel.sql("#{ sort_sql } #{ order[1] }, posts.id #{ order[1] }"))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.to_a
|
||||
|
||||
@@ -2,18 +2,71 @@ class TagsController < ApplicationController
|
||||
def index
|
||||
post_id = params[:post]
|
||||
|
||||
tags =
|
||||
name = params[:name].presence
|
||||
category = params[:category].presence
|
||||
post_count_between = (params[:post_count_gte].presence || -1).to_i,
|
||||
(params[:post_count_lte].presence || -1).to_i
|
||||
post_count_between[0] = nil if post_count_between[0] < 0
|
||||
post_count_between[1] = nil if post_count_between[1] < 0
|
||||
created_between = params[:created_from].presence, params[:created_to].presence
|
||||
updated_between = params[:updated_from].presence, params[:updated_to].presence
|
||||
|
||||
order = params[:order].to_s.split(':', 2).map(&:strip)
|
||||
unless order[0].in?(['name', 'category', 'post_count', 'created_at', 'updated_at'])
|
||||
order[0] = 'post_count'
|
||||
end
|
||||
unless order[1].in?(['asc', 'desc'])
|
||||
order[1] = order[0].in?(['name', 'category']) ? 'asc' : 'desc'
|
||||
end
|
||||
|
||||
page = (params[:page].presence || 1).to_i
|
||||
limit = (params[:limit].presence || 20).to_i
|
||||
|
||||
page = 1 if page < 1
|
||||
limit = 1 if limit < 1
|
||||
|
||||
offset = (page - 1) * limit
|
||||
|
||||
q =
|
||||
if post_id.present?
|
||||
Tag.joins(:posts, :tag_name)
|
||||
else
|
||||
Tag.joins(:tag_name)
|
||||
end
|
||||
.includes(:tag_name, tag_name: :wiki_page)
|
||||
if post_id.present?
|
||||
tags = tags.where(posts: { id: post_id })
|
||||
end
|
||||
q = q.where(posts: { id: post_id }) if post_id.present?
|
||||
|
||||
render json: TagRepr.base(tags)
|
||||
q = q.where('tag_names.name LIKE ?', "%#{ name }%") if name
|
||||
q = q.where(category: category) if category
|
||||
q = q.where('tags.post_count >= ?', post_count_between[0]) if post_count_between[0]
|
||||
q = q.where('tags.post_count <= ?', post_count_between[1]) if post_count_between[1]
|
||||
q = q.where('tags.created_at >= ?', created_between[0]) if created_between[0]
|
||||
q = q.where('tags.created_at <= ?', created_between[1]) if created_between[1]
|
||||
q = q.where('tags.updated_at >= ?', updated_between[0]) if updated_between[0]
|
||||
q = q.where('tags.updated_at <= ?', updated_between[1]) if updated_between[1]
|
||||
|
||||
sort_sql =
|
||||
case order[0]
|
||||
when 'name'
|
||||
'tag_names.name'
|
||||
when 'category'
|
||||
'CASE tags.category ' +
|
||||
"WHEN 'deerjikist' THEN 0 " +
|
||||
"WHEN 'meme' THEN 1 " +
|
||||
"WHEN 'character' THEN 2 " +
|
||||
"WHEN 'general' THEN 3 " +
|
||||
"WHEN 'material' THEN 4 " +
|
||||
"WHEN 'meta' THEN 5 " +
|
||||
"WHEN 'nico' THEN 6 END"
|
||||
else
|
||||
"tags.#{ order[0] }"
|
||||
end
|
||||
tags = q.order(Arel.sql("#{ sort_sql } #{ order[1] }, tags.id #{ order[1] }"))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.to_a
|
||||
|
||||
render json: { tags: TagRepr.base(tags), count: q.size }
|
||||
end
|
||||
|
||||
def autocomplete
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
|
||||
module TagRepr
|
||||
BASE = { only: [:id, :category, :post_count], methods: [:name, :has_wiki] }.freeze
|
||||
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at],
|
||||
methods: [:name, :has_wiki] }.freeze
|
||||
|
||||
module_function
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
|
||||
|
||||
export default <T extends string,>({ by, label, currentOrder, defaultDirection }: {
|
||||
by: T
|
||||
label: string
|
||||
currentOrder: `${ T }:${ 'asc' | 'desc' }`
|
||||
defaultDirection: Record<T, 'asc' | 'desc'> }) => {
|
||||
const [fld, dir] = currentOrder.split (':')
|
||||
|
||||
const location = useLocation ()
|
||||
const qs = new URLSearchParams (location.search)
|
||||
const nextDir =
|
||||
(by === fld)
|
||||
? (dir === 'asc' ? 'desc' : 'asc')
|
||||
: (defaultDirection[by] || 'desc')
|
||||
qs.set ('order', `${ by }:${ nextDir }`)
|
||||
qs.set ('page', '1')
|
||||
|
||||
return (
|
||||
<PrefetchLink
|
||||
className="text-inherit visited:text-inherit hover:text-inherit"
|
||||
to={`${ location.pathname }?${ qs.toString () }`}>
|
||||
<span className="font-bold">
|
||||
{label}
|
||||
{by === fld && (dir === 'asc' ? ' ▲' : ' ▼')}
|
||||
</span>
|
||||
</PrefetchLink>)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ const getPages = (
|
||||
}
|
||||
|
||||
|
||||
export default (({ page, totalPages, siblingCount = 4 }) => {
|
||||
export default (({ page, totalPages, siblingCount = 2 }) => {
|
||||
const location = useLocation ()
|
||||
|
||||
const buildTo = (p: number) => {
|
||||
@@ -63,19 +63,31 @@ export default (({ page, totalPages, siblingCount = 4 }) => {
|
||||
<nav className="mt-4 flex justify-center" aria-label="Pagination">
|
||||
<div className="flex items-center gap-2">
|
||||
{(page > 1)
|
||||
? <PrefetchLink to={buildTo (page - 1)} aria-label="前のページ"><</PrefetchLink>
|
||||
: <span aria-hidden><</span>}
|
||||
? (
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page - 1)}
|
||||
aria-label="前のページ">
|
||||
<
|
||||
</PrefetchLink>)
|
||||
: <span className="p-2" aria-hidden><</span>}
|
||||
|
||||
{pages.map ((p, idx) => (
|
||||
(p === '…')
|
||||
? <span key={`dots-${ idx }`}>…</span>
|
||||
? <span key={`dots-${ idx }`} className="p-2">…</span>
|
||||
: ((p === page)
|
||||
? <span key={p} className="font-bold" aria-current="page">{p}</span>
|
||||
: <PrefetchLink key={p} to={buildTo (p)}>{p}</PrefetchLink>)))}
|
||||
? <span key={p} className="font-bold p-2" aria-current="page">{p}</span>
|
||||
: <PrefetchLink key={p} className="p-2" to={buildTo (p)}>{p}</PrefetchLink>)))}
|
||||
|
||||
{(page < totalPages)
|
||||
? <PrefetchLink to={buildTo (page + 1)} aria-label="次のページ">></PrefetchLink>
|
||||
: <span aria-hidden>></span>}
|
||||
? (
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page + 1)}
|
||||
aria-label="次のページ">
|
||||
>
|
||||
</PrefetchLink>)
|
||||
: <span className="p-2" aria-hidden>></span>}
|
||||
</div>
|
||||
</nav>)
|
||||
}) satisfies FC<Props>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FetchPostsParams } from '@/types'
|
||||
import type { FetchPostsParams, FetchTagsParams } from '@/types'
|
||||
|
||||
export const postsKeys = {
|
||||
root: ['posts'] as const,
|
||||
@@ -10,7 +10,7 @@ export const postsKeys = {
|
||||
|
||||
export const tagsKeys = {
|
||||
root: ['tags'] as const,
|
||||
index: (p) => ['tags', 'index', p] as const,
|
||||
index: (p: FetchTagsParams) => ['tags', 'index', p] as const,
|
||||
show: (name: string) => ['tags', name] as const }
|
||||
|
||||
export const wikiKeys = {
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { Tag } from '@/types'
|
||||
import type { FetchTagsParams, Tag } from '@/types'
|
||||
|
||||
|
||||
export const fetchTags = async (
|
||||
{ name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
||||
updatedFrom, updatedTo },
|
||||
{ post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
||||
updatedFrom, updatedTo, page, limit, order }: FetchTagsParams,
|
||||
): Promise<{ tags: Tag[]
|
||||
count: number }> =>
|
||||
await apiGet ('/tags', { params: {
|
||||
...(post != null && { post }),
|
||||
...(name && { name }),
|
||||
...(category && { category }),
|
||||
...(postCountGTE && { post_count_gte: postCountGTE }),
|
||||
...(postCountLTE && { post_count_lte: postCountLTE }),
|
||||
...(postCountGTE != null && { post_count_gte: postCountGTE }),
|
||||
...(postCountLTE != null && { post_count_lte: postCountLTE }),
|
||||
...(createdFrom && { created_from: createdFrom }),
|
||||
...(createdTo && { created_to: createdTo }),
|
||||
...(updatedFrom && { updated_from: updatedFrom }),
|
||||
...(updatedTo && { updated_to: updatedTo }) } })
|
||||
...(updatedTo && { updated_to: updatedTo }),
|
||||
...(page && { page }),
|
||||
...(limit && { limit }),
|
||||
...(order && { order }) } })
|
||||
|
||||
|
||||
export const fetchTagByName = async (name: string): Promise<Tag | null> => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import SortHeader from '@/components/SortHeader'
|
||||
import TagLink from '@/components/TagLink'
|
||||
import TagSearchBox from '@/components/TagSearchBox'
|
||||
import DateTimeField from '@/components/common/DateTimeField'
|
||||
@@ -102,28 +103,6 @@ export default (() => {
|
||||
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
||||
}, [location.search])
|
||||
|
||||
const SortHeader = ({ by, label }: { by: FetchPostsOrderField; label: string }) => {
|
||||
const [fld, dir] = order.split (':')
|
||||
|
||||
const qs = new URLSearchParams (location.search)
|
||||
const nextDir =
|
||||
(by === fld)
|
||||
? (dir === 'asc' ? 'desc' : 'asc')
|
||||
: (['title', 'url'].includes (by) ? 'asc' : 'desc')
|
||||
qs.set ('order', `${ by }:${ nextDir }`)
|
||||
qs.set ('page', '1')
|
||||
|
||||
return (
|
||||
<PrefetchLink
|
||||
className="text-inherit visited:text-inherit hover:text-inherit"
|
||||
to={`${ location.pathname }?${ qs.toString () }`}>
|
||||
<span className="font-bold">
|
||||
{label}
|
||||
{by === fld && (dir === 'asc' ? ' ▲' : ' ▼')}
|
||||
</span>
|
||||
</PrefetchLink>)
|
||||
}
|
||||
|
||||
// TODO: TagSearch からのコピペのため,共通化を考へる.
|
||||
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
setTagsStr (ev.target.value)
|
||||
@@ -188,7 +167,7 @@ export default (() => {
|
||||
setIf (qs, 'updated_from', updatedFrom)
|
||||
setIf (qs, 'updated_to', updatedTo)
|
||||
qs.set ('match', matchType)
|
||||
qs.set ('page', String ('1'))
|
||||
qs.set ('page', '1')
|
||||
qs.set ('order', order)
|
||||
navigate (`${ location.pathname }?${ qs.toString () }`)
|
||||
}
|
||||
@@ -207,6 +186,12 @@ export default (() => {
|
||||
search ()
|
||||
}
|
||||
|
||||
const defaultDirection = { title: 'asc',
|
||||
url: 'asc',
|
||||
original_created_at: 'desc',
|
||||
created_at: 'desc',
|
||||
updated_at: 'desc' } as const
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
@@ -339,20 +324,40 @@ export default (() => {
|
||||
<tr>
|
||||
<th className="p-2 text-left whitespace-nowrap">投稿</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="title" label="タイトル"/>
|
||||
<SortHeader<FetchPostsOrderField>
|
||||
by="title"
|
||||
label="タイトル"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="url" label="URL"/>
|
||||
<SortHeader<FetchPostsOrderField>
|
||||
by="url"
|
||||
label="URL"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">タグ</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="original_created_at" label="オリジナルの投稿日時"/>
|
||||
<SortHeader<FetchPostsOrderField>
|
||||
by="original_created_at"
|
||||
label="オリジナルの投稿日時"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="created_at" label="投稿日時"/>
|
||||
<SortHeader<FetchPostsOrderField>
|
||||
by="created_at"
|
||||
label="投稿日時"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="updated_at" label="更新日時"/>
|
||||
<SortHeader<FetchPostsOrderField>
|
||||
by="updated_at"
|
||||
label="更新日時"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
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 { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import SortHeader from '@/components/SortHeader'
|
||||
import TagLink from '@/components/TagLink'
|
||||
import DateTimeField from '@/components/common/DateTimeField'
|
||||
import Label from '@/components/common/Label'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
|
||||
import { tagsKeys } from '@/lib/queryKeys'
|
||||
import { fetchTags } from '@/lib/tags'
|
||||
import { dateString } from '@/lib/utils'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, FormEvent } from 'react'
|
||||
|
||||
import type { Category, FetchTagsOrder } from '@/types'
|
||||
import type { Category, FetchTagsOrder, FetchTagsOrderField } from '@/types'
|
||||
|
||||
|
||||
const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
|
||||
const t = v?.trim ()
|
||||
if (t)
|
||||
qs.set (k, t)
|
||||
}
|
||||
|
||||
|
||||
export default (() => {
|
||||
const location = useLocation ()
|
||||
|
||||
const navigate = useNavigate ()
|
||||
|
||||
const query = useMemo (() => new URLSearchParams (location.search), [location.search])
|
||||
|
||||
const page = Number (query.get ('page') ?? 1)
|
||||
@@ -26,7 +41,8 @@ export default (() => {
|
||||
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 qPostCountLTE =
|
||||
query.get ('post_count_lte') ? Number (query.get ('post_count_lte')) : null
|
||||
const qCreatedFrom = query.get ('created_from') ?? ''
|
||||
const qCreatedTo = query.get ('created_to') ?? ''
|
||||
const qUpdatedFrom = query.get ('updated_from') ?? ''
|
||||
@@ -36,17 +52,28 @@ export default (() => {
|
||||
const [name, setName] = useState ('')
|
||||
const [category, setCategory] = useState<Category | null> (null)
|
||||
const [postCountGTE, setPostCountGTE] = useState (1)
|
||||
const [postCountLTE, setPostCountLTE] = useState (-1)
|
||||
const [postCountLTE, setPostCountLTE] = useState<number | null> (null)
|
||||
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 keys = {
|
||||
page, limit, order,
|
||||
post: null,
|
||||
name: qName,
|
||||
category: qCategory,
|
||||
postCountGTE: qPostCountGTE,
|
||||
postCountLTE: qPostCountLTE,
|
||||
createdFrom: qCreatedFrom,
|
||||
createdTo: qCreatedTo,
|
||||
updatedFrom: qUpdatedFrom,
|
||||
updatedTo: qUpdatedTo }
|
||||
const { data, isLoading: loading } = useQuery ({
|
||||
queryKey: tagsKeys.index (keys),
|
||||
queryFn: () => fetchTags (keys) })
|
||||
const results = data?.tags ?? []
|
||||
const totalPages = data ? Math.ceil (data.count / limit) : 0
|
||||
|
||||
useEffect (() => {
|
||||
setName (qName)
|
||||
@@ -57,12 +84,36 @@ export default (() => {
|
||||
setCreatedTo (qCreatedTo)
|
||||
setUpdatedFrom (qUpdatedFrom)
|
||||
setUpdatedTo (qUpdatedTo)
|
||||
}, [])
|
||||
|
||||
const handleSearch = () => {
|
||||
;
|
||||
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
||||
}, [location.search])
|
||||
|
||||
const handleSearch = (e: FormEvent) => {
|
||||
e.preventDefault ()
|
||||
|
||||
const qs = new URLSearchParams ()
|
||||
setIf (qs, 'name', name)
|
||||
setIf (qs, 'category', category)
|
||||
if (postCountGTE !== 1)
|
||||
qs.set ('post_count_gte', String (postCountGTE))
|
||||
if (postCountLTE != null)
|
||||
qs.set ('post_count_lte', String (postCountLTE))
|
||||
setIf (qs, 'created_from', createdFrom)
|
||||
setIf (qs, 'created_to', createdTo)
|
||||
setIf (qs, 'updated_from', updatedFrom)
|
||||
setIf (qs, 'updated_to', updatedTo)
|
||||
qs.set ('page', '1')
|
||||
qs.set ('order', order)
|
||||
|
||||
navigate (`${ location.pathname }?${ qs.toString () }`)
|
||||
}
|
||||
|
||||
const defaultDirection = { name: 'asc',
|
||||
category: 'asc',
|
||||
post_count: 'desc',
|
||||
created_at: 'desc',
|
||||
updated_at: 'desc' } as const
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
@@ -88,7 +139,7 @@ export default (() => {
|
||||
<Label>カテゴリ</Label>
|
||||
<select
|
||||
value={category ?? ''}
|
||||
onChange={e => setCategory(e.target.value || null)}
|
||||
onChange={e => setCategory((e.target.value || null) as Category | null)}
|
||||
className="w-full border p-2 rounded">
|
||||
<option value=""> </option>
|
||||
{CATEGORIES.map (cat => (
|
||||
@@ -104,15 +155,15 @@ export default (() => {
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={postCountGTE < 0 ? '' : String (postCountGTE)}
|
||||
onChange={e => setPostCountGTE (Number (e.target.value || (-1)))}
|
||||
value={postCountGTE < 0 ? 0 : String (postCountGTE)}
|
||||
onChange={e => setPostCountGTE (Number (e.target.value || 0))}
|
||||
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)))}
|
||||
value={postCountLTE == null ? '' : String (postCountLTE)}
|
||||
onChange={e => setPostCountLTE (e.target.value ? Number (e.target.value) : null)}
|
||||
className="border rounded p-2"/>
|
||||
</div>
|
||||
|
||||
@@ -165,19 +216,39 @@ export default (() => {
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="name" label="タグ"/>
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="name"
|
||||
label="タグ"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="category" label="カテゴリ"/>
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="category"
|
||||
label="カテゴリ"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="post_count" label="件数"/>
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="post_count"
|
||||
label="件数"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="created_at" label="最初の記載日時"/>
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="created_at"
|
||||
label="最初の記載日時"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="updated_at" label="更新日時"/>
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="updated_at"
|
||||
label="更新日時"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -189,13 +260,15 @@ export default (() => {
|
||||
<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 text-right">{row.postCount}</td>
|
||||
<td className="p-2">{dateString (row.createdAt)}</td>
|
||||
<td className="p-2">{dateString (row.updatedAt)}</td>
|
||||
</tr>))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination page={page} totalPages={totalPages}/>
|
||||
</div>) : '結果ないよ(笑)')}
|
||||
</MainArea>)
|
||||
}) satisfies FC
|
||||
|
||||
+17
-1
@@ -32,9 +32,23 @@ export type FetchTagsOrderField =
|
||||
| 'name'
|
||||
| 'category'
|
||||
| 'post_count'
|
||||
| 'create_at'
|
||||
| 'created_at'
|
||||
| 'updated_at'
|
||||
|
||||
export type FetchTagsParams = {
|
||||
post: number | null
|
||||
name: string
|
||||
category: Category | null
|
||||
postCountGTE: number
|
||||
postCountLTE: number | null
|
||||
createdFrom: string
|
||||
createdTo: string
|
||||
updatedFrom: string
|
||||
updatedTo: string
|
||||
page: number
|
||||
limit: number
|
||||
order: FetchTagsOrder }
|
||||
|
||||
export type Menu = MenuItem[]
|
||||
|
||||
export type MenuItem = {
|
||||
@@ -80,6 +94,8 @@ export type Tag = {
|
||||
name: string
|
||||
category: Category
|
||||
postCount: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
hasWiki: boolean
|
||||
children?: Tag[]
|
||||
matchedAlias?: string | null }
|
||||
|
||||
Reference in New Issue
Block a user