| @@ -1,15 +1,16 @@ | |||
| class WikiPagesController < ApplicationController | |||
| def show | |||
| 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 | |||
| def show_by_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 | |||
| def create | |||
| @@ -29,7 +30,7 @@ class WikiPagesController < ApplicationController | |||
| return head :unauthorized unless current_user | |||
| 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.set_body params[:body], user: current_user | |||
| @@ -21,7 +21,7 @@ class WikiPage < ApplicationRecord | |||
| email: 'dummy@example.com' } | |||
| if page | |||
| page.update(content, commit: commit_info) | |||
| wiki.update_page(page, id.to_s, :markdown, content, commit_info) | |||
| else | |||
| wiki.write_page(id.to_s, :markdown, content, commit_info) | |||
| end | |||
| @@ -8,6 +8,7 @@ import PostPage from '@/pages/PostPage' | |||
| import PostNewPage from '@/pages/PostNewPage' | |||
| import PostDetailPage from '@/pages/PostDetailPage' | |||
| import WikiNewPage from '@/pages/WikiNewPage' | |||
| import WikiEditPage from '@/pages/WikiEditPage' | |||
| import WikiDetailPage from '@/pages/WikiDetailPage' | |||
| import { API_BASE_URL } from '@/config' | |||
| import axios from 'axios' | |||
| @@ -62,7 +63,7 @@ export default () => { | |||
| <Route path="/tags/:tag" element={<TagPage />} /> | |||
| <Route path="/wiki/:name" element={<WikiDetailPage />} /> | |||
| <Route path="/wiki/new" element={<WikiNewPage />} /> | |||
| {/* <Route path="/wiki/:id/edit" element={<WikiEditPage />} /> */} | |||
| <Route path="/wiki/:id/edit" element={<WikiEditPage />} /> | |||
| </Routes> | |||
| </div> | |||
| </div> | |||
| @@ -3,6 +3,7 @@ import { Link, useLocation, useParams } from 'react-router-dom' | |||
| import SettingsDialogue from './SettingsDialogue' | |||
| import { Button } from './ui/button' | |||
| import clsx from 'clsx' | |||
| import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' | |||
| import type { User } from '@/types' | |||
| @@ -21,6 +22,7 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { | |||
| const [settingsVisible, setSettingsVisible] = useState (false) | |||
| const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None) | |||
| const [wikiId, setWikiId] = useState (WikiIdBus.get ()) | |||
| const MyLink = ({ to, title, menu, base }: { to: string | |||
| title: string | |||
| @@ -33,6 +35,8 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { | |||
| {title} | |||
| </Link>) | |||
| useEffect (() => WikiIdBus.subscribe (setWikiId), []) | |||
| useEffect (() => { | |||
| if (location.pathname.startsWith ('/posts')) | |||
| setSelectedMenu (Menu.Post) | |||
| @@ -90,8 +94,8 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { | |||
| <> | |||
| <Separator /> | |||
| <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>) | |||
| } | |||
| @@ -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 { API_BASE_URL } from '@/config' | |||
| import MainArea from '@/components/layout/MainArea' | |||
| import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' | |||
| export default () => { | |||
| @@ -22,7 +23,10 @@ export default () => { | |||
| } | |||
| 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))) | |||
| }, [name]) | |||
| @@ -30,7 +34,7 @@ export default () => { | |||
| <MainArea> | |||
| <div className="prose mx-auto p-4"> | |||
| <ReactMarkdown components={{ a: ( | |||
| ({ href, children }) => (href?.startsWith ('/') | |||
| ({ href, children }) => (['/', '.'].some (e => href?.startsWith (e)) | |||
| ? <Link to={href!}>{children}</Link> | |||
| : <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | |||
| {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>) | |||
| } | |||