This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/>}/>
|
||||
|
||||
@@ -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,58 +13,84 @@ 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}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={async e => {
|
||||
e.preventDefault ()
|
||||
if (!(tagFetchedFlags[t.id]))
|
||||
{
|
||||
try
|
||||
{
|
||||
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
|
||||
})
|
||||
setTagFetchedFlags (prev => ({ ...prev, [t.id]: true }))
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}
|
||||
setOpenTags (prev => ({ ...prev, [t.id]: !(prev[t.id]) }))
|
||||
}}>
|
||||
{openTags[t.id] ? '-' : '+'}
|
||||
</a>
|
||||
<TagLink
|
||||
tag={t}
|
||||
nestLevel={nestLevel}
|
||||
withCount={false}
|
||||
withWiki={false}
|
||||
to={`/materials?tag=${ encodeURIComponent (t.name) }`}/>
|
||||
</li>
|
||||
{openTags[t.id] && renderTags (t.children, nestLevel + 1)}
|
||||
</>))}
|
||||
</>)
|
||||
ts.map (t => (
|
||||
<Fragment key={t.id}>
|
||||
<li>
|
||||
<div className="flex">
|
||||
<div className="flex-none w-4">
|
||||
{t.hasChildren && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={async e => {
|
||||
e.preventDefault ()
|
||||
if (!(tagFetchedFlags[t.id]))
|
||||
{
|
||||
try
|
||||
{
|
||||
const data =
|
||||
await apiGet<TagWithDepth[]> (
|
||||
'/tags/with-depth', { params: { parent: String (t.id) } })
|
||||
setTags (prev => setChildrenById (prev, t.id, data))
|
||||
setTagFetchedFlags (prev => ({ ...prev, [t.id]: true }))
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}
|
||||
setOpenTags (prev => ({ ...prev, [t.id]: !(prev[t.id]) }))
|
||||
}}>
|
||||
{openTags[t.id] ? <>−</> : '+'}
|
||||
</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>
|
||||
<ul>
|
||||
{renderTags (tags)}
|
||||
</ul>
|
||||
<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
|
||||
Reference in New Issue
Block a user