| @@ -13,6 +13,22 @@ class WikiPagesController < ApplicationController | |||||
| render_wiki_page_or_404 WikiPage.find_by(title: params[:title]) | render_wiki_page_or_404 WikiPage.find_by(title: params[:title]) | ||||
| end | 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 | def diff | ||||
| id = params[:id] | id = params[:id] | ||||
| from = params[:from] | from = params[:from] | ||||
| @@ -6,6 +6,7 @@ class NicoTagRelation < ApplicationRecord | |||||
| validates :tag_id, presence: true | validates :tag_id, presence: true | ||||
| validate :nico_tag_must_be_nico | validate :nico_tag_must_be_nico | ||||
| validate :tag_mustnt_be_nico | |||||
| private | private | ||||
| @@ -1,46 +1,60 @@ | |||||
| Rails.application.routes.draw do | 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 | resources :users, only: [:create, :update] do | ||||
| collection do | collection do | ||||
| post :verify | post :verify | ||||
| get :me | get :me | ||||
| post 'code/renew', action: :renew | |||||
| end | end | ||||
| 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 | end | ||||
| @@ -1,5 +1,6 @@ | |||||
| import { AnimatePresence, motion } from 'framer-motion' | import { AnimatePresence, motion } from 'framer-motion' | ||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||
| import { Link } from 'react-router-dom' | |||||
| import TagLink from '@/components/TagLink' | import TagLink from '@/components/TagLink' | ||||
| import TagSearch from '@/components/TagSearch' | import TagSearch from '@/components/TagSearch' | ||||
| @@ -128,6 +129,9 @@ export default (({ post }: Props) => { | |||||
| && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} | && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} | ||||
| </>)} | </>)} | ||||
| </li> | </li> | ||||
| <li> | |||||
| <Link to={`/posts/changes?id=${ post.id }`}>履歴</Link> | |||||
| </li> | |||||
| </ul> | </ul> | ||||
| </div>)} | </div>)} | ||||
| </motion.div> | </motion.div> | ||||
| @@ -1,5 +1,8 @@ | |||||
| import axios from 'axios' | |||||
| import { useEffect, useState } from 'react' | |||||
| import { Link } from 'react-router-dom' | import { Link } from 'react-router-dom' | ||||
| import { API_BASE_URL } from '@/config' | |||||
| import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts' | import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts' | ||||
| import { cn } from '@/lib/utils' | import { cn } from '@/lib/utils' | ||||
| @@ -27,6 +30,27 @@ export default (({ tag, | |||||
| withWiki = true, | withWiki = true, | ||||
| withCount = true, | withCount = true, | ||||
| ...props }: Props) => { | ...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 ( | const spanClass = cn ( | ||||
| `text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`, | `text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`, | ||||
| `dark:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE }`) | `dark:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE }`) | ||||
| @@ -39,10 +63,19 @@ export default (({ tag, | |||||
| <> | <> | ||||
| {(linkFlg && withWiki) && ( | {(linkFlg && withWiki) && ( | ||||
| <span className="mr-1"> | <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>)} | </span>)} | ||||
| {nestLevel > 0 && ( | {nestLevel > 0 && ( | ||||
| <span | <span | ||||
| @@ -96,3 +96,15 @@ button:focus-visible | |||||
| background-color: #f9f9f9; | 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 page = Number (query.get ('page') ?? 1) | ||||
| const limit = Number (query.get ('limit') ?? 20) | const limit = Number (query.get ('limit') ?? 20) | ||||
| // 投稿列の結合で使用 | |||||
| let rowsCnt: number | |||||
| useEffect (() => { | useEffect (() => { | ||||
| void (async () => { | void (async () => { | ||||
| const res = await axios.get (`${ API_BASE_URL }/posts/changes`, | 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 { | const data = toCamel (res.data as any, { deep: true }) as { | ||||
| changes: PostTagChange[] | changes: PostTagChange[] | ||||
| count: number } | count: number } | ||||
| setChanges (data.changes) | setChanges (data.changes) | ||||
| setTotalPages (Math.trunc ((data.count - 1) / limit)) | |||||
| setTotalPages (Math.ceil (data.count / limit)) | |||||
| }) () | }) () | ||||
| }, [location.search]) | |||||
| }, [id, page, limit]) | |||||
| return ( | return ( | ||||
| <MainArea> | <MainArea> | ||||
| @@ -47,7 +48,7 @@ export default (() => { | |||||
| <PageTitle> | <PageTitle> | ||||
| 耕作履歴 | 耕作履歴 | ||||
| {Boolean (id) && <>: 投稿 {<Link to={`/posts/${ id }`}>#{id}</Link>}</>} | |||||
| {id && <>: 投稿 {<Link to={`/posts/${ id }`}>#{id}</Link>}</>} | |||||
| </PageTitle> | </PageTitle> | ||||
| <table className="table-auto w-full border-collapse"> | <table className="table-auto w-full border-collapse"> | ||||
| @@ -59,29 +60,42 @@ export default (() => { | |||||
| </tr> | </tr> | ||||
| </thead> | </thead> | ||||
| <tbody> | <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> | </tbody> | ||||
| </table> | </table> | ||||
| @@ -36,9 +36,16 @@ export default () => { | |||||
| if (/^\d+$/.test (title)) | if (/^\d+$/.test (title)) | ||||
| { | { | ||||
| void (async () => { | 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 | return | ||||
| @@ -51,6 +58,8 @@ export default () => { | |||||
| `${ API_BASE_URL }/wiki/title/${ encodeURIComponent (title) }`, | `${ API_BASE_URL }/wiki/title/${ encodeURIComponent (title) }`, | ||||
| { params: version ? { version } : { } }) | { params: version ? { version } : { } }) | ||||
| const data = toCamel (res.data as any, { deep: true }) as WikiPage | const data = toCamel (res.data as any, { deep: true }) as WikiPage | ||||
| if (data.title !== title) | |||||
| navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true }) | |||||
| setWikiPage (data) | setWikiPage (data) | ||||
| WikiIdBus.set (data.id) | WikiIdBus.set (data.id) | ||||
| } | } | ||||