| @@ -13,6 +13,22 @@ class WikiPagesController < ApplicationController | |||
| render_wiki_page_or_404 WikiPage.find_by(title: params[:title]) | |||
| end | |||
| def exists | |||
| if WikiPage.exists?(params[:id]) | |||
| head :no_content | |||
| else | |||
| head :not_found | |||
| end | |||
| end | |||
| def exists_by_title | |||
| if WikiPage.exists?(title: params[:title]) | |||
| head :no_content | |||
| else | |||
| head :not_found | |||
| end | |||
| end | |||
| def diff | |||
| id = params[:id] | |||
| from = params[:from] | |||
| @@ -6,6 +6,7 @@ class NicoTagRelation < ApplicationRecord | |||
| validates :tag_id, presence: true | |||
| validate :nico_tag_must_be_nico | |||
| validate :tag_mustnt_be_nico | |||
| private | |||
| @@ -1,46 +1,60 @@ | |||
| Rails.application.routes.draw do | |||
| get 'tags/nico', to: 'nico_tags#index' | |||
| put 'tags/nico/:id', to: 'nico_tags#update' | |||
| get 'tags/autocomplete', to: 'tags#autocomplete' | |||
| get 'tags/name/:name', to: 'tags#show_by_name' | |||
| get 'posts/random', to: 'posts#random' | |||
| get 'posts/changes', to: 'posts#changes' | |||
| 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/:title', to: 'wiki_pages#show_by_title' | |||
| get 'wiki/search', to: 'wiki_pages#search' | |||
| get 'wiki/changes', to: 'wiki_pages#changes' | |||
| get 'wiki/:id/diff', to: 'wiki_pages#diff' | |||
| get 'wiki/:id', to: 'wiki_pages#show' | |||
| get 'wiki', to: 'wiki_pages#index' | |||
| post 'wiki', to: 'wiki_pages#create' | |||
| put 'wiki/:id', to: 'wiki_pages#update' | |||
| post 'users/code/renew', to: 'users#renew' | |||
| resources :posts | |||
| resources :ip_addresses | |||
| resources :nico_tag_relations | |||
| resources :post_tags | |||
| resources :settings | |||
| resources :tag_aliases | |||
| resources :tags | |||
| resources :user_ips | |||
| resources :user_post_views | |||
| resources :nico_tags, path: 'tags/nico', only: [:index, :update] | |||
| resources :tags do | |||
| collection do | |||
| get :autocomplete | |||
| get 'name/:name', action: :show_by_name | |||
| end | |||
| end | |||
| scope :preview, controller: :preview do | |||
| get :title | |||
| get :thumbnail | |||
| end | |||
| resources :wiki_pages, path: 'wiki', only: [:index, :show, :create, :update] do | |||
| collection do | |||
| get :search | |||
| get :changes | |||
| scope :title do | |||
| get ':title/exists', action: :exists_by_title | |||
| get ':title', action: :show_by_title | |||
| end | |||
| end | |||
| member do | |||
| get :exists | |||
| get :diff | |||
| end | |||
| end | |||
| resources :posts do | |||
| collection do | |||
| get :random | |||
| get :changes | |||
| end | |||
| member do | |||
| post :viewed | |||
| delete :viewed, action: :unviewed | |||
| end | |||
| end | |||
| resources :users, only: [:create, :update] do | |||
| collection do | |||
| post :verify | |||
| get :me | |||
| post 'code/renew', action: :renew | |||
| end | |||
| end | |||
| # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html | |||
| # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. | |||
| # Can be used by load balancers and uptime monitors to verify that the app is live. | |||
| # get "up" => "rails/health#show", as: :rails_health_check | |||
| # Defines the root path route ("/") | |||
| # root "posts#index" | |||
| resources :ip_addresses | |||
| resources :nico_tag_relations | |||
| resources :post_tags | |||
| resources :settings | |||
| resources :tag_aliases | |||
| resources :user_ips | |||
| resources :user_post_views | |||
| end | |||
| @@ -1,5 +1,6 @@ | |||
| import { AnimatePresence, motion } from 'framer-motion' | |||
| import { useEffect, useState } from 'react' | |||
| import { Link } from 'react-router-dom' | |||
| import TagLink from '@/components/TagLink' | |||
| import TagSearch from '@/components/TagSearch' | |||
| @@ -128,6 +129,9 @@ export default (({ post }: Props) => { | |||
| && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} | |||
| </>)} | |||
| </li> | |||
| <li> | |||
| <Link to={`/posts/changes?id=${ post.id }`}>履歴</Link> | |||
| </li> | |||
| </ul> | |||
| </div>)} | |||
| </motion.div> | |||
| @@ -1,5 +1,8 @@ | |||
| import axios from 'axios' | |||
| import { useEffect, useState } from 'react' | |||
| import { Link } from 'react-router-dom' | |||
| import { API_BASE_URL } from '@/config' | |||
| import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts' | |||
| import { cn } from '@/lib/utils' | |||
| @@ -27,6 +30,27 @@ export default (({ tag, | |||
| withWiki = true, | |||
| withCount = true, | |||
| ...props }: Props) => { | |||
| const [havingWiki, setHavingWiki] = useState (true) | |||
| const wikiExists = async (tagName: string) => { | |||
| try | |||
| { | |||
| await axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (tagName) }/exists`) | |||
| setHavingWiki (true) | |||
| } | |||
| catch | |||
| { | |||
| setHavingWiki (false) | |||
| } | |||
| } | |||
| useEffect (() => { | |||
| if (!(linkFlg) || !(withWiki)) | |||
| return | |||
| wikiExists (tag.name) | |||
| }, [tag.name, linkFlg, withWiki]) | |||
| const spanClass = cn ( | |||
| `text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`, | |||
| `dark:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE }`) | |||
| @@ -39,10 +63,19 @@ export default (({ tag, | |||
| <> | |||
| {(linkFlg && withWiki) && ( | |||
| <span className="mr-1"> | |||
| <Link to={`/wiki/${ encodeURIComponent (tag.name) }`} | |||
| className={linkClass}> | |||
| ? | |||
| </Link> | |||
| {havingWiki | |||
| ? ( | |||
| <Link to={`/wiki/${ encodeURIComponent (tag.name) }`} | |||
| className={linkClass}> | |||
| ? | |||
| </Link>) | |||
| : ( | |||
| <Link to={`/wiki/${ encodeURIComponent (tag.name) }`} | |||
| className="animate-[wiki-blink_.25s_steps(2,end)_infinite] | |||
| dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]" | |||
| title={`${ tag.name } Wiki が存在しません.`}> | |||
| ! | |||
| </Link>)} | |||
| </span>)} | |||
| {nestLevel > 0 && ( | |||
| <span | |||
| @@ -96,3 +96,15 @@ button:focus-visible | |||
| background-color: #f9f9f9; | |||
| } | |||
| } | |||
| @keyframes wiki-blink | |||
| { | |||
| 0%, 100% { color: #dc2626; } | |||
| 50% { color: #2563eb; } | |||
| } | |||
| @keyframes wiki-blink-dark | |||
| { | |||
| 0%, 100% { color: #f87171; } | |||
| 50% { color: #60a5fa; } | |||
| } | |||
| @@ -25,19 +25,20 @@ export default (() => { | |||
| const page = Number (query.get ('page') ?? 1) | |||
| const limit = Number (query.get ('limit') ?? 20) | |||
| // 投稿列の結合で使用 | |||
| let rowsCnt: number | |||
| useEffect (() => { | |||
| void (async () => { | |||
| const res = await axios.get (`${ API_BASE_URL }/posts/changes`, | |||
| { params: { ...(id && { id }), | |||
| ...(page && { page }), | |||
| ...(limit && { limit }) } }) | |||
| { params: { ...(id && { id }), page, limit } }) | |||
| const data = toCamel (res.data as any, { deep: true }) as { | |||
| changes: PostTagChange[] | |||
| count: number } | |||
| setChanges (data.changes) | |||
| setTotalPages (Math.trunc ((data.count - 1) / limit)) | |||
| setTotalPages (Math.ceil (data.count / limit)) | |||
| }) () | |||
| }, [location.search]) | |||
| }, [id, page, limit]) | |||
| return ( | |||
| <MainArea> | |||
| @@ -47,7 +48,7 @@ export default (() => { | |||
| <PageTitle> | |||
| 耕作履歴 | |||
| {Boolean (id) && <>: 投稿 {<Link to={`/posts/${ id }`}>#{id}</Link>}</>} | |||
| {id && <>: 投稿 {<Link to={`/posts/${ id }`}>#{id}</Link>}</>} | |||
| </PageTitle> | |||
| <table className="table-auto w-full border-collapse"> | |||
| @@ -59,29 +60,42 @@ export default (() => { | |||
| </tr> | |||
| </thead> | |||
| <tbody> | |||
| {changes.map (change => ( | |||
| <tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}> | |||
| <td> | |||
| <Link to={`/posts/${ change.post.id }`}> | |||
| <img src={change.post.thumbnail || change.post.thumbnailBase || undefined} | |||
| alt={change.post.title || change.post.url} | |||
| title={change.post.title || change.post.url || undefined} | |||
| className="w-40"/> | |||
| </Link> | |||
| </td> | |||
| <td> | |||
| <TagLink tag={change.tag} withWiki={false} withCount={false}/> | |||
| {`を${ change.changeType === 'add' ? '追加' : '削除' }`} | |||
| </td> | |||
| <td> | |||
| {change.user ? ( | |||
| <Link to={`/users/${ change.user.id }`}> | |||
| {change.user.name} | |||
| </Link>) : 'bot 操作'} | |||
| <br/> | |||
| {change.timestamp} | |||
| </td> | |||
| </tr>))} | |||
| {changes.map ((change, i) => { | |||
| let withPost = i === 0 || change.post.id !== changes[i - 1].post.id | |||
| if (withPost) | |||
| { | |||
| rowsCnt = 1 | |||
| for (let j = i + 1; | |||
| (j < changes.length | |||
| && change.post.id === changes[j].post.id); | |||
| ++j) | |||
| ++rowsCnt | |||
| } | |||
| return ( | |||
| <tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}> | |||
| {withPost && ( | |||
| <td className="align-top" rowSpan={rowsCnt}> | |||
| <Link to={`/posts/${ change.post.id }`}> | |||
| <img src={change.post.thumbnail || change.post.thumbnailBase || undefined} | |||
| alt={change.post.title || change.post.url} | |||
| title={change.post.title || change.post.url || undefined} | |||
| className="w-40"/> | |||
| </Link> | |||
| </td>)} | |||
| <td> | |||
| <TagLink tag={change.tag} withWiki={false} withCount={false}/> | |||
| {`を${ change.changeType === 'add' ? '追加' : '削除' }`} | |||
| </td> | |||
| <td> | |||
| {change.user ? ( | |||
| <Link to={`/users/${ change.user.id }`}> | |||
| {change.user.name} | |||
| </Link>) : 'bot 操作'} | |||
| <br/> | |||
| {change.timestamp} | |||
| </td> | |||
| </tr>) | |||
| })} | |||
| </tbody> | |||
| </table> | |||
| @@ -36,9 +36,16 @@ export default () => { | |||
| if (/^\d+$/.test (title)) | |||
| { | |||
| void (async () => { | |||
| const res = await axios.get (`${ API_BASE_URL }/wiki/${ title }`) | |||
| const data = res.data as WikiPage | |||
| navigate (`/wiki/${ data.title }`, { replace: true }) | |||
| try | |||
| { | |||
| const res = await axios.get (`${ API_BASE_URL }/wiki/${ title }`) | |||
| const data = res.data as WikiPage | |||
| navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true }) | |||
| } | |||
| catch | |||
| { | |||
| ; | |||
| } | |||
| }) () | |||
| return | |||
| @@ -51,6 +58,8 @@ export default () => { | |||
| `${ API_BASE_URL }/wiki/title/${ encodeURIComponent (title) }`, | |||
| { params: version ? { version } : { } }) | |||
| const data = toCamel (res.data as any, { deep: true }) as WikiPage | |||
| if (data.title !== title) | |||
| navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true }) | |||
| setWikiPage (data) | |||
| WikiIdBus.set (data.id) | |||
| } | |||