このコミットが含まれているのは:
@@ -2,10 +2,12 @@ import { useLocation } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
|
||||
export default <T extends string,>({ by, label, currentOrder, defaultDirection }: {
|
||||
by: T
|
||||
label: string
|
||||
label: ReactNode
|
||||
currentOrder: `${ T }:${ 'asc' | 'desc' }`
|
||||
defaultDirection: Record<T, 'asc' | 'desc'> }) => {
|
||||
const [fld, dir] = currentOrder.split (':')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import ResponsiveMarqueeText from '@/components/ResponsiveMarqueeText'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { ComponentProps, CSSProperties, FC, HTMLAttributes } from 'react'
|
||||
@@ -67,6 +68,10 @@ const TagLink: FC<Props> = ({ tag,
|
||||
style,
|
||||
title,
|
||||
...props }) => {
|
||||
const { message } = useUITranslation ()
|
||||
const missingMaterial = message ('tagLink.missingMaterial', { name: tag.name })
|
||||
const missingDeerjikist = message ('tagLink.missingDeerjikist', { name: tag.name })
|
||||
const missingWiki = message ('tagLink.missingWiki', { name: tag.name })
|
||||
const colourStyle = {
|
||||
'--tag-link-colour': `var(--tag-colour-${ tag.category })`,
|
||||
'--tag-link-hover-colour': `var(--tag-colour-${ tag.category }-hover)`,
|
||||
@@ -121,7 +126,8 @@ const TagLink: FC<Props> = ({ tag,
|
||||
to={`/materials/new?tag=${ encodeURIComponent (tag.name) }`}
|
||||
className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
|
||||
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
|
||||
title={`${ tag.name } 素材情報が存在しません.`}>
|
||||
lang={missingMaterial.language}
|
||||
title={missingMaterial.text}>
|
||||
!
|
||||
</PrefetchLink>)
|
||||
: (
|
||||
@@ -131,7 +137,8 @@ const TagLink: FC<Props> = ({ tag,
|
||||
to={`/tags/${ tag.id }/deerjikists`}
|
||||
className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
|
||||
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
|
||||
title={`${ tag.name } に関する情報が存在しません.`}>
|
||||
lang={missingDeerjikist.language}
|
||||
title={missingDeerjikist.text}>
|
||||
!
|
||||
</PrefetchLink>)
|
||||
: (
|
||||
@@ -139,7 +146,8 @@ const TagLink: FC<Props> = ({ tag,
|
||||
to={`/wiki/${ encodeURIComponent (tag.name) }`}
|
||||
className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
|
||||
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
|
||||
title={`${ tag.name } Wiki が存在しません.`}>
|
||||
lang={missingWiki.language}
|
||||
title={missingWiki.text}>
|
||||
!
|
||||
</PrefetchLink>)))}
|
||||
</span>)}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import LocalisedText from '@/components/LocalisedText'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
type Props = {
|
||||
messageKey: string
|
||||
@@ -13,12 +11,11 @@ type Props = {
|
||||
const TranslatedText = (
|
||||
{ messageKey, namespace = 'navigation', values, className }: Props,
|
||||
) => {
|
||||
const locale = useLocale ()
|
||||
const { t } = useTranslation (namespace, { lng: locale })
|
||||
const result = t (messageKey, { ...values, returnDetails: true })
|
||||
const { message } = useUITranslation (namespace)
|
||||
const result = message (messageKey, values)
|
||||
return (
|
||||
<LocalisedText language={result.usedLng} className={className}>
|
||||
{result.res}
|
||||
<LocalisedText language={result.language} className={className}>
|
||||
{result.text}
|
||||
</LocalisedText>)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
@@ -49,6 +50,7 @@ const getPages = (
|
||||
|
||||
const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
|
||||
const location = useLocation ()
|
||||
const { message } = useUITranslation ()
|
||||
|
||||
const buildTo = (p: number) => {
|
||||
const qs = new URLSearchParams (location.search)
|
||||
@@ -59,7 +61,9 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
|
||||
const pages = getPages (page, totalPages, siblingCount)
|
||||
|
||||
return (
|
||||
<nav className="mt-4 flex justify-center" aria-label="Pagination">
|
||||
<nav className="mt-4 flex justify-center"
|
||||
lang={message ('pagination.label').language}
|
||||
aria-label={message ('pagination.label').text}>
|
||||
<div className="flex items-center gap-2">
|
||||
{(page > 1)
|
||||
? (
|
||||
@@ -67,13 +71,15 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
|
||||
<PrefetchLink
|
||||
className="md:hidden p-2"
|
||||
to={buildTo (1)}
|
||||
aria-label="最初のページ">
|
||||
lang={message ('pagination.first').language}
|
||||
aria-label={message ('pagination.first').text}>
|
||||
|<
|
||||
</PrefetchLink>
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page - 1)}
|
||||
aria-label="前のページ">
|
||||
lang={message ('pagination.previous').language}
|
||||
aria-label={message ('pagination.previous').text}>
|
||||
<
|
||||
</PrefetchLink>
|
||||
</>)
|
||||
@@ -106,13 +112,15 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page + 1)}
|
||||
aria-label="次のページ">
|
||||
lang={message ('pagination.next').language}
|
||||
aria-label={message ('pagination.next').text}>
|
||||
>
|
||||
</PrefetchLink>
|
||||
<PrefetchLink
|
||||
className="md:hidden p-2"
|
||||
to={buildTo (totalPages)}
|
||||
aria-label="最後のページ">
|
||||
lang={message ('pagination.last').language}
|
||||
aria-label={message ('pagination.last').text}>
|
||||
>|
|
||||
</PrefetchLink>
|
||||
</>)
|
||||
|
||||
+36
-6
@@ -2,12 +2,36 @@ import { createInstance } from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
import ainuNavigation from '@/locales/ain-Kana/navigation.json'
|
||||
import ainuCommon from '@/locales/ain-Kana/common.json'
|
||||
import ainuTags from '@/locales/ain-Kana/tags.json'
|
||||
import nipponeseNavigation from '@/locales/ja/navigation.json'
|
||||
import nipponeseCommon from '@/locales/ja/common.json'
|
||||
import nipponeseTags from '@/locales/ja/tags.json'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
|
||||
export const i18n = createInstance ()
|
||||
|
||||
|
||||
// Keep Japanese placeholders editable without reporting them as completed translations.
|
||||
const translatedResource = (resource: Record<string, unknown>): Record<string, unknown> => {
|
||||
const untranslated = new Set (
|
||||
Array.isArray (resource._untranslated) ? resource._untranslated : [])
|
||||
const select = (entries: Record<string, unknown>, prefix = ''): Record<string, unknown> =>
|
||||
Object.fromEntries (Object.entries (entries).flatMap (([key, value]) => {
|
||||
const path = prefix ? `${ prefix }.${ key }` : key
|
||||
if (key === '_untranslated' || untranslated.has (path))
|
||||
return []
|
||||
const translated =
|
||||
(value != null && typeof value === 'object' && !(Array.isArray (value)))
|
||||
? select (value as Record<string, unknown>, path)
|
||||
: value
|
||||
return [[key, translated]]
|
||||
}))
|
||||
return select (resource)
|
||||
}
|
||||
|
||||
|
||||
void i18n.use (initReactI18next).init ({
|
||||
lng: 'ja',
|
||||
fallbackLng: 'ja',
|
||||
@@ -15,10 +39,13 @@ void i18n.use (initReactI18next).init ({
|
||||
load: 'currentOnly',
|
||||
initAsync: false,
|
||||
defaultNS: 'navigation',
|
||||
ns: ['navigation'],
|
||||
ns: ['navigation', 'common', 'tags'],
|
||||
resources: {
|
||||
ja: { navigation: nipponeseNavigation },
|
||||
'ain-Kana': { navigation: ainuNavigation } },
|
||||
ja: { navigation: nipponeseNavigation, common: nipponeseCommon, tags: nipponeseTags },
|
||||
'ain-Kana': {
|
||||
navigation: ainuNavigation,
|
||||
common: translatedResource (ainuCommon),
|
||||
tags: translatedResource (ainuTags) } },
|
||||
interpolation: { escapeValue: false },
|
||||
returnNull: false,
|
||||
returnEmptyString: false,
|
||||
@@ -50,7 +77,10 @@ export const untranslatedKeys = (
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
{
|
||||
const missing = untranslatedKeys ('ain-Kana')
|
||||
if (missing.length > 0)
|
||||
console.warn ('[i18n] ain-Kana: untranslated navigation keys', missing)
|
||||
for (const namespace of ['navigation', 'common', 'tags'])
|
||||
{
|
||||
const missing = untranslatedKeys ('ain-Kana', namespace)
|
||||
if (missing.length > 0)
|
||||
console.warn (`[i18n] ain-Kana: untranslated ${ namespace } keys`, missing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
|
||||
|
||||
export const useUITranslation = (namespace = 'common') => {
|
||||
const locale = useLocale ()
|
||||
const { t } = useTranslation (namespace, { lng: locale })
|
||||
|
||||
const message = (key: string, values?: Record<string, string | number>) => {
|
||||
const result = t (key, { ...values, returnDetails: true })
|
||||
return { text: result.res, language: result.usedLng }
|
||||
}
|
||||
|
||||
return { message }
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import type { ClassValue } from 'clsx'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
|
||||
|
||||
export const toDate = (d: string | Date): Date => typeof d === 'string' ? new Date (d) : d
|
||||
|
||||
@@ -25,6 +27,24 @@ export const dateString = (
|
||||
second: unknown == null ? 'numeric' : undefined })
|
||||
|
||||
|
||||
export const localisedDateString = (value: string | Date, locale: LocaleCode): string => {
|
||||
if (locale === 'ja')
|
||||
return dateString (value)
|
||||
|
||||
const date = toDate (value)
|
||||
if (Number.isNaN (date.getTime ()))
|
||||
return dateString (value)
|
||||
|
||||
const parts = new Intl.DateTimeFormat ('en-CA', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hourCycle: 'h23' }).formatToParts (date)
|
||||
const part = (type: string) => parts.find (entry => entry.type === type)?.value ?? ''
|
||||
return `${ part ('year') }-${ part ('month') }-${ part ('day') }\u3000${
|
||||
part ('hour') }:${ part ('minute') }:${ part ('second') }`
|
||||
}
|
||||
|
||||
|
||||
export const originalCreatedAtString = (
|
||||
f: string | Date | null,
|
||||
b: string | Date | null): string => {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"_untranslated": [
|
||||
"countLabel", "categoryLabel", "stateLabel", "loading", "fetchError",
|
||||
"states", "categories.deerjikist", "categories.meme", "categories.character",
|
||||
"categories.general", "categories.meta", "categories.nico", "pagination", "tagLink"
|
||||
],
|
||||
"countLabel": "件数",
|
||||
"categoryLabel": "カテゴリ",
|
||||
"stateLabel": "状態",
|
||||
"search": "イフナラ",
|
||||
"loading": "Loading...",
|
||||
"fetchError": "データ取得エラー",
|
||||
"states": {
|
||||
"active": "有効",
|
||||
"deprecated": "廃止"
|
||||
},
|
||||
"categories": {
|
||||
"deerjikist": "ニジラー",
|
||||
"meme": "原作・ネタ元・ミーム等",
|
||||
"character": "キャラクター",
|
||||
"general": "一般",
|
||||
"material": "チカㇻペ",
|
||||
"meta": "メタタグ",
|
||||
"nico": "ニコニコタグ"
|
||||
},
|
||||
"pagination": {
|
||||
"label": "Pagination",
|
||||
"first": "最初のページ",
|
||||
"previous": "前のページ",
|
||||
"next": "次のページ",
|
||||
"last": "最後のページ"
|
||||
},
|
||||
"tagLink": {
|
||||
"missingMaterial": "{{name}} 素材情報が存在しません.",
|
||||
"missingDeerjikist": "{{name}} に関する情報が存在しません.",
|
||||
"missingWiki": "{{name}} Wiki が存在しません."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"_untranslated": [
|
||||
"name", "postCount", "firstRecordedAt", "definitionUpdatedAt", "aliases",
|
||||
"parents", "firstRecordedColumn", "updatedAt", "empty"
|
||||
],
|
||||
"title": "タㇰ",
|
||||
"name": "名前",
|
||||
"postCount": "広場の投稿数",
|
||||
"firstRecordedAt": "はじめて記載された日時",
|
||||
"definitionUpdatedAt": "定義の更新日時",
|
||||
"aliases": "別名",
|
||||
"parents": "上位タグ",
|
||||
"firstRecordedColumn": "最初の記載日時",
|
||||
"updatedAt": "更新日時",
|
||||
"history": "フㇱコアンペ",
|
||||
"empty": "結果ないよ(笑)"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"countLabel": "件数",
|
||||
"categoryLabel": "カテゴリ",
|
||||
"stateLabel": "状態",
|
||||
"search": "検索",
|
||||
"loading": "Loading...",
|
||||
"fetchError": "データ取得エラー",
|
||||
"states": {
|
||||
"active": "有効",
|
||||
"deprecated": "廃止"
|
||||
},
|
||||
"categories": {
|
||||
"deerjikist": "ニジラー",
|
||||
"meme": "原作・ネタ元・ミーム等",
|
||||
"character": "キャラクター",
|
||||
"general": "一般",
|
||||
"material": "素材",
|
||||
"meta": "メタタグ",
|
||||
"nico": "ニコニコタグ"
|
||||
},
|
||||
"pagination": {
|
||||
"label": "Pagination",
|
||||
"first": "最初のページ",
|
||||
"previous": "前のページ",
|
||||
"next": "次のページ",
|
||||
"last": "最後のページ"
|
||||
},
|
||||
"tagLink": {
|
||||
"missingMaterial": "{{name}} 素材情報が存在しません.",
|
||||
"missingDeerjikist": "{{name}} に関する情報が存在しません.",
|
||||
"missingWiki": "{{name}} Wiki が存在しません."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "タグ",
|
||||
"name": "名前",
|
||||
"postCount": "広場の投稿数",
|
||||
"firstRecordedAt": "はじめて記載された日時",
|
||||
"definitionUpdatedAt": "定義の更新日時",
|
||||
"aliases": "別名",
|
||||
"parents": "上位タグ",
|
||||
"firstRecordedColumn": "最初の記載日時",
|
||||
"updatedAt": "更新日時",
|
||||
"history": "耕作履歴",
|
||||
"empty": "結果ないよ(笑)"
|
||||
}
|
||||
@@ -4,16 +4,18 @@ import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
|
||||
import LocalisedText from '@/components/LocalisedText'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import SortHeader from '@/components/SortHeader'
|
||||
import TagLink from '@/components/TagLink'
|
||||
import TranslatedText from '@/components/TranslatedText'
|
||||
import DateTimeField from '@/components/common/DateTimeField'
|
||||
import FormField from '@/components/common/FormField'
|
||||
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 { CATEGORIES } from '@/consts'
|
||||
import { clientScrollBehaviour } from '@/lib/clientAnimation'
|
||||
import { tagsKeys } from '@/lib/queryKeys'
|
||||
import {
|
||||
@@ -26,8 +28,9 @@ import {
|
||||
} from '@/lib/settings'
|
||||
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
|
||||
import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { fetchTags } from '@/lib/tags'
|
||||
import { dateString, inputClass } from '@/lib/utils'
|
||||
import { inputClass, localisedDateString } from '@/lib/utils'
|
||||
|
||||
import type { FC, FormEvent } from 'react'
|
||||
|
||||
@@ -45,8 +48,6 @@ const boolFromQuery = (value: string | null): boolean =>
|
||||
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
||||
|
||||
|
||||
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
|
||||
|
||||
const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
|
||||
const n = Number (value)
|
||||
return n === 20 || n === 50 || n === 100 ? n : null
|
||||
@@ -75,6 +76,8 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => {
|
||||
|
||||
const TagListPage: FC = () => {
|
||||
const locale = useLocale ()
|
||||
const { message } = useUITranslation ('tags')
|
||||
const label = (key: string) => <TranslatedText namespace="tags" messageKey={key}/>
|
||||
const behaviourSettings = useClientBehaviourSettings ()
|
||||
const animationMode = behaviourSettings.animation ?? 'normal'
|
||||
const location = useLocation ()
|
||||
@@ -99,7 +102,8 @@ const TagListPage: FC = () => {
|
||||
const qCreatedTo = query.get ('created_to') ?? ''
|
||||
const qUpdatedFrom = query.get ('updated_from') ?? ''
|
||||
const qUpdatedTo = query.get ('updated_to') ?? ''
|
||||
const qDeprecated = query.has ('deprecated')
|
||||
const qDeprecated =
|
||||
query.has ('deprecated')
|
||||
? boolFromQuery (query.get ('deprecated'))
|
||||
: null
|
||||
const order = (
|
||||
@@ -130,7 +134,7 @@ const TagListPage: FC = () => {
|
||||
updatedFrom: qUpdatedFrom,
|
||||
updatedTo: qUpdatedTo,
|
||||
deprecated: qDeprecated }
|
||||
const { data, isLoading: loading } = useQuery ({
|
||||
const { data, isLoading: loading, isError } = useQuery ({
|
||||
queryKey: tagsKeys.index (keys, locale),
|
||||
queryFn: () => fetchTags (keys, locale) })
|
||||
const results = data?.tags ?? []
|
||||
@@ -203,32 +207,28 @@ const TagListPage: FC = () => {
|
||||
|
||||
useKeyboardShortcuts (useMemo (() => ({
|
||||
...(page > 1
|
||||
? {
|
||||
'pagination.previous': () => {
|
||||
? { 'pagination.previous': () => {
|
||||
updatePage (page - 1)
|
||||
},
|
||||
}
|
||||
} }
|
||||
: { }),
|
||||
...(totalPages > 0 && page < totalPages
|
||||
? {
|
||||
'pagination.next': () => {
|
||||
? { 'pagination.next': () => {
|
||||
updatePage (Math.min (page + 1, totalPages))
|
||||
},
|
||||
}
|
||||
} }
|
||||
: { }),
|
||||
}), [page, totalPages, location.search]))
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>タグ | {SITE_TITLE}</title>
|
||||
<title>{message ('title').text} | {SITE_TITLE}</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<PageTitle>タグ</PageTitle>
|
||||
<PageTitle>{label ('title')}</PageTitle>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<span>{label ('common:countLabel')}</span>
|
||||
<select
|
||||
value={limit}
|
||||
onChange={ev => updateListQuery ({
|
||||
@@ -246,7 +246,7 @@ const TagListPage: FC = () => {
|
||||
|
||||
<form onSubmit={handleSearch} className="space-y-2">
|
||||
{/* 名前 */}
|
||||
<FormField label="名前">
|
||||
<FormField label={label ('name')}>
|
||||
{({ invalid }) => (
|
||||
<input
|
||||
type="text"
|
||||
@@ -257,7 +257,7 @@ const TagListPage: FC = () => {
|
||||
</FormField>
|
||||
|
||||
{/* カテゴリ */}
|
||||
<FormField label="カテゴリ">
|
||||
<FormField label={label ('common:categoryLabel')}>
|
||||
{({ invalid }) => (
|
||||
<select
|
||||
value={category ?? ''}
|
||||
@@ -265,14 +265,15 @@ const TagListPage: FC = () => {
|
||||
className={inputClass (invalid)}>
|
||||
<option value=""> </option>
|
||||
{CATEGORIES.map (cat => (
|
||||
<option key={cat} value={cat}>
|
||||
{CATEGORY_NAMES[cat]}
|
||||
<option key={cat} value={cat}
|
||||
lang={message (`common:categories.${ cat }`).language}>
|
||||
{message (`common:categories.${ cat }`).text}
|
||||
</option>))}
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
{/* 広場の投稿数 */}
|
||||
<FormField label="広場の投稿数">
|
||||
<FormField label={label ('postCount')}>
|
||||
{({ invalid }) => (
|
||||
<>
|
||||
<input
|
||||
@@ -294,34 +295,38 @@ const TagListPage: FC = () => {
|
||||
</FormField>
|
||||
|
||||
{/* はじめて記載された日時 */}
|
||||
<FormField label="はじめて記載された日時">
|
||||
<FormField label={label ('firstRecordedAt')}>
|
||||
{() => (
|
||||
<>
|
||||
<DateTimeField
|
||||
lang={locale}
|
||||
value={createdFrom ?? undefined}
|
||||
onChange={setCreatedFrom}/>
|
||||
<span className="mx-1">〜</span>
|
||||
<DateTimeField
|
||||
lang={locale}
|
||||
value={createdTo ?? undefined}
|
||||
onChange={setCreatedTo}/>
|
||||
</>)}
|
||||
</FormField>
|
||||
|
||||
{/* 定義の更新日時 */}
|
||||
<FormField label="定義の更新日時">
|
||||
<FormField label={label ('definitionUpdatedAt')}>
|
||||
{() => (
|
||||
<>
|
||||
<DateTimeField
|
||||
lang={locale}
|
||||
value={updatedFrom ?? undefined}
|
||||
onChange={setUpdatedFrom}/>
|
||||
<span className="mx-1">〜</span>
|
||||
<DateTimeField
|
||||
lang={locale}
|
||||
value={updatedTo ?? undefined}
|
||||
onChange={setUpdatedTo}/>
|
||||
</>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="状態">
|
||||
<FormField label={label ('common:stateLabel')}>
|
||||
{({ invalid }) => (
|
||||
<select
|
||||
value={deprecated == null ? '' : (deprecated ? '1' : '0')}
|
||||
@@ -331,8 +336,12 @@ const TagListPage: FC = () => {
|
||||
: e.target.value === '1')}
|
||||
className={inputClass (invalid)}>
|
||||
<option value=""> </option>
|
||||
<option value="0">有効</option>
|
||||
<option value="1">廃止</option>
|
||||
<option value="0" lang={message ('common:states.active').language}>
|
||||
{message ('common:states.active').text}
|
||||
</option>
|
||||
<option value="1" lang={message ('common:states.deprecated').language}>
|
||||
{message ('common:states.deprecated').text}
|
||||
</option>
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
@@ -340,13 +349,14 @@ const TagListPage: FC = () => {
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||
検索
|
||||
{label ('common:search')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{loading ? 'Loading...' : (results.length > 0 ? (
|
||||
{isError && <p role="alert">{label ('common:fetchError')}</p>}
|
||||
{loading ? label ('common:loading') : (results.length > 0 ? (
|
||||
<div className="mt-4">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[2000px] table-fixed border-collapse">
|
||||
@@ -367,38 +377,44 @@ const TagListPage: FC = () => {
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="name"
|
||||
label="タグ"
|
||||
label={label ('title')}
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="post_count"
|
||||
label="件数"
|
||||
label={label ('common:countLabel')}
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="category"
|
||||
label="カテゴリ"
|
||||
label={label ('common:categoryLabel')}
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</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">
|
||||
{label ('common:stateLabel')}
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
{label ('aliases')}
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
{label ('parents')}
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="created_at"
|
||||
label="最初の記載日時"
|
||||
label={label ('firstRecordedColumn')}
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="updated_at"
|
||||
label="更新日時"
|
||||
label={label ('updatedAt')}
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
@@ -420,8 +436,12 @@ const TagListPage: FC = () => {
|
||||
withCount={false}/>
|
||||
</td>
|
||||
<td className="p-2 text-right">{row.postCount}</td>
|
||||
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
||||
<td className="p-2">{tagStateLabel (row.deprecatedAt)}</td>
|
||||
<td className="p-2">
|
||||
{label (`common:categories.${ row.category }`)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{row.deprecatedAt ? label ('common:states.deprecated') : ''}
|
||||
</td>
|
||||
<td className="p-2">{row.aliases.join (' ')}</td>
|
||||
<td className="p-2">
|
||||
{row.parents.map (t => (
|
||||
@@ -432,14 +452,26 @@ const TagListPage: FC = () => {
|
||||
withCount={false}/>
|
||||
</span>))}
|
||||
</td>
|
||||
<td className="p-2">{dateString (row.createdAt)}</td>
|
||||
<td className="p-2">{dateString (row.updatedAt)}</td>
|
||||
<td className="p-2">
|
||||
<time dateTime={row.createdAt} lang={locale}>
|
||||
<LocalisedText language={locale}>
|
||||
{localisedDateString (row.createdAt, locale)}
|
||||
</LocalisedText>
|
||||
</time>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<time dateTime={row.updatedAt} lang={locale}>
|
||||
<LocalisedText language={locale}>
|
||||
{localisedDateString (row.updatedAt, locale)}
|
||||
</LocalisedText>
|
||||
</time>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<PrefetchLink
|
||||
to={row.category === 'nico'
|
||||
? `/posts/changes?external_tag=${ row.id }`
|
||||
: `/posts/changes?tag=${ row.id }`}>
|
||||
耕作履歴
|
||||
{label ('history')}
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
</tr>))}
|
||||
@@ -448,7 +480,7 @@ const TagListPage: FC = () => {
|
||||
</div>
|
||||
|
||||
<Pagination page={page} totalPages={totalPages}/>
|
||||
</div>) : '結果ないよ(笑)')}
|
||||
</div>) : (isError ? null : label ('empty')))}
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする