Browse Source

ニジラー詳細ページ作成 (#63) (#341)

#63

#63

#63

#63

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/341
main
みてるぞ 2 days ago
parent
commit
52aa1615b6
12 changed files with 494 additions and 42 deletions
  1. +48
    -2
      backend/app/controllers/tags_controller.rb
  2. +2
    -0
      backend/app/models/tag.rb
  3. +1
    -1
      backend/app/representations/tag_repr.rb
  4. +1
    -0
      backend/config/routes.rb
  5. +227
    -17
      backend/spec/requests/tags_deerjikists_spec.rb
  6. +2
    -0
      frontend/src/App.tsx
  7. +32
    -14
      frontend/src/components/TagLink.tsx
  8. +6
    -1
      frontend/src/consts.ts
  9. +6
    -5
      frontend/src/lib/queryKeys.ts
  10. +7
    -1
      frontend/src/lib/tags.ts
  11. +155
    -0
      frontend/src/pages/deerjikists/DeerjikistDetailPage.tsx
  12. +7
    -1
      frontend/src/types.ts

+ 48
- 2
backend/app/controllers/tags_controller.rb View File

@@ -1,3 +1,7 @@
require 'net/http'
require 'uri'


class TagsController < ApplicationController class TagsController < ApplicationController
def index def index
post_id = params[:post] post_id = params[:post]
@@ -182,7 +186,8 @@ class TagsController < ApplicationController
.find_by(id: params[:id]) .find_by(id: params[:id])
return head :not_found unless tag return head :not_found unless tag


render json: DeerjikistRepr.many(tag.deerjikists)
render json: { tag: TagRepr.base(tag),
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
end end


def deerjikists_by_name def deerjikists_by_name
@@ -194,7 +199,31 @@ class TagsController < ApplicationController
.find_by(tag_names: { name: }) .find_by(tag_names: { name: })
return head :not_found unless tag return head :not_found unless tag


render json: DeerjikistRepr.many(tag.deerjikists)
render json: { tag: TagRepr.base(tag),
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
end

def update_deerjikists
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?

tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(id: params[:id])
return head :not_found unless tag

ApplicationRecord.transaction do
tag.deerjikists = []
params[:_json].each do
platform = _1[:platform]
code = normalise_deerjikist_code(platform, _1[:code])
deerjikist = Deerjikist.find_or_initialize_by(platform:, code:)
deerjikist.tag = tag
deerjikist.save!
end
end

render json: DeerjikistRepr.many(tag.reload.deerjikists)
end end


def materials_by_name def materials_by_name
@@ -391,4 +420,21 @@ class TagsController < ApplicationController
TagImplication.create!(tag:, parent_tag:) TagImplication.create!(tag:, parent_tag:)
end end
end end

def normalise_deerjikist_code platform, code
return code if platform != 'youtube' || code[0] != '@'

url = "https://www.youtube.com/#{ code }"

html = Net::HTTP.get(URI(url))

canonical = html[
/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[a-zA-Z0-9_-]{22})"/,
1]
return canonical if canonical

html[/"channelId":"(UC[a-zA-Z0-9_-]{22})"/, 1] || html[/\bUC[a-zA-Z0-9_-]{22}\b/]
rescue
nil
end
end end

+ 2
- 0
backend/app/models/tag.rb View File

@@ -79,6 +79,8 @@ class Tag < ApplicationRecord


def material_id = materials.first&.id def material_id = materials.first&.id


def has_deerjikists = deerjikists.present?

def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta) def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta) def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta) def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)


+ 1
- 1
backend/app/representations/tag_repr.rb View File

@@ -3,7 +3,7 @@


module TagRepr module TagRepr
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at], BASE = { only: [:id, :category, :post_count, :created_at, :updated_at],
methods: [:name, :has_wiki, :material_id] }.freeze
methods: [:name, :has_wiki, :material_id, :has_deerjikists] }.freeze


module_function module_function




+ 1
- 0
backend/config/routes.rb View File

@@ -24,6 +24,7 @@ Rails.application.routes.draw do
patch '', action: :update patch '', action: :update


get :deerjikists get :deerjikists
put :deerjikists, action: :update_deerjikists
end end
end end




+ 227
- 17
backend/spec/requests/tags_deerjikists_spec.rb View File

@@ -8,23 +8,35 @@ RSpec.describe 'Tags deerjikists API', type: :request do


let!(:tag) { create(:tag, category: :deerjikist) } let!(:tag) { create(:tag, category: :deerjikist) }


let(:member) { create(:user, :member) }
let(:guest) { create(:user, role: :guest) }

before do before do
# show_by_name / deerjikists_by_name 用に名前を固定
tag.tag_name.update!(name: 'deerjika') tag.tag_name.update!(name: 'deerjika')
end end


describe 'GET /tags/:id/deerjikists' do describe 'GET /tags/:id/deerjikists' do
subject(:do_request) do subject(:do_request) do
get "/tags/#{ tag_id }/deerjikists"
get "/tags/#{tag_id}/deerjikists"
end end


let(:tag_id) { tag.id } let(:tag_id) { tag.id }


context 'when tag exists and has no deerjikists' do context 'when tag exists and has no deerjikists' do
it 'returns 200 and empty array' do
it 'returns 200 with tag and empty deerjikists array' do
do_request do_request

expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json).to eq([])

expect(json).to be_a(Hash)

expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(false)

expect(json['deerjikists']).to eq([])
end end
end end


@@ -34,17 +46,27 @@ RSpec.describe 'Tags deerjikists API', type: :request do
Deerjikist.create!(platform: platform2, code: code2, tag: tag) Deerjikist.create!(platform: platform2, code: code2, tag: tag)
end end


it 'returns 200 and deerjikists array' do
it 'returns 200 with tag and deerjikists array' do
do_request do_request

expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)


expect(json).to be_a(Array)
expect(json.size).to eq(2)
expect(json).to be_a(Hash)


expect(json.map { |h| [h['platform'], h['code']] }).to contain_exactly(
[platform1, code1],
[platform2, code2],
)
expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(true)

expect(json['deerjikists']).to be_a(Array)
expect(json['deerjikists'].size).to eq(2)

expect(json['deerjikists'].map { |h| [h['platform'], h['code']] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
end end
end end


@@ -53,6 +75,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do


it 'returns 404' do it 'returns 404' do
do_request do_request

expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
end end
end end
@@ -60,7 +83,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do


describe 'GET /tags/name/:name/deerjikists' do describe 'GET /tags/name/:name/deerjikists' do
subject(:do_request) do subject(:do_request) do
get "/tags/name/#{ name }/deerjikists"
get "/tags/name/#{name}/deerjikists"
end end


let(:name) { 'deerjika' } let(:name) { 'deerjika' }
@@ -70,6 +93,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do


it 'returns 400' do it 'returns 400' do
do_request do_request

expect(response).to have_http_status(:bad_request) expect(response).to have_http_status(:bad_request)
end end
end end
@@ -79,23 +103,209 @@ RSpec.describe 'Tags deerjikists API', type: :request do


it 'returns 404' do it 'returns 404' do
do_request do_request

expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
end end
end end


context 'when tag exists and has no deerjikists' do
it 'returns 200 with tag and empty deerjikists array' do
do_request

expect(response).to have_http_status(:ok)

expect(json).to be_a(Hash)

expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(false)

expect(json['deerjikists']).to eq([])
end
end

context 'when tag exists and has deerjikists' do context 'when tag exists and has deerjikists' do
before do before do
Deerjikist.create!(platform: platform1, code: code1, tag: tag) Deerjikist.create!(platform: platform1, code: code1, tag: tag)
end end


it 'returns 200 and deerjikists array' do
it 'returns 200 with tag and deerjikists array' do
do_request do_request

expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)


expect(json).to be_a(Array)
expect(json.size).to eq(1)
expect(json[0]['platform']).to eq(platform1)
expect(json[0]['code']).to eq(code1)
expect(json).to be_a(Hash)

expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(true)

expect(json['deerjikists']).to be_a(Array)
expect(json['deerjikists'].size).to eq(1)

expect(json['deerjikists'][0]['platform']).to eq(platform1)
expect(json['deerjikists'][0]['code']).to eq(code1)
end
end
end

describe 'PUT /tags/:id/deerjikists' do
subject(:do_request) do
put "/tags/#{tag_id}/deerjikists", params: payload, as: :json
end

let(:tag_id) { tag.id }
let(:payload) do
[
{ platform: platform1, code: code1 },
{ platform: platform2, code: code2 },
]
end

context 'when not logged in' do
it 'returns 401' do
do_request

expect(response).to have_http_status(:unauthorized)
end
end

context 'when logged in but not member' do
before do
sign_in_as guest
end

it 'returns 403' do
do_request

expect(response).to have_http_status(:forbidden)
end
end

context 'when tag does not exist' do
let(:tag_id) { 9_999_999 }

before do
sign_in_as member
end

it 'returns 404' do
do_request

expect(response).to have_http_status(:not_found)
end
end

context 'when logged in as member' do
before do
sign_in_as member
end

context 'when tag has no deerjikists' do
it 'creates deerjikists and returns deerjikists array' do
expect {
do_request
}.to change { Deerjikist.where(tag: tag).count }.from(0).to(2)

expect(response).to have_http_status(:ok)

expect(json).to be_a(Array)
expect(json.map { |h| [h['platform'], h['code']] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)

expect(Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
end
end

context 'when tag already has deerjikists' do
before do
Deerjikist.create!(platform: platform1, code: 'old-code-1', tag: tag)
Deerjikist.create!(platform: platform2, code: 'old-code-2', tag: tag)
end

it 'replaces deerjikists and returns deerjikists array' do
do_request

expect(response).to have_http_status(:ok)

expect(Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)

expect(Deerjikist.exists?(platform: platform1, code: 'old-code-1')).to eq(false)
expect(Deerjikist.exists?(platform: platform2, code: 'old-code-2')).to eq(false)

expect(json).to be_a(Array)
expect(json.map { |h| [h['platform'], h['code']] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
end
end

context 'when payload is empty array' do
let(:payload) { [] }

before do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
Deerjikist.create!(platform: platform2, code: code2, tag: tag)
end

it 'clears deerjikists and returns empty array' do
expect {
do_request
}.to change { Deerjikist.where(tag: tag).count }.from(2).to(0)

expect(response).to have_http_status(:ok)
expect(json).to eq([])
end
end

context 'when youtube code is handle' do
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
let(:payload) do
[
{ platform: 'youtube', code: '@deerjika' },
]
end

before do
allow(Net::HTTP).to receive(:get).and_return(
%(<link rel="canonical" href="https://www.youtube.com/channel/#{channel_id}">),
)
end

it 'normalises youtube handle to channel id' do
expect {
do_request
}.to change { Deerjikist.where(tag: tag).count }.from(0).to(1)

expect(response).to have_http_status(:ok)

expect(Net::HTTP).to have_received(:get)

expect(Deerjikist.exists?(platform: 'youtube', code: channel_id, tag: tag))
.to eq(true)

expect(json).to be_a(Array)
expect(json.size).to eq(1)
expect(json[0]['platform']).to eq('youtube')
expect(json[0]['code']).to eq(channel_id)
end
end end
end end
end end


+ 2
- 0
frontend/src/App.tsx View File

@@ -10,6 +10,7 @@ import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
import TopNav from '@/components/TopNav' import TopNav from '@/components/TopNav'
import { Toaster } from '@/components/ui/toaster' import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api' import { apiPost, isApiError } from '@/lib/api'
import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage'
import MaterialBasePage from '@/pages/materials/MaterialBasePage' import MaterialBasePage from '@/pages/materials/MaterialBasePage'
import MaterialDetailPage from '@/pages/materials/MaterialDetailPage' import MaterialDetailPage from '@/pages/materials/MaterialDetailPage'
import MaterialListPage from '@/pages/materials/MaterialListPage' import MaterialListPage from '@/pages/materials/MaterialListPage'
@@ -58,6 +59,7 @@ const RouteTransitionWrapper = ({ user, setUser }: {
<Route path="/posts/changes" element={<PostHistoryPage/>}/> <Route path="/posts/changes" element={<PostHistoryPage/>}/>
<Route path="/tags" element={<TagListPage/>}/> <Route path="/tags" element={<TagListPage/>}/>
<Route path="/tags/:id" element={<TagDetailPage/>}/> <Route path="/tags/:id" element={<TagDetailPage/>}/>
<Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/> <Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="/tags/changes" element={<TagHistoryPage/>}/> <Route path="/tags/changes" element={<TagHistoryPage/>}/>
<Route path="/theatres/:id" element={<TheatreDetailPage/>}/> <Route path="/theatres/:id" element={<TheatreDetailPage/>}/>


+ 32
- 14
frontend/src/components/TagLink.tsx View File

@@ -45,9 +45,9 @@ export default (({ tag,
<> <>
{(linkFlg && withWiki) && ( {(linkFlg && withWiki) && (
<span className="mr-1"> <span className="mr-1">
{(tag.materialId != null || tag.hasWiki)
{(tag.materialId != null || tag.hasWiki || tag.hasDeerjikists)
? ( ? (
tag.materialId == null
tag.materialId == null && !(tag.hasDeerjikists)
? ( ? (
<PrefetchLink <PrefetchLink
to={`/wiki/${ encodeURIComponent (tag.name) }`} to={`/wiki/${ encodeURIComponent (tag.name) }`}
@@ -55,11 +55,19 @@ export default (({ tag,
? ?
</PrefetchLink>) </PrefetchLink>)
: ( : (
<PrefetchLink
to={`/materials/${ tag.materialId }`}
className={linkClass}>
?
</PrefetchLink>))
tag.materialId != null
? (
<PrefetchLink
to={`/materials/${ tag.materialId }`}
className={linkClass}>
?
</PrefetchLink>)
: (
<PrefetchLink
to={`/tags/${ tag.id }/deerjikists`}
className={linkClass}>
?
</PrefetchLink>)))
: ( : (
['character', 'material'].includes (tag.category) ['character', 'material'].includes (tag.category)
? ( ? (
@@ -71,13 +79,23 @@ export default (({ tag,
! !
</PrefetchLink>) </PrefetchLink>)
: ( : (
<PrefetchLink
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 が存在しません.`}>
!
</PrefetchLink>))}
tag.category === 'deerjikist'
? (
<PrefetchLink
to={`/tags/${ tag.id }/deerjikists`}
className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
title={`${ tag.name } に関する情報が存在しません.`}>
!
</PrefetchLink>)
: (
<PrefetchLink
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 が存在しません.`}>
!
</PrefetchLink>)))}
</span>)} </span>)}
{nestLevel > 0 && ( {nestLevel > 0 && (
<span <span


+ 6
- 1
frontend/src/consts.ts View File

@@ -1,4 +1,4 @@
import type { Category } from 'types'
import type { Category, Platform } from 'types'


export const LIGHT_COLOUR_SHADE = 800 export const LIGHT_COLOUR_SHADE = 800
export const DARK_COLOUR_SHADE = 300 export const DARK_COLOUR_SHADE = 300
@@ -31,6 +31,11 @@ export const FETCH_POSTS_ORDER_FIELDS = [
'updated_at', 'updated_at',
] as const ] as const


export const PLATFORMS = ['nico', 'youtube'] as const

export const PLATFORM_NAMES: Record<Platform, string> =
{ nico: 'ニコニコ', youtube: 'YouTube' } as const

export const TAG_COLOUR = { export const TAG_COLOUR = {
deerjikist: 'rose', deerjikist: 'rose',
meme: 'purple', meme: 'purple',


+ 6
- 5
frontend/src/lib/queryKeys.ts View File

@@ -9,11 +9,12 @@ export const postsKeys = {
['posts', 'changes', p] as const } ['posts', 'changes', p] as const }


export const tagsKeys = { export const tagsKeys = {
root: ['tags'] as const,
index: (p: FetchTagsParams) => ['tags', 'index', p] as const,
show: (name: string) => ['tags', name] as const,
changes: (p: { id?: string; page: number; limit: number }) =>
['tags', 'changes', p] as const }
root: ['tags'] as const,
index: (p: FetchTagsParams) => ['tags', 'index', p] as const,
show: (name: string) => ['tags', name] as const,
changes: (p: { id?: string; page: number; limit: number }) =>
['tags', 'changes', p] as const,
deerjikists: (id: string) => ['tags', 'deerjikists', id] as const }


export const wikiKeys = { export const wikiKeys = {
root: ['wiki'] as const, root: ['wiki'] as const,


+ 7
- 1
frontend/src/lib/tags.ts View File

@@ -1,6 +1,6 @@
import { apiGet } from '@/lib/api' import { apiGet } from '@/lib/api'


import type { FetchTagsParams, Tag, TagVersion } from '@/types'
import type { Deerjikist, FetchTagsParams, Tag, TagVersion } from '@/types'




export const fetchTags = async ( export const fetchTags = async (
@@ -56,3 +56,9 @@ export const fetchTagChanges = async (
versions: TagVersion[] versions: TagVersion[]
count: number }> => count: number }> =>
await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } }) await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } })


export const fetchDeerjikistsByTag = async (
id: string,
): Promise<{ tag: Tag; deerjikists: Deerjikist[]}> =>
await apiGet (`/tags/${ id }/deerjikists`)

+ 155
- 0
frontend/src/pages/deerjikists/DeerjikistDetailPage.tsx View File

@@ -0,0 +1,155 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'

import TagLink from '@/components/TagLink'
import Label from '@/components/common/Label'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
import { PLATFORM_NAMES, PLATFORMS } from '@/consts'
import { apiPut } from '@/lib/api'
import { tagsKeys } from '@/lib/queryKeys'
import { fetchDeerjikistsByTag } from '@/lib/tags'
import { cn } from '@/lib/utils'

import type { FC, FormEvent } from 'react'

import type { Deerjikist, Platform } from '@/types'


export default (() => {
const { id } = useParams ()
const tagId = String (id ?? '')
const tagKey = tagsKeys.deerjikists (tagId)

const { data: qData, isLoading: loading } =
useQuery ({ queryKey: tagKey, queryFn: () => fetchDeerjikistsByTag (tagId) })
const tag = qData?.tag
const deerjikists = qData?.deerjikists ?? []

const [data, setData] =
useState<(Omit<Deerjikist, 'platform'> & { platform: Platform | null })[]> ([])
const [disabled, setDisabled] = useState (true)

const qc = useQueryClient ()

const handleSubmit = async (e: FormEvent) => {
e.preventDefault ()

try
{
setDisabled (true)

setData (await apiPut<Deerjikist[]> (`/tags/${ id }/deerjikists`, data))
qc.invalidateQueries ({ queryKey: tagsKeys.root })

toast ({ description: '更新しました.' })
}
catch
{
toast ({ title: '更新失敗', description: '入力内容を確認してください.' })
}
finally
{
setDisabled (false)
}
}

useEffect (() => {
if (!(tag))
{
setDisabled (true)
return
}

setData (deerjikists)
setDisabled (false)
}, [tag, deerjikists])

return (
<MainArea>
{(loading || !(tag)) ? 'Loading...' : (
<div className="max-w-xl">
<PageTitle>
<TagLink tag={tag} withWiki={false} withCount={false}/>
</PageTitle>

<form onSubmit={handleSubmit} className="my-4 space-y-2">
{data.map ((datum, i) => (
<fieldset key={i} className="min-w-0 rounded-lg border border-gray-300
dark:border-gray-700 p-4">
<legend className="px-2 text-sm font-semibold text-gray-700
dark:text-gray-300">
<button
type="button"
disabled={disabled}
onClick={() => setData (prev => [...prev.slice (0, i),
...prev.slice (i + 1)])}>
#{i + 1}
</button>
</legend>

{/* プラットフォーム */}
<div>
<Label>プラットフォーム</Label>
<select
className="w-full border p-2 rounded"
disabled={disabled}
value={datum.platform ?? ''}
onChange={e => setData (prev => {
const rtn = [...prev]
rtn[i] = { ...rtn[i],
platform: (e.target.value || null) as Platform | null }
return rtn
})}>
<option value="">&nbsp;</option>
{PLATFORMS.map (p => (
<option key={p} value={p}>
{PLATFORM_NAMES[p]}
</option>))}
</select>
</div>

{/* コード */}
<div>
<Label>コード</Label>
<input
type="text"
disabled={disabled}
className="w-full border p-2 rounded"
value={datum.code}
onChange={e => setData (prev => {
const rtn = [...prev]
rtn[i] = { ...rtn[i], code: e.target.value }
return rtn
})}/>
</div>
</fieldset>
))}

<div className="py-3">
<button
type="button"
disabled={disabled}
onClick={() => setData (prev => [...prev, { platform: null, code: '' }])}>
+
</button>
</div>

<div className="py-3">
<button
type="submit"
disabled={disabled}
className={cn ('px-4 py-2 rounded',
(disabled
? 'text-gray-300 bg-gray-500'
: 'text-white bg-blue-500'))}>
更新
</button>
</div>
</form>
</div>
)}
</MainArea>)
}) satisfies FC

+ 7
- 1
frontend/src/types.ts View File

@@ -1,5 +1,6 @@
import { CATEGORIES, import { CATEGORIES,
FETCH_POSTS_ORDER_FIELDS, FETCH_POSTS_ORDER_FIELDS,
PLATFORMS,
USER_ROLES, USER_ROLES,
ViewFlagBehavior } from '@/consts' ViewFlagBehavior } from '@/consts'


@@ -7,6 +8,8 @@ import type { ReactNode } from 'react'


export type Category = typeof CATEGORIES[number] export type Category = typeof CATEGORIES[number]


export type Deerjikist = { platform: Platform; code: string }

export type FetchPostsOrder = `${ FetchPostsOrderField }:${ 'asc' | 'desc' }` export type FetchPostsOrder = `${ FetchPostsOrderField }:${ 'asc' | 'desc' }`


export type FetchPostsOrderField = typeof FETCH_POSTS_ORDER_FIELDS[number] export type FetchPostsOrderField = typeof FETCH_POSTS_ORDER_FIELDS[number]
@@ -114,6 +117,8 @@ export type NiconicoViewerHandle = {
showComments: () => void showComments: () => void
hideComments: () => void } hideComments: () => void }


export type Platform = typeof PLATFORMS[number]

export type Post = { export type Post = {
id: number id: number
url: string url: string
@@ -178,7 +183,8 @@ export type Tag = {
createdAt: string createdAt: string
updatedAt: string updatedAt: string
hasWiki: boolean hasWiki: boolean
materialId: number
materialId: number | null
hasDeerjikists: boolean
children?: Tag[] children?: Tag[]
matchedAlias?: string | null } matchedAlias?: string | null }




Loading…
Cancel
Save