#44 完了
This commit is contained in:
@@ -28,6 +28,7 @@ class WikiPagesController < ApplicationController
|
||||
|
||||
def update
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless ['admin', 'member'].include?(current_user.role)
|
||||
|
||||
wiki_page = WikiPage.find(params[:id])
|
||||
return head :not_found unless wiki_page
|
||||
@@ -37,4 +38,15 @@ class WikiPagesController < ApplicationController
|
||||
wiki_page.save!
|
||||
head :ok
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@ Rails.application.routes.draw do
|
||||
get 'preview/title', to: 'preview#title'
|
||||
get 'preview/thumbnail', to: 'preview#thumbnail'
|
||||
get 'wiki/title/:title', to: 'wiki_pages#show_by_title'
|
||||
get 'wiki/search', to: 'wiki_pages#search'
|
||||
get 'wiki/:id', to: 'wiki_pages#show'
|
||||
post 'wiki', to: 'wiki_pages#create'
|
||||
put 'wiki/:id', to: 'wiki_pages#update'
|
||||
|
||||
@@ -7,6 +7,7 @@ import TagDetailSidebar from '@/components/TagDetailSidebar'
|
||||
import PostPage from '@/pages/PostPage'
|
||||
import PostNewPage from '@/pages/PostNewPage'
|
||||
import PostDetailPage from '@/pages/PostDetailPage'
|
||||
import WikiPage from '@/pages/WikiPage'
|
||||
import WikiNewPage from '@/pages/WikiNewPage'
|
||||
import WikiEditPage from '@/pages/WikiEditPage'
|
||||
import WikiDetailPage from '@/pages/WikiDetailPage'
|
||||
@@ -61,6 +62,7 @@ export default () => {
|
||||
<Route path="/posts/new" element={<PostNewPage />} />
|
||||
<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/new" element={<WikiNewPage />} />
|
||||
<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 TagSearch from './TagSearch'
|
||||
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 }
|
||||
|
||||
@@ -15,7 +16,7 @@ type Props = { post: Post | null }
|
||||
export default ({ post }: Props) => {
|
||||
const [tags, setTags] = useState<TagByCategory> ({ })
|
||||
|
||||
const categoryNames: { [key: string]: string } = {
|
||||
const categoryNames: { [key: Category]: string } = {
|
||||
general: '一般',
|
||||
deerjikist: 'ニジラー',
|
||||
nico: 'ニコニコタグ' }
|
||||
@@ -43,7 +44,7 @@ export default ({ post }: Props) => {
|
||||
return (
|
||||
<SidebarComponent>
|
||||
<TagSearch />
|
||||
{['general', 'deerjikist', 'nico'].map (cat => cat in tags && (
|
||||
{CATEGORIES.map ((cat: Category) => cat in tags && (
|
||||
<>
|
||||
<h2>{categoryNames[cat]}</h2>
|
||||
<ul>
|
||||
|
||||
@@ -8,8 +8,8 @@ import type { Tag } from '@/types'
|
||||
|
||||
|
||||
const TagSearch: React.FC = () => {
|
||||
const navigate = useNavigate ()
|
||||
const location = useLocation ()
|
||||
const navigate = useNavigate ()
|
||||
|
||||
const [search, setSearch] = useState ('')
|
||||
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Button } from './ui/button'
|
||||
import clsx from 'clsx'
|
||||
@@ -19,10 +19,15 @@ const enum Menu { None,
|
||||
|
||||
const TopNav: React.FC = ({ user, setUser }: Props) => {
|
||||
const location = useLocation ()
|
||||
const navigate = useNavigate ()
|
||||
|
||||
const [settingsVisible, setSettingsVisible] = useState (false)
|
||||
const [settingsVsbl, setSettingsVsbl] = useState (false)
|
||||
const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None)
|
||||
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
|
||||
title: string
|
||||
@@ -35,7 +40,32 @@ const TopNav: React.FC = ({ user, setUser }: Props) => {
|
||||
{title}
|
||||
</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 (() => {
|
||||
if (location.pathname.startsWith ('/posts'))
|
||||
@@ -61,9 +91,9 @@ const TopNav: React.FC = ({ user, setUser }: Props) => {
|
||||
<MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" />
|
||||
</div>
|
||||
<div className="ml-auto pr-4">
|
||||
<Button onClick={() => setSettingsVisible (true)}>{user?.name || '名もなきニジラー'}</Button>
|
||||
<SettingsDialogue visible={settingsVisible}
|
||||
onVisibleChange={setSettingsVisible}
|
||||
<Button onClick={() => setSettingsVsbl (true)}>{user?.name || '名もなきニジラー'}</Button>
|
||||
<SettingsDialogue visible={settingsVsbl}
|
||||
onVisibleChange={setSettingsVsbl}
|
||||
user={user}
|
||||
setUser={setUser} />
|
||||
</div>
|
||||
@@ -84,8 +114,14 @@ const TopNav: React.FC = ({ user, setUser }: Props) => {
|
||||
case Menu.Wiki:
|
||||
return (
|
||||
<div className={className}>
|
||||
<input className={inputBox}
|
||||
placeholder="Wiki 検索" />
|
||||
<input type="text"
|
||||
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/new" 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)
|
||||
WikiIdBus.set (res.data.id)
|
||||
})
|
||||
.catch (() => setMarkdown (null)))
|
||||
.catch (() => setMarkdown ('')))
|
||||
}, [name])
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<div className="prose mx-auto p-4">
|
||||
{markdown == null ? '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 })。`}
|
||||
</ReactMarkdown>
|
||||
</>)}
|
||||
</div>
|
||||
</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 = {
|
||||
id: number
|
||||
url: string
|
||||
@@ -9,11 +13,13 @@ export type Post = {
|
||||
export type Tag = {
|
||||
id: number
|
||||
name: string
|
||||
category: string
|
||||
category: Category
|
||||
count?: number}
|
||||
|
||||
export type User = {
|
||||
id: number
|
||||
name: string | null
|
||||
inheritanceCode: string
|
||||
role: string }
|
||||
role: UserRole }
|
||||
|
||||
export type UserRole = typeof USER_ROLES[number]
|
||||
|
||||
Reference in New Issue
Block a user