| @@ -1,25 +1,10 @@ | |||
| class WikiPagesController < ApplicationController | |||
| def show | |||
| wiki_page = WikiPage.find(params[:id]) | |||
| return head :not_found unless wiki_page | |||
| render json: wiki_page.as_json.merge(body: wiki_page.body) | |||
| render_wiki_page_or_404 WikiPage.find(params[:id]) | |||
| end | |||
| def show_by_title | |||
| title = params[:title] | |||
| version = params[:version].presence | |||
| wiki_page = WikiPage.find_by(title:) | |||
| return head :not_found unless wiki_page | |||
| wiki_page.sha = version | |||
| body = wiki_page.body | |||
| sha = wiki_page.sha | |||
| pred = wiki_page.pred | |||
| succ = wiki_page.succ | |||
| render json: wiki_page.as_json.merge(body:, sha:, pred:, succ:) | |||
| render_wiki_page_or_404 WikiPage.find_by(title: params[:title]) | |||
| end | |||
| def diff | |||
| @@ -117,4 +102,16 @@ class WikiPagesController < ApplicationController | |||
| def wiki | |||
| @wiki ||= Gollum::Wiki.new(WIKI_PATH) | |||
| end | |||
| def render_wiki_page_or_404 wiki_page | |||
| return head :not_found unless wiki_page | |||
| wiki_page.sha = params[:version].presence | |||
| body = wiki_page.body | |||
| sha = wiki_page.sha | |||
| pred = wiki_page.pred | |||
| succ = wiki_page.succ | |||
| render json: wiki_page.as_json.merge(body:, sha:, pred:, succ:) | |||
| end | |||
| end | |||
| @@ -20,6 +20,7 @@ class WikiPage < ApplicationRecord | |||
| idx = vers.find_index { |ver| ver.id == @sha } | |||
| @pred = vers[idx + 1]&.id | |||
| @succ = idx.positive? ? vers[idx - 1].id : nil | |||
| @updated_at = vers[idx].authored_date | |||
| @sha | |||
| end | |||
| @@ -35,6 +36,10 @@ class WikiPage < ApplicationRecord | |||
| @succ | |||
| end | |||
| def updated_at | |||
| @updated_at | |||
| end | |||
| def body | |||
| sha = nil unless @page | |||
| @page&.raw_data&.force_encoding('UTF-8') | |||
| @@ -10,6 +10,7 @@ import PostDetailPage from '@/pages/PostDetailPage' | |||
| import WikiPage from '@/pages/WikiPage' | |||
| import WikiNewPage from '@/pages/WikiNewPage' | |||
| import WikiEditPage from '@/pages/WikiEditPage' | |||
| import WikiDiffPage from '@/pages/WikiDiffPage' | |||
| import WikiDetailPage from '@/pages/WikiDetailPage' | |||
| import WikiHistoryPage from '@/pages/WikiHistoryPage' | |||
| import { API_BASE_URL } from '@/config' | |||
| @@ -64,9 +65,10 @@ export default () => { | |||
| <Route path="/posts/:id" element={<PostDetailPage user={user} />} /> | |||
| <Route path="/tags/:tag" element={<TagPage />} /> | |||
| <Route path="/wiki" element={<WikiPage />} /> | |||
| <Route path="/wiki/:name" element={<WikiDetailPage />} /> | |||
| <Route path="/wiki/:title" element={<WikiDetailPage />} /> | |||
| <Route path="/wiki/new" element={<WikiNewPage />} /> | |||
| <Route path="/wiki/:id/edit" element={<WikiEditPage />} /> | |||
| <Route path="/wiki/:id/diff" element={<WikiDiffPage />} /> | |||
| <Route path="/wiki/changes" element={<WikiHistoryPage />} /> | |||
| </Routes> | |||
| </div> | |||
| @@ -1,45 +1,68 @@ | |||
| import { useEffect, useState } from 'react' | |||
| import { Link, useParams, useNavigate } from 'react-router-dom' | |||
| import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | |||
| 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' | |||
| import type { WikiPage } from '@/types' | |||
| export default () => { | |||
| const { name } = useParams () | |||
| const { title } = useParams () | |||
| const location = useLocation () | |||
| const navigate = useNavigate () | |||
| const [markdown, setMarkdown] = useState<string | null> (null) | |||
| const [wikiPage, setWikiPage] = useState<WikiPage | null | undefined> (undefined) | |||
| const query = new URLSearchParams (location.search) | |||
| const version = query.get ('version') | |||
| useEffect (() => { | |||
| if (/^\d+$/.test (name)) | |||
| if (/^\d+$/.test (title)) | |||
| { | |||
| void (axios.get (`${ API_BASE_URL }/wiki/${ name }`) | |||
| void (axios.get (`${ API_BASE_URL }/wiki/${ title }`) | |||
| .then (res => navigate (`/wiki/${ res.data.title }`, { replace: true }))) | |||
| return | |||
| } | |||
| void (axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (name) }`) | |||
| void (axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (title) }`, version && { params: { version } }) | |||
| .then (res => { | |||
| setMarkdown (res.data.body) | |||
| setWikiPage (res.data) | |||
| WikiIdBus.set (res.data.id) | |||
| }) | |||
| .catch (() => setMarkdown (''))) | |||
| }, [name]) | |||
| .catch (() => setWikiPage (null))) | |||
| }, [title, location.search]) | |||
| return ( | |||
| <MainArea> | |||
| {(wikiPage && version) && ( | |||
| <div className="text-sm flex gap-3 items-center justify-center border border-gray-700 rounded px-2 py-1 mb-4"> | |||
| {wikiPage.pred ? ( | |||
| <Link to={`/wiki/${ title }?version=${ wikiPage.pred }`} | |||
| className="text-blue-400 hover:underline"> | |||
| < 前 | |||
| </Link>) : <>< 前</>} | |||
| <span>{wikiPage.updated_at}</span> | |||
| {wikiPage.succ ? ( | |||
| <Link to={`/wiki/${ title }?version=${ wikiPage.succ }`} | |||
| className="text-blue-400 hover:underline"> | |||
| 後 > | |||
| </Link>) : <>後 ></>} | |||
| </div>)} | |||
| <h1>{title}</h1> | |||
| <div className="prose mx-auto p-4"> | |||
| {markdown == null ? 'Loading...' : ( | |||
| {wikiPage === undefined ? 'Loading...' : ( | |||
| <> | |||
| <ReactMarkdown components={{ a: ( | |||
| ({ 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 })。`} | |||
| {wikiPage?.body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ title })。`} | |||
| </ReactMarkdown> | |||
| </>)} | |||
| </div> | |||
| @@ -0,0 +1,37 @@ | |||
| import { useEffect, useState } from 'react' | |||
| import { Link, useLocation, useParams } from 'react-router-dom' | |||
| import axios from 'axios' | |||
| import MainArea from '@/components/layout/MainArea' | |||
| import { API_BASE_URL } from '@/config' | |||
| import type { WikiPageDiff } from '@/types' | |||
| export default () => { | |||
| const { id } = useParams () | |||
| const location = useLocation () | |||
| const [diff, setDiff] = useState<WikiPageDiff | null> (null) | |||
| const query = new URLSearchParams (location.search) | |||
| const from = query.get ('from') | |||
| const to = query.get ('to') | |||
| useEffect (() => { | |||
| void (axios.get (`${ API_BASE_URL }/wiki/${ id }/diff`, { params: { from, to } }) | |||
| .then (res => setDiff (res.data))) | |||
| }, []) | |||
| return ( | |||
| <MainArea> | |||
| <h1>{diff?.title}</h1> | |||
| <div className="prose mx-auto p-4"> | |||
| {diff ? ( | |||
| diff.diff.map (d => ( | |||
| <span className={d.type === 'added' ? 'bg-green-800' : d.type === 'removed' ? 'bg-red-800' : ''}> | |||
| {d.content == '\n' ? <br /> : d.content} | |||
| </span>))) : 'Loading...'} | |||
| </div> | |||
| </MainArea>) | |||
| } | |||
| @@ -33,10 +33,12 @@ export default () => { | |||
| <tbody> | |||
| {changes.map (change => ( | |||
| <tr key={change.sha}> | |||
| <td>{change.change_type === 'update' && ( | |||
| <Link> | |||
| 差分 | |||
| </Link>)}</td> | |||
| <td> | |||
| {change.change_type === 'update' && ( | |||
| <Link to={`/wiki/${ change.wiki_page.id }/diff?from=${ change.pred }&to=${ change.sha }`}> | |||
| 差分 | |||
| </Link>)} | |||
| </td> | |||
| <td className="p-2"> | |||
| <Link to={`/wiki/${ encodeURIComponent (change.wiki_page.title) }?version=${ change.sha }`} | |||
| className="text-blue-400 hover:underline"> | |||
| @@ -25,13 +25,29 @@ export type User = { | |||
| export type WikiPage = { | |||
| id: number | |||
| title: string | |||
| sha: string | |||
| pred?: string | |||
| succ?: string | |||
| updated_at?: string } | |||
| export type WikiPageChange = { | |||
| sha: string | |||
| pred?: string | |||
| succ?: string | |||
| wiki_page: WikiPage | |||
| user: User | |||
| change_type: string | |||
| timestamp: string } | |||
| export type WikiPageDiff = { | |||
| wiki_page_id: number | |||
| title: string | |||
| older_sha: string | |||
| newer_sha: string | |||
| diff: WikiPageDiffDiff[] } | |||
| export type WikiPageDiffDiff = { | |||
| type: 'context' | 'added' | 'removed' | |||
| content: string } | |||
| export type UserRole = typeof USER_ROLES[number] | |||