@@ -1,15 +1,16 @@ | |||||
class WikiPagesController < ApplicationController | class WikiPagesController < ApplicationController | ||||
def show | def show | ||||
wiki_page = WikiPage.find(params[:id]) | wiki_page = WikiPage.find(params[:id]) | ||||
render json: wiki_page.as_json | |||||
return head :not_found unless wiki_page | |||||
render json: wiki_page.as_json.merge(body: wiki_page.body) | |||||
end | end | ||||
def show_by_title | def show_by_title | ||||
wiki_page = WikiPage.find_by(title: params[:title]) | wiki_page = WikiPage.find_by(title: params[:title]) | ||||
body = wiki_page&.body | |||||
return head :not_found unless body | |||||
return head :not_found unless wiki_page | |||||
render plain: body | |||||
render json: wiki_page.as_json.merge(body: wiki_page.body) | |||||
end | end | ||||
def create | def create | ||||
@@ -29,7 +30,7 @@ class WikiPagesController < ApplicationController | |||||
return head :unauthorized unless current_user | return head :unauthorized unless current_user | ||||
wiki_page = WikiPage.find(params[:id]) | wiki_page = WikiPage.find(params[:id]) | ||||
return head :not_found unless wiki_pages | |||||
return head :not_found unless wiki_page | |||||
wiki_page.updated_user = current_user | wiki_page.updated_user = current_user | ||||
wiki_page.set_body params[:body], user: current_user | wiki_page.set_body params[:body], user: current_user | ||||
@@ -21,7 +21,7 @@ class WikiPage < ApplicationRecord | |||||
email: 'dummy@example.com' } | email: 'dummy@example.com' } | ||||
if page | if page | ||||
page.update(content, commit: commit_info) | |||||
wiki.update_page(page, id.to_s, :markdown, content, commit_info) | |||||
else | else | ||||
wiki.write_page(id.to_s, :markdown, content, commit_info) | wiki.write_page(id.to_s, :markdown, content, commit_info) | ||||
end | end | ||||
@@ -8,6 +8,7 @@ import PostPage from '@/pages/PostPage' | |||||
import PostNewPage from '@/pages/PostNewPage' | import PostNewPage from '@/pages/PostNewPage' | ||||
import PostDetailPage from '@/pages/PostDetailPage' | import PostDetailPage from '@/pages/PostDetailPage' | ||||
import WikiNewPage from '@/pages/WikiNewPage' | import WikiNewPage from '@/pages/WikiNewPage' | ||||
import WikiEditPage from '@/pages/WikiEditPage' | |||||
import WikiDetailPage from '@/pages/WikiDetailPage' | import WikiDetailPage from '@/pages/WikiDetailPage' | ||||
import { API_BASE_URL } from '@/config' | import { API_BASE_URL } from '@/config' | ||||
import axios from 'axios' | import axios from 'axios' | ||||
@@ -62,7 +63,7 @@ export default () => { | |||||
<Route path="/tags/:tag" element={<TagPage />} /> | <Route path="/tags/:tag" element={<TagPage />} /> | ||||
<Route path="/wiki/:name" element={<WikiDetailPage />} /> | <Route path="/wiki/:name" element={<WikiDetailPage />} /> | ||||
<Route path="/wiki/new" element={<WikiNewPage />} /> | <Route path="/wiki/new" element={<WikiNewPage />} /> | ||||
{/* <Route path="/wiki/:id/edit" element={<WikiEditPage />} /> */} | |||||
<Route path="/wiki/:id/edit" element={<WikiEditPage />} /> | |||||
</Routes> | </Routes> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -3,6 +3,7 @@ import { Link, useLocation, useParams } from 'react-router-dom' | |||||
import SettingsDialogue from './SettingsDialogue' | import SettingsDialogue from './SettingsDialogue' | ||||
import { Button } from './ui/button' | import { Button } from './ui/button' | ||||
import clsx from 'clsx' | import clsx from 'clsx' | ||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' | |||||
import type { User } from '@/types' | import type { User } from '@/types' | ||||
@@ -21,6 +22,7 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { | |||||
const [settingsVisible, setSettingsVisible] = useState (false) | const [settingsVisible, setSettingsVisible] = useState (false) | ||||
const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None) | const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None) | ||||
const [wikiId, setWikiId] = useState (WikiIdBus.get ()) | |||||
const MyLink = ({ to, title, menu, base }: { to: string | const MyLink = ({ to, title, menu, base }: { to: string | ||||
title: string | title: string | ||||
@@ -33,6 +35,8 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { | |||||
{title} | {title} | ||||
</Link>) | </Link>) | ||||
useEffect (() => WikiIdBus.subscribe (setWikiId), []) | |||||
useEffect (() => { | useEffect (() => { | ||||
if (location.pathname.startsWith ('/posts')) | if (location.pathname.startsWith ('/posts')) | ||||
setSelectedMenu (Menu.Post) | setSelectedMenu (Menu.Post) | ||||
@@ -90,8 +94,8 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { | |||||
<> | <> | ||||
<Separator /> | <Separator /> | ||||
<Link to={`/posts?tags=${ location.pathname.split ('/')[2] }`} className={subClass}>投稿</Link> | <Link to={`/posts?tags=${ location.pathname.split ('/')[2] }`} className={subClass}>投稿</Link> | ||||
<Link to={`/wiki/${ location.pathname.split ('/')[2] }/history`} className={subClass}>履歴</Link> | |||||
<Link to={`/wiki/${ location.pathname.split ('/')[2] }/edit`} className={subClass}>編輯</Link> | |||||
<Link to={`/wiki/${ wikiId || location.pathname.split ('/')[2] }/history`} className={subClass}>履歴</Link> | |||||
<Link to={`/wiki/${ wikiId || location.pathname.split ('/')[2] }/edit`} className={subClass}>編輯</Link> | |||||
</>} | </>} | ||||
</div>) | </div>) | ||||
} | } | ||||
@@ -0,0 +1,35 @@ | |||||
export class EventBus<T> | |||||
{ | |||||
private value: T | |||||
private subscribers: ((val: T) => void)[] = [] | |||||
constructor ( | |||||
initialValue: T) | |||||
{ | |||||
this.value = initialValue | |||||
} | |||||
get () | |||||
: T | |||||
{ | |||||
return this.value | |||||
} | |||||
set ( | |||||
val: T) | |||||
: void | |||||
{ | |||||
this.value = val | |||||
this.subscribers.forEach (f => f (val)) | |||||
} | |||||
subscribe ( | |||||
func: (val: T) => void) | |||||
: () => void | |||||
{ | |||||
this.subscribers.push (func) | |||||
return () => { | |||||
this.subscribers = this.subscribers.filter (sub => sub !== func) | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
import { EventBus } from './EventBus' | |||||
export const WikiIdBus = new EventBus<number | null> (null) |
@@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown' | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import { API_BASE_URL } from '@/config' | import { API_BASE_URL } from '@/config' | ||||
import MainArea from '@/components/layout/MainArea' | import MainArea from '@/components/layout/MainArea' | ||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' | |||||
export default () => { | export default () => { | ||||
@@ -22,7 +23,10 @@ export default () => { | |||||
} | } | ||||
void (axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (name) }`) | void (axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (name) }`) | ||||
.then (res => setMarkdown (res.data)) | |||||
.then (res => { | |||||
setMarkdown (res.data.body) | |||||
WikiIdBus.set (res.data.id) | |||||
}) | |||||
.catch (() => setMarkdown (null))) | .catch (() => setMarkdown (null))) | ||||
}, [name]) | }, [name]) | ||||
@@ -30,7 +34,7 @@ export default () => { | |||||
<MainArea> | <MainArea> | ||||
<div className="prose mx-auto p-4"> | <div className="prose mx-auto p-4"> | ||||
<ReactMarkdown components={{ a: ( | <ReactMarkdown components={{ a: ( | ||||
({ href, children }) => (href?.startsWith ('/') | |||||
({ href, children }) => (['/', '.'].some (e => href?.startsWith (e)) | |||||
? <Link to={href!}>{children}</Link> | ? <Link to={href!}>{children}</Link> | ||||
: <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | : <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | ||||
{markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} | {markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} | ||||
@@ -0,0 +1,85 @@ | |||||
import React, { useEffect, useState, useRef } from 'react' | |||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | |||||
import axios from 'axios' | |||||
import { API_BASE_URL, SITE_TITLE } from '@/config' | |||||
import NicoViewer from '@/components/NicoViewer' | |||||
import { Button } from '@/components/ui/button' | |||||
import { toast } from '@/components/ui/use-toast' | |||||
import { cn } from '@/lib/utils' | |||||
import MarkdownIt from 'markdown-it' | |||||
import MdEditor from 'react-markdown-editor-lite' | |||||
import 'react-markdown-editor-lite/lib/index.css' | |||||
import MainArea from '@/components/layout/MainArea' | |||||
import type { Tag } from '@/types' | |||||
const mdParser = new MarkdownIt | |||||
export default () => { | |||||
const { id } = useParams () | |||||
const location = useLocation () | |||||
const navigate = useNavigate () | |||||
const [title, setTitle] = useState ('') | |||||
const [body, setBody] = useState ('') | |||||
const handleSubmit = () => { | |||||
const formData = new FormData () | |||||
formData.append ('title', title) | |||||
formData.append ('body', body) | |||||
void (axios.put (`${ API_BASE_URL }/wiki/${ id }`, formData, { headers: { | |||||
'Content-Type': 'multipart/form-data', | |||||
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) | |||||
.then (res => { | |||||
toast ({ title: '投稿成功!' }) | |||||
navigate (`/wiki/${ title }`) | |||||
}) | |||||
.catch (e => toast ({ title: '投稿失敗', | |||||
description: '入力を確認してください。' }))) | |||||
} | |||||
useEffect (() => { | |||||
void (axios.get (`${ API_BASE_URL }/wiki/${ id }`) | |||||
.then (res => { | |||||
setTitle (res.data.title) | |||||
setBody (res.data.body) | |||||
})) | |||||
document.title = `Wiki ページを編輯 | ${ SITE_TITLE }` | |||||
}, [id]) | |||||
return ( | |||||
<MainArea> | |||||
<div className="max-w-xl mx-auto p-4 space-y-4"> | |||||
<h1 className="text-2xl font-bold mb-2">Wiki ページを編輯</h1> | |||||
{/* タイトル */} | |||||
{/* TODO: タグ補完 */} | |||||
<div> | |||||
<label className="block font-semibold mb-1">タイトル</label> | |||||
<input type="text" | |||||
value={title} | |||||
onChange={e => setTitle (e.target.value)} | |||||
className="w-full border p-2 rounded" /> | |||||
</div> | |||||
{/* 本文 */} | |||||
<div> | |||||
<label className="block font-semibold mb-1">本文</label> | |||||
<MdEditor value={body} | |||||
style={{ height: '500px' }} | |||||
renderHTML={text => mdParser.render (text)} | |||||
onChange={({ text }) => setBody (text)} /> | |||||
</div> | |||||
{/* 送信 */} | |||||
<button onClick={handleSubmit} | |||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | |||||
追加 | |||||
</button> | |||||
</div> | |||||
</MainArea>) | |||||
} |