| @@ -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>) | |||||
| } | |||||