| @@ -1,16 +1,24 @@ | |||
| class WikiPagesController < ApplicationController | |||
| def index | |||
| end | |||
| def show | |||
| end | |||
| def create | |||
| p params | |||
| wiki_page = WikiPage.find_by(title: params[:title]) | |||
| if wiki_page | |||
| render plain: wiki_page.markdown | |||
| else | |||
| head :not_found | |||
| end | |||
| end | |||
| def update | |||
| end | |||
| return head :unauthorized unless current_user | |||
| def destroy | |||
| title = params[:title] | |||
| wiki_page = WikiPage.find_by(title: title) | |||
| unless wiki_page | |||
| wiki_page = WikiPage.new(title: title, created_user: current_user, updated_user: current_user) | |||
| end | |||
| wiki_page.markdown = params[:markdown] | |||
| wiki_page.save! | |||
| head :ok | |||
| end | |||
| end | |||
| @@ -1,31 +0,0 @@ | |||
| require 'net/http' | |||
| require 'uri' | |||
| class WikiProxyController < ApplicationController | |||
| def edit | |||
| tag = params[:tag] | |||
| uri = "http://localhost:4567/gollum/edit/#{ URI.encode_www_form_component(tag) }" | |||
| begin | |||
| res = fetch_with_redirect(uri) | |||
| render html: res.body.html_safe, content_type: res.content_type | |||
| rescue => e | |||
| render plain: "Wiki システムとの通信に失敗しました:#{ e.message }", status: 502 | |||
| end | |||
| end | |||
| private | |||
| def fetch_with_redirect uri_str, limit = 5 | |||
| raise 'Too many redirects' if limit == 0 | |||
| uri = URI.parse(uri_str) | |||
| res = Net::HTTP.get_response(uri) | |||
| if res.is_a? Net::HTTPRedirection | |||
| location = res['location'] | |||
| fetch_with_redirect(location, limit - 1) | |||
| else | |||
| res | |||
| end | |||
| end | |||
| end | |||
| @@ -1,11 +1,34 @@ | |||
| require 'gollum-lib' | |||
| class WikiPage < ApplicationRecord | |||
| WIKI_PATH = Rails.root.join('wiki').to_s | |||
| belongs_to :tag, optional: true | |||
| belongs_to :created_user, class_name: 'User', foreign_key: 'created_user_id' | |||
| belongs_to :updated_user, class_name: 'User', foreign_key: 'updated_user_id' | |||
| validates :title, presence: true, length: { maximum: 255 }, uniqueness: true | |||
| def gollum_path | |||
| "wiki/#{ title.parameterize }.md" | |||
| def markdown | |||
| wiki = Gollum::Wiki.new(WIKI_PATH) | |||
| page = wiki.page(title) | |||
| page&.raw_data | |||
| end | |||
| def markdown= content | |||
| wiki = Gollum::Wiki.new(WIKI_PATH) | |||
| page = wiki.page(title) | |||
| commit_info = { message: "Update #{ title }", | |||
| name: current_user.id, | |||
| email: 'dummy@example.com' } | |||
| if page | |||
| page.update(content, commit: commit_info) | |||
| else | |||
| wiki.write_page(title, :markdown, content, commit_info) | |||
| end | |||
| end | |||
| end | |||
| @@ -1,62 +1,4 @@ | |||
| Rails.application.routes.draw do | |||
| get "wiki_pages/index" | |||
| get "wiki_pages/show" | |||
| get "wiki_pages/create" | |||
| get "wiki_pages/update" | |||
| get "wiki_pages/destroy" | |||
| get "users/index" | |||
| get "users/show" | |||
| get "users/create" | |||
| get "users/update" | |||
| get "users/destroy" | |||
| get "user_post_views/index" | |||
| get "user_post_views/show" | |||
| get "user_post_views/create" | |||
| get "user_post_views/update" | |||
| get "user_post_views/destroy" | |||
| get "user_ips/index" | |||
| get "user_ips/show" | |||
| get "user_ips/create" | |||
| get "user_ips/update" | |||
| get "user_ips/destroy" | |||
| get "tags/index" | |||
| get "tags/show" | |||
| get "tags/create" | |||
| get "tags/update" | |||
| get "tags/destroy" | |||
| get 'tags/autocomplete', to: 'tags#autocomplete' | |||
| get "tag_aliases/index" | |||
| get "tag_aliases/show" | |||
| get "tag_aliases/create" | |||
| get "tag_aliases/update" | |||
| get "tag_aliases/destroy" | |||
| get "settings/index" | |||
| get "settings/show" | |||
| get "settings/create" | |||
| get "settings/update" | |||
| get "settings/destroy" | |||
| get "post_tags/index" | |||
| get "post_tags/show" | |||
| get "post_tags/create" | |||
| get "post_tags/update" | |||
| get "post_tags/destroy" | |||
| post 'posts/:id/viewed', to: 'posts#viewed' | |||
| delete 'posts/:id/viewed', to: 'posts#unviewed' | |||
| get "nico_tag_relation/index" | |||
| get "nico_tag_relation/show" | |||
| get "nico_tag_relation/create" | |||
| get "nico_tag_relation/update" | |||
| get "nico_tag_relation/destroy" | |||
| get "ip_addresses/index" | |||
| get "ip_addresses/show" | |||
| get "ip_addresses/create" | |||
| get "ip_addresses/update" | |||
| get "ip_addresses/destroy" | |||
| get 'preview/title', to: 'preview#title' | |||
| get 'preview/thumbnail', to: 'preview#thumbnail' | |||
| get '/wiki/edit/:tag', to: 'wiki_proxy#edit' | |||
| root 'home#index' | |||
| resources :posts | |||
| resources :ip_addresses | |||
| resources :nico_tag_relations | |||
| @@ -72,7 +14,14 @@ Rails.application.routes.draw do | |||
| get :me | |||
| end | |||
| end | |||
| resources :wiki_pages | |||
| get 'tags/autocomplete', to: 'tags#autocomplete' | |||
| post 'posts/:id/viewed', to: 'posts#viewed' | |||
| delete 'posts/:id/viewed', to: 'posts#unviewed' | |||
| get 'preview/title', to: 'preview#title' | |||
| get 'preview/thumbnail', to: 'preview#thumbnail' | |||
| get 'wiki/*title', to: 'wiki_pages#show', format: false | |||
| post 'wiki/*title', to: 'wiki_pages#save', format: false | |||
| # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html | |||
| @@ -21,6 +21,7 @@ | |||
| "marked": "^15.0.12", | |||
| "react": "^19.1.0", | |||
| "react-dom": "^19.1.0", | |||
| "react-markdown": "^10.1.0", | |||
| "react-markdown-editor-lite": "^1.3.4", | |||
| "react-router-dom": "^6.30.0", | |||
| "tailwind-merge": "^3.3.0" | |||
| @@ -7,7 +7,7 @@ import TagSidebar from './components/TagSidebar' | |||
| import PostPage from './pages/PostPage' | |||
| import PostNewPage from './pages/PostNewPage' | |||
| import PostDetailPage from './pages/PostDetailPage' | |||
| import WikiEditPage from './pages/WikiEditPage' | |||
| import WikiShowPage from './pages/WikiShowPage' | |||
| import { API_BASE_URL } from './config' | |||
| import axios from 'axios' | |||
| import { Toaster } from '@/components/ui/toaster' | |||
| @@ -81,7 +81,7 @@ const App = () => { | |||
| <Route path="/posts/new" element={<PostNewPage />} /> | |||
| <Route path="/posts/:id" element={<PostDetailPage posts={posts} setPosts={setPosts} />} /> | |||
| <Route path="/tags/:tag" element={<TagPage />} /> | |||
| <Route path="/wiki/:name" element={<WikiEditPage />} /> | |||
| <Route path="/wiki/:name" element={<WikiShowPage />} /> | |||
| </Routes> | |||
| </main> | |||
| </div> | |||
| @@ -0,0 +1,19 @@ | |||
| import MdEditor from 'react-markdown-editor-lite' | |||
| import 'react-markdown-editor-lite/lib/index.css' | |||
| import { marked } from 'marked' | |||
| type Props = { value: string | |||
| onChange: (text: string) => void | |||
| onSave: () => void } | |||
| const WikiEditor = ({ value, onChange, onSave }) => ( | |||
| <div className="wiki-editor"> | |||
| <MdEditor value={value} | |||
| style={{ height: '500px' }} | |||
| renderHTML={text => marked (text)} /> | |||
| <button onClick={onSave}>保存</button> | |||
| </div>) | |||
| export default WikiEditor | |||
| @@ -1,40 +0,0 @@ | |||
| import { useParams } from 'react-router-dom' | |||
| import { useEffect, useState } from 'react' | |||
| import axios from 'axios' | |||
| const WikiEditPage = () => { | |||
| const { name } = useParams<{ name: string }> () | |||
| const [html, setHtml] = useState('') | |||
| const [loading, setLoading] = useState(true) | |||
| useEffect(() => { | |||
| const link = document.createElement('link') | |||
| link.rel = 'stylesheet' | |||
| link.href = '/wiki/custom.css' // Gollum の静的 CSS にアクセスできるようにしとく | |||
| document.head.appendChild(link) | |||
| return () => { | |||
| document.head.removeChild(link) | |||
| } | |||
| }, []) | |||
| useEffect(() => { | |||
| setLoading(true) | |||
| axios | |||
| .get(`/api/wiki/edit/${encodeURIComponent(name)}`, { | |||
| headers: { 'X-Transfer-Code': localStorage.getItem('user_code') || '' } | |||
| }) | |||
| .then((res) => setHtml(res.data)) | |||
| .finally(() => setLoading(false)) | |||
| }, [name]) | |||
| return loading ? ( | |||
| <p>読み込み中...</p> | |||
| ) : ( | |||
| <div dangerouslySetInnerHTML={{ __html: html }} /> | |||
| ) | |||
| } | |||
| export default WikiEditPage | |||
| @@ -0,0 +1,22 @@ | |||
| import axios from 'axios' | |||
| import { useState, useEffect } from 'react' | |||
| import WikiEditor from '@/components/WikiEditor' | |||
| import { API_BASE_URL } from '../config' | |||
| const WikiPage = () => { | |||
| const [text, setText] = useState ('') | |||
| useEffect (() => { | |||
| void (axios.get (`${ API_BASE_URL }/wiki/load`, { params: { page: 'xxx' } }) | |||
| .then (res => setText (res.data.markdown))) | |||
| }, []) | |||
| const save = () => { | |||
| void (axios.post ('/api/wiki/save', { page: 'xxx', | |||
| markdown: text }) | |||
| .catch (err => console.error ('保存失敗', err))) | |||
| } | |||
| return <WikiEditor value={text} onChange={setText} onSave={save} /> | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| import { useParams } from 'react-router-dom' | |||
| import ReactMarkdown from 'react-markdown' | |||
| import { useEffect, useState } from 'react' | |||
| import axios from 'axios' | |||
| import { API_BASE_URL } from '@/config' | |||
| const WikiShowPage = () => { | |||
| const { name } = useParams () | |||
| const [markdown, setMarkdown] = useState ('') | |||
| useEffect (() => { | |||
| void (axios.get (`${ API_BASE_URL }/wiki/${ encodeURIComponent (name) }`) | |||
| .then (res => setMarkdown (res.data)) | |||
| .catch (() => setMarkdown ('# ページが存在しません'))) | |||
| }, [name]) | |||
| return ( | |||
| <div className="prose mx-auto p-4"> | |||
| <ReactMarkdown>{markdown}</ReactMarkdown> | |||
| </div>) | |||
| } | |||
| export default WikiShowPage | |||