Browse Source

#44 完了

#23
みてるぞ 1 month ago
parent
commit
ceeefa9b7c
10 changed files with 180 additions and 22 deletions
  1. +12
    -0
      backend/app/controllers/wiki_pages_controller.rb
  2. +1
    -0
      backend/config/routes.rb
  3. +2
    -0
      frontend/src/App.tsx
  4. +5
    -4
      frontend/src/components/TagDetailSidebar.tsx
  5. +1
    -1
      frontend/src/components/TagSearch.tsx
  6. +44
    -8
      frontend/src/components/TopNav.tsx
  7. +9
    -0
      frontend/src/consts.ts
  8. +10
    -7
      frontend/src/pages/WikiDetailPage.tsx
  9. +88
    -0
      frontend/src/pages/WikiPage.tsx
  10. +8
    -2
      frontend/src/types.ts

+ 12
- 0
backend/app/controllers/wiki_pages_controller.rb View File

@@ -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

+ 1
- 0
backend/config/routes.rb View File

@@ -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'


+ 2
- 0
frontend/src/App.tsx View File

@@ -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 />} />


+ 5
- 4
frontend/src/components/TagDetailSidebar.tsx View File

@@ -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>


+ 1
- 1
frontend/src/components/TagSearch.tsx View File

@@ -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[]> ([])


+ 44
- 8
frontend/src/components/TopNav.tsx View File

@@ -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>


+ 9
- 0
frontend/src/consts.ts View File

@@ -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

+ 10
- 7
frontend/src/pages/WikiDetailPage.tsx View File

@@ -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">
<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>
{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>)
}

+ 88
- 0
frontend/src/pages/WikiPage.tsx View File

@@ -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>)
}

+ 8
- 2
frontend/src/types.ts View File

@@ -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]

Loading…
Cancel
Save