プリフェッチ実装(#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
This commit was merged in pull request #256.
This commit is contained in:
+7
-11
@@ -1,5 +1,3 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { AnimatePresence, LayoutGroup } from 'framer-motion'
|
import { AnimatePresence, LayoutGroup } from 'framer-motion'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { BrowserRouter,
|
import { BrowserRouter,
|
||||||
@@ -11,7 +9,7 @@ import { BrowserRouter,
|
|||||||
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
|
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
|
||||||
import TopNav from '@/components/TopNav'
|
import TopNav from '@/components/TopNav'
|
||||||
import { Toaster } from '@/components/ui/toaster'
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { apiPost, isApiError } from '@/lib/api'
|
||||||
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
|
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
|
||||||
import NotFound from '@/pages/NotFound'
|
import NotFound from '@/pages/NotFound'
|
||||||
import PostDetailPage from '@/pages/posts/PostDetailPage'
|
import PostDetailPage from '@/pages/posts/PostDetailPage'
|
||||||
@@ -75,12 +73,11 @@ export default (() => {
|
|||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const createUser = async () => {
|
const createUser = async () => {
|
||||||
const res = await axios.post (`${ API_BASE_URL }/users`)
|
const data = await apiPost<{ code: string; user: User }> ('/users')
|
||||||
const data = res.data as { code: string; user: any }
|
|
||||||
if (data.code)
|
if (data.code)
|
||||||
{
|
{
|
||||||
localStorage.setItem ('user_code', data.code)
|
localStorage.setItem ('user_code', data.code)
|
||||||
setUser (toCamel (data.user, { deep: true }) as User)
|
setUser (data.user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,17 +87,16 @@ export default (() => {
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const res = await axios.post (`${ API_BASE_URL }/users/verify`, { code })
|
const data = await apiPost<{ valid: boolean; user: User }> ('/users/verify', { code })
|
||||||
const data = res.data as { valid: boolean, user: any }
|
|
||||||
if (data.valid)
|
if (data.valid)
|
||||||
setUser (toCamel (data.user, { deep: true }))
|
setUser (data.user)
|
||||||
else
|
else
|
||||||
await createUser ()
|
await createUser ()
|
||||||
}
|
}
|
||||||
catch (err)
|
catch (err)
|
||||||
{
|
{
|
||||||
if (axios.isAxiosError (err))
|
if (isApiError (err))
|
||||||
setStatus (err.status ?? 200)
|
setStatus (err.response?.status ?? 200)
|
||||||
}
|
}
|
||||||
}) ()
|
}) ()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import PostFormTagsArea from '@/components/PostFormTagsArea'
|
import PostFormTagsArea from '@/components/PostFormTagsArea'
|
||||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||||
import Label from '@/components/common/Label'
|
import Label from '@/components/common/Label'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { apiPut } from '@/lib/api'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
@@ -41,14 +39,11 @@ export default (({ post, onSave }: Props) => {
|
|||||||
const [tags, setTags] = useState<string> ('')
|
const [tags, setTags] = useState<string> ('')
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const res = await axios.put (
|
const data = await apiPut<Post> (
|
||||||
`${ API_BASE_URL }/posts/${ post.id }`,
|
`/posts/${ post.id }`,
|
||||||
{ title, tags,
|
{ title, tags, original_created_from: originalCreatedFrom,
|
||||||
original_created_from: originalCreatedFrom,
|
|
||||||
original_created_before: originalCreatedBefore },
|
original_created_before: originalCreatedBefore },
|
||||||
{ headers: { 'Content-Type': 'multipart/form-data',
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
const data = toCamel (res.data as any, { deep: true }) as Post
|
|
||||||
onSave ({ ...post,
|
onSave ({ ...post,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
import TagSearchBox from '@/components/TagSearchBox'
|
import TagSearchBox from '@/components/TagSearchBox'
|
||||||
import Label from '@/components/common/Label'
|
import Label from '@/components/common/Label'
|
||||||
import TextArea from '@/components/common/TextArea'
|
import TextArea from '@/components/common/TextArea'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { apiGet } from '@/lib/api'
|
||||||
|
|
||||||
import type { FC, SyntheticEvent } from 'react'
|
import type { FC, SyntheticEvent } from 'react'
|
||||||
|
|
||||||
@@ -59,8 +57,7 @@ export default (({ tags, setTags }: Props) => {
|
|||||||
const recompute = async (pos: number, v: string = tags) => {
|
const recompute = async (pos: number, v: string = tags) => {
|
||||||
const { start, end, token } = getTokenAt (v, pos)
|
const { start, end, token } = getTokenAt (v, pos)
|
||||||
setBounds ({ start, end })
|
setBounds ({ start, end })
|
||||||
const res = await axios.get (`${ API_BASE_URL }/tags/autocomplete`, { params: { q: token } })
|
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q: token } })
|
||||||
const data = toCamel (res.data as any, { deep: true }) as Tag[]
|
|
||||||
setSuggestions (data.filter (t => t.postCount > 0))
|
setSuggestions (data.filter (t => t.postCount > 0))
|
||||||
setSuggestionsVsbl (suggestions.length > 0)
|
setSuggestionsVsbl (suggestions.length > 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,53 +21,50 @@ export default (({ posts, onClick }: Props) => {
|
|||||||
const cardRef = useRef<HTMLDivElement> (null)
|
const cardRef = useRef<HTMLDivElement> (null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-wrap gap-6 p-4">
|
||||||
<div className="flex flex-wrap gap-6 p-4">
|
{posts.map ((post, i) => {
|
||||||
{posts.map ((post, i) => {
|
const sharedId = `page-${ post.id }`
|
||||||
const id2 = `page-${ post.id }`
|
const layoutId = sharedId
|
||||||
const layoutId = id2
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrefetchLink
|
<PrefetchLink
|
||||||
to={`/posts/${ post.id }`}
|
to={`/posts/${ post.id }`}
|
||||||
key={post.id}
|
key={post.id}
|
||||||
className="w-40 h-40"
|
className="w-40 h-40"
|
||||||
state={{ sharedId: `page-${ post.id }` }}
|
state={{ sharedId }}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
const sharedId = `page-${ post.id }`
|
setForLocationKey (location.key, sharedId)
|
||||||
setForLocationKey (location.key, sharedId)
|
onClick?.(e)
|
||||||
onClick?.(e)
|
}}>
|
||||||
}}>
|
<motion.div
|
||||||
<motion.div
|
ref={cardRef}
|
||||||
ref={cardRef}
|
layoutId={layoutId}
|
||||||
layoutId={layoutId}
|
className="w-full h-full overflow-hidden rounded-xl shadow
|
||||||
className="w-full h-full overflow-hidden rounded-xl shadow
|
transform-gpu will-change-transform"
|
||||||
transform-gpu will-change-transform"
|
whileHover={{ scale: 1.02 }}
|
||||||
whileHover={{ scale: 1.02 }}
|
onLayoutAnimationStart={() => {
|
||||||
onLayoutAnimationStart={() => {
|
if (!(cardRef.current))
|
||||||
if (cardRef.current)
|
return
|
||||||
{
|
|
||||||
cardRef.current.style.position = 'relative'
|
cardRef.current.style.position = 'relative'
|
||||||
cardRef.current.style.zIndex = '9999'
|
cardRef.current.style.zIndex = '9999'
|
||||||
}
|
}}
|
||||||
}}
|
onLayoutAnimationComplete={() => {
|
||||||
onLayoutAnimationComplete={() => {
|
if (!(cardRef.current))
|
||||||
if (cardRef.current)
|
return
|
||||||
{
|
|
||||||
cardRef.current.style.zIndex = ''
|
cardRef.current.style.zIndex = ''
|
||||||
cardRef.current.style.position = ''
|
cardRef.current.style.position = ''
|
||||||
}
|
}}
|
||||||
}}
|
transition={{ type: 'spring', stiffness: 500, damping: 40, mass: .5 }}>
|
||||||
transition={{ type: 'spring', stiffness: 500, damping: 40, mass: .5 }}>
|
<img src={post.thumbnail || post.thumbnailBase || undefined}
|
||||||
<img src={post.thumbnail || post.thumbnailBase || undefined}
|
alt={post.title || post.url}
|
||||||
alt={post.title || post.url}
|
title={post.title || post.url || undefined}
|
||||||
title={post.title || post.url || undefined}
|
loading={i < 12 ? 'eager' : 'lazy'}
|
||||||
loading={i < 12 ? 'eager' : 'lazy'}
|
decoding="async"
|
||||||
decoding="async"
|
className="object-cover w-full h-full"/>
|
||||||
className="object-cover w-full h-full"/>
|
</motion.div>
|
||||||
</motion.div>
|
</PrefetchLink>)
|
||||||
</PrefetchLink>)
|
})}
|
||||||
})}
|
</div>)
|
||||||
</div>
|
|
||||||
</>)
|
|
||||||
}) satisfies FC<Props>
|
}) satisfies FC<Props>
|
||||||
|
|||||||
@@ -6,21 +6,19 @@ import { DndContext,
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors } from '@dnd-kit/core'
|
useSensors } from '@dnd-kit/core'
|
||||||
import { restrictToWindowEdges } from '@dnd-kit/modifiers'
|
import { restrictToWindowEdges } from '@dnd-kit/modifiers'
|
||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow'
|
import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow'
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import TagLink from '@/components/TagLink'
|
import TagLink from '@/components/TagLink'
|
||||||
import TagSearch from '@/components/TagSearch'
|
import TagSearch from '@/components/TagSearch'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
||||||
import SidebarComponent from '@/components/layout/SidebarComponent'
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { API_BASE_URL } from '@/config'
|
|
||||||
import { CATEGORIES } from '@/consts'
|
import { CATEGORIES } from '@/consts'
|
||||||
|
import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api'
|
||||||
|
|
||||||
import type { DragEndEvent } from '@dnd-kit/core'
|
import type { DragEndEvent } from '@dnd-kit/core'
|
||||||
import type { FC, MutableRefObject, ReactNode } from 'react'
|
import type { FC, MutableRefObject, ReactNode } from 'react'
|
||||||
@@ -132,10 +130,7 @@ const changeCategory = async (
|
|||||||
tagId: number,
|
tagId: number,
|
||||||
category: Category,
|
category: Category,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await axios.patch (
|
await apiPatch (`/tags/${ tagId }`, { category })
|
||||||
`${ API_BASE_URL }/tags/${ tagId }`,
|
|
||||||
{ category },
|
|
||||||
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -170,12 +165,7 @@ export default (({ post }: Props) => {
|
|||||||
if (!(post))
|
if (!(post))
|
||||||
return
|
return
|
||||||
|
|
||||||
const res = await axios.get (
|
setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`)))
|
||||||
`${ API_BASE_URL }/posts/${ post.id }`,
|
|
||||||
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
const data = toCamel (res.data as any, { deep: true }) as Post
|
|
||||||
|
|
||||||
setTags (buildTagByCategory (data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDragEnd = async (e: DragEndEvent) => {
|
const onDragEnd = async (e: DragEndEvent) => {
|
||||||
@@ -216,16 +206,9 @@ export default (({ post }: Props) => {
|
|||||||
return
|
return
|
||||||
|
|
||||||
if (fromParentId != null)
|
if (fromParentId != null)
|
||||||
{
|
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)
|
||||||
await axios.delete (
|
|
||||||
`${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`,
|
|
||||||
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios.post (
|
await apiPost (`/tags/${ parentId }/children/${ childId }`, { })
|
||||||
`${ API_BASE_URL }/tags/${ parentId }/children/${ childId }`,
|
|
||||||
{ },
|
|
||||||
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
|
|
||||||
await reloadTags ()
|
await reloadTags ()
|
||||||
toast ({
|
toast ({
|
||||||
@@ -245,11 +228,7 @@ export default (({ post }: Props) => {
|
|||||||
await changeCategory (childId, cat)
|
await changeCategory (childId, cat)
|
||||||
|
|
||||||
if (fromParentId != null)
|
if (fromParentId != null)
|
||||||
{
|
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)
|
||||||
await axios.delete (
|
|
||||||
`${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`,
|
|
||||||
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromParent = fromParentId == null ? null : findTag (tags, fromParentId)
|
const fromParent = fromParentId == null ? null : findTag (tags, fromParentId)
|
||||||
|
|
||||||
@@ -358,9 +337,9 @@ export default (({ post }: Props) => {
|
|||||||
<>耕作者: </>
|
<>耕作者: </>
|
||||||
{post.uploadedUser
|
{post.uploadedUser
|
||||||
? (
|
? (
|
||||||
<Link to={`/users/${ post.uploadedUser.id }`}>
|
<PrefetchLink to={`/users/${ post.uploadedUser.id }`}>
|
||||||
{post.uploadedUser.name || '名もなきニジラー'}
|
{post.uploadedUser.name || '名もなきニジラー'}
|
||||||
</Link>)
|
</PrefetchLink>)
|
||||||
: 'bot操作'}
|
: 'bot操作'}
|
||||||
</li>
|
</li>
|
||||||
*/}
|
*/}
|
||||||
@@ -389,7 +368,7 @@ export default (({ post }: Props) => {
|
|||||||
</>)}
|
</>)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to={`/posts/changes?id=${ post.id }`}>履歴</Link>
|
<PrefetchLink to={`/posts/changes?id=${ post.id }`}>履歴</PrefetchLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import { API_BASE_URL } from '@/config'
|
|
||||||
import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
|
import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
|
||||||
|
import { apiGet } from '@/lib/api'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { ComponentProps, FC, HTMLAttributes } from 'react'
|
import type { ComponentProps, FC, HTMLAttributes } from 'react'
|
||||||
|
|
||||||
import type { Tag } from '@/types'
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
type CommonProps = { tag: Tag
|
type CommonProps = {
|
||||||
nestLevel?: number
|
tag: Tag
|
||||||
withWiki?: boolean
|
nestLevel?: number
|
||||||
withCount?: boolean
|
withWiki?: boolean
|
||||||
prefetch?: boolean }
|
withCount?: boolean
|
||||||
|
prefetch?: boolean }
|
||||||
|
|
||||||
type PropsWithLink =
|
type PropsWithLink =
|
||||||
CommonProps & { linkFlg?: true } & Partial<ComponentProps<typeof Link>>
|
& CommonProps
|
||||||
|
& { linkFlg?: true }
|
||||||
|
& Partial<ComponentProps<typeof PrefetchLink>>
|
||||||
|
|
||||||
type PropsWithoutLink =
|
type PropsWithoutLink =
|
||||||
CommonProps & { linkFlg: false } & Partial<HTMLAttributes<HTMLSpanElement>>
|
& CommonProps
|
||||||
|
& { linkFlg: false }
|
||||||
|
& Partial<HTMLAttributes<HTMLSpanElement>>
|
||||||
|
|
||||||
type Props = PropsWithLink | PropsWithoutLink
|
type Props =
|
||||||
|
| PropsWithLink
|
||||||
|
| PropsWithoutLink
|
||||||
|
|
||||||
|
|
||||||
export default (({ tag,
|
export default (({ tag,
|
||||||
@@ -46,7 +51,7 @@ export default (({ tag,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (tagName) }/exists`)
|
await apiGet (`/wiki/title/${ encodeURIComponent (tagName) }/exists`)
|
||||||
setHavingWiki (true)
|
setHavingWiki (true)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -76,17 +81,17 @@ export default (({ tag,
|
|||||||
<span className="mr-1">
|
<span className="mr-1">
|
||||||
{havingWiki
|
{havingWiki
|
||||||
? (
|
? (
|
||||||
<Link to={`/wiki/${ encodeURIComponent (tag.name) }`}
|
<PrefetchLink to={`/wiki/${ encodeURIComponent (tag.name) }`}
|
||||||
className={linkClass}>
|
className={linkClass}>
|
||||||
?
|
?
|
||||||
</Link>)
|
</PrefetchLink>)
|
||||||
: (
|
: (
|
||||||
<Link to={`/wiki/${ encodeURIComponent (tag.name) }`}
|
<PrefetchLink to={`/wiki/${ encodeURIComponent (tag.name) }`}
|
||||||
className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
|
className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
|
||||||
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
|
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
|
||||||
title={`${ tag.name } Wiki が存在しません.`}>
|
title={`${ tag.name } Wiki が存在しません.`}>
|
||||||
!
|
!
|
||||||
</Link>)}
|
</PrefetchLink>)}
|
||||||
</span>)}
|
</span>)}
|
||||||
{nestLevel > 0 && (
|
{nestLevel > 0 && (
|
||||||
<span
|
<span
|
||||||
@@ -110,12 +115,12 @@ export default (({ tag,
|
|||||||
{...props}>
|
{...props}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</PrefetchLink>
|
</PrefetchLink>
|
||||||
: <Link
|
: <PrefetchLink
|
||||||
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
||||||
className={linkClass}
|
className={linkClass}
|
||||||
{...props}>
|
{...props}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</Link>)
|
</PrefetchLink>)
|
||||||
: (
|
: (
|
||||||
<span className={spanClass}
|
<span className={spanClass}
|
||||||
{...props}>
|
{...props}>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import axios from 'axios'
|
import { useEffect, useState } from 'react'
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { API_BASE_URL } from '@/config'
|
import { apiGet } from '@/lib/api'
|
||||||
|
|
||||||
import TagSearchBox from './TagSearchBox'
|
import TagSearchBox from './TagSearchBox'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { ChangeEvent, FC, KeyboardEvent } from 'react'
|
||||||
|
|
||||||
import type { Tag } from '@/types'
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
@@ -21,7 +19,7 @@ export default (() => {
|
|||||||
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||||
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
||||||
|
|
||||||
const whenChanged = async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearch (ev.target.value)
|
setSearch (ev.target.value)
|
||||||
|
|
||||||
const q = ev.target.value.trim ().split (' ').at (-1)
|
const q = ev.target.value.trim ().split (' ').at (-1)
|
||||||
@@ -31,14 +29,13 @@ export default (() => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await axios.get (`${ API_BASE_URL }/tags/autocomplete`, { params: { q } })
|
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q } })
|
||||||
const data = toCamel (res.data, { deep: true }) as Tag[]
|
|
||||||
setSuggestions (data.filter (t => t.postCount > 0))
|
setSuggestions (data.filter (t => t.postCount > 0))
|
||||||
if (suggestions.length > 0)
|
if (suggestions.length > 0)
|
||||||
setSuggestionsVsbl (true)
|
setSuggestionsVsbl (true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (ev: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (ev: KeyboardEvent<HTMLInputElement>) => {
|
||||||
switch (ev.key)
|
switch (ev.key)
|
||||||
{
|
{
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
@@ -7,8 +6,8 @@ import TagLink from '@/components/TagLink'
|
|||||||
import TagSearch from '@/components/TagSearch'
|
import TagSearch from '@/components/TagSearch'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
import SidebarComponent from '@/components/layout/SidebarComponent'
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||||
import { API_BASE_URL } from '@/config'
|
|
||||||
import { CATEGORIES } from '@/consts'
|
import { CATEGORIES } from '@/consts'
|
||||||
|
import { apiGet } from '@/lib/api'
|
||||||
|
|
||||||
import type { FC, MouseEvent } from 'react'
|
import type { FC, MouseEvent } from 'react'
|
||||||
|
|
||||||
@@ -77,10 +76,10 @@ export default (({ posts, onClick }: Props) => {
|
|||||||
void ((async () => {
|
void ((async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const { data } = await axios.get (`${ API_BASE_URL }/posts/random`,
|
const data = await apiGet<Post> ('/posts/random',
|
||||||
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
|
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
|
||||||
match: (anyFlg ? 'any' : 'all') } })
|
match: (anyFlg ? 'any' : 'all') } })
|
||||||
navigate (`/posts/${ (data as Post).id }`)
|
navigate (`/posts/${ data.id }`)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
@@ -6,6 +7,7 @@ import Separator from '@/components/MenuSeparator'
|
|||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import TopNavUser from '@/components/TopNavUser'
|
import TopNavUser from '@/components/TopNavUser'
|
||||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||||
|
import { tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { fetchTagByName } from '@/lib/tags'
|
import { fetchTagByName } from '@/lib/tags'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { fetchWikiPage } from '@/lib/wiki'
|
import { fetchWikiPage } from '@/lib/wiki'
|
||||||
@@ -44,11 +46,26 @@ export default (({ user }: Props) => {
|
|||||||
visible: false })
|
visible: false })
|
||||||
const [menuOpen, setMenuOpen] = useState (false)
|
const [menuOpen, setMenuOpen] = useState (false)
|
||||||
const [openItemIdx, setOpenItemIdx] = useState (-1)
|
const [openItemIdx, setOpenItemIdx] = useState (-1)
|
||||||
const [postCount, setPostCount] = useState<number | null> (null)
|
|
||||||
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
||||||
|
|
||||||
|
const wikiIdStr = String (wikiId ?? '')
|
||||||
|
|
||||||
|
const { data: wikiPage } = useQuery ({
|
||||||
|
enabled: Boolean (wikiIdStr),
|
||||||
|
queryKey: wikiKeys.show (wikiIdStr, { }),
|
||||||
|
queryFn: () => fetchWikiPage (wikiIdStr, { }) })
|
||||||
|
|
||||||
|
const effectiveTitle = wikiPage?.title ?? ''
|
||||||
|
|
||||||
|
const { data: tag } = useQuery ({
|
||||||
|
enabled: Boolean (effectiveTitle),
|
||||||
|
queryKey: tagsKeys.show (effectiveTitle),
|
||||||
|
queryFn: () => fetchTagByName (effectiveTitle) })
|
||||||
|
|
||||||
|
const postCount = tag?.postCount ?? 0
|
||||||
|
|
||||||
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
|
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
|
||||||
const wikiTitle = location.pathname.split ('/')[2]
|
const wikiTitle = location.pathname.split ('/')[2] ?? ''
|
||||||
const menu: Menu = [
|
const menu: Menu = [
|
||||||
{ name: '広場', to: '/posts', subMenu: [
|
{ name: '広場', to: '/posts', subMenu: [
|
||||||
{ name: '一覧', to: '/posts' },
|
{ name: '一覧', to: '/posts' },
|
||||||
@@ -113,26 +130,6 @@ export default (({ user }: Props) => {
|
|||||||
location.pathname.startsWith (item.base || item.to))))
|
location.pathname.startsWith (item.base || item.to))))
|
||||||
}, [location])
|
}, [location])
|
||||||
|
|
||||||
useEffect (() => {
|
|
||||||
if (!(wikiId))
|
|
||||||
return
|
|
||||||
|
|
||||||
const fetchPostCount = async () => {
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const wikiPage = await fetchWikiPage (String (wikiId ?? ''))
|
|
||||||
const tag = await fetchTagByName (wikiPage.title)
|
|
||||||
|
|
||||||
setPostCount (tag.postCount)
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
setPostCount (0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchPostCount ()
|
|
||||||
}, [wikiId])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="px-3 flex justify-between items-center w-full min-h-[48px]
|
<nav className="px-3 flex justify-between items-center w-full min-h-[48px]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
import Separator from '@/components/MenuSeparator'
|
import Separator from '@/components/MenuSeparator'
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
@@ -24,9 +23,9 @@ export default (({ user, sp }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{sp && <Separator/>}
|
{sp && <Separator/>}
|
||||||
<Link to="/users/settings"
|
<PrefetchLink to="/users/settings"
|
||||||
className={className}>
|
className={className}>
|
||||||
{user.name || '名もなきニジラー'}
|
{user.name || '名もなきニジラー'}
|
||||||
</Link>
|
</PrefetchLink>
|
||||||
</>)
|
</>)
|
||||||
}) satisfies FC<Props>
|
}) satisfies FC<Props>
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import axios from 'axios'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import toCamel from 'camelcase-keys'
|
import { useMemo } from 'react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import remarkGFM from 'remark-gfm'
|
import remarkGFM from 'remark-gfm'
|
||||||
|
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { wikiKeys } from '@/lib/queryKeys'
|
||||||
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
|
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
|
||||||
|
import { fetchWikiPages } from '@/lib/wiki'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { Components } from 'react-markdown'
|
import type { Components } from 'react-markdown'
|
||||||
|
|
||||||
import type { WikiPage } from '@/types'
|
|
||||||
|
|
||||||
type Props = { title: string
|
type Props = { title: string
|
||||||
body?: string }
|
body?: string }
|
||||||
|
|
||||||
@@ -24,7 +22,7 @@ const mdComponents = { h1: ({ children }) => <SectionTitle>{children}</SectionT
|
|||||||
ul: ({ children }) => <ul className="list-disc pl-6">{children}</ul>,
|
ul: ({ children }) => <ul className="list-disc pl-6">{children}</ul>,
|
||||||
a: (({ href, children }) => (
|
a: (({ href, children }) => (
|
||||||
['/', '.'].some (e => href?.startsWith (e))
|
['/', '.'].some (e => href?.startsWith (e))
|
||||||
? <Link to={href!}>{children}</Link>
|
? <PrefetchLink to={href!}>{children}</PrefetchLink>
|
||||||
: (
|
: (
|
||||||
<a href={href}
|
<a href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -34,26 +32,15 @@ const mdComponents = { h1: ({ children }) => <SectionTitle>{children}</SectionT
|
|||||||
|
|
||||||
|
|
||||||
export default (({ title, body }: Props) => {
|
export default (({ title, body }: Props) => {
|
||||||
const [pageNames, setPageNames] = useState<string[]> ([])
|
const { data } = useQuery ({
|
||||||
|
enabled: Boolean (body),
|
||||||
|
queryKey: wikiKeys.index ({ }),
|
||||||
|
queryFn: () => fetchWikiPages ({ }) })
|
||||||
|
const pageNames = (data ?? []).map (page => page.title).sort ((a, b) => b.length - a.length)
|
||||||
|
|
||||||
const remarkPlugins = useMemo (
|
const remarkPlugins = useMemo (
|
||||||
() => [() => remarkWikiAutoLink (pageNames), remarkGFM], [pageNames])
|
() => [() => remarkWikiAutoLink (pageNames), remarkGFM], [pageNames])
|
||||||
|
|
||||||
useEffect (() => {
|
|
||||||
void (async () => {
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const res = await axios.get (`${ API_BASE_URL }/wiki`)
|
|
||||||
const data: WikiPage[] = toCamel (res.data as any, { deep: true })
|
|
||||||
setPageNames (data.map (page => page.title).sort ((a, b) => b.length - a.length))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
setPageNames ([])
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
|
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
|
||||||
{body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`}
|
{body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -8,7 +6,7 @@ import { Dialog,
|
|||||||
DialogTitle } from '@/components/ui/dialog'
|
DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { apiPost } from '@/lib/api'
|
||||||
|
|
||||||
import type { User } from '@/types'
|
import type { User } from '@/types'
|
||||||
|
|
||||||
@@ -26,12 +24,12 @@ export default ({ visible, onVisibleChange, setUser }: Props) => {
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const res = await axios.post (`${ API_BASE_URL }/users/verify`, { code: inputCode })
|
const data = await apiPost<{ valid: boolean; user: User }> (
|
||||||
const data = res.data as { valid: boolean; user: any }
|
'/users/verify', { code: inputCode })
|
||||||
if (data.valid)
|
if (data.valid)
|
||||||
{
|
{
|
||||||
localStorage.setItem ('user_code', inputCode)
|
localStorage.setItem ('user_code', inputCode)
|
||||||
setUser (toCamel (data.user, { deep: true }))
|
setUser (data.user)
|
||||||
toast ({ title: '引継ぎ成功!' })
|
toast ({ title: '引継ぎ成功!' })
|
||||||
onVisibleChange (false)
|
onVisibleChange (false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Dialog,
|
import { Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle } from '@/components/ui/dialog'
|
DialogTitle } from '@/components/ui/dialog'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { apiPost } from '@/lib/api'
|
||||||
|
|
||||||
import type { User } from '@/types'
|
import type { User } from '@/types'
|
||||||
|
|
||||||
@@ -23,10 +21,8 @@ export default ({ visible, onVisibleChange, user, setUser }: Props) => {
|
|||||||
if (!(confirm ('引継ぎコードを再発行しますか?\n再発行するとほかのブラウザからはログアウトされます.')))
|
if (!(confirm ('引継ぎコードを再発行しますか?\n再発行するとほかのブラウザからはログアウトされます.')))
|
||||||
return
|
return
|
||||||
|
|
||||||
const res = await axios.post (`${ API_BASE_URL }/users/code/renew`, { }, { headers: {
|
const data = await apiPost<{ code: string }> ('/users/code/renew', { },
|
||||||
'Content-Type': 'multipart/form-data',
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
|
|
||||||
const data = res.data as { code: string }
|
|
||||||
if (data.code)
|
if (data.code)
|
||||||
{
|
{
|
||||||
localStorage.setItem ('user_code', data.code)
|
localStorage.setItem ('user_code', data.code)
|
||||||
|
|||||||
+12
-2
@@ -3,9 +3,12 @@ import toCamel from 'camelcase-keys'
|
|||||||
|
|
||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
|
|
||||||
|
import type { AxiosError, AxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
type Opt = {
|
type Opt = {
|
||||||
params?: Record<string, unknown>
|
params?: AxiosRequestConfig['params']
|
||||||
headers?: Record<string, string> }
|
headers?: Record<string, string>
|
||||||
|
responseType?: 'blob' }
|
||||||
|
|
||||||
const client = axios.create ({ baseURL: API_BASE_URL })
|
const client = axios.create ({ baseURL: API_BASE_URL })
|
||||||
|
|
||||||
@@ -23,6 +26,8 @@ const apiP = async <T> (
|
|||||||
opt?: Opt,
|
opt?: Opt,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const res = await client[method] (path, body ?? { }, withUserCode (opt))
|
const res = await client[method] (path, body ?? { }, withUserCode (opt))
|
||||||
|
if (opt?.responseType === 'blob')
|
||||||
|
return res.data as T
|
||||||
return toCamel (res.data as any, { deep: true }) as T
|
return toCamel (res.data as any, { deep: true }) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +37,8 @@ export const apiGet = async <T> (
|
|||||||
opt?: Opt,
|
opt?: Opt,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const res = await client.get (path, withUserCode (opt))
|
const res = await client.get (path, withUserCode (opt))
|
||||||
|
if (opt?.responseType === 'blob')
|
||||||
|
return res.data as T
|
||||||
return toCamel (res.data as any, { deep: true }) as T
|
return toCamel (res.data as any, { deep: true }) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,3 +70,6 @@ export const apiDelete = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await client.delete (path, withUserCode (opt))
|
await client.delete (path, withUserCode (opt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const isApiError = (err: unknown): err is AxiosError => axios.isAxiosError (err)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { apiDelete, apiGet, apiPost } from '@/lib/api'
|
import { apiDelete, apiGet, apiPost } from '@/lib/api'
|
||||||
|
|
||||||
import type { Post } from '@/types'
|
import type { Post, PostTagChange } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
export const fetchPosts = async (
|
export const fetchPosts = async (
|
||||||
@@ -13,8 +13,8 @@ export const fetchPosts = async (
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
posts: Post[]
|
posts: Post[]
|
||||||
count: number
|
count: number
|
||||||
nextCursor: string }> => await apiGet ('/posts', {
|
nextCursor: string }> =>
|
||||||
params: {
|
await apiGet ('/posts', { params: {
|
||||||
tags,
|
tags,
|
||||||
match,
|
match,
|
||||||
...(page && { page }),
|
...(page && { page }),
|
||||||
@@ -25,6 +25,17 @@ export const fetchPosts = async (
|
|||||||
export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`)
|
export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`)
|
||||||
|
|
||||||
|
|
||||||
|
export const fetchPostChanges = async (
|
||||||
|
{ id, page, limit }: {
|
||||||
|
id?: string
|
||||||
|
page: number
|
||||||
|
limit: number },
|
||||||
|
): Promise<{
|
||||||
|
changes: PostTagChange[]
|
||||||
|
count: number }> =>
|
||||||
|
await apiGet ('/posts/changes', { params: { ...(id && { id }), page, limit } })
|
||||||
|
|
||||||
|
|
||||||
export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => {
|
export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => {
|
||||||
await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`)
|
await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,69 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query'
|
import { QueryClient } from '@tanstack/react-query'
|
||||||
import { match } from 'path-to-regexp'
|
import { match } from 'path-to-regexp'
|
||||||
|
|
||||||
import { fetchPost, fetchPosts } from '@/lib/posts'
|
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
||||||
import { postsKeys } from '@/lib/queryKeys'
|
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
|
import { fetchTagByName } from '@/lib/tags'
|
||||||
|
import { fetchWikiPage,
|
||||||
|
fetchWikiPageByTitle,
|
||||||
|
fetchWikiPages } from '@/lib/wiki'
|
||||||
|
|
||||||
type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
|
type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
|
||||||
|
|
||||||
const mPost = match<{ id: string }> ('/posts/:id')
|
const mPost = match<{ id: string }> ('/posts/:id')
|
||||||
|
const mWiki = match<{ title: string }> ('/wiki/:title')
|
||||||
|
|
||||||
|
|
||||||
|
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
||||||
|
const title = url.searchParams.get ('title') ?? ''
|
||||||
|
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: wikiKeys.index ({ title }),
|
||||||
|
queryFn: () => fetchWikiPages ({ title }) })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
|
||||||
|
const m = mWiki (url.pathname)
|
||||||
|
if (!(m))
|
||||||
|
return
|
||||||
|
|
||||||
|
const title = decodeURIComponent (m.params.title)
|
||||||
|
const version = url.searchParams.get ('version') || undefined
|
||||||
|
|
||||||
|
const wikiPage = await qc.fetchQuery ({
|
||||||
|
queryKey: wikiKeys.show (title, { version }),
|
||||||
|
queryFn: () => fetchWikiPageByTitle (title, { version }) })
|
||||||
|
|
||||||
|
if (wikiPage)
|
||||||
|
{
|
||||||
|
const effectiveId = String (wikiPage.id ?? '')
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: wikiKeys.show (effectiveId, { }),
|
||||||
|
queryFn: () => fetchWikiPage (effectiveId, { } ) })
|
||||||
|
|
||||||
|
if (wikiPage.body)
|
||||||
|
{
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: wikiKeys.index ({ }),
|
||||||
|
queryFn: () => fetchWikiPages ({ }) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveTitle = wikiPage?.title ?? title
|
||||||
|
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: tagsKeys.show (effectiveTitle),
|
||||||
|
queryFn: () => fetchTagByName (effectiveTitle) })
|
||||||
|
|
||||||
|
if (version)
|
||||||
|
return
|
||||||
|
|
||||||
|
const p = { tags: effectiveTitle, match: 'all', page: 1, limit: 8 } as const
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: postsKeys.index (p),
|
||||||
|
queryFn: () => fetchPosts (p) })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const prefetchPostsIndex: Prefetcher = async (qc, url) => {
|
const prefetchPostsIndex: Prefetcher = async (qc, url) => {
|
||||||
@@ -14,6 +71,7 @@ const prefetchPostsIndex: Prefetcher = async (qc, url) => {
|
|||||||
const m = url.searchParams.get ('match') === 'any' ? 'any' : 'all'
|
const m = url.searchParams.get ('match') === 'any' ? 'any' : 'all'
|
||||||
const page = Number (url.searchParams.get ('page') || 1)
|
const page = Number (url.searchParams.get ('page') || 1)
|
||||||
const limit = Number (url.searchParams.get ('limit') || 20)
|
const limit = Number (url.searchParams.get ('limit') || 20)
|
||||||
|
|
||||||
await qc.prefetchQuery ({
|
await qc.prefetchQuery ({
|
||||||
queryKey: postsKeys.index ({ tags, match: m, page, limit }),
|
queryKey: postsKeys.index ({ tags, match: m, page, limit }),
|
||||||
queryFn: () => fetchPosts ({ tags, match: m, page, limit }) })
|
queryFn: () => fetchPosts ({ tags, match: m, page, limit }) })
|
||||||
@@ -26,24 +84,40 @@ const prefetchPostShow: Prefetcher = async (qc, url) => {
|
|||||||
return
|
return
|
||||||
|
|
||||||
const { id } = m.params
|
const { id } = m.params
|
||||||
|
|
||||||
await qc.prefetchQuery ({
|
await qc.prefetchQuery ({
|
||||||
queryKey: postsKeys.show (id),
|
queryKey: postsKeys.show (id),
|
||||||
queryFn: () => fetchPost (id) })
|
queryFn: () => fetchPost (id) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const routePrefetchers: {
|
const prefetchPostChanges: Prefetcher = async (qc, url) => {
|
||||||
test: (u: URL) => boolean
|
const id = url.searchParams.get ('id')
|
||||||
run: Prefetcher }[] = [
|
const page = Number (url.searchParams.get ('page') || 1)
|
||||||
|
const limit = Number (url.searchParams.get ('limit') || 20)
|
||||||
|
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: postsKeys.changes ({ ...(id && { id }), page, limit }),
|
||||||
|
queryFn: () => fetchPostChanges ({ ...(id && { id }), page, limit }) })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
|
||||||
{ test: u => u.pathname === '/' || u.pathname === '/posts', run: prefetchPostsIndex },
|
{ test: u => u.pathname === '/' || u.pathname === '/posts', run: prefetchPostsIndex },
|
||||||
{ test: u => Boolean (mPost (u.pathname)), run: prefetchPostShow }]
|
{ test: u => (!(['/posts/new', '/posts/changes'].includes (u.pathname))
|
||||||
|
&& Boolean (mPost (u.pathname))),
|
||||||
|
run: prefetchPostShow },
|
||||||
|
{ test: u => u.pathname === '/posts/changes', run: prefetchPostChanges },
|
||||||
|
{ test: u => u.pathname === '/wiki', run: prefetchWikiPagesIndex },
|
||||||
|
{ test: u => (!(['/wiki/new', '/wiki/changes'].includes (u.pathname))
|
||||||
|
&& Boolean (mWiki (u.pathname))),
|
||||||
|
run: prefetchWikiPageShow }]
|
||||||
|
|
||||||
|
|
||||||
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
|
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
|
||||||
const u = new URL (urlLike, location.origin)
|
const u = new URL (urlLike, location.origin)
|
||||||
const jobs = routePrefetchers.filter (r => r.test (u)).map (r => r.run (qc, u))
|
const r = routePrefetchers.find (x => x.test (u))
|
||||||
if (jobs.length === 0)
|
if (!(r))
|
||||||
return
|
return
|
||||||
|
await r.run (qc, u)
|
||||||
await Promise.all (jobs)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ export const postsKeys = {
|
|||||||
index: (p: { tags: string; match: 'any' | 'all'; page: number; limit: number }) =>
|
index: (p: { tags: string; match: 'any' | 'all'; page: number; limit: number }) =>
|
||||||
['posts', 'index', p] as const,
|
['posts', 'index', p] as const,
|
||||||
show: (id: string) => ['posts', id] as const,
|
show: (id: string) => ['posts', id] as const,
|
||||||
related: (id: string) => ['related', id] as const }
|
related: (id: string) => ['related', id] as const,
|
||||||
|
changes: (p: { id?: string; page: number; limit: number }) =>
|
||||||
|
['posts', 'changes', p] as const }
|
||||||
|
|
||||||
|
export const tagsKeys = {
|
||||||
|
root: ['tags'] as const,
|
||||||
|
show: (name: string) => ['tags', name] as const }
|
||||||
|
|
||||||
export const wikiKeys = {
|
export const wikiKeys = {
|
||||||
root: ['wiki'] as const,
|
root: ['wiki'] as const,
|
||||||
show: (title: string, p: { version: string }) => ['wiki', title, p] as const }
|
index: (p: { title?: string }) => ['wiki', 'index', p] as const,
|
||||||
|
show: (title: string, p: { version?: string }) => ['wiki', title, p] as const }
|
||||||
|
|||||||
@@ -3,5 +3,13 @@ import { apiGet } from '@/lib/api'
|
|||||||
import type { Tag } from '@/types'
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
export const fetchTagByName = async (name: string): Promise<Tag> =>
|
export const fetchTagByName = async (name: string): Promise<Tag | null> => {
|
||||||
await apiGet (`/tags/name/${ name }`)
|
try
|
||||||
|
{
|
||||||
|
return await apiGet (`/tags/name/${ name }`)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,29 @@ import { apiGet } from '@/lib/api'
|
|||||||
import type { WikiPage } from '@/types'
|
import type { WikiPage } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
export const fetchWikiPage = async (id: string): Promise<WikiPage> =>
|
export const fetchWikiPages = async (
|
||||||
await apiGet (`/wiki/${ id }`)
|
{ title }: { title?: string },
|
||||||
|
): Promise<WikiPage[]> =>
|
||||||
|
await apiGet ('/wiki', { params: { title } })
|
||||||
|
|
||||||
|
|
||||||
|
export const fetchWikiPage = async (
|
||||||
|
id: string,
|
||||||
|
{ version }: { version?: string },
|
||||||
|
): Promise<WikiPage> =>
|
||||||
|
await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } })
|
||||||
|
|
||||||
|
|
||||||
export const fetchWikiPageByTitle = async (
|
export const fetchWikiPageByTitle = async (
|
||||||
title: string,
|
title: string,
|
||||||
{ version }: { version?: string },
|
{ version }: { version?: string },
|
||||||
): Promise<WikiPage> =>
|
): Promise<WikiPage | null> => {
|
||||||
await apiGet (`/wiki/title/${ title }`, { params: version ? { version } : { } })
|
try
|
||||||
|
{
|
||||||
|
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -91,67 +91,65 @@ export default (({ user }: Props) => {
|
|||||||
: 'bg-gray-500 hover:bg-gray-600')
|
: 'bg-gray-500 hover:bg-gray-600')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="md:flex md:flex-1">
|
||||||
<div className="md:flex md:flex-1">
|
<Helmet>
|
||||||
<Helmet>
|
{(post?.thumbnail || post?.thumbnailBase) && (
|
||||||
{(post?.thumbnail || post?.thumbnailBase) && (
|
<meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)}
|
||||||
<meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)}
|
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
|
||||||
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
|
</Helmet>
|
||||||
</Helmet>
|
|
||||||
|
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<TagDetailSidebar post={post ?? null}/>
|
<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>
|
</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>
|
}) satisfies FC<Props>
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import axios from 'axios'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Helmet } from 'react-helmet-async'
|
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 TagLink from '@/components/TagLink'
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import PageTitle from '@/components/common/PageTitle'
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import Pagination from '@/components/common/Pagination'
|
import Pagination from '@/components/common/Pagination'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
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 { FC } from 'react'
|
||||||
|
|
||||||
import type { PostTagChange } from '@/types'
|
|
||||||
|
|
||||||
|
|
||||||
export default (() => {
|
export default (() => {
|
||||||
const [changes, setChanges] = useState<PostTagChange[]> ([])
|
|
||||||
const [totalPages, setTotalPages] = useState<number> (0)
|
|
||||||
|
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const query = new URLSearchParams (location.search)
|
const query = new URLSearchParams (location.search)
|
||||||
const id = query.get ('id')
|
const id = query.get ('id')
|
||||||
@@ -28,17 +24,11 @@ export default (() => {
|
|||||||
// 投稿列の結合で使用
|
// 投稿列の結合で使用
|
||||||
let rowsCnt: number
|
let rowsCnt: number
|
||||||
|
|
||||||
useEffect (() => {
|
const { data, isLoading: loading } = useQuery ({
|
||||||
void (async () => {
|
queryKey: postsKeys.changes ({ ...(id && { id }), page, limit }),
|
||||||
const res = await axios.get (`${ API_BASE_URL }/posts/changes`,
|
queryFn: () => fetchPostChanges ({ ...(id && { id }), page, limit }) })
|
||||||
{ params: { ...(id && { id }), page, limit } })
|
const changes = data?.changes ?? []
|
||||||
const data = toCamel (res.data as any, { deep: true }) as {
|
const totalPages = data ? Math.ceil (data.count / limit) : 0
|
||||||
changes: PostTagChange[]
|
|
||||||
count: number }
|
|
||||||
setChanges (data.changes)
|
|
||||||
setTotalPages (Math.ceil (data.count / limit))
|
|
||||||
}) ()
|
|
||||||
}, [id, page, limit])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea>
|
||||||
@@ -48,57 +38,60 @@ export default (() => {
|
|||||||
|
|
||||||
<PageTitle>
|
<PageTitle>
|
||||||
耕作履歴
|
耕作履歴
|
||||||
{id && <>: 投稿 {<Link to={`/posts/${ id }`}>#{id}</Link>}</>}
|
{id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<table className="table-auto w-full border-collapse">
|
{loading ? 'Loading...' : (
|
||||||
<thead>
|
<>
|
||||||
<tr>
|
<table className="table-auto w-full border-collapse">
|
||||||
<th className="p-2 text-left">投稿</th>
|
<thead>
|
||||||
<th className="p-2 text-left">変更</th>
|
<tr>
|
||||||
<th className="p-2 text-left">日時</th>
|
<th className="p-2 text-left">投稿</th>
|
||||||
</tr>
|
<th className="p-2 text-left">変更</th>
|
||||||
</thead>
|
<th className="p-2 text-left">日時</th>
|
||||||
<tbody>
|
</tr>
|
||||||
{changes.map ((change, i) => {
|
</thead>
|
||||||
let withPost = i === 0 || change.post.id !== changes[i - 1].post.id
|
<tbody>
|
||||||
if (withPost)
|
{changes.map ((change, i) => {
|
||||||
{
|
let withPost = i === 0 || change.post.id !== changes[i - 1].post.id
|
||||||
rowsCnt = 1
|
if (withPost)
|
||||||
for (let j = i + 1;
|
{
|
||||||
(j < changes.length
|
rowsCnt = 1
|
||||||
&& change.post.id === changes[j].post.id);
|
for (let j = i + 1;
|
||||||
++j)
|
(j < changes.length
|
||||||
++rowsCnt
|
&& change.post.id === changes[j].post.id);
|
||||||
}
|
++j)
|
||||||
return (
|
++rowsCnt
|
||||||
<tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}>
|
}
|
||||||
{withPost && (
|
return (
|
||||||
<td className="align-top" rowSpan={rowsCnt}>
|
<tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}>
|
||||||
<Link to={`/posts/${ change.post.id }`}>
|
{withPost && (
|
||||||
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
|
<td className="align-top" rowSpan={rowsCnt}>
|
||||||
alt={change.post.title || change.post.url}
|
<PrefetchLink to={`/posts/${ change.post.id }`}>
|
||||||
title={change.post.title || change.post.url || undefined}
|
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
|
||||||
className="w-40"/>
|
alt={change.post.title || change.post.url}
|
||||||
</Link>
|
title={change.post.title || change.post.url || undefined}
|
||||||
</td>)}
|
className="w-40"/>
|
||||||
<td>
|
</PrefetchLink>
|
||||||
<TagLink tag={change.tag} withWiki={false} withCount={false}/>
|
</td>)}
|
||||||
{`を${ change.changeType === 'add' ? '追加' : '削除' }`}
|
<td>
|
||||||
</td>
|
<TagLink tag={change.tag} withWiki={false} withCount={false}/>
|
||||||
<td>
|
{`を${ change.changeType === 'add' ? '記載' : '消除' }`}
|
||||||
{change.user ? (
|
</td>
|
||||||
<Link to={`/users/${ change.user.id }`}>
|
<td>
|
||||||
{change.user.name}
|
{change.user ? (
|
||||||
</Link>) : 'bot 操作'}
|
<PrefetchLink to={`/users/${ change.user.id }`}>
|
||||||
<br/>
|
{change.user.name}
|
||||||
{change.timestamp}
|
</PrefetchLink>) : 'bot 操作'}
|
||||||
</td>
|
<br/>
|
||||||
</tr>)
|
{change.timestamp}
|
||||||
})}
|
</td>
|
||||||
</tbody>
|
</tr>)
|
||||||
</table>
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<Pagination page={page} totalPages={totalPages}/>
|
<Pagination page={page} totalPages={totalPages}/>
|
||||||
|
</>)}
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}) satisfies FC
|
}) satisfies FC
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
@@ -11,7 +10,8 @@ import PageTitle from '@/components/common/PageTitle'
|
|||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
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 Forbidden from '@/pages/Forbidden'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
@@ -55,9 +55,7 @@ export default (({ user }: Props) => {
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.post (`${ API_BASE_URL }/posts`, formData, { headers: {
|
await apiPost ('/posts', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
toast ({ title: '投稿成功!' })
|
toast ({ title: '投稿成功!' })
|
||||||
navigate ('/posts')
|
navigate ('/posts')
|
||||||
}
|
}
|
||||||
@@ -91,10 +89,7 @@ export default (({ user }: Props) => {
|
|||||||
const fetchTitle = async () => {
|
const fetchTitle = async () => {
|
||||||
setTitle ('')
|
setTitle ('')
|
||||||
setTitleLoading (true)
|
setTitleLoading (true)
|
||||||
const res = await axios.get (`${ API_BASE_URL }/preview/title`, {
|
const data = await apiGet<{ title: string }> ('/preview/title', { params: { url } })
|
||||||
params: { url },
|
|
||||||
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
|
|
||||||
const data = res.data as { title: string }
|
|
||||||
setTitle (data.title || '')
|
setTitle (data.title || '')
|
||||||
setTitleLoading (false)
|
setTitleLoading (false)
|
||||||
}
|
}
|
||||||
@@ -105,11 +100,8 @@ export default (({ user }: Props) => {
|
|||||||
setThumbnailLoading (true)
|
setThumbnailLoading (true)
|
||||||
if (thumbnailPreview)
|
if (thumbnailPreview)
|
||||||
URL.revokeObjectURL (thumbnailPreview)
|
URL.revokeObjectURL (thumbnailPreview)
|
||||||
const res = await axios.get (`${ API_BASE_URL }/preview/thumbnail`, {
|
const data = await apiGet<Blob> ('/preview/thumbnail',
|
||||||
params: { url },
|
{ params: { url }, responseType: 'blob' })
|
||||||
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' },
|
|
||||||
responseType: 'blob' })
|
|
||||||
const data = res.data as Blob
|
|
||||||
const imageURL = URL.createObjectURL (data)
|
const imageURL = URL.createObjectURL (data)
|
||||||
setThumbnailPreview (imageURL)
|
setThumbnailPreview (imageURL)
|
||||||
setThumbnailFile (new File ([data],
|
setThumbnailFile (new File ([data],
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
|
||||||
@@ -8,7 +6,8 @@ import SectionTitle from '@/components/common/SectionTitle'
|
|||||||
import TextArea from '@/components/common/TextArea'
|
import TextArea from '@/components/common/TextArea'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
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'
|
import type { NicoTag, Tag, User } from '@/types'
|
||||||
|
|
||||||
@@ -29,10 +28,8 @@ export default ({ user }: Props) => {
|
|||||||
const loadMore = async (withCursor: boolean) => {
|
const loadMore = async (withCursor: boolean) => {
|
||||||
setLoading (true)
|
setLoading (true)
|
||||||
|
|
||||||
const res = await axios.get (`${ API_BASE_URL }/tags/nico`, {
|
const data = await apiGet<{ tags: NicoTag[]; nextCursor: string }> (
|
||||||
params: { ...(withCursor ? { cursor } : { }) } })
|
'/tags/nico', { params: withCursor ? { cursor } : { } })
|
||||||
const data = toCamel (res.data as any, { deep: true }) as { tags: NicoTag[]
|
|
||||||
nextCursor: string }
|
|
||||||
|
|
||||||
setNicoTags (tags => [...(withCursor ? tags : []), ...data.tags])
|
setNicoTags (tags => [...(withCursor ? tags : []), ...data.tags])
|
||||||
setCursor (data.nextCursor)
|
setCursor (data.nextCursor)
|
||||||
@@ -53,10 +50,8 @@ export default ({ user }: Props) => {
|
|||||||
const formData = new FormData
|
const formData = new FormData
|
||||||
formData.append ('tags', rawTags[id])
|
formData.append ('tags', rawTags[id])
|
||||||
|
|
||||||
const res = await axios.put (`${ API_BASE_URL }/tags/nico/${ id }`, formData, { headers: {
|
const data = await apiPut<Tag[]> (`/tags/nico/${ id }`, formData,
|
||||||
'Content-Type': 'multipart/form-data',
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
|
||||||
const data = toCamel (res.data as any, { deep: true }) as Tag[]
|
|
||||||
setNicoTags (nicoTags => {
|
setNicoTags (nicoTags => {
|
||||||
nicoTags.find (t => t.id === id)!.linkedTags = data
|
nicoTags.find (t => t.id === id)!.linkedTags = data
|
||||||
return [...nicoTags]
|
return [...nicoTags]
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
|
||||||
@@ -10,7 +9,8 @@ import InheritDialogue from '@/components/users/InheritDialogue'
|
|||||||
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
|
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
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'
|
import type { User } from '@/types'
|
||||||
|
|
||||||
@@ -32,10 +32,9 @@ export default ({ user, setUser }: Props) => {
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const res = await axios.put (`${ API_BASE_URL }/users/${ user.id }`, formData, {
|
const data = await apiPut<User> (
|
||||||
headers: { 'Content-Type': 'multipart/form-data',
|
`/users/${ user.id }`, formData,
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
const data = res.data as User
|
|
||||||
setUser (user => ({ ...user, ...data }))
|
setUser (user => ({ ...user, ...data }))
|
||||||
toast ({ title: '設定を更新しました.' })
|
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 { Helmet } from 'react-helmet-async'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
@@ -12,10 +13,11 @@ import MainArea from '@/components/layout/MainArea'
|
|||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||||
import { fetchPosts } from '@/lib/posts'
|
import { fetchPosts } from '@/lib/posts'
|
||||||
|
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { fetchTagByName } from '@/lib/tags'
|
import { fetchTagByName } from '@/lib/tags'
|
||||||
import { fetchWikiPage, fetchWikiPageByTitle } from '@/lib/wiki'
|
import { fetchWikiPage, fetchWikiPageByTitle } from '@/lib/wiki'
|
||||||
|
|
||||||
import type { Post, Tag, WikiPage } from '@/types'
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@@ -25,76 +27,57 @@ export default () => {
|
|||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const navigate = useNavigate ()
|
const navigate = useNavigate ()
|
||||||
|
|
||||||
const defaultTag = { name: title, category: 'general' } as Tag
|
const defaultTag = useMemo (() => ({ name: title, category: 'general' } as Tag), [title])
|
||||||
|
|
||||||
const [posts, setPosts] = useState<Post[]> ([])
|
|
||||||
const [tag, setTag] = useState (defaultTag)
|
|
||||||
const [wikiPage, setWikiPage] = useState<WikiPage | null | undefined> (undefined)
|
|
||||||
|
|
||||||
const query = new URLSearchParams (location.search)
|
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 (() => {
|
useEffect (() => {
|
||||||
if (/^\d+$/.test (title))
|
if (!(wikiPage))
|
||||||
{
|
return
|
||||||
void (async () => {
|
|
||||||
setWikiPage (undefined)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const data = await fetchWikiPage (title)
|
|
||||||
navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true })
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
;
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
return
|
WikiIdBus.set (wikiPage.id)
|
||||||
}
|
|
||||||
|
|
||||||
void (async () => {
|
if (wikiPage.title !== title)
|
||||||
setWikiPage (undefined)
|
navigate (`/wiki/${ encodeURIComponent(wikiPage.title) }`, { replace: true })
|
||||||
try
|
|
||||||
{
|
return () => WikiIdBus.set (null)
|
||||||
const data = await fetchWikiPageByTitle (title, version ? { version } : { })
|
}, [wikiPage, title, navigate])
|
||||||
if (data.title !== title)
|
|
||||||
navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true })
|
useEffect (() => {
|
||||||
setWikiPage (data)
|
if (!(/^\d+$/.test (title)))
|
||||||
WikiIdBus.set (data.id)
|
return
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
setWikiPage (null)
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
setPosts ([])
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const data = await fetchPosts ({ tags: title, match: 'all', limit: 8 })
|
const data = await fetchWikiPage (title, { })
|
||||||
setPosts (data.posts)
|
navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true })
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}) ()
|
}) ()
|
||||||
|
}, [title, navigate])
|
||||||
void (async () => {
|
|
||||||
try
|
|
||||||
{
|
|
||||||
setTag (await fetchTagByName (title))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
setTag (defaultTag)
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
return () => WikiIdBus.set (null)
|
|
||||||
}, [title, location.search])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea>
|
||||||
@@ -104,7 +87,8 @@ export default () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
{(wikiPage && version) && (
|
{(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 ? (
|
{wikiPage.pred ? (
|
||||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.pred }`}>
|
<PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.pred }`}>
|
||||||
< 古
|
< 古
|
||||||
@@ -119,15 +103,13 @@ export default () => {
|
|||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
<PageTitle>
|
<PageTitle>
|
||||||
<TagLink tag={tag}
|
<TagLink tag={tag ?? defaultTag}
|
||||||
withWiki={false}
|
withWiki={false}
|
||||||
withCount={false}
|
withCount={false}
|
||||||
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<div className="prose mx-auto p-4">
|
<div className="prose mx-auto p-4">
|
||||||
{wikiPage === undefined
|
{loading ? 'Loading...' : <WikiBody title={title} body={wikiPage?.body}/>}
|
||||||
? 'Loading...'
|
|
||||||
: <WikiBody title={title} body={wikiPage?.body}/>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(!(version) && posts.length > 0) && (
|
{(!(version) && posts.length > 0) && (
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import PageTitle from '@/components/common/PageTitle'
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
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 { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { WikiPageDiff } from '@/types'
|
import type { WikiPageDiff } from '@/types'
|
||||||
@@ -25,8 +24,7 @@ export default () => {
|
|||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const res = await axios.get (`${ API_BASE_URL }/wiki/${ id }/diff`, { params: { from, to } })
|
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { params: { from, to } }))
|
||||||
setDiff (toCamel (res.data as any, { deep: true }) as WikiPageDiff)
|
|
||||||
}) ()
|
}) ()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from 'axios'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
@@ -7,7 +7,9 @@ import { useParams, useNavigate } from 'react-router-dom'
|
|||||||
|
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
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 Forbidden from '@/pages/Forbidden'
|
||||||
|
|
||||||
import 'react-markdown-editor-lite/lib/index.css'
|
import 'react-markdown-editor-lite/lib/index.css'
|
||||||
@@ -29,6 +31,8 @@ export default (({ user }: Props) => {
|
|||||||
|
|
||||||
const navigate = useNavigate ()
|
const navigate = useNavigate ()
|
||||||
|
|
||||||
|
const qc = useQueryClient ()
|
||||||
|
|
||||||
const [body, setBody] = useState ('')
|
const [body, setBody] = useState ('')
|
||||||
const [loading, setLoading] = useState (true)
|
const [loading, setLoading] = useState (true)
|
||||||
const [title, setTitle] = useState ('')
|
const [title, setTitle] = useState ('')
|
||||||
@@ -40,9 +44,11 @@ export default (({ user }: Props) => {
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.put (`${ API_BASE_URL }/wiki/${ id }`, formData, { headers: {
|
await apiPut (`/wiki/${ id }`, formData,
|
||||||
'Content-Type': 'multipart/form-data',
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
qc.setQueryData (wikiKeys.show (title, { }),
|
||||||
|
(prev: WikiPage) => ({ ...prev, title, body }))
|
||||||
|
qc.invalidateQueries ({ queryKey: wikiKeys.root })
|
||||||
toast ({ title: '投稿成功!' })
|
toast ({ title: '投稿成功!' })
|
||||||
navigate (`/wiki/${ title }`)
|
navigate (`/wiki/${ title }`)
|
||||||
}
|
}
|
||||||
@@ -55,8 +61,7 @@ export default (({ user }: Props) => {
|
|||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
setLoading (true)
|
setLoading (true)
|
||||||
const res = await axios.get (`${ API_BASE_URL }/wiki/${ id }`)
|
const data = await apiGet<WikiPage> (`/wiki/${ id }`)
|
||||||
const data = res.data as WikiPage
|
|
||||||
setTitle (data.title)
|
setTitle (data.title)
|
||||||
setBody (data.body)
|
setBody (data.body)
|
||||||
setLoading (false)
|
setLoading (false)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
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 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'
|
import type { WikiPageChange } from '@/types'
|
||||||
|
|
||||||
@@ -19,9 +19,7 @@ export default () => {
|
|||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const res = await axios.get (`${ API_BASE_URL }/wiki/changes`,
|
setChanges (await apiGet<WikiPageChange[]> ('/wiki/changes', { params: id ? { id } : { } }))
|
||||||
{ params: { ...(id ? { id } : { }) } })
|
|
||||||
setChanges (toCamel (res.data as any, { deep: true }) as WikiPageChange[])
|
|
||||||
}) ()
|
}) ()
|
||||||
}, [location.search])
|
}, [location.search])
|
||||||
|
|
||||||
@@ -44,22 +42,24 @@ export default () => {
|
|||||||
<tr key={change.revisionId}>
|
<tr key={change.revisionId}>
|
||||||
<td>
|
<td>
|
||||||
{change.pred != null && (
|
{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>
|
||||||
<td className="p-2">
|
<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}
|
{change.wikiPage.title}
|
||||||
</Link>
|
</PrefetchLink>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{change.pred == null ? '新規' : '更新'}
|
{change.pred == null ? '新規' : '更新'}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<Link to={`/users/${ change.user.id }`}>
|
<PrefetchLink to={`/users/${ change.user.id }`}>
|
||||||
{change.user.name}
|
{change.user.name}
|
||||||
</Link>
|
</PrefetchLink>
|
||||||
<br/>
|
<br/>
|
||||||
{change.timestamp}
|
{change.timestamp}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
@@ -7,7 +6,8 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|||||||
|
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
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 Forbidden from '@/pages/Forbidden'
|
||||||
|
|
||||||
import 'react-markdown-editor-lite/lib/index.css'
|
import 'react-markdown-editor-lite/lib/index.css'
|
||||||
@@ -39,10 +39,8 @@ export default ({ user }: Props) => {
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const res = await axios.post (`${ API_BASE_URL }/wiki`, formData, { headers: {
|
const data = await apiPost<WikiPage> ('/wiki', formData,
|
||||||
'Content-Type': 'multipart/form-data',
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
|
|
||||||
const data = res.data as WikiPage
|
|
||||||
toast ({ title: '投稿成功!' })
|
toast ({ title: '投稿成功!' })
|
||||||
navigate (`/wiki/${ data.title }`)
|
navigate (`/wiki/${ data.title }`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import axios from 'axios'
|
import { useEffect, useState } from 'react'
|
||||||
import toCamel from 'camelcase-keys'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
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'
|
import type { WikiPage } from '@/types'
|
||||||
|
|
||||||
@@ -17,11 +18,10 @@ export default () => {
|
|||||||
const [results, setResults] = useState<WikiPage[]> ([])
|
const [results, setResults] = useState<WikiPage[]> ([])
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
const res = await axios.get (`${ API_BASE_URL }/wiki/search`, { params: { title } })
|
setResults (await apiGet ('/wiki', { params: { title } }))
|
||||||
setResults (toCamel (res.data as any, { deep: true }) as WikiPage[])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = (ev: React.FormEvent) => {
|
const handleSearch = (ev: FormEvent) => {
|
||||||
ev.preventDefault ()
|
ev.preventDefault ()
|
||||||
search ()
|
search ()
|
||||||
}
|
}
|
||||||
@@ -78,9 +78,9 @@ export default () => {
|
|||||||
{results.map (page => (
|
{results.map (page => (
|
||||||
<tr key={page.id}>
|
<tr key={page.id}>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<Link to={`/wiki/${ encodeURIComponent (page.title) }`}>
|
<PrefetchLink to={`/wiki/${ encodeURIComponent (page.title) }`}>
|
||||||
{page.title}
|
{page.title}
|
||||||
</Link>
|
</PrefetchLink>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 text-gray-100 text-sm">
|
<td className="p-2 text-gray-100 text-sm">
|
||||||
{page.updatedAt}
|
{page.updatedAt}
|
||||||
|
|||||||
Reference in New Issue
Block a user