プリフェッチ実装(#140) (#256)
Merge branch 'main' into feature/140 #140 Merge remote-tracking branch 'origin/main' into feature/140 #140 #140 #140 #140 #140 Merge remote-tracking branch 'origin/main' into feature/140 #140 #140 #140 #140 #140 #140 #140 #140 #140 #140 #140 Merge remote-tracking branch 'origin/main' into feature/140 Merge remote-tracking branch 'origin/main' into feature/140 #140 ぼちぼち Merge remote-tracking branch 'origin/main' into feature/140 #140 #140 #140 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #256
このコミットはPull リクエスト #256 でマージされました.
このコミットが含まれているのは:
@@ -91,67 +91,65 @@ export default (({ user }: Props) => {
|
||||
: 'bg-gray-500 hover:bg-gray-600')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="md:flex md:flex-1">
|
||||
<Helmet>
|
||||
{(post?.thumbnail || post?.thumbnailBase) && (
|
||||
<meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)}
|
||||
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
|
||||
</Helmet>
|
||||
<div className="md:flex md:flex-1">
|
||||
<Helmet>
|
||||
{(post?.thumbnail || post?.thumbnailBase) && (
|
||||
<meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)}
|
||||
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
|
||||
</Helmet>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<TagDetailSidebar post={post ?? null}/>
|
||||
</div>
|
||||
|
||||
<MainArea className="relative">
|
||||
{post
|
||||
? (
|
||||
<>
|
||||
{(post.thumbnail || post.thumbnailBase) && (
|
||||
<motion.div
|
||||
layoutId={`page-${ id }`}
|
||||
className="absolute top-4 left-4 w-[min(640px,calc(100vw-2rem))] h-[360px]
|
||||
overflow-hidden rounded-xl pointer-events-none z-50"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 0 }}
|
||||
transition={{ duration: .2, ease: 'easeOut' }}>
|
||||
<img src={post.thumbnail || post.thumbnailBase}
|
||||
alt={post.title || post.url}
|
||||
title={post.title || post.url || undefined}
|
||||
className="object-cover w-full h-full"/>
|
||||
</motion.div>)}
|
||||
|
||||
<PostEmbed post={post}/>
|
||||
<Button onClick={() => changeViewedFlg.mutate ()}
|
||||
disabled={changeViewedFlg.isPending}
|
||||
className={cn ('text-white', viewedClass)}>
|
||||
{post.viewed ? '閲覧済' : '未閲覧'}
|
||||
</Button>
|
||||
<TabGroup>
|
||||
<Tab name="関聯">
|
||||
{post.related.length > 0
|
||||
? <PostList posts={post.related}/>
|
||||
: 'まだないよ(笑)'}
|
||||
</Tab>
|
||||
{['admin', 'member'].some (r => user?.role === r) && (
|
||||
<Tab name="編輯">
|
||||
<PostEditForm
|
||||
post={post}
|
||||
onSave={newPost => {
|
||||
qc.setQueryData (postsKeys.show (postId),
|
||||
(prev: any) => newPost ?? prev)
|
||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||
toast ({ description: '更新しました.' })
|
||||
}}/>
|
||||
</Tab>)}
|
||||
</TabGroup>
|
||||
</>)
|
||||
: 'Loading...'}
|
||||
</MainArea>
|
||||
|
||||
<div className="md:hidden">
|
||||
<TagDetailSidebar post={post ?? null}/>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<TagDetailSidebar post={post ?? null}/>
|
||||
</div>
|
||||
</>)
|
||||
|
||||
<MainArea className="relative">
|
||||
{post
|
||||
? (
|
||||
<>
|
||||
{(post.thumbnail || post.thumbnailBase) && (
|
||||
<motion.div
|
||||
layoutId={`page-${ id }`}
|
||||
className="absolute top-4 left-4 w-[min(640px,calc(100vw-2rem))] h-[360px]
|
||||
overflow-hidden rounded-xl pointer-events-none z-50"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 0 }}
|
||||
transition={{ duration: .2, ease: 'easeOut' }}>
|
||||
<img src={post.thumbnail || post.thumbnailBase}
|
||||
alt={post.title || post.url}
|
||||
title={post.title || post.url || undefined}
|
||||
className="object-cover w-full h-full"/>
|
||||
</motion.div>)}
|
||||
|
||||
<PostEmbed post={post}/>
|
||||
<Button onClick={() => changeViewedFlg.mutate ()}
|
||||
disabled={changeViewedFlg.isPending}
|
||||
className={cn ('text-white', viewedClass)}>
|
||||
{post.viewed ? '閲覧済' : '未閲覧'}
|
||||
</Button>
|
||||
<TabGroup>
|
||||
<Tab name="関聯">
|
||||
{post.related.length > 0
|
||||
? <PostList posts={post.related}/>
|
||||
: 'まだないよ(笑)'}
|
||||
</Tab>
|
||||
{['admin', 'member'].some (r => user?.role === r) && (
|
||||
<Tab name="編輯">
|
||||
<PostEditForm
|
||||
post={post}
|
||||
onSave={newPost => {
|
||||
qc.setQueryData (postsKeys.show (postId),
|
||||
(prev: any) => newPost ?? prev)
|
||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||
toast ({ description: '更新しました.' })
|
||||
}}/>
|
||||
</Tab>)}
|
||||
</TabGroup>
|
||||
</>)
|
||||
: 'Loading...'}
|
||||
</MainArea>
|
||||
|
||||
<div className="md:hidden">
|
||||
<TagDetailSidebar post={post ?? null}/>
|
||||
</div>
|
||||
</div>)
|
||||
}) satisfies FC<Props>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import TagLink from '@/components/TagLink'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { fetchPostChanges } from '@/lib/posts'
|
||||
import { postsKeys } from '@/lib/queryKeys'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { PostTagChange } from '@/types'
|
||||
|
||||
|
||||
export default (() => {
|
||||
const [changes, setChanges] = useState<PostTagChange[]> ([])
|
||||
const [totalPages, setTotalPages] = useState<number> (0)
|
||||
|
||||
const location = useLocation ()
|
||||
const query = new URLSearchParams (location.search)
|
||||
const id = query.get ('id')
|
||||
@@ -28,17 +24,11 @@ export default (() => {
|
||||
// 投稿列の結合で使用
|
||||
let rowsCnt: number
|
||||
|
||||
useEffect (() => {
|
||||
void (async () => {
|
||||
const res = await axios.get (`${ API_BASE_URL }/posts/changes`,
|
||||
{ params: { ...(id && { id }), page, limit } })
|
||||
const data = toCamel (res.data as any, { deep: true }) as {
|
||||
changes: PostTagChange[]
|
||||
count: number }
|
||||
setChanges (data.changes)
|
||||
setTotalPages (Math.ceil (data.count / limit))
|
||||
}) ()
|
||||
}, [id, page, limit])
|
||||
const { data, isLoading: loading } = useQuery ({
|
||||
queryKey: postsKeys.changes ({ ...(id && { id }), page, limit }),
|
||||
queryFn: () => fetchPostChanges ({ ...(id && { id }), page, limit }) })
|
||||
const changes = data?.changes ?? []
|
||||
const totalPages = data ? Math.ceil (data.count / limit) : 0
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
@@ -48,57 +38,60 @@ export default (() => {
|
||||
|
||||
<PageTitle>
|
||||
耕作履歴
|
||||
{id && <>: 投稿 {<Link to={`/posts/${ id }`}>#{id}</Link>}</>}
|
||||
{id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>}
|
||||
</PageTitle>
|
||||
|
||||
<table className="table-auto w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2 text-left">投稿</th>
|
||||
<th className="p-2 text-left">変更</th>
|
||||
<th className="p-2 text-left">日時</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{changes.map ((change, i) => {
|
||||
let withPost = i === 0 || change.post.id !== changes[i - 1].post.id
|
||||
if (withPost)
|
||||
{
|
||||
rowsCnt = 1
|
||||
for (let j = i + 1;
|
||||
(j < changes.length
|
||||
&& change.post.id === changes[j].post.id);
|
||||
++j)
|
||||
++rowsCnt
|
||||
}
|
||||
return (
|
||||
<tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}>
|
||||
{withPost && (
|
||||
<td className="align-top" rowSpan={rowsCnt}>
|
||||
<Link to={`/posts/${ change.post.id }`}>
|
||||
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
|
||||
alt={change.post.title || change.post.url}
|
||||
title={change.post.title || change.post.url || undefined}
|
||||
className="w-40"/>
|
||||
</Link>
|
||||
</td>)}
|
||||
<td>
|
||||
<TagLink tag={change.tag} withWiki={false} withCount={false}/>
|
||||
{`を${ change.changeType === 'add' ? '追加' : '削除' }`}
|
||||
</td>
|
||||
<td>
|
||||
{change.user ? (
|
||||
<Link to={`/users/${ change.user.id }`}>
|
||||
{change.user.name}
|
||||
</Link>) : 'bot 操作'}
|
||||
<br/>
|
||||
{change.timestamp}
|
||||
</td>
|
||||
</tr>)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{loading ? 'Loading...' : (
|
||||
<>
|
||||
<table className="table-auto w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2 text-left">投稿</th>
|
||||
<th className="p-2 text-left">変更</th>
|
||||
<th className="p-2 text-left">日時</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{changes.map ((change, i) => {
|
||||
let withPost = i === 0 || change.post.id !== changes[i - 1].post.id
|
||||
if (withPost)
|
||||
{
|
||||
rowsCnt = 1
|
||||
for (let j = i + 1;
|
||||
(j < changes.length
|
||||
&& change.post.id === changes[j].post.id);
|
||||
++j)
|
||||
++rowsCnt
|
||||
}
|
||||
return (
|
||||
<tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}>
|
||||
{withPost && (
|
||||
<td className="align-top" rowSpan={rowsCnt}>
|
||||
<PrefetchLink to={`/posts/${ change.post.id }`}>
|
||||
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
|
||||
alt={change.post.title || change.post.url}
|
||||
title={change.post.title || change.post.url || undefined}
|
||||
className="w-40"/>
|
||||
</PrefetchLink>
|
||||
</td>)}
|
||||
<td>
|
||||
<TagLink tag={change.tag} withWiki={false} withCount={false}/>
|
||||
{`を${ change.changeType === 'add' ? '記載' : '消除' }`}
|
||||
</td>
|
||||
<td>
|
||||
{change.user ? (
|
||||
<PrefetchLink to={`/users/${ change.user.id }`}>
|
||||
{change.user.name}
|
||||
</PrefetchLink>) : 'bot 操作'}
|
||||
<br/>
|
||||
{change.timestamp}
|
||||
</td>
|
||||
</tr>)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination page={page} totalPages={totalPages}/>
|
||||
<Pagination page={page} totalPages={totalPages}/>
|
||||
</>)}
|
||||
</MainArea>)
|
||||
}) satisfies FC
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from 'axios'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -11,7 +10,8 @@ import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet, apiPost } from '@/lib/api'
|
||||
import Forbidden from '@/pages/Forbidden'
|
||||
|
||||
import type { FC } from 'react'
|
||||
@@ -55,9 +55,7 @@ export default (({ user }: Props) => {
|
||||
|
||||
try
|
||||
{
|
||||
await axios.post (`${ API_BASE_URL }/posts`, formData, { headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
||||
await apiPost ('/posts', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
toast ({ title: '投稿成功!' })
|
||||
navigate ('/posts')
|
||||
}
|
||||
@@ -91,10 +89,7 @@ export default (({ user }: Props) => {
|
||||
const fetchTitle = async () => {
|
||||
setTitle ('')
|
||||
setTitleLoading (true)
|
||||
const res = await axios.get (`${ API_BASE_URL }/preview/title`, {
|
||||
params: { url },
|
||||
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
|
||||
const data = res.data as { title: string }
|
||||
const data = await apiGet<{ title: string }> ('/preview/title', { params: { url } })
|
||||
setTitle (data.title || '')
|
||||
setTitleLoading (false)
|
||||
}
|
||||
@@ -105,11 +100,8 @@ export default (({ user }: Props) => {
|
||||
setThumbnailLoading (true)
|
||||
if (thumbnailPreview)
|
||||
URL.revokeObjectURL (thumbnailPreview)
|
||||
const res = await axios.get (`${ API_BASE_URL }/preview/thumbnail`, {
|
||||
params: { url },
|
||||
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' },
|
||||
responseType: 'blob' })
|
||||
const data = res.data as Blob
|
||||
const data = await apiGet<Blob> ('/preview/thumbnail',
|
||||
{ params: { url }, responseType: 'blob' })
|
||||
const imageURL = URL.createObjectURL (data)
|
||||
setThumbnailPreview (imageURL)
|
||||
setThumbnailFile (new File ([data],
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
|
||||
@@ -8,7 +6,8 @@ import SectionTitle from '@/components/common/SectionTitle'
|
||||
import TextArea from '@/components/common/TextArea'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet, apiPut } from '@/lib/api'
|
||||
|
||||
import type { NicoTag, Tag, User } from '@/types'
|
||||
|
||||
@@ -29,10 +28,8 @@ export default ({ user }: Props) => {
|
||||
const loadMore = async (withCursor: boolean) => {
|
||||
setLoading (true)
|
||||
|
||||
const res = await axios.get (`${ API_BASE_URL }/tags/nico`, {
|
||||
params: { ...(withCursor ? { cursor } : { }) } })
|
||||
const data = toCamel (res.data as any, { deep: true }) as { tags: NicoTag[]
|
||||
nextCursor: string }
|
||||
const data = await apiGet<{ tags: NicoTag[]; nextCursor: string }> (
|
||||
'/tags/nico', { params: withCursor ? { cursor } : { } })
|
||||
|
||||
setNicoTags (tags => [...(withCursor ? tags : []), ...data.tags])
|
||||
setCursor (data.nextCursor)
|
||||
@@ -53,10 +50,8 @@ export default ({ user }: Props) => {
|
||||
const formData = new FormData
|
||||
formData.append ('tags', rawTags[id])
|
||||
|
||||
const res = await axios.put (`${ API_BASE_URL }/tags/nico/${ id }`, formData, { headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
||||
const data = toCamel (res.data as any, { deep: true }) as Tag[]
|
||||
const data = await apiPut<Tag[]> (`/tags/nico/${ id }`, formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
setNicoTags (nicoTags => {
|
||||
nicoTags.find (t => t.id === id)!.linkedTags = data
|
||||
return [...nicoTags]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from 'axios'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
|
||||
@@ -10,7 +9,8 @@ import InheritDialogue from '@/components/users/InheritDialogue'
|
||||
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiPut } from '@/lib/api'
|
||||
|
||||
import type { User } from '@/types'
|
||||
|
||||
@@ -32,10 +32,9 @@ export default ({ user, setUser }: Props) => {
|
||||
|
||||
try
|
||||
{
|
||||
const res = await axios.put (`${ API_BASE_URL }/users/${ user.id }`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data',
|
||||
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
|
||||
const data = res.data as User
|
||||
const data = await apiPut<User> (
|
||||
`/users/${ user.id }`, formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
setUser (user => ({ ...user, ...data }))
|
||||
toast ({ title: '設定を更新しました.' })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
@@ -12,10 +13,11 @@ import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||
import { fetchPosts } from '@/lib/posts'
|
||||
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchTagByName } from '@/lib/tags'
|
||||
import { fetchWikiPage, fetchWikiPageByTitle } from '@/lib/wiki'
|
||||
|
||||
import type { Post, Tag, WikiPage } from '@/types'
|
||||
import type { Tag } from '@/types'
|
||||
|
||||
|
||||
export default () => {
|
||||
@@ -25,76 +27,57 @@ export default () => {
|
||||
const location = useLocation ()
|
||||
const navigate = useNavigate ()
|
||||
|
||||
const defaultTag = { name: title, category: 'general' } as Tag
|
||||
|
||||
const [posts, setPosts] = useState<Post[]> ([])
|
||||
const [tag, setTag] = useState (defaultTag)
|
||||
const [wikiPage, setWikiPage] = useState<WikiPage | null | undefined> (undefined)
|
||||
const defaultTag = useMemo (() => ({ name: title, category: 'general' } as Tag), [title])
|
||||
|
||||
const query = new URLSearchParams (location.search)
|
||||
const version = query.get ('version')
|
||||
const version = query.get ('version') || undefined
|
||||
|
||||
const { data: wikiPage, isLoading: loading } = useQuery ({
|
||||
enabled: Boolean (title) && !(/^\d+$/.test (title)),
|
||||
queryKey: wikiKeys.show (title, { version }),
|
||||
queryFn: () => fetchWikiPageByTitle (title, { version }) })
|
||||
|
||||
const effectiveTitle = wikiPage?.title ?? title
|
||||
|
||||
const { data: tag } = useQuery ({
|
||||
enabled: Boolean (effectiveTitle),
|
||||
queryKey: tagsKeys.show (effectiveTitle),
|
||||
queryFn: () => fetchTagByName (effectiveTitle) })
|
||||
|
||||
const { data } = useQuery ({
|
||||
enabled: Boolean (effectiveTitle) && !(version),
|
||||
queryKey: postsKeys.index ({ tags: effectiveTitle, match: 'all', page: 1, limit: 8 }),
|
||||
queryFn: () => fetchPosts ({ tags: effectiveTitle, match: 'all', page: 1, limit: 8 }) })
|
||||
const posts = data?.posts || []
|
||||
|
||||
useEffect (() => {
|
||||
if (/^\d+$/.test (title))
|
||||
{
|
||||
void (async () => {
|
||||
setWikiPage (undefined)
|
||||
try
|
||||
{
|
||||
const data = await fetchWikiPage (title)
|
||||
navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true })
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}) ()
|
||||
if (!(wikiPage))
|
||||
return
|
||||
|
||||
return
|
||||
}
|
||||
WikiIdBus.set (wikiPage.id)
|
||||
|
||||
void (async () => {
|
||||
setWikiPage (undefined)
|
||||
try
|
||||
{
|
||||
const data = await fetchWikiPageByTitle (title, version ? { version } : { })
|
||||
if (data.title !== title)
|
||||
navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true })
|
||||
setWikiPage (data)
|
||||
WikiIdBus.set (data.id)
|
||||
}
|
||||
catch
|
||||
{
|
||||
setWikiPage (null)
|
||||
}
|
||||
}) ()
|
||||
if (wikiPage.title !== title)
|
||||
navigate (`/wiki/${ encodeURIComponent(wikiPage.title) }`, { replace: true })
|
||||
|
||||
return () => WikiIdBus.set (null)
|
||||
}, [wikiPage, title, navigate])
|
||||
|
||||
useEffect (() => {
|
||||
if (!(/^\d+$/.test (title)))
|
||||
return
|
||||
|
||||
setPosts ([])
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
const data = await fetchPosts ({ tags: title, match: 'all', limit: 8 })
|
||||
setPosts (data.posts)
|
||||
const data = await fetchWikiPage (title, { })
|
||||
navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true })
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}) ()
|
||||
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
setTag (await fetchTagByName (title))
|
||||
}
|
||||
catch
|
||||
{
|
||||
setTag (defaultTag)
|
||||
}
|
||||
}) ()
|
||||
|
||||
return () => WikiIdBus.set (null)
|
||||
}, [title, location.search])
|
||||
}, [title, navigate])
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
@@ -104,7 +87,8 @@ export default () => {
|
||||
</Helmet>
|
||||
|
||||
{(wikiPage && version) && (
|
||||
<div className="text-sm flex gap-3 items-center justify-center border border-gray-700 rounded px-2 py-1 mb-4">
|
||||
<div className="text-sm flex gap-3 items-center justify-center
|
||||
border border-gray-700 rounded px-2 py-1 mb-4">
|
||||
{wikiPage.pred ? (
|
||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.pred }`}>
|
||||
< 古
|
||||
@@ -119,15 +103,13 @@ export default () => {
|
||||
</div>)}
|
||||
|
||||
<PageTitle>
|
||||
<TagLink tag={tag}
|
||||
<TagLink tag={tag ?? defaultTag}
|
||||
withWiki={false}
|
||||
withCount={false}
|
||||
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
||||
</PageTitle>
|
||||
<div className="prose mx-auto p-4">
|
||||
{wikiPage === undefined
|
||||
? 'Loading...'
|
||||
: <WikiBody title={title} body={wikiPage?.body}/>}
|
||||
{loading ? 'Loading...' : <WikiBody title={title} body={wikiPage?.body}/>}
|
||||
</div>
|
||||
|
||||
{(!(version) && posts.length > 0) && (
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet } from '@/lib/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { WikiPageDiff } from '@/types'
|
||||
@@ -25,8 +24,7 @@ export default () => {
|
||||
|
||||
useEffect (() => {
|
||||
void (async () => {
|
||||
const res = await axios.get (`${ API_BASE_URL }/wiki/${ id }/diff`, { params: { from, to } })
|
||||
setDiff (toCamel (res.data as any, { deep: true }) as WikiPageDiff)
|
||||
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { params: { from, to } }))
|
||||
}) ()
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'axios'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
@@ -7,7 +7,9 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet, apiPut } from '@/lib/api'
|
||||
import { wikiKeys } from '@/lib/queryKeys'
|
||||
import Forbidden from '@/pages/Forbidden'
|
||||
|
||||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
@@ -29,6 +31,8 @@ export default (({ user }: Props) => {
|
||||
|
||||
const navigate = useNavigate ()
|
||||
|
||||
const qc = useQueryClient ()
|
||||
|
||||
const [body, setBody] = useState ('')
|
||||
const [loading, setLoading] = useState (true)
|
||||
const [title, setTitle] = useState ('')
|
||||
@@ -40,9 +44,11 @@ export default (({ user }: Props) => {
|
||||
|
||||
try
|
||||
{
|
||||
await axios.put (`${ API_BASE_URL }/wiki/${ id }`, formData, { headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
||||
await apiPut (`/wiki/${ id }`, formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
qc.setQueryData (wikiKeys.show (title, { }),
|
||||
(prev: WikiPage) => ({ ...prev, title, body }))
|
||||
qc.invalidateQueries ({ queryKey: wikiKeys.root })
|
||||
toast ({ title: '投稿成功!' })
|
||||
navigate (`/wiki/${ title }`)
|
||||
}
|
||||
@@ -55,8 +61,7 @@ export default (({ user }: Props) => {
|
||||
useEffect (() => {
|
||||
void (async () => {
|
||||
setLoading (true)
|
||||
const res = await axios.get (`${ API_BASE_URL }/wiki/${ id }`)
|
||||
const data = res.data as WikiPage
|
||||
const data = await apiGet<WikiPage> (`/wiki/${ id }`)
|
||||
setTitle (data.title)
|
||||
setBody (data.body)
|
||||
setLoading (false)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { WikiPageChange } from '@/types'
|
||||
|
||||
@@ -19,9 +19,7 @@ export default () => {
|
||||
|
||||
useEffect (() => {
|
||||
void (async () => {
|
||||
const res = await axios.get (`${ API_BASE_URL }/wiki/changes`,
|
||||
{ params: { ...(id ? { id } : { }) } })
|
||||
setChanges (toCamel (res.data as any, { deep: true }) as WikiPageChange[])
|
||||
setChanges (await apiGet<WikiPageChange[]> ('/wiki/changes', { params: id ? { id } : { } }))
|
||||
}) ()
|
||||
}, [location.search])
|
||||
|
||||
@@ -44,22 +42,24 @@ export default () => {
|
||||
<tr key={change.revisionId}>
|
||||
<td>
|
||||
{change.pred != null && (
|
||||
<Link to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
|
||||
<PrefetchLink
|
||||
to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
|
||||
差分
|
||||
</Link>)}
|
||||
</PrefetchLink>)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<Link to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.revisionId }`}>
|
||||
<PrefetchLink
|
||||
to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.revisionId }`}>
|
||||
{change.wikiPage.title}
|
||||
</Link>
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{change.pred == null ? '新規' : '更新'}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<Link to={`/users/${ change.user.id }`}>
|
||||
<PrefetchLink to={`/users/${ change.user.id }`}>
|
||||
{change.user.name}
|
||||
</Link>
|
||||
</PrefetchLink>
|
||||
<br/>
|
||||
{change.timestamp}
|
||||
</td>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from 'axios'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
@@ -7,7 +6,8 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiPost } from '@/lib/api'
|
||||
import Forbidden from '@/pages/Forbidden'
|
||||
|
||||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
@@ -39,10 +39,8 @@ export default ({ user }: Props) => {
|
||||
|
||||
try
|
||||
{
|
||||
const res = await axios.post (`${ API_BASE_URL }/wiki`, formData, { headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
|
||||
const data = res.data as WikiPage
|
||||
const data = await apiPost<WikiPage> ('/wiki', formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
toast ({ title: '投稿成功!' })
|
||||
navigate (`/wiki/${ data.title }`)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import SectionTitle from '@/components/common/SectionTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
import type { WikiPage } from '@/types'
|
||||
|
||||
@@ -17,11 +18,10 @@ export default () => {
|
||||
const [results, setResults] = useState<WikiPage[]> ([])
|
||||
|
||||
const search = async () => {
|
||||
const res = await axios.get (`${ API_BASE_URL }/wiki/search`, { params: { title } })
|
||||
setResults (toCamel (res.data as any, { deep: true }) as WikiPage[])
|
||||
setResults (await apiGet ('/wiki', { params: { title } }))
|
||||
}
|
||||
|
||||
const handleSearch = (ev: React.FormEvent) => {
|
||||
const handleSearch = (ev: FormEvent) => {
|
||||
ev.preventDefault ()
|
||||
search ()
|
||||
}
|
||||
@@ -78,9 +78,9 @@ export default () => {
|
||||
{results.map (page => (
|
||||
<tr key={page.id}>
|
||||
<td className="p-2">
|
||||
<Link to={`/wiki/${ encodeURIComponent (page.title) }`}>
|
||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (page.title) }`}>
|
||||
{page.title}
|
||||
</Link>
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
<td className="p-2 text-gray-100 text-sm">
|
||||
{page.updatedAt}
|
||||
|
||||
新しい課題から参照
ユーザをブロックする