diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index 4a25deb..4c1f364 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -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 diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 2495357..91f8c56 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -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' diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7482327..4d678a0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 5cea00d..c14bbb0 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -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 ({ }) - const categoryNames: { [key: string]: string } = { + const categoryNames: { [key: Category]: string } = { general: '一般', deerjikist: 'ニジラー', nico: 'ニコニコタグ' } @@ -43,7 +44,7 @@ export default ({ post }: Props) => { return ( - {['general', 'deerjikist', 'nico'].map (cat => cat in tags && ( + {CATEGORIES.map ((cat: Category) => cat in tags && ( <>

{categoryNames[cat]}

    diff --git a/frontend/src/components/TagSearch.tsx b/frontend/src/components/TagSearch.tsx index f060a7b..9989225 100644 --- a/frontend/src/components/TagSearch.tsx +++ b/frontend/src/components/TagSearch.tsx @@ -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 ([]) diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index a8ddca7..27b0b82 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -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.None) const [wikiId, setWikiId] = useState (WikiIdBus.get ()) + const [wikiSearch, setWikiSearch] = useState ('') + const [activeIndex, setActiveIndex] = useState (-1) + const [suggestions, setSuggestions] = useState ([]) + 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} ) - 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) => { + 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) => {
    - - setSettingsVsbl (true)}>{user?.name || '名もなきニジラー'} +
    @@ -84,8 +114,14 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { case Menu.Wiki: return (
    - + setSuggestionsVsbl (true)} + onBlur={() => setSuggestionsVsbl (false)} + onKeyDown={handleKeyDown} /> 検索 新規 全体履歴 diff --git a/frontend/src/consts.ts b/frontend/src/consts.ts new file mode 100644 index 0000000..4b3e57b --- /dev/null +++ b/frontend/src/consts.ts @@ -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 diff --git a/frontend/src/pages/WikiDetailPage.tsx b/frontend/src/pages/WikiDetailPage.tsx index a07e595..9a34762 100644 --- a/frontend/src/pages/WikiDetailPage.tsx +++ b/frontend/src/pages/WikiDetailPage.tsx @@ -27,18 +27,21 @@ export default () => { setMarkdown (res.data.body) WikiIdBus.set (res.data.id) }) - .catch (() => setMarkdown (null))) + .catch (() => setMarkdown (''))) }, [name]) return (
    - (['/', '.'].some (e => href?.startsWith (e)) - ? {children} - : {children})) }}> - {markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} - + {markdown == null ? 'Loading...' : ( + <> + (['/', '.'].some (e => href?.startsWith (e)) + ? {children} + : {children})) }}> + {markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} + + )}
    ) } diff --git a/frontend/src/pages/WikiPage.tsx b/frontend/src/pages/WikiPage.tsx new file mode 100644 index 0000000..f76af57 --- /dev/null +++ b/frontend/src/pages/WikiPage.tsx @@ -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 (null) + const [results, setResults] = useState ([]) + + 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 ( + +
    +

    Wiki

    +
    + {/* タイトル */} +
    +
    + setTitle (e.target.value)} + className="border p-1 w-full" /> +
    + + {/* 内容 */} +
    +
    + setText (e.target.value)} + className="border p-1 w-full" /> +
    + + {/* 検索 */} +
    + +
    +
    +
    + +
    + + + + + + + + + {results.map (page => ( + + + + ))} + +
    タイトル最終更新
    + + {page.title} + + + {page.updated_at} +
    +
    +
    ) +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 22c3069..dc685b7 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -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]