Merge remote-tracking branch 'origin/main' into feature/169

This commit is contained in:
2025-12-30 10:58:30 +09:00
8 changed files with 175 additions and 72 deletions
@@ -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]
+1
View File
@@ -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
+50 -36
View File
@@ -1,46 +1,60 @@
Rails.application.routes.draw do Rails.application.routes.draw do
get 'tags/nico', to: 'nico_tags#index' resources :nico_tags, path: 'tags/nico', only: [:index, :update]
put 'tags/nico/:id', to: 'nico_tags#update'
get 'tags/autocomplete', to: 'tags#autocomplete' resources :tags do
get 'tags/name/:name', to: 'tags#show_by_name' collection do
get 'posts/random', to: 'posts#random' get :autocomplete
get 'posts/changes', to: 'posts#changes' get 'name/:name', action: :show_by_name
post 'posts/:id/viewed', to: 'posts#viewed' end
delete 'posts/:id/viewed', to: 'posts#unviewed' end
get 'preview/title', to: 'preview#title'
get 'preview/thumbnail', to: 'preview#thumbnail' scope :preview, controller: :preview do
get 'wiki/title/:title', to: 'wiki_pages#show_by_title' get :title
get 'wiki/search', to: 'wiki_pages#search' get :thumbnail
get 'wiki/changes', to: 'wiki_pages#changes' end
get 'wiki/:id/diff', to: 'wiki_pages#diff'
get 'wiki/:id', to: 'wiki_pages#show' resources :wiki_pages, path: 'wiki', only: [:index, :show, :create, :update] do
get 'wiki', to: 'wiki_pages#index' collection do
post 'wiki', to: 'wiki_pages#create' get :search
put 'wiki/:id', to: 'wiki_pages#update' get :changes
post 'users/code/renew', to: 'users#renew'
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
resources :posts
resources :ip_addresses resources :ip_addresses
resources :nico_tag_relations resources :nico_tag_relations
resources :post_tags resources :post_tags
resources :settings resources :settings
resources :tag_aliases resources :tag_aliases
resources :tags
resources :user_ips resources :user_ips
resources :user_post_views resources :user_post_views
resources :users, only: [:create, :update] do
collection do
post :verify
get :me
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"
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>
+34 -1
View File
@@ -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">
{havingWiki
? (
<Link to={`/wiki/${ encodeURIComponent (tag.name) }`} <Link to={`/wiki/${ encodeURIComponent (tag.name) }`}
className={linkClass}> className={linkClass}>
? ?
</Link> </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
+12
View File
@@ -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; }
}
+24 -10
View File
@@ -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 }), { params: { ...(id && { id }), page, limit } })
...(page && { page }),
...(limit && { 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,16 +60,28 @@ export default (() => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{changes.map (change => ( {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 }`}> <tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}>
<td> {withPost && (
<td className="align-top" rowSpan={rowsCnt}>
<Link to={`/posts/${ change.post.id }`}> <Link to={`/posts/${ change.post.id }`}>
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined} <img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
alt={change.post.title || change.post.url} alt={change.post.title || change.post.url}
title={change.post.title || change.post.url || undefined} title={change.post.title || change.post.url || undefined}
className="w-40"/> className="w-40"/>
</Link> </Link>
</td> </td>)}
<td> <td>
<TagLink tag={change.tag} withWiki={false} withCount={false}/> <TagLink tag={change.tag} withWiki={false} withCount={false}/>
{`${ change.changeType === 'add' ? '追加' : '削除' }`} {`${ change.changeType === 'add' ? '追加' : '削除' }`}
@@ -81,7 +94,8 @@ export default (() => {
<br/> <br/>
{change.timestamp} {change.timestamp}
</td> </td>
</tr>))} </tr>)
})}
</tbody> </tbody>
</table> </table>
+10 -1
View File
@@ -36,9 +36,16 @@ export default () => {
if (/^\d+$/.test (title)) if (/^\d+$/.test (title))
{ {
void (async () => { void (async () => {
try
{
const res = await axios.get (`${ API_BASE_URL }/wiki/${ title }`) const res = await axios.get (`${ API_BASE_URL }/wiki/${ title }`)
const data = res.data as WikiPage const data = res.data as WikiPage
navigate (`/wiki/${ data.title }`, { replace: true }) 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)
} }