This commit is contained in:
2026-03-26 00:01:29 +09:00
parent 04b01bf1c6
commit ef6219dcb1
6 changed files with 103 additions and 38 deletions
@@ -126,7 +126,7 @@ class WikiPagesController < ApplicationController
message:, message:,
base_revision_id:) base_revision_id:)
render json: WikiPageRepr.base(page).merge(body:), status: :created render json: WikiPageRepr.base(page).merge(body:)
end end
def search def search
+3 -29
View File
@@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown'
import remarkGFM from 'remark-gfm' import remarkGFM from 'remark-gfm'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import WikiMarkdown from '@/components/WikiMarkdown'
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 { wikiKeys } from '@/lib/queryKeys' import { wikiKeys } from '@/lib/queryKeys'
@@ -16,33 +17,6 @@ import type { Components } from 'react-markdown'
type Props = { title: string type Props = { title: string
body?: string } body?: string }
const mdComponents = { h1: ({ children }) => <SectionTitle>{children}</SectionTitle>,
h2: ({ children }) => <SubsectionTitle>{children}</SubsectionTitle>,
ol: ({ children }) => <ol className="list-decimal pl-6">{children}</ol>,
ul: ({ children }) => <ul className="list-disc pl-6">{children}</ul>,
a: (({ href, children }) => (
['/', '.'].some (e => href?.startsWith (e))
? <PrefetchLink to={href!}>{children}</PrefetchLink>
: (
<a href={href}
target="_blank"
rel="noopener noreferrer">
{children}
</a>))) } as const satisfies Components
export default (({ title, body }: Props) =>
export default (({ title, body }: Props) => { <WikiMarkdown title={title} body={body ?? ''}/>) satisfies FC<Props>
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 (
() => [() => remarkWikiAutoLink (pageNames), remarkGFM], [pageNames])
return (
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
{body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`}
</ReactMarkdown>)
}) satisfies FC<Props>
+21 -3
View File
@@ -12,10 +12,12 @@ type Props = {
title: string title: string
body: string body: string
onSubmit: (title: string, body: string) => void onSubmit: (title: string, body: string) => void
forEdit?: boolean } id?: number | null }
export default (({ title: initTitle, body: initBody, onSubmit, forEdit }: Props) => { export default (({ title: initTitle, body: initBody, onSubmit, id }: Props) => {
const forEdit = id != null
const [title, setTitle] = useState<string> (initTitle) const [title, setTitle] = useState<string> (initTitle)
const [body, setBody] = useState<string> (initBody) const [body, setBody] = useState<string> (initBody)
@@ -24,6 +26,21 @@ export default (({ title: initTitle, body: initBody, onSubmit, forEdit }: Props)
setBody (initBody) setBody (initBody)
}, [initTitle, initBody]) }, [initTitle, initBody])
const handleImageUpload = async (file: File) => {
if (!(forEdit))
throw new Error ('画像は Wiki 作成前に追加することができません.')
const formData = new FormData
formData.append ('file', file)
const asset = await apiPost<WikiAsset> (
`/wiki/${ id }/assets`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
return `{{img:${ asset.no }}}`
}
return ( return (
<> <>
{/* タイトル */} {/* タイトル */}
@@ -44,7 +61,8 @@ export default (({ title: initTitle, body: initBody, onSubmit, forEdit }: Props)
value={body} value={body}
style={{ height: '500px' }} style={{ height: '500px' }}
renderHTML={text => mdParser.render (text)} renderHTML={text => mdParser.render (text)}
onChange={({ text }) => setBody (text)}/> onChange={({ text }) => setBody (text)}
onImageUpload={handleImageUpload}/>
</div> </div>
{/* 送信 */} {/* 送信 */}
+77
View File
@@ -0,0 +1,77 @@
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGFM from 'remark-gfm'
import PrefetchLink from '@/components/PrefetchLink'
import SectionTitle from '@/components/common/SectionTitle'
import SubsectionTitle from '@/components/common/SubsectionTitle'
import { wikiKeys } from '@/lib/queryKeys'
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
import { fetchWikiPages } from '@/lib/wiki'
import type { FC } from 'react'
import type { Components } from 'react-markdown'
type Props = {
title?: string
body: string
preview?: boolean }
const makeComponents = (preview = false) => (
{ h1: ({ children }) => <SectionTitle>{children}</SectionTitle>,
h2: ({ children }) => <SubsectionTitle>{children}</SubsectionTitle>,
ol: ({ children }) => <ol className="list-decimal pl-6">{children}</ol>,
ul: ({ children }) => <ul className="list-disc pl-6">{children}</ul>,
a: ({ href, children }) => {
if (!(href))
return <>{children}</>
if (!(preview) && ['/', '.'].some (e => href.startsWith (e)))
return <PrefetchLink to={href}>{children}</PrefetchLink>
const ext = /^(?:https?:)?\/\//.test (href)
return (
<a href={href}
target={ext ? '_blank' : undefined}
rel={ext ? 'noopener noreferrer' : undefined}>
{children}
</a>)
},
img: (({ src, alt }) => (
<img src={src ?? ''}
alt={alt ?? ''}
className="max-w-full h-auto rounded"/>)),
} as const satisfies Components)
export default (({ title, body, preview = false }: Props) => {
const { data } = useQuery ({
queryKey: wikiKeys.index ({ }),
queryFn: () => fetchWikiPages ({ }) })
const pageNames = useMemo (
() => (data ?? []).map ((page) => page.title).sort ((a, b) => b.length - a.length),
[data])
const remarkPlugins = useMemo (
() => [() => remarkWikiAutoLink (pageNames), remarkGFM],
[pageNames])
const components = useMemo (
() => makeComponents (preview),
[preview])
return (
<ReactMarkdown
components={components}
remarkPlugins={remarkPlugins}>
{body
|| (title
? ('このページは存在しません。'
+`[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`)
: '')}
</ReactMarkdown>)
}) satisfies FC<Props>
+1 -3
View File
@@ -43,8 +43,6 @@ export default (({ user }: Props) => {
{ {
await apiPut (`/wiki/${ id }`, formData, await apiPut (`/wiki/${ id }`, formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }) { headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (title, { }),
(prev: WikiPage) => ({ ...prev, title, body }))
qc.invalidateQueries ({ queryKey: wikiKeys.root }) qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ title: '投稿成功!' }) toast ({ title: '投稿成功!' })
navigate (`/wiki/${ title }`) navigate (`/wiki/${ title }`)
@@ -78,7 +76,7 @@ export default (({ user }: Props) => {
title={title} title={title}
body={body} body={body}
onSubmit={handleSubmit} onSubmit={handleSubmit}
forEdit/>)} id={Number (id)}/>)}
</div> </div>
</MainArea>) </MainArea>)
}) satisfies FC<Props> }) satisfies FC<Props>
-2
View File
@@ -38,8 +38,6 @@ export default ({ user }: Props) => {
{ {
const data = await apiPost<WikiPage> ('/wiki', formData, const data = await apiPost<WikiPage> ('/wiki', formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }) { headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (data.title, { }),
(prev: WikiPage) => ({ ...prev, title: data.title, body: data.body }))
qc.invalidateQueries ({ queryKey: wikiKeys.root }) qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ title: '投稿成功!' }) toast ({ title: '投稿成功!' })
navigate (`/wiki/${ data.title }`) navigate (`/wiki/${ data.title }`)