This commit is contained in:
2026-04-03 00:42:37 +09:00
parent 6ed7f81151
commit f576373c8f
5 changed files with 168 additions and 47 deletions
+25 -1
View File
@@ -84,6 +84,7 @@ class TagsController < ApplicationController
Tag
.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.where(category: [:meme, :character, :material])
.where(id: tag_ids)
.order('tag_names.name')
.distinct
@@ -93,7 +94,12 @@ class TagsController < ApplicationController
if tags.empty?
[]
else
TagImplication.where(parent_tag_id: tags.map(&:id)).distinct.pluck(:parent_tag_id)
TagImplication
.joins(:tag)
.where(parent_tag_id: tags.map(&:id),
tags: { category: [:meme, :character, :material] })
.distinct
.pluck(:parent_tag_id)
end
render json: tags.map { |tag|
@@ -101,6 +107,18 @@ class TagsController < ApplicationController
}
end
def materials_by_name
name = params[:name].to_s.strip
return head :bad_request if name.blank?
tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(tag_names: { name: })
return :not_found unless tag
render json: build_tag_children(tag)
end
def autocomplete
q = params[:q].to_s.strip.sub(/\Anot:/i, '')
@@ -210,4 +228,10 @@ class TagsController < ApplicationController
render json: TagRepr.base(tag)
end
private
def build_tag_children tag
TagRepr.base(tag).merge(children: tag.children.map { build_tag_children(_1) })
end
end
+1
View File
@@ -13,6 +13,7 @@ Rails.application.routes.draw do
scope :name do
get ':name/deerjikists', action: :deerjikists_by_name
get ':name/materials', action: :materials_by_name
get ':name', action: :show_by_name
end
end
+3 -1
View File
@@ -10,8 +10,9 @@ import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
import TopNav from '@/components/TopNav'
import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api'
import MaterialSearchPage from '@/pages/materials/MaterialSearchPage'
// import MaterialDetailPage from '@/pages/materials/MaterialDetailPage'
import MaterialListPage from '@/pages/materials/MaterialListPage'
import MaterialSearchPage from '@/pages/materials/MaterialSearchPage'
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
import NotFound from '@/pages/NotFound'
import PostDetailPage from '@/pages/posts/PostDetailPage'
@@ -53,6 +54,7 @@ const RouteTransitionWrapper = ({ user, setUser }: {
<Route path="/tags" element={<TagListPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="/theatres/:id" element={<TheatreDetailPage/>}/>
<Route path="/materials" element={<MaterialListPage/>}/>
<Route path="/materials/search" element={<MaterialSearchPage/>}/>
{/* <Route path="/materials/:tagName" element ={<MaterialDetailPage/>}/> */}
<Route path="/wiki" element={<WikiSearchPage/>}/>
+41 -15
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { Fragment, useEffect, useState } from 'react'
import TagLink from '@/components/TagLink'
import SidebarComponent from '@/components/layout/SidebarComponent'
@@ -13,16 +13,40 @@ type TagWithDepth = Tag & {
children: TagWithDepth[] }
const setChildrenById = (
tags: TagWithDepth[],
targetId: number,
children: TagWithDepth[],
): TagWithDepth[] => (
tags.map (tag => {
if (tag.id === targetId)
return { ...tag, children }
if (tag.children.length === 0)
return tag
return { ...tag, children: setChildrenById (tag.children, targetId, children) }
}))
export default (() => {
const [tags, setTags] = useState<TagWithDepth[]> ([])
const [openTags, setOpenTags] = useState<Record<number, boolean>> ({ })
const [tagFetchedFlags, setTagFetchedFlags] = useState<Record<number, boolean>> ({ })
useEffect (() => {
void (async () => {
setTags (await apiGet<TagWithDepth[]> ('/tags/with-depth'))
}) ()
}, [])
const renderTags = (ts: TagWithDepth[], nestLevel = 0): ReactNode => (
<>
{ts.map (t => (
<>
<li key={t.id}>
ts.map (t => (
<Fragment key={t.id}>
<li>
<div className="flex">
<div className="flex-none w-4">
{t.hasChildren && (
<a
href="#"
onClick={async e => {
@@ -33,12 +57,8 @@ export default (() => {
{
const data =
await apiGet<TagWithDepth[]> (
'/tags/with-depth', { params: { parent: t.id } })
setTags (prev => {
const rtn = structuredClone (prev)
rtn.find (x => x.id === t.id)!.children = data
return rtn
})
'/tags/with-depth', { params: { parent: String (t.id) } })
setTags (prev => setChildrenById (prev, t.id, data))
setTagFetchedFlags (prev => ({ ...prev, [t.id]: true }))
}
catch
@@ -48,23 +68,29 @@ export default (() => {
}
setOpenTags (prev => ({ ...prev, [t.id]: !(prev[t.id]) }))
}}>
{openTags[t.id] ? '-' : '+'}
</a>
{openTags[t.id] ? <>&minus;</> : '+'}
</a>)}
</div>
<div className="flex-1 truncate">
<TagLink
tag={t}
nestLevel={nestLevel}
title={t.name}
withCount={false}
withWiki={false}
to={`/materials?tag=${ encodeURIComponent (t.name) }`}/>
</div>
</div>
</li>
{openTags[t.id] && renderTags (t.children, nestLevel + 1)}
</>))}
</>)
</Fragment>)))
return (
<SidebarComponent>
<div className="md:h-[calc(100dvh-120px)] md:overflow-y-auto">
<ul>
{renderTags (tags)}
</ul>
</div>
</SidebarComponent>)
}) satisfies FC
@@ -0,0 +1,68 @@
import { Fragment, useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation } from 'react-router-dom'
import MaterialSidebar from '@/components/MaterialSidebar'
import PageTitle from '@/components/common/PageTitle'
import SectionTitle from '@/components/common/SectionTitle'
import SubsectionTitle from '@/components/common/SubsectionTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { apiGet } from '@/lib/api'
import type { FC } from 'react'
import type { Tag } from '@/types'
type TagWithMaterial = Omit<Tag, 'children'> & {
children: TagWithMaterial[]
material: any | null }
export default (() => {
const [tag, setTag] = useState<TagWithMaterial | null> (null)
const location = useLocation ()
const query = new URLSearchParams (location.search)
const tagQuery = query.get ('tag') ?? ''
useEffect (() => {
void (async () => {
setTag (
await apiGet<TagWithMaterial> (`/tags/name/${ encodeURIComponent (tagQuery) }/materials`))
}) ()
}, [location.search])
return (
<div className="md:flex md:flex-1">
<Helmet>
<title>{`${ tag ? `${ tag.name } 素材集` : '素材集' } | ${ SITE_TITLE }`}</title>
</Helmet>
<MaterialSidebar/>
<MainArea>
{tag
? (
<>
<PageTitle>{tag.name}</PageTitle>
{tag.children.map (c2 => (
<Fragment key={c2.id}>
<SectionTitle>{c2.name}</SectionTitle>
{c2.children.map (c3 => (
<SubsectionTitle key={c3.id}>{c3.name}</SubsectionTitle>))}
</Fragment>))}
</>)
: (
<>
<p></p>
<p></p>
<ul>
<li><Link to="/materials/new"></Link></li>
<li><a href="#"></a></li>
</ul>
</>)}
</MainArea>
</div>)
}) satisfies FC