アイヌ語追加 (#428) (#433)

Reviewed-on: #433
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
2026-10-20 20:44:14 +09:00
committed by みてるぞ
コミット 084a090cab
294個のファイルの変更、13978行の追加、3461行の削除
+1 -1
ファイルの表示
@@ -63,7 +63,7 @@ describe ('PostDetailPage', () => {
renderPage ()
await waitFor (() => {
expect (postsApi.fetchPost).toHaveBeenCalledWith ('9')
expect (postsApi.fetchPost).toHaveBeenCalledWith ('9', 'ja')
})
expect (await screen.findByText ('Embed:https://example.com/9')).toBeInTheDocument ()
expect (screen.getByRole ('button', { name: '未閲覧' })).toBeInTheDocument ()
+23 -30
ファイルの表示
@@ -1,8 +1,8 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useEffect, useRef } from 'react'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PostEditForm from '@/components/PostEditForm'
import PostEmbed from '@/components/PostEmbed'
import PostList from '@/components/PostList'
@@ -11,13 +11,14 @@ import TabGroup, { Tab } from '@/components/common/TabGroup'
import MainArea from '@/components/layout/MainArea'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import PageHead from '@/components/common/PageHead'
import { isApiError } from '@/lib/api'
import { fetchPost, toggleViewedFlg } from '@/lib/posts'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { canEditContent } from '@/lib/users'
import { cn } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import NotFound from '@/pages/NotFound'
import ServiceUnavailable from '@/pages/ServiceUnavailable'
@@ -29,6 +30,8 @@ type Props = { user: User | null }
const PostDetailPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const { message, label } = useUITranslation ('posts')
const editable = canEditContent (user)
const behaviourSettings = useClientBehaviourSettings ()
const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal'
@@ -42,18 +45,18 @@ const PostDetailPage: FC<Props> = ({ user }) => {
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const }
const postId = String (id ?? '')
const postKey = postsKeys.show (postId)
const postKey = postsKeys.show (postId, locale)
const { data: post, isError: errorFlg, error } = useQuery ({
enabled: Boolean (id),
queryKey: postKey,
queryFn: () => fetchPost (postId) })
queryFn: () => fetchPost (postId, locale) })
const qc = useQueryClient ()
const embedRef = useRef<NiconicoViewerHandle> (null)
const [status, setStatus] = useState (200)
const status = (errorFlg && isApiError (error)) ? error.response?.status ?? 200 : 200
const changeViewedFlg = useMutation ({
mutationFn: async (next: boolean) => {
@@ -70,24 +73,15 @@ const PostDetailPage: FC<Props> = ({ user }) => {
onError: (...[, , ctx]) => {
if (ctx?.prev)
qc.setQueryData (postKey, ctx.prev)
toast ({ title: '失敗……', description: '通信に失敗しました……' })
toast ({ title: label ('shared:messages.failed'),
description: label ('detail.networkFailed') })
},
onSuccess: () => {
qc.invalidateQueries ({ queryKey: postsKeys.root })} })
useEffect (() => {
if (!(errorFlg))
return
const code = isApiError (error) ? error.response?.status : undefined
if (code)
setStatus (code)
}, [errorFlg, error])
useEffect (() => {
scroll (0, 0)
setStatus (200)
}, [id])
switch (status)
@@ -106,11 +100,10 @@ const PostDetailPage: FC<Props> = ({ user }) => {
<div
data-sidebar-container
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
<Helmet>
<PageHead title={post?.title || post?.url || message ('navigation:posts')}>
{(post?.thumbnail || post?.thumbnailBase) && (
<meta name="thumbnail" content={post.thumbnail! || post.thumbnailBase!}/>)}
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
</Helmet>
</PageHead>
{post && <TagDetailSidebar post={post} className="hidden md:block"/>}
@@ -120,7 +113,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
<>
{(post.childPosts ?? []).length > 0 && (
<div className="mb-4 bg-green-200 dark:bg-green-800 text-sm p-2 rounded-md">
<p>この投稿には {post.childPosts!.length} 件の子投稿があります.</p>
<p>{label ('detail.children', { count: post.childPosts!.length })}</p>
<PostList posts={[{ ...post, childPosts: [{ } as Post] },
...post.childPosts!.map (p => ({
...p, parentPosts: [{ } as Post] }))]}/>
@@ -135,9 +128,9 @@ const PostDetailPage: FC<Props> = ({ user }) => {
key={pp.id}
className="mb-4 bg-yellow-200 dark:bg-yellow-800 text-sm p-2 rounded-md">
<p>
この投稿には 1 件の親投稿{
siblings.length > 1
&& `と ${ siblings.length - 1 } 件の姉妹投稿`}があります.
{siblings.length > 1
? label ('detail.parentAndSiblings', { count: siblings.length - 1 })
: label ('detail.parent')}
</p>
<PostList posts={[{ ...pp, childPosts: [{ } as Post] },
...siblings.map (p => ({
@@ -168,20 +161,20 @@ const PostDetailPage: FC<Props> = ({ user }) => {
<Button onClick={() => changeViewedFlg.mutate (!(post.viewed))}
disabled={changeViewedFlg.isPending}
className={cn ('text-white', viewedClass)}>
{post.viewed ? '閲覧済' : '未閲覧'}
{label (post.viewed ? 'detail.viewed' : 'detail.unviewed')}
</Button>
<TabGroup>
<Tab name="関聯">
<Tab name={label ('shared:labels.relations')}>
{post.related.length > 0
? <PostList posts={post.related}/>
: 'まだないよ(笑)'}
: label ('detail.emptyRelated')}
</Tab>
{editable && (
<Tab name="編輯">
<Tab name={label ('shared:actions.edit')}>
<PostEditForm
post={post}
onSave={newPost => {
qc.setQueryData (postsKeys.show (postId),
qc.setQueryData (postsKeys.show (postId, locale),
(prev: Post | undefined) => newPost ?? prev)
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
@@ -189,7 +182,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
</Tab>)}
</TabGroup>
</>)
: 'Loading...'}
: label ('common:loading')}
</MainArea>
{post && <TagDetailSidebar post={post} sp className="md:hidden"/>}
+2 -2
ファイルの表示
@@ -47,10 +47,10 @@ describe ('PostHistoryPage', () => {
externalTag: '7',
page: 1,
limit: 20,
})
}, 'ja')
})
expect (tagsApi.fetchExternalTag).toHaveBeenCalledWith ('7')
expect (tagsApi.fetchExternalTag).toHaveBeenCalledWith ('7', 'ja')
expect (tagsApi.fetchTag).not.toHaveBeenCalled ()
expect (
+60 -55
ファイルの表示
@@ -1,24 +1,25 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import { useDialogue } from '@/components/dialogues/DialogueProvider'
import { useDialogue } from '@/lib/dialogues/useDialogue'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import PageHead from '@/components/common/PageHead'
import { clientAnimationTransition,
clientScrollBehaviour } from '@/lib/clientAnimation'
import { fetchPostChanges, updatePost } from '@/lib/posts'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { fetchExternalTag, fetchTag } from '@/lib/tags'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn, dateString, originalCreatedAtString } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import { cn, localisedDateString, originalCreatedAtString } from '@/lib/utils'
import type { FC, MouseEvent } from 'react'
@@ -39,6 +40,8 @@ const renderDiff = (diff: { current: string | null; prev: string | null }) => (
const PostHistoryPage: FC = () => {
const { message, label } = useUITranslation ('history')
const locale = useLocale ()
const dialogue = useDialogue ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
@@ -61,29 +64,30 @@ const PostHistoryPage: FC = () => {
const tagQueryId = tagId ?? ''
const { data: tag } = useQuery ({
enabled: Boolean (tagId),
queryKey: tagsKeys.show (tagQueryId),
queryFn: () => fetchTag (tagQueryId) })
queryKey: tagsKeys.show (tagQueryId, locale),
queryFn: () => fetchTag (tagQueryId, locale) })
const externalTagQueryId = externalTagId ?? ''
const { data: externalTag } = useQuery ({
enabled: Boolean (externalTagId),
queryKey: tagsKeys.externalShow (externalTagQueryId),
queryFn: () => fetchExternalTag (externalTagQueryId) })
queryKey: tagsKeys.externalShow (externalTagQueryId, locale),
queryFn: () => fetchExternalTag (externalTagQueryId, locale) })
const { data, isLoading: loading } = useQuery ({
const { data, isLoading: loading, isError } = useQuery ({
queryKey: postsKeys.changes ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && { externalTag: externalTagId }),
page, limit }),
page, limit }, locale),
queryFn: () => fetchPostChanges ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && {
externalTag: externalTagId }),
page, limit }) })
page, limit }, locale) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
const displayedTag = externalTag ?? tag
const pageTitleLabel = `耕作履歴${ id ? `: 投稿 #${ id }` : '' }${
const pageTitleLabel = `${message ('postTitle').text}${
id ? `${message ('postPrefix').text}#${id}` : '' }${
displayedTag ? `(${ displayedTag.name })` : '' }`
const qc = useQueryClient ()
@@ -92,27 +96,24 @@ const PostHistoryPage: FC = () => {
e.preventDefault ()
if (!(await dialogue.confirm ({
title: '差戻の確認',
description: `『${ change.title.current || change.url.current }』を版 ${
change.versionNo } に差戻します.\nよろしいですか?`,
confirmText: '差戻' })))
title: label ('confirmTitle'),
description: label ('confirmPost', {
name: change.title.current || change.url.current, version: change.versionNo }),
confirmText: label ('revert'),
cancelText: label ('settings:actions.cancel'),
closeLabel: label ('settings:actions.close') })))
return
try
{
const id = change.postId
const title = change.title.current
const tags =
change.tags
.filter (t => t.type !== 'removed')
.map (t => t.name)
.filter (t => t.slice (0, 5) !== 'nico:')
.join (' ')
const parentPostIds =
(change.parentPosts ?? [])
.filter (p => p.type !== 'removed')
.map (p => p.id)
.join (' ')
const currentTags = change.tags.filter (tag => tag.type !== 'removed' && !(tag.external))
if (currentTags.some (tag => tag.restoreName == null)
|| typeof change.parentPostIds !== 'string')
throw new Error ()
const tags = currentTags.map (tag => tag.restoreName).join (' ')
const parentPostIds = change.parentPostIds
const duration =
change.videoMs.current == null
? null
@@ -121,16 +122,16 @@ const PostHistoryPage: FC = () => {
const originalCreatedBefore = change.originalCreatedBefore.current
await updatePost ({ id, title, tags, duration, parentPostIds,
originalCreatedFrom, originalCreatedBefore },
{ force: true })
{ force: true }, locale)
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
toast ({ description: '差戻しました.' })
toast ({ description: label ('restored') })
}
catch
{
toast ({ description: '差戻に失敗……' })
toast ({ description: label ('restoreFailed') })
}
}
@@ -143,13 +144,14 @@ const PostHistoryPage: FC = () => {
return (
<MainArea>
<Helmet>
<title>{`耕作履歴 | ${ SITE_TITLE }`}</title>
</Helmet>
<PageHead title={message ('postTitle')}/>
<PageTitle aria-label={pageTitleLabel}>
耕作履歴
{id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>}
<PageTitle aria-label={pageTitleLabel} lang={message ('postTitle').language}>
{label ('postTitle')}
{id && (
<>{label ('postPrefix')}
<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>
</>)}
{displayedTag && (
<>(<TagLink
tag={displayedTag}
@@ -157,7 +159,8 @@ const PostHistoryPage: FC = () => {
withCount={false}/>)</>)}
</PageTitle>
{loading ? 'Loading...' : (
{isError && <p role="alert">{label ('common:fetchError')}</p>}
{loading ? label ('common:loading') : (
<>
<div className="overflow-x-auto">
<table className="w-full min-w-[1200px] table-fixed border-collapse">
@@ -184,15 +187,15 @@ const PostHistoryPage: FC = () => {
<thead className="border-b-2 border-black dark:border-white">
<tr>
<th className="p-2 text-left">投稿</th>
<th className="p-2 text-left">版</th>
<th className="p-2 text-left">タイトル</th>
<th className="p-2 text-left">URL</th>
<th className="p-2 text-left">タグ</th>
<th className="p-2 text-left">{label ('post')}</th>
<th className="p-2 text-left">{label ('version')}</th>
<th className="p-2 text-left">{label ('shared:labels.title')}</th>
<th className="p-2 text-left">{label ('shared:url')}</th>
<th className="p-2 text-left">{label ('navigation:tags')}</th>
{/* TODO: 親投稿の履歴 */}
{/* <th className="p-2 text-left">親投稿</th> */}
<th className="p-2 text-left">オリジナルの投稿日時</th>
<th className="p-2 text-left">更新日時</th>
<th className="p-2 text-left">{label ('originalPostedAt')}</th>
<th className="p-2 text-left">{label ('tags:updatedAt')}</th>
<th className="p-2"/>
</tr>
</thead>
@@ -284,32 +287,34 @@ const PostHistoryPage: FC = () => {
{pp.title}
</span>))))}
</td> */}
<td className="p-2">
<td className="p-2"
lang={message ('unknownDate').language}>
{change.versionNo === 1
? originalCreatedAtString (change.originalCreatedFrom.current,
change.originalCreatedBefore.current)
? originalCreatedAtString (
change.originalCreatedFrom.current,
change.originalCreatedBefore.current, locale)
: renderDiff ({
current: originalCreatedAtString (
change.originalCreatedFrom.current,
change.originalCreatedBefore.current),
change.originalCreatedFrom.current,
change.originalCreatedBefore.current, locale),
prev: originalCreatedAtString (
change.originalCreatedFrom.prev,
change.originalCreatedBefore.prev) })}
change.originalCreatedFrom.prev,
change.originalCreatedBefore.prev, locale) })}
</td>
<td className="p-2">
{change.createdByUser
? (
<PrefetchLink to={`/users/${ change.createdByUser.id }`}>
{change.createdByUser.name
|| `名もなきニジラー(#${ change.createdByUser.id })`}
|| label ('anonymous', { id: change.createdByUser.id })}
</PrefetchLink>)
: 'bot 操作'}
: label ('bot')}
<br/>
{dateString (change.createdAt)}
{localisedDateString (change.createdAt, locale)}
</td>
<td className="p-2">
<a href="#" onClick={async e => await handleRevert (e, change)}>
復元
{label ('shared:actions.restore')}
</a>
</td>
</tr>)
+9 -4
ファイルの表示
@@ -130,7 +130,9 @@ describe ('PostImportReviewPage', () => {
renderReviewPage (['https://example.com/post'])
fireEvent.click (await screen.findByRole ('button', { name: '追加' }))
const submit = await screen.findByRole ('button', { name: '追加' })
await waitFor (() => expect (submit).toBeEnabled ())
fireEvent.click (submit)
expect (await screen.findAllByText ('登録失敗')).toHaveLength (2)
expect (screen.getAllByText ('invalid title')).toHaveLength (2)
@@ -147,11 +149,14 @@ describe ('PostImportReviewPage', () => {
renderReviewPage (['https://example.com/post'])
fireEvent.click (await screen.findByRole ('button', { name: '追加' }))
const submit = await screen.findByRole ('button', { name: '追加' })
await waitFor (() => expect (submit).toBeEnabled ())
fireEvent.click (submit)
await waitFor (() => {
expect (screen.getByLabelText ('current-location')).toHaveTextContent ('/posts')
expect (screen.getByLabelText ('current-location').textContent).toBe ('/ja/posts')
})
expect (api.apiPost).toHaveBeenCalledWith ('/posts/bulk', expect.any (FormData))
expect (api.apiPost).toHaveBeenCalledWith (
'/posts/bulk', expect.any (FormData), { locale: 'ja' })
})
})
+66 -57
ファイルの表示
@@ -1,19 +1,22 @@
import { AnimatePresence, motion } from 'framer-motion'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { ChevronRight } from 'lucide-react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PrefetchLink from '@/components/PrefetchLink'
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary'
import { renderPostImportMessage } from '@/components/posts/import/postImportRowStatus'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import PageHead from '@/components/common/PageHead'
import { apiGet, apiPost, isApiError } from '@/lib/api'
import { translateUIMessage } from '@/lib/i18n'
import { useUITranslation } from '@/lib/useUITranslation'
import useDialogue from '@/lib/dialogues/useDialogue'
import { parsePostNewReviewUrls } from '@/lib/postNewQueryState'
import { validateImportSource } from '@/lib/postImportSourceValidation'
@@ -99,8 +102,9 @@ type ExistingSkippedRowsProps = {
id: string
rows: PostImportRow[] }
const DUPLICATE_URL_MESSAGE = 'URL が重複しています.'
const BULK_PROCESSING_FAILED_MESSAGE = '登録中にエラーが発生しました.'
const DUPLICATE_URL_MESSAGE = translateUIMessage ('validation.duplicateUrl', 'imports', 'ja').text
const BULK_PROCESSING_FAILED_MESSAGE =
translateUIMessage ('validation.processingFailed', 'imports', 'ja').text
const emptyAttributes = () => ({
title: '',
@@ -595,10 +599,12 @@ const editableRowDirty = (row: PostImportRow): boolean =>
const PostImportReviewPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const { message, label } = useUITranslation ('imports')
const editable = canEditContent (user)
const dialogue = useDialogue ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const existingRowsTransition =
@@ -608,7 +614,8 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const }
const [rows, setRows] = useState<PostImportRow[] | null> (null)
const [rows, setRows] = useState<PostImportRow[] | null> (
() => buildInitialRows (parsePostNewReviewUrls (location.search)))
const [metadataLoading, setMetadataLoading] = useState (false)
const [submitting, setSubmitting] = useState (false)
const [busyRowIds, setBusyRowIds] = useState<Set<number>> (new Set ())
@@ -649,9 +656,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const finishImport = useCallback (() => {
allowNextNavigation ()
clearPostImportSourceDraft (message =>
toast ({ title: '入力内容を削除できませんでした', description: message }))
toast ({ title: label ('review.clearFailed'),
description: renderPostImportMessage (message, locale) }))
navigate ('/posts')
}, [allowNextNavigation, navigate])
}, [allowNextNavigation, navigate, label, locale])
useEffect (() => {
const urls = parsePostNewReviewUrls (location.search)
@@ -696,7 +704,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
try
{
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', { locale,
params: { url: currentRow.url },
signal: controller.signal })
if (!(active) || metadataSequenceRef.current !== sequence)
@@ -770,7 +778,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
active = false
controllers.forEach (controller => controller.abort ())
}
}, [commitRows, currentRowBySource, location.search, updateRows])
}, [commitRows, currentRowBySource, location.search, updateRows, locale])
useEffect (() => {
if (editingRow == null || resultRepairMode (rowsRef.current) !== 'failed')
@@ -871,12 +879,12 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
urlChanged
? mergePreviewRow (
nextRow,
await apiGet<PostMetadataResponse> ('/posts/metadata', {
await apiGet<PostMetadataResponse> ('/posts/metadata', { locale,
params: { url: nextRow.url } }))
: nextRow
const dryRun = await apiPost<PostMetadataResponse> (
'/posts?dry=1',
buildDryRunFormData (candidateRow))
buildDryRunFormData (candidateRow), { locale })
const mergedRow = mergeDryRunRow (candidateRow, dryRun)
const mergedRows = updateRows (currentRows =>
replaceImportRow (currentRows, mergedRow))
@@ -910,7 +918,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
return { saved: false, row: applyThumbnailWarning (errorRow) }
}
toast ({ title: '行の再検証に失敗しました' })
toast ({ title: label ('review.validateFailed') })
return { saved: false, row: null }
}
}
@@ -924,8 +932,8 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
saveDraft (row, { draft, resetRequested })
await dialogue.form ({
title: '投稿を編輯',
cancelText: '取消',
title: label ('review.editPost'),
cancelText: label ('shared:actions.cancel'),
size: 'large',
body: controls => (
<PostImportRowForm
@@ -974,11 +982,11 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const result = await apiPost<{ results: BulkApiRow[] }>(
'/posts/bulk',
buildBulkFormData ([target]))
buildBulkFormData ([target]), { locale })
if (result.results.length !== 1)
{
updateRows (currentRows => replaceImportRow (currentRows, originalRow))
toast ({ title: '登録結果が不完全でした' })
toast ({ title: label ('review.incomplete') })
return
}
@@ -993,7 +1001,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
catch
{
updateRows (currentRows => replaceImportRow (currentRows, originalRow))
toast ({ title: '再試行に失敗しました' })
toast ({ title: label ('review.retryFailed') })
}
finally
{
@@ -1018,10 +1026,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
{
const result = await apiPost<{ results: BulkApiRow[] }>(
'/posts/bulk',
buildBulkFormData (latestProcessingRows))
buildBulkFormData (latestProcessingRows), { locale })
if (result.results.length !== latestProcessingRows.length)
{
toast ({ title: '登録結果が不完全でした' })
toast ({ title: label ('review.incomplete') })
return
}
@@ -1035,7 +1043,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
}
catch
{
toast ({ title: '登録に失敗しました' })
toast ({ title: label ('review.submitFailed') })
}
finally
{
@@ -1051,13 +1059,11 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
return (
<>
<Helmet>
<title>{`追加内容確認 | ${ SITE_TITLE }`}</title>
</Helmet>
<PageHead title={message ('review.title')}/>
<MainArea className="pb-40">
<div className="mx-auto max-w-6xl space-y-6">
<PageTitle>追加内容確認</PageTitle>
<PageTitle>{label ('review.title')}</PageTitle>
{existingRows.length > 0 && (
<div className="rounded-lg border p-4">
@@ -1076,7 +1082,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
? ''
: 'transition-transform')}/>
</span>
既存投稿による自動スキップ {counts.existingSkipped}件
{label ('review.existingSkipped', { count: counts.existingSkipped })}
</button>
<AnimatePresence initial={false}>
{showExistingRows && (
@@ -1154,36 +1160,39 @@ const PostImportFooter = (
pendingOrErrorCount: number
onBack: () => void
onSubmit: () => void },
) => (
<div
className="shrink-0 border-t bg-white/95 p-4 backdrop-blur
dark:border-neutral-700 dark:bg-neutral-950/95">
<div className="mx-auto flex max-w-6xl flex-col gap-3 md:flex-row
md:items-center md:justify-between">
<div className="flex flex-wrap items-center gap-2 text-sm">
<span>作成対象 {creatableCount}件</span>
<span>手動スキップ {manualSkippedCount}件</span>
<span>既存投稿による自動スキップ {existingSkippedCount}件</span>
<span>未処理 {pendingOrErrorCount}件</span>
) => {
const { label } = useUITranslation ('imports')
return (
<div
className="shrink-0 border-t bg-white/95 p-4 backdrop-blur
dark:border-neutral-700 dark:bg-neutral-950/95">
<div className="mx-auto flex max-w-6xl flex-col gap-3 md:flex-row
md:items-center md:justify-between">
<div className="flex flex-wrap items-center gap-2 text-sm">
<span>{label ('review.creatable', { count: creatableCount })}</span>
<span>{label ('review.manualSkipped', { count: manualSkippedCount })}</span>
<span>{label ('review.existingSkipped', { count: existingSkippedCount })}</span>
<span>{label ('review.pending', { count: pendingOrErrorCount })}</span>
</div>
<div className="flex flex-col gap-2 md:flex-row">
<Button
type="button"
variant="outline"
className="w-full md:w-auto"
onClick={onBack}
disabled={submitting}>
{label ('review.back')}
</Button>
<Button
type="button"
className="w-full md:w-auto"
onClick={onSubmit}
disabled={metadataLoading || submitting || busyRowsPresent || !(canSubmit)}>
{creatableCount > 1 ? label ('review.addAll') : label ('shared:actions.add')}
</Button>
</div>
</div>
<div className="flex flex-col gap-2 md:flex-row">
<Button
type="button"
variant="outline"
className="w-full md:w-auto"
onClick={onBack}
disabled={submitting}>
戻る
</Button>
<Button
type="button"
className="w-full md:w-auto"
onClick={onSubmit}
disabled={metadataLoading || submitting || busyRowsPresent || !(canSubmit)}>
{creatableCount > 1 ? '一括追加' : '追加'}
</Button>
</div>
</div>
</div>)
</div>)
}
export default PostImportReviewPage
+4 -3
ファイルの表示
@@ -36,7 +36,8 @@ describe ('PostImportSourcePage', () => {
target: { value: '\nftp://example.com/file\nhttps://example.com/valid' } })
fireEvent.click (screen.getByRole ('button', { name: '次へ' }))
expect (screen.getByText (/2 行目: HTTP または HTTPS/)).toBeInTheDocument ()
expect (screen.getByText ('ftp://example.com/file').closest ('li'))
.toHaveTextContent ('2 行目: HTTP または HTTPS')
expect (screen.getByText ('ftp://example.com/file')).toBeInTheDocument ()
expect (input).toHaveAttribute ('aria-invalid', 'true')
expect (router.navigate).not.toHaveBeenCalled ()
@@ -51,8 +52,8 @@ describe ('PostImportSourcePage', () => {
fireEvent.click (screen.getByRole ('button', { name: '次へ' }))
expect (router.navigate).toHaveBeenCalledWith (
'/posts/new?urls=https%3A%2F%2Fexample.com%2Fone%2Ba'
+ '+https%3A%2F%2Fexample.com%2Ftwo%3Fvalue%3Db%2Bc')
'/ja/posts/new?urls=https%3A%2F%2Fexample.com%2Fone%2Ba'
+ '+https%3A%2F%2Fexample.com%2Ftwo%3Fvalue%3Db%2Bc', undefined)
})
it ('disables Next when the encoded request target reaches 4096 bytes', () => {
+27 -20
ファイルの表示
@@ -1,7 +1,9 @@
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useNavigate } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import { renderPostImportMessage } from '@/components/posts/import/postImportRowStatus'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
@@ -10,7 +12,7 @@ import TextArea from '@/components/common/TextArea'
import MainArea from '@/components/layout/MainArea'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import PageHead from '@/components/common/PageHead'
import {
buildPostNewReviewPath,
isPostNewReviewPathWithinLimit,
@@ -39,8 +41,10 @@ const SOURCE_ISSUES_ID = 'post-import-source-issues'
const PostImportSourcePage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const { message: uiMessage, label } = useUITranslation ('imports')
const editable = canEditContent (user)
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [source, setSource] = useState ('')
const [loading, setLoading] = useState (false)
const [sourceIssues, setSourceIssues] = useState<ReturnType<typeof validateImportSource>> ([])
@@ -51,7 +55,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
const lineCount = countImportSourceLines (source)
const sourceUrls = extractImportSourceUrls (source)
const withinPathLimit = isPostNewReviewPathWithinLimit (sourceUrls)
const messages = sourceError != null ? [sourceError] : []
const messages = sourceError != null ? [label ('source.urlRequired')] : []
const sourceDescribedBy =
[sourceError != null ? SOURCE_ERROR_ID : null,
sourceIssues.length > 0 ? SOURCE_ISSUES_ID : null]
@@ -60,7 +64,8 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
useEffect (() => {
const draft = loadPostImportSourceDraft (message =>
toast ({ title: '保存済み入力を復元できませんでした', description: message }))
toast ({ title: label ('source.restoreFailed'),
description: renderPostImportMessage (message, locale) }))
if (!(editedRef.current))
{
@@ -69,7 +74,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
? draft.source
: current)
}
}, [])
}, [label, locale])
useEffect (() => {
if (saveTimer.current != null)
@@ -77,20 +82,21 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
saveTimer.current = window.setTimeout (() => {
savePostImportSourceDraft (source, message =>
toast ({ title: '入力内容を保存できませんでした', description: message }))
toast ({ title: label ('source.saveFailed'),
description: renderPostImportMessage (message, locale) }))
}, 300)
return () => {
if (saveTimer.current != null)
window.clearTimeout (saveTimer.current)
}
}, [source])
}, [source, label, locale])
const preview = async () => {
if (lineCount === 0)
{
setSourceIssues ([])
setSourceError ('URL を入力してください.')
setSourceError (uiMessage ('source.urlRequired').text)
return
}
@@ -122,13 +128,11 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
return (
<MainArea>
<Helmet>
<title>{`広場に投稿を追加 | ${ SITE_TITLE }`}</title>
</Helmet>
<PageHead title={uiMessage ('source.title')}/>
<Form className="max-w-4xl">
<PageTitle>広場に投稿を追加</PageTitle>
<FormField label="URL リスト(1 行 1 URL)">
<PageTitle>{label ('source.title')}</PageTitle>
<FormField label={label ('source.urls')}>
{() => (
<TextArea
value={source}
@@ -139,8 +143,8 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
onBlur={() => {
savePostImportSourceDraft (source, message =>
toast ({
title: '入力内容を保存できませんでした',
description: message }))
title: label ('source.saveFailed'),
description: renderPostImportMessage (message, locale) }))
}}
onChange={ev => {
editedRef.current = true
@@ -157,21 +161,24 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
className="space-y-2 text-sm text-red-700 dark:text-red-300">
{sourceIssues.map (issue => (
<li key={`${ issue.sourceRow }-${ issue.message }-${ issue.url }`}>
<div>{issue.sourceRow} 行目: {issue.message}</div>
<div>
{label ('source.rowPrefix', { row: issue.sourceRow })}
{renderPostImportMessage (issue.message, locale)}
</div>
<div className="break-all font-mono text-xs">{issue.url}</div>
</li>))}
</ul>
<div className="relative z-10 flex items-center justify-between gap-3">
<div className="text-sm text-neutral-600 dark:text-neutral-300">
入力行数 {lineCount} / {MAX_ROWS}
{label ('source.lineCount', { count: lineCount, limit: MAX_ROWS })}
</div>
<Button
type="button"
className="pointer-events-auto shrink-0"
onClick={preview}
disabled={loading || !(withinPathLimit)}>
次へ
{label ('source.next')}
</Button>
</div>
</Form>
+5 -5
ファイルの表示
@@ -43,12 +43,12 @@ describe ('PostListPage', () => {
page: 2,
limit: 20,
order: 'original_created_at:desc',
}),
}), 'ja',
)
})
expect (await screen.findByRole ('link', { name: '投稿5' })).toHaveAttribute (
'href',
'/posts/5',
'/ja/posts/5',
)
expect (screen.getByText ('広場')).toBeInTheDocument ()
})
@@ -75,7 +75,7 @@ describe ('PostListPage', () => {
expect (await screen.findByText ('Wiki body')).toBeInTheDocument ()
expect (screen.getByRole ('link', { name: 'Wiki を見る' })).toHaveAttribute (
'href',
'/wiki/%E8%99%B9%E5%A4%8F',
'/ja/wiki/%E8%99%B9%E5%A4%8F',
)
})
@@ -92,7 +92,7 @@ describe ('PostListPage', () => {
expect.objectContaining ({
limit: 50,
order: 'original_created_at:desc',
}),
}), 'ja',
)
})
})
@@ -106,7 +106,7 @@ describe ('PostListPage', () => {
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenLastCalledWith (
expect.objectContaining ({ page: 1, limit: 100 }),
expect.objectContaining ({ page: 1, limit: 100 }), 'ja',
)
})
})
+33 -24
ファイルの表示
@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PostList from '@/components/PostList'
import PrefetchLink from '@/components/PrefetchLink'
import TagSidebar from '@/components/TagSidebar'
@@ -10,8 +10,9 @@ import WikiBody from '@/components/WikiBody'
import Pagination from '@/components/common/Pagination'
import TabGroup, { Tab } from '@/components/common/TabGroup'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import PageHead from '@/components/common/PageHead'
import { fetchPosts } from '@/lib/posts'
import { localeLanguage } from '@/lib/locales'
import { postsKeys } from '@/lib/queryKeys'
import {
DEFAULT_POST_LIST_LIMIT,
@@ -21,6 +22,8 @@ import {
} from '@/lib/settings'
import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
import { fetchWikiPageByTitle } from '@/lib/wiki'
import { useUITranslation } from '@/lib/useUITranslation'
import { useHydrated } from '@/lib/useHydrated'
import type { FC } from 'react'
@@ -33,12 +36,15 @@ const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
const PostListPage: FC = () => {
const locale = useLocale ()
const hydrated = useHydrated ()
const { message, label, siteTitle } = useUITranslation ('posts')
const containerRef = useRef<HTMLDivElement | null> (null)
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = new URLSearchParams (location.search)
const tagsQuery = query.get ('tags') ?? ''
const anyFlg = query.get ('match') === 'any'
@@ -48,26 +54,29 @@ const PostListPage: FC = () => {
const page = Number (query.get ('page') ?? 1)
const limit = (
parseLimit (query.get ('limit'))
?? getClientListLimit ('postList')
?? (hydrated ? getClientListLimit ('postList') : null)
?? DEFAULT_POST_LIST_LIMIT
)
const order = 'original_created_at:desc' as const
const keys = {
tags: tagsKey, match, page, limit,
...(query.get ('tag_refs') && { tagReferences: query.get ('tag_refs')! }),
url: '', title: '', originalCreatedFrom: '', originalCreatedTo: '',
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
order } as const
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const posts = data?.posts ?? []
const cursor = ''
const totalPages = data ? Math.ceil (data.count / limit) : 0
useEffect (() => {
if (!hydrated)
return
setClientListSettings ('postList', { limit })
}, [limit])
}, [hydrated, limit])
const updatePage = (nextPage: number) => {
const params = new URLSearchParams (location.search)
@@ -111,27 +120,25 @@ const PostListPage: FC = () => {
try
{
const tagName = tags[0]
setWikiPage (await fetchWikiPageByTitle (tagName, { }))
setWikiPage (await fetchWikiPageByTitle (tagName, { }, locale))
}
catch
{
;
}
}) ()
}, [location.search, tags])
}, [location.search, tags, locale])
return (
<div
data-sidebar-container
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden"
ref={containerRef}>
<Helmet>
<title>
{tags.length
? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }`
: `${ SITE_TITLE }\u3000〜 ぼざろクリーチャーシリーズ綜合リンク集サイト`}
</title>
</Helmet>
<PageHead
title={tags.length
? tags.join (message (anyFlg ? 'list.or' : 'list.and').text)
: message ('list.tagline', { brand: siteTitle.text })}
includeSiteName={tags.length > 0}/>
<TagSidebar posts={posts.slice (0, 20)} onClick={() => {
const statesToSave = {
@@ -145,7 +152,7 @@ const PostListPage: FC = () => {
<TabGroup
rightAddon={
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<span>件数</span>
<span>{label ('common:countLabel')}</span>
<select
className="rounded border border-border bg-background px-2 py-1 text-sm
text-foreground"
@@ -160,22 +167,24 @@ const PostListPage: FC = () => {
</select>
</label>
}>
<Tab name="広場">
<Tab name={label ('navigation:posts')}>
{posts.length > 0
? (
<>
<PostList posts={posts}/>
<Pagination page={page} totalPages={totalPages}/>
</>)
: !(loading) && '広場には何もありませんよ.'}
{loading && 'Loading...'}
: !(loading) && label ('list.empty')}
{loading && label ('common:loading')}
</Tab>
{tags.length === 1 && (
<Tab name="Wiki">
<WikiBody title={tags[0]} body={wikiPage?.body}/>
<Tab name={label ('navigation:wiki')}>
<WikiBody title={tags[0]} body={wikiPage?.body}
language={wikiPage?.languageCode ?? (wikiPage?.resourceType === 'tag_description'
? localeLanguage (locale) : 'und')}/>
<div className="my-2">
<PrefetchLink to={`/wiki/${ encodeURIComponent (tags[0]) }`}>
Wiki を見る
{label ('list.viewWiki')}
</PrefetchLink>
</div>
</Tab>)}
+5 -5
ファイルの表示
@@ -43,12 +43,12 @@ describe ('PostSearchPage', () => {
match: 'any',
page: 2,
limit: 20,
}),
}), 'ja',
)
})
expect ((await screen.findAllByRole ('link', { name: '検索対象' }))[0]).toHaveAttribute (
'href',
'/posts/4',
'/ja/posts/4',
)
})
@@ -72,7 +72,7 @@ describe ('PostSearchPage', () => {
tags: 'tag',
match: 'any',
page: 1,
}),
}), 'ja',
)
})
})
@@ -98,7 +98,7 @@ describe ('PostSearchPage', () => {
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenCalledWith (
expect.objectContaining ({ limit: 50, order: 'created_at:desc' }),
expect.objectContaining ({ limit: 50, order: 'created_at:desc' }), 'ja',
)
})
expect (JSON.parse (localStorage.getItem (CLIENT_SETTINGS_STORAGE_KEY) ?? '{}'))
@@ -117,7 +117,7 @@ describe ('PostSearchPage', () => {
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenLastCalledWith (
expect.objectContaining ({ page: 1, limit: 100 }),
expect.objectContaining ({ page: 1, limit: 100 }), 'ja',
)
})
})
+55 -38
ファイルの表示
@@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import LocalisedText from '@/components/LocalisedText'
import SortHeader from '@/components/SortHeader'
import TagLink from '@/components/TagLink'
import DateTimeField from '@/components/common/DateTimeField'
@@ -13,7 +14,7 @@ import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import TagInput from '@/components/common/TagInput'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import PageHead from '@/components/common/PageHead'
import { clientAnimationTransition,
clientScrollBehaviour } from '@/lib/clientAnimation'
import { fetchPosts } from '@/lib/posts'
@@ -28,7 +29,9 @@ import {
} from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
import { dateString, inputClass, originalCreatedAtString } from '@/lib/utils'
import { localisedDateString, inputClass, originalCreatedAtString } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import { useHydrated } from '@/lib/useHydrated'
import type { FC, FormEvent } from 'react'
@@ -70,6 +73,9 @@ const parseOrder = (value: string | null): FetchPostsOrder | null => {
const PostSearchPage: FC = () => {
const locale = useLocale ()
const hydrated = useHydrated ()
const { message, label } = useUITranslation ('posts')
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const thumbnailTransition = clientAnimationTransition (
@@ -79,7 +85,7 @@ const PostSearchPage: FC = () => {
)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = useMemo (() => new URLSearchParams (location.search),
[location.search])
@@ -87,7 +93,7 @@ const PostSearchPage: FC = () => {
const page = Number (query.get ('page') ?? 1)
const limit = (
parseLimit (query.get ('limit'))
?? getClientListLimit ('postSearch')
?? (hydrated ? getClientListLimit ('postSearch') : null)
?? DEFAULT_POST_LIST_LIMIT
)
@@ -103,7 +109,7 @@ const PostSearchPage: FC = () => {
const qUpdatedTo = query.get ('updated_to') ?? ''
const order = (
parseOrder (query.get ('order'))
?? getClientListOrder<FetchPostsOrder> ('postSearch')
?? (hydrated ? getClientListOrder<FetchPostsOrder> ('postSearch') : null)
?? DEFAULT_POST_LIST_ORDER
)
@@ -120,6 +126,7 @@ const PostSearchPage: FC = () => {
const keys: FetchPostsParams = {
tags: qTags, match: qMatch, page, limit,
...(query.get ('tag_refs') && { tagReferences: query.get ('tag_refs')! }),
url: qURL,
title: qTitle,
originalCreatedFrom: qOriginalCreatedFrom,
@@ -130,14 +137,16 @@ const PostSearchPage: FC = () => {
updatedTo: qUpdatedTo,
order }
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const results = data?.posts ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
useEffect (() => {
if (!hydrated)
return
setClientListSettings ('postSearch', { limit, order })
}, [limit, order])
}, [hydrated, limit, order])
useEffect (() => {
setURL (qURL ?? '')
@@ -220,15 +229,13 @@ const PostSearchPage: FC = () => {
return (
<MainArea>
<Helmet>
<title>広場検索 | {SITE_TITLE}</title>
</Helmet>
<PageHead title={message ('search.title')}/>
<div className="max-w-xl">
<div className="flex flex-wrap items-center justify-between gap-3">
<PageTitle>広場検索</PageTitle>
<PageTitle>{label ('search.title')}</PageTitle>
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<span>件数</span>
<span>{label ('common:countLabel')}</span>
<select
className="rounded border border-border bg-background px-2 py-1 text-sm
text-foreground"
@@ -246,7 +253,7 @@ const PostSearchPage: FC = () => {
<form onSubmit={handleSearch} className="space-y-2">
{/* タイトル */}
<FormField label="タイトル">
<FormField label={label ('shared:labels.title')}>
{({ invalid }) => (
<input
type="text"
@@ -257,7 +264,7 @@ const PostSearchPage: FC = () => {
</FormField>
{/* URL */}
<FormField label="URL">
<FormField label={label ('shared:url')}>
{({ invalid }) => (
<input
type="text"
@@ -267,7 +274,7 @@ const PostSearchPage: FC = () => {
</FormField>
{/* タグ */}
<FormField label="タグ">
<FormField label={label ('navigation:tags')}>
{() => (
<TagInput
includeNico={true}
@@ -276,14 +283,14 @@ const PostSearchPage: FC = () => {
</FormField>
<div>
<fieldset className="w-full my-2">
<label>検索区分:</label>
<label>{label ('search.matchType')}</label>
<label className="mx-2">
<input
type="radio"
name="match-type"
checked={matchType === 'all'}
onChange={() => setMatchType ('all')}/>
AND
{label ('search.all')}
</label>
<label className="mx-2">
<input
@@ -291,13 +298,13 @@ const PostSearchPage: FC = () => {
name="match-type"
checked={matchType === 'any'}
onChange={() => setMatchType ('any')}/>
OR
{label ('search.any')}
</label>
</fieldset>
</div>
{/* オリジナルの投稿日時 */}
<FormField label="オリジナルの投稿日時">
<FormField label={label ('history:originalPostedAt')}>
{() => (
<>
<DateTimeField
@@ -311,7 +318,7 @@ const PostSearchPage: FC = () => {
</FormField>
{/* 投稿日時 */}
<FormField label="投稿日時">
<FormField label={label ('search.createdAt')}>
{() => (
<>
<DateTimeField
@@ -325,7 +332,7 @@ const PostSearchPage: FC = () => {
</FormField>
{/* 更新日時 */}
<FormField label="更新日時">
<FormField label={label ('tags:updatedAt')}>
{() => (
<>
<DateTimeField
@@ -343,13 +350,13 @@ const PostSearchPage: 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 ? (
{loading ? label ('common:loading') : (results.length > 0 ? (
<div className="mt-4">
<div className="overflow-x-auto">
<table className="w-full min-w-[1200px] table-fixed border-collapse">
@@ -365,40 +372,40 @@ const PostSearchPage: FC = () => {
<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">{label ('history:post')}</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchPostsOrderField>
by="title"
label="タイトル"
label={label ('shared:labels.title')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchPostsOrderField>
by="url"
label="URL"
label={label ('shared:url')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">タグ</th>
<th className="p-2 text-left whitespace-nowrap">{label ('navigation:tags')}</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchPostsOrderField>
by="original_created_at"
label="オリジナルの投稿日時"
label={label ('history:originalPostedAt')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchPostsOrderField>
by="created_at"
label="投稿日時"
label={label ('search.createdAt')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchPostsOrderField>
by="updated_at"
label="更新日時"
label={label ('tags:updatedAt')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
@@ -443,18 +450,28 @@ const PostSearchPage: FC = () => {
</span>))}
</td>
<td className="p-2">
{originalCreatedAtString (row.originalCreatedFrom,
row.originalCreatedBefore)}
<LocalisedText language={locale}>
{originalCreatedAtString (row.originalCreatedFrom,
row.originalCreatedBefore, locale)}
</LocalisedText>
</td>
<td className="p-2">
<LocalisedText language={locale}>
{localisedDateString (row.createdAt, locale)}
</LocalisedText>
</td>
<td className="p-2">
<LocalisedText language={locale}>
{localisedDateString (row.updatedAt, locale)}
</LocalisedText>
</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>) : '結果ないよ(笑)')}
</div>) : label ('tags:empty'))}
</MainArea>)
}