diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb
index a3c611d..9b1b06a 100644
--- a/backend/app/controllers/nico_tags_controller.rb
+++ b/backend/app/controllers/nico_tags_controller.rb
@@ -3,7 +3,7 @@ class NicoTagsController < ApplicationController
limit = (params[:limit] || 20).to_i
cursor = params[:cursor].presence
- q = Tag.nico_tags.includes(:linked_tags)
+ q = Tag.nico_tags.includes(:linked_tags).order(updated_at: :desc)
q = q.where('tags.updated_at < ?', Time.iso8601(cursor)) if cursor
tags = q.limit(limit + 1)
@@ -19,6 +19,23 @@ class NicoTagsController < ApplicationController
}, next_cursor: }
end
- def upload
+ def update
+ return head :unauthorized unless current_user
+ return head :forbidden unless current_user.member?
+
+ id = params[:id].to_i
+
+ tag = Tag.find(id)
+ return head :bad_request if tag.category != 'nico'
+
+ linked_tag_names = params[:tags].to_s.split(' ')
+ linked_tags = Tag.normalise_tags(linked_tag_names, with_tagme: false)
+ return head :bad_request if linked_tags.filter { |t| t.category == 'nico' }.present?
+
+ tag.linked_tags = linked_tags
+ tag.updated_at = Time.now
+ tag.update!
+
+ render json: tag.linked_tags, status: :ok
end
end
diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb
index f401882..60f3577 100644
--- a/backend/app/controllers/posts_controller.rb
+++ b/backend/app/controllers/posts_controller.rb
@@ -63,7 +63,7 @@ class PostsController < ApplicationController
post.thumbnail.attach(thumbnail)
if post.save
post.resized_thumbnail!
- post.tags = normalise_tags(tags_names)
+ post.tags = Tag.normalise_tags(tags_names)
render json: post.as_json(include: { tags: { only: [:id, :name, :category] } }),
status: :created
else
@@ -94,10 +94,10 @@ class PostsController < ApplicationController
tag_names = params[:tags].to_s.split(' ')
post = Post.find(params[:id].to_i)
- tags = post.tags.where(category: 'nico').to_a + normalise_tags(tag_names)
+ tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names)
if post.update(title:, tags:)
render json: post.as_json(include: { tags: { only: [:id, :name, :category] } }),
- status: :created
+ status: :ok
else
render json: post.errors, status: :unprocessable_entity
end
@@ -109,14 +109,6 @@ class PostsController < ApplicationController
private
- CATEGORY_PREFIXES = {
- 'gen:' => 'general',
- 'djk:' => 'deerjikist',
- 'meme:' => 'meme',
- 'chr:' => 'character',
- 'mtr:' => 'material',
- 'meta:' => 'meta' }.freeze
-
def filtered_posts
tag_names = params[:tags]&.split(' ')
match_type = params[:match]
@@ -134,19 +126,4 @@ class PostsController < ApplicationController
end
posts.distinct
end
-
- def normalise_tags tag_names
- tags = tag_names.map do |name|
- pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil]
- name.delete_prefix!(pf)
- Tag.find_or_initialize_by(name:).tap do |tag|
- if cat && tag.category != cat
- tag.category = cat
- tag.save!
- end
- end
- end
- tags << Tag.tagme if tags.size < 20 && tags.none?(Tag.tagme)
- tags.uniq
- end
end
diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb
index a8239f6..137afff 100644
--- a/backend/app/models/tag.rb
+++ b/backend/app/models/tag.rb
@@ -26,6 +26,14 @@ class Tag < ApplicationRecord
scope :nico_tags, -> { where(category: :nico) }
+ CATEGORY_PREFIXES = {
+ 'gen:' => 'general',
+ 'djk:' => 'deerjikist',
+ 'meme:' => 'meme',
+ 'chr:' => 'character',
+ 'mtr:' => 'material',
+ 'meta:' => 'meta' }.freeze
+
def self.tagme
@tagme ||= Tag.find_or_initialize_by(name: 'タグ希望') do |tag|
tag.category = 'meta'
@@ -38,11 +46,26 @@ class Tag < ApplicationRecord
end
end
+ def self.normalise_tags tag_names, with_tagme: true
+ tags = tag_names.map do |name|
+ pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil]
+ name.delete_prefix!(pf)
+ Tag.find_or_initialize_by(name:).tap do |tag|
+ if cat && tag.category != cat
+ tag.category = cat
+ tag.save!
+ end
+ end
+ end
+ tags << Tag.tagme if with_tagme && tags.size < 20 && tags.none?(Tag.tagme)
+ tags.uniq
+ end
+
private
def nico_tag_name_must_start_with_nico
- if ((category == 'nico' && name&.[](0, 5) != 'nico:') ||
- (category != 'nico' && name&.[](0, 5) == 'nico:'))
+ if ((category == 'nico' && !(name.start_with?('nico:'))) ||
+ (category != 'nico' && name.start_with?('nico:')))
errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.'
end
end
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 691fc67..0e8f93f 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -63,7 +63,7 @@ export default () => {
} />
} />
} />
- } />
+ } />
} />
} />
} />
diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx
index bcd3ab2..70af9c2 100644
--- a/frontend/src/components/TopNav.tsx
+++ b/frontend/src/components/TopNav.tsx
@@ -166,7 +166,8 @@ export default ({ user }: Props) => {
return (
一覧
- 投稿追加
+ {['admin', 'member'].some (r => user?.role === r) && (
+ 投稿追加)}
ヘルプ
)
case Menu.Tag:
diff --git a/frontend/src/components/ui/use-toast.tsx b/frontend/src/components/ui/use-toast.tsx
index 83c24fd..43c3e4b 100644
--- a/frontend/src/components/ui/use-toast.tsx
+++ b/frontend/src/components/ui/use-toast.tsx
@@ -6,7 +6,7 @@ import * as React from "react"
import type {
ToastActionElement,
ToastProps,
-} from "@/registry/default/ui/toast"
+} from "./toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
diff --git a/frontend/src/pages/posts/PostDetailPage.tsx b/frontend/src/pages/posts/PostDetailPage.tsx
index 86293e5..20ac87d 100644
--- a/frontend/src/pages/posts/PostDetailPage.tsx
+++ b/frontend/src/pages/posts/PostDetailPage.tsx
@@ -102,7 +102,7 @@ export default ({ user }: Props) => {
{post.viewed ? '閲覧済' : '未閲覧'}
- {(['admin', 'member'].some (r => r === user?.role) && editing) && (
+ {(['admin', 'member'].some (r => user?.role === r) && editing) && (
{
diff --git a/frontend/src/pages/posts/PostNewPage.tsx b/frontend/src/pages/posts/PostNewPage.tsx
index a6e67dc..349ce99 100644
--- a/frontend/src/pages/posts/PostNewPage.tsx
+++ b/frontend/src/pages/posts/PostNewPage.tsx
@@ -28,22 +28,25 @@ export default () => {
const previousURLRef = useRef ('')
const handleSubmit = async () => {
- const formData = new FormData ()
+ const formData = new FormData
formData.append ('title', title)
formData.append ('url', url)
formData.append ('tags', tags)
if (thumbnailFile)
formData.append ('thumbnail', thumbnailFile)
- void (axios.post (`${ API_BASE_URL }/posts`, formData, { headers: {
- 'Content-Type': 'multipart/form-data',
- 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
- .then (() => {
- toast ({ title: '投稿成功!' })
- navigate ('/posts')
- })
- .catch (() => toast ({ title: '投稿失敗',
- description: '入力を確認してください。' })))
+ try
+ {
+ await axios.post (`${ API_BASE_URL }/posts`, formData, { headers: {
+ 'Content-Type': 'multipart/form-data',
+ 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
+ toast ({ title: '投稿成功!' })
+ navigate ('/posts')
+ }
+ catch
+ {
+ toast ({ title: '投稿失敗', description: '入力を確認してください。' })
+ }
}
useEffect (() => {
diff --git a/frontend/src/pages/tags/NicoTagListPage.tsx b/frontend/src/pages/tags/NicoTagListPage.tsx
index 72ac3dc..b9959d8 100644
--- a/frontend/src/pages/tags/NicoTagListPage.tsx
+++ b/frontend/src/pages/tags/NicoTagListPage.tsx
@@ -1,35 +1,88 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import SectionTitle from '@/components/common/SectionTitle'
import TextArea from '@/components/common/TextArea'
import MainArea from '@/components/layout/MainArea'
+import { toast } from '@/components/ui/use-toast'
import { API_BASE_URL, SITE_TITLE } from '@/config'
-import type { NicoTag } from '@/types'
+import type { NicoTag, Tag, User } from '@/types'
+type Props = { user: User | null }
-export default () => {
+
+export default ({ user }: Props) => {
const [nicoTags, setNicoTags] = useState ([])
+ const [cursor, setCursor] = useState ('')
+ const [loading, setLoading] = useState (false)
const [editing, setEditing] = useState<{ [key: number]: boolean }> ({ })
const [rawTags, setRawTags] = useState<{ [key: number]: string }> ({ })
- useEffect (() => {
- void (async () => {
- const res = await axios.get (`${ API_BASE_URL }/tags/nico`)
- const data = toCamel (res.data as any, { deep: true }) as { tags: NicoTag[] }
+ const loaderRef = useRef (null)
+
+ const memberFlg = ['admin', 'member'].some (r => user?.role === r)
+
+ const loadMore = async (withCursor: boolean) => {
+ setLoading (true)
+
+ const res = await axios.get (`${ API_BASE_URL }/tags/nico`, {
+ params: { ...(withCursor ? { cursor } : { }) } })
+ const data = toCamel (res.data as any, { deep: true }) as { tags: NicoTag[]
+ nextCursor: string }
+
+ setNicoTags (tags => [...(withCursor ? tags : []), ...data.tags])
+ setCursor (data.nextCursor)
+
+ const newEditing = Object.fromEntries (data.tags.map (t => [t.id, false]))
+ setEditing (editing => ({ ...editing, ...newEditing }))
+
+ const newRawTags = Object.fromEntries (
+ data.tags.map (t => [t.id, t.linkedTags.map (lt => lt.name).join (' ')]))
+ setRawTags (rawTags => ({ ...rawTags, ...newRawTags }))
- setNicoTags (data.tags)
+ setLoading (false)
+ }
- const newEditing = Object.fromEntries (data.tags.map (t => [t.id, false]))
- setEditing (editing => ({ ...editing, ...newEditing }))
+ const handleEdit = async (id: number) => {
+ if (editing[id])
+ {
+ const formData = new FormData
+ formData.append ('tags', rawTags[id])
- const newRawTags = Object.fromEntries (
- data.tags.map (t => [t.id, t.linkedTags.map (lt => lt.name).join (' ')]))
- setRawTags (rawTags => ({ ...rawTags, ...newRawTags }))
- }) ()
+ const res = await axios.put (`${ API_BASE_URL }/tags/nico/${ id }`, formData, { headers: {
+ 'Content-Type': 'multipart/form-data',
+ 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
+ const data = toCamel (res.data as any, { deep: true }) as Tag[]
+ setRawTags (rawTags => ({ ...rawTags, [id]: data.map (t => t.name).join (' ') }))
+
+ toast ({ title: '更新しました.' })
+ }
+
+ setEditing (editing => ({ ...editing, [id]: !(editing[id]) }))
+ }
+
+ useEffect(() => {
+ const observer = new IntersectionObserver (entries => {
+ if (entries[0].isIntersecting && !(loading) && cursor)
+ loadMore (true)
+ }, { threshold: 1 })
+
+ const target = loaderRef.current
+ if (target)
+ observer.observe (target)
+
+ return () => {
+ if (target)
+ observer.unobserve (target)
+ }
+ }, [loaderRef, loading])
+
+ useEffect (() => {
+ setNicoTags ([])
+ loadMore (false)
}, [])
return (
@@ -43,38 +96,45 @@ export default () => {
-
+ {nicoTags.length > 0 && (
+
)}
+ {loading && 'Loading...'}
+
)
}