#44 完了
This commit is contained in:
@@ -28,6 +28,7 @@ class WikiPagesController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
|
return head :forbidden unless ['admin', 'member'].include?(current_user.role)
|
||||||
|
|
||||||
wiki_page = WikiPage.find(params[:id])
|
wiki_page = WikiPage.find(params[:id])
|
||||||
return head :not_found unless wiki_page
|
return head :not_found unless wiki_page
|
||||||
@@ -37,4 +38,15 @@ class WikiPagesController < ApplicationController
|
|||||||
wiki_page.save!
|
wiki_page.save!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def search
|
||||||
|
q = WikiPage.all
|
||||||
|
|
||||||
|
if params[:title].present?
|
||||||
|
title = params[:title].to_s.strip
|
||||||
|
q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: q.limit(20)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Rails.application.routes.draw do
|
|||||||
get 'preview/title', to: 'preview#title'
|
get 'preview/title', to: 'preview#title'
|
||||||
get 'preview/thumbnail', to: 'preview#thumbnail'
|
get 'preview/thumbnail', to: 'preview#thumbnail'
|
||||||
get 'wiki/title/:title', to: 'wiki_pages#show_by_title'
|
get 'wiki/title/:title', to: 'wiki_pages#show_by_title'
|
||||||
|
get 'wiki/search', to: 'wiki_pages#search'
|
||||||
get 'wiki/:id', to: 'wiki_pages#show'
|
get 'wiki/:id', to: 'wiki_pages#show'
|
||||||
post 'wiki', to: 'wiki_pages#create'
|
post 'wiki', to: 'wiki_pages#create'
|
||||||
put 'wiki/:id', to: 'wiki_pages#update'
|
put 'wiki/:id', to: 'wiki_pages#update'
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import TagDetailSidebar from '@/components/TagDetailSidebar'
|
|||||||
import PostPage from '@/pages/PostPage'
|
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 WikiPage from '@/pages/WikiPage'
|
||||||
import WikiNewPage from '@/pages/WikiNewPage'
|
import WikiNewPage from '@/pages/WikiNewPage'
|
||||||
import WikiEditPage from '@/pages/WikiEditPage'
|
import WikiEditPage from '@/pages/WikiEditPage'
|
||||||
import WikiDetailPage from '@/pages/WikiDetailPage'
|
import WikiDetailPage from '@/pages/WikiDetailPage'
|
||||||
@@ -61,6 +62,7 @@ export default () => {
|
|||||||
<Route path="/posts/new" element={<PostNewPage />} />
|
<Route path="/posts/new" element={<PostNewPage />} />
|
||||||
<Route path="/posts/:id" element={<PostDetailPage user={user} />} />
|
<Route path="/posts/:id" element={<PostDetailPage user={user} />} />
|
||||||
<Route path="/tags/:tag" element={<TagPage />} />
|
<Route path="/tags/:tag" element={<TagPage />} />
|
||||||
|
<Route path="/wiki" element={<WikiPage />} />
|
||||||
<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 />} />
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { Link, useParams } from 'react-router-dom'
|
|||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
import TagSearch from './TagSearch'
|
import TagSearch from './TagSearch'
|
||||||
import SidebarComponent from './layout/SidebarComponent'
|
import SidebarComponent from './layout/SidebarComponent'
|
||||||
|
import { CATEGORIES } from '@/consts'
|
||||||
|
|
||||||
import type { Post, Tag } from '@/types'
|
import type { Category, Post, Tag } from '@/types'
|
||||||
|
|
||||||
type TagByCategory = { [key: string]: Tag[] }
|
type TagByCategory = { [key: Category]: Tag[] }
|
||||||
|
|
||||||
type Props = { post: Post | null }
|
type Props = { post: Post | null }
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ type Props = { post: Post | null }
|
|||||||
export default ({ post }: Props) => {
|
export default ({ post }: Props) => {
|
||||||
const [tags, setTags] = useState<TagByCategory> ({ })
|
const [tags, setTags] = useState<TagByCategory> ({ })
|
||||||
|
|
||||||
const categoryNames: { [key: string]: string } = {
|
const categoryNames: { [key: Category]: string } = {
|
||||||
general: '一般',
|
general: '一般',
|
||||||
deerjikist: 'ニジラー',
|
deerjikist: 'ニジラー',
|
||||||
nico: 'ニコニコタグ' }
|
nico: 'ニコニコタグ' }
|
||||||
@@ -43,7 +44,7 @@ export default ({ post }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<SidebarComponent>
|
<SidebarComponent>
|
||||||
<TagSearch />
|
<TagSearch />
|
||||||
{['general', 'deerjikist', 'nico'].map (cat => cat in tags && (
|
{CATEGORIES.map ((cat: Category) => cat in tags && (
|
||||||
<>
|
<>
|
||||||
<h2>{categoryNames[cat]}</h2>
|
<h2>{categoryNames[cat]}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import type { Tag } from '@/types'
|
|||||||
|
|
||||||
|
|
||||||
const TagSearch: React.FC = () => {
|
const TagSearch: React.FC = () => {
|
||||||
const navigate = useNavigate ()
|
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
|
const navigate = useNavigate ()
|
||||||
|
|
||||||
const [search, setSearch] = useState ('')
|
const [search, setSearch] = useState ('')
|
||||||
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Link, useLocation, useParams } from 'react-router-dom'
|
import { Link, useLocation, useNavigate, 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'
|
||||||
@@ -19,10 +19,15 @@ const enum Menu { None,
|
|||||||
|
|
||||||
const TopNav: React.FC = ({ user, setUser }: Props) => {
|
const TopNav: React.FC = ({ user, setUser }: Props) => {
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
|
const navigate = useNavigate ()
|
||||||
|
|
||||||
const [settingsVisible, setSettingsVisible] = useState (false)
|
const [settingsVsbl, setSettingsVsbl] = useState (false)
|
||||||
const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None)
|
const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None)
|
||||||
const [wikiId, setWikiId] = useState (WikiIdBus.get ())
|
const [wikiId, setWikiId] = useState (WikiIdBus.get ())
|
||||||
|
const [wikiSearch, setWikiSearch] = useState ('')
|
||||||
|
const [activeIndex, setActiveIndex] = useState (-1)
|
||||||
|
const [suggestions, setSuggestions] = useState<WikiPage[]> ([])
|
||||||
|
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
||||||
|
|
||||||
const MyLink = ({ to, title, menu, base }: { to: string
|
const MyLink = ({ to, title, menu, base }: { to: string
|
||||||
title: string
|
title: string
|
||||||
@@ -35,7 +40,32 @@ const TopNav: React.FC = ({ user, setUser }: Props) => {
|
|||||||
{title}
|
{title}
|
||||||
</Link>)
|
</Link>)
|
||||||
|
|
||||||
useEffect (() => WikiIdBus.subscribe (setWikiId), [])
|
const whenWikiSearchChanged = e => {
|
||||||
|
setWikiSearch (e.target.value)
|
||||||
|
|
||||||
|
const q: string = e.target.value.split (' ').at (-1)
|
||||||
|
if (!(q))
|
||||||
|
{
|
||||||
|
setSuggestions ([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// void (axios.get(`${ API_BASE_URL }/`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter' && wikiSearch.length && (!(suggestionsVsbl) || activeIndex < 0))
|
||||||
|
{
|
||||||
|
navigate (`/wiki/${ encodeURIComponent (wikiSearch) }`)
|
||||||
|
setSuggestionsVsbl (false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTagSelect = (tag: Tag) => {
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
WikiIdBus.subscribe (setWikiId)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (location.pathname.startsWith ('/posts'))
|
if (location.pathname.startsWith ('/posts'))
|
||||||
@@ -61,9 +91,9 @@ const TopNav: React.FC = ({ user, setUser }: Props) => {
|
|||||||
<MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" />
|
<MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto pr-4">
|
<div className="ml-auto pr-4">
|
||||||
<Button onClick={() => setSettingsVisible (true)}>{user?.name || '名もなきニジラー'}</Button>
|
<Button onClick={() => setSettingsVsbl (true)}>{user?.name || '名もなきニジラー'}</Button>
|
||||||
<SettingsDialogue visible={settingsVisible}
|
<SettingsDialogue visible={settingsVsbl}
|
||||||
onVisibleChange={setSettingsVisible}
|
onVisibleChange={setSettingsVsbl}
|
||||||
user={user}
|
user={user}
|
||||||
setUser={setUser} />
|
setUser={setUser} />
|
||||||
</div>
|
</div>
|
||||||
@@ -84,8 +114,14 @@ const TopNav: React.FC = ({ user, setUser }: Props) => {
|
|||||||
case Menu.Wiki:
|
case Menu.Wiki:
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<input className={inputBox}
|
<input type="text"
|
||||||
placeholder="Wiki 検索" />
|
className={inputBox}
|
||||||
|
placeholder="Wiki 検索"
|
||||||
|
value={wikiSearch}
|
||||||
|
onChange={whenWikiSearchChanged}
|
||||||
|
onFocus={() => setSuggestionsVsbl (true)}
|
||||||
|
onBlur={() => setSuggestionsVsbl (false)}
|
||||||
|
onKeyDown={handleKeyDown} />
|
||||||
<Link to="/wiki" className={subClass}>検索</Link>
|
<Link to="/wiki" className={subClass}>検索</Link>
|
||||||
<Link to="/wiki/new" className={subClass}>新規</Link>
|
<Link to="/wiki/new" className={subClass}>新規</Link>
|
||||||
<Link to="/wiki/changes" className={subClass}>全体履歴</Link>
|
<Link to="/wiki/changes" className={subClass}>全体履歴</Link>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const CATEGORIES = ['general',
|
||||||
|
'character',
|
||||||
|
'deerjikist',
|
||||||
|
'meme',
|
||||||
|
'material',
|
||||||
|
'nico',
|
||||||
|
'meta'] as const
|
||||||
|
|
||||||
|
export const USER_ROLES = ['admin', 'member', 'guest'] as const
|
||||||
@@ -27,18 +27,21 @@ export default () => {
|
|||||||
setMarkdown (res.data.body)
|
setMarkdown (res.data.body)
|
||||||
WikiIdBus.set (res.data.id)
|
WikiIdBus.set (res.data.id)
|
||||||
})
|
})
|
||||||
.catch (() => setMarkdown (null)))
|
.catch (() => setMarkdown ('')))
|
||||||
}, [name])
|
}, [name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea>
|
||||||
<div className="prose mx-auto p-4">
|
<div className="prose mx-auto p-4">
|
||||||
|
{markdown == null ? 'Loading...' : (
|
||||||
|
<>
|
||||||
<ReactMarkdown components={{ a: (
|
<ReactMarkdown components={{ a: (
|
||||||
({ href, children }) => (['/', '.'].some (e => href?.startsWith (e))
|
({ 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 })。`}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import axios from 'axios'
|
||||||
|
import MainArea from '@/components/layout/MainArea'
|
||||||
|
import { API_BASE_URL } from '@/config'
|
||||||
|
|
||||||
|
import type { Category, WikiPage } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const [title, setTitle] = useState ('')
|
||||||
|
const [text, setText] = useState ('')
|
||||||
|
const [category, setCategory] = useState<Category | null> (null)
|
||||||
|
const [results, setResults] = useState<WikiPage[]> ([])
|
||||||
|
|
||||||
|
const search = () => {
|
||||||
|
void (axios.get (`${ API_BASE_URL }/wiki/search`, { params: { title } })
|
||||||
|
.then (res => setResults (res.data)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault ()
|
||||||
|
search ()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
search ()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainArea>
|
||||||
|
<div className="max-w-xl">
|
||||||
|
<h2 className="text-xl mb-4">Wiki</h2>
|
||||||
|
<form onSubmit={handleSearch} className="space-y-2">
|
||||||
|
{/* タイトル */}
|
||||||
|
<div>
|
||||||
|
<label>タイトル:</label><br />
|
||||||
|
<input type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={e => setTitle (e.target.value)}
|
||||||
|
className="border p-1 w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div>
|
||||||
|
<label>内容:</label><br />
|
||||||
|
<input type="text"
|
||||||
|
value={text}
|
||||||
|
onChange={e => setText (e.target.value)}
|
||||||
|
className="border p-1 w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 検索 */}
|
||||||
|
<div className="py-3">
|
||||||
|
<button type="submit"
|
||||||
|
className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||||
|
検索
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<table className="table-auto w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="p-2 text-left">タイトル</th>
|
||||||
|
<th className="p-2 text-left">最終更新</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{results.map (page => (
|
||||||
|
<tr key={page.id}>
|
||||||
|
<td className="p-2">
|
||||||
|
<Link to={`/wiki/${ encodeURIComponent (page.title) }`}
|
||||||
|
className="text-blue-400 hover:underline">
|
||||||
|
{page.title}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-gray-100 text-sm">
|
||||||
|
{page.updated_at}
|
||||||
|
</td>
|
||||||
|
</tr>))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</MainArea>)
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { CATEGORIES, USER_ROLES } from '@/consts'
|
||||||
|
|
||||||
|
export type Category = typeof CATEGORIES[number]
|
||||||
|
|
||||||
export type Post = {
|
export type Post = {
|
||||||
id: number
|
id: number
|
||||||
url: string
|
url: string
|
||||||
@@ -9,11 +13,13 @@ export type Post = {
|
|||||||
export type Tag = {
|
export type Tag = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
category: string
|
category: Category
|
||||||
count?: number}
|
count?: number}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number
|
id: number
|
||||||
name: string | null
|
name: string | null
|
||||||
inheritanceCode: string
|
inheritanceCode: string
|
||||||
role: string }
|
role: UserRole }
|
||||||
|
|
||||||
|
export type UserRole = typeof USER_ROLES[number]
|
||||||
|
|||||||
Reference in New Issue
Block a user