Browse Source

#47

feature/047
みてるぞ 8 hours ago
parent
commit
ef6219dcb1
6 changed files with 103 additions and 38 deletions
  1. +1
    -1
      backend/app/controllers/wiki_pages_controller.rb
  2. +3
    -29
      frontend/src/components/WikiBody.tsx
  3. +21
    -3
      frontend/src/components/WikiEditForm.tsx
  4. +77
    -0
      frontend/src/components/WikiMarkdown.tsx
  5. +1
    -3
      frontend/src/pages/wiki/WikiEditPage.tsx
  6. +0
    -2
      frontend/src/pages/wiki/WikiNewPage.tsx

+ 1
- 1
backend/app/controllers/wiki_pages_controller.rb View File

@@ -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
frontend/src/components/WikiBody.tsx 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) => {
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>
export default (({ title, body }: Props) =>
<WikiMarkdown title={title} body={body ?? ''}/>) satisfies FC<Props>

+ 21
- 3
frontend/src/components/WikiEditForm.tsx 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
- 0
frontend/src/components/WikiMarkdown.tsx 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
frontend/src/pages/wiki/WikiEditPage.tsx 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>

+ 0
- 2
frontend/src/pages/wiki/WikiNewPage.tsx 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 }`)


Loading…
Cancel
Save