| @@ -1,3 +1,7 @@ | |||
| require 'net/http' | |||
| require 'uri' | |||
| class TagsController < ApplicationController | |||
| def index | |||
| post_id = params[:post] | |||
| @@ -182,7 +186,8 @@ class TagsController < ApplicationController | |||
| .find_by(id: params[:id]) | |||
| 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 deerjikists_by_name | |||
| @@ -194,7 +199,31 @@ class TagsController < ApplicationController | |||
| .find_by(tag_names: { name: }) | |||
| 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 | |||
| def materials_by_name | |||
| @@ -391,4 +420,21 @@ class TagsController < ApplicationController | |||
| TagImplication.create!(tag:, parent_tag:) | |||
| 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 | |||
| @@ -79,6 +79,8 @@ class Tag < ApplicationRecord | |||
| def material_id = materials.first&.id | |||
| def has_deerjikists = deerjikists.present? | |||
| 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.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta) | |||
| @@ -3,7 +3,7 @@ | |||
| module TagRepr | |||
| 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 | |||
| @@ -24,6 +24,7 @@ Rails.application.routes.draw do | |||
| patch '', action: :update | |||
| get :deerjikists | |||
| put :deerjikists, action: :update_deerjikists | |||
| end | |||
| end | |||
| @@ -8,23 +8,35 @@ RSpec.describe 'Tags deerjikists API', type: :request do | |||
| let!(:tag) { create(:tag, category: :deerjikist) } | |||
| let(:member) { create(:user, :member) } | |||
| let(:guest) { create(:user, role: :guest) } | |||
| before do | |||
| # show_by_name / deerjikists_by_name 用に名前を固定 | |||
| tag.tag_name.update!(name: 'deerjika') | |||
| end | |||
| describe 'GET /tags/:id/deerjikists' do | |||
| subject(:do_request) do | |||
| get "/tags/#{ tag_id }/deerjikists" | |||
| get "/tags/#{tag_id}/deerjikists" | |||
| end | |||
| let(:tag_id) { tag.id } | |||
| 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 | |||
| 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 | |||
| @@ -34,17 +46,27 @@ RSpec.describe 'Tags deerjikists API', type: :request do | |||
| Deerjikist.create!(platform: platform2, code: code2, tag: tag) | |||
| end | |||
| it 'returns 200 and deerjikists array' do | |||
| it 'returns 200 with tag and deerjikists array' do | |||
| do_request | |||
| 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 | |||
| @@ -53,6 +75,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do | |||
| it 'returns 404' do | |||
| do_request | |||
| expect(response).to have_http_status(:not_found) | |||
| end | |||
| end | |||
| @@ -60,7 +83,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do | |||
| describe 'GET /tags/name/:name/deerjikists' do | |||
| subject(:do_request) do | |||
| get "/tags/name/#{ name }/deerjikists" | |||
| get "/tags/name/#{name}/deerjikists" | |||
| end | |||
| let(:name) { 'deerjika' } | |||
| @@ -70,6 +93,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do | |||
| it 'returns 400' do | |||
| do_request | |||
| expect(response).to have_http_status(:bad_request) | |||
| end | |||
| end | |||
| @@ -79,23 +103,209 @@ RSpec.describe 'Tags deerjikists API', type: :request do | |||
| it 'returns 404' do | |||
| do_request | |||
| expect(response).to have_http_status(:not_found) | |||
| 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 | |||
| before do | |||
| Deerjikist.create!(platform: platform1, code: code1, tag: tag) | |||
| end | |||
| it 'returns 200 and deerjikists array' do | |||
| it 'returns 200 with tag and deerjikists array' do | |||
| do_request | |||
| 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 | |||
| @@ -10,6 +10,7 @@ import RouteBlockerOverlay from '@/components/RouteBlockerOverlay' | |||
| import TopNav from '@/components/TopNav' | |||
| import { Toaster } from '@/components/ui/toaster' | |||
| import { apiPost, isApiError } from '@/lib/api' | |||
| import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage' | |||
| import MaterialBasePage from '@/pages/materials/MaterialBasePage' | |||
| import MaterialDetailPage from '@/pages/materials/MaterialDetailPage' | |||
| import MaterialListPage from '@/pages/materials/MaterialListPage' | |||
| @@ -58,6 +59,7 @@ const RouteTransitionWrapper = ({ user, setUser }: { | |||
| <Route path="/posts/changes" element={<PostHistoryPage/>}/> | |||
| <Route path="/tags" element={<TagListPage/>}/> | |||
| <Route path="/tags/:id" element={<TagDetailPage/>}/> | |||
| <Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/> | |||
| <Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/> | |||
| <Route path="/tags/changes" element={<TagHistoryPage/>}/> | |||
| <Route path="/theatres/:id" element={<TheatreDetailPage/>}/> | |||
| @@ -45,9 +45,9 @@ export default (({ tag, | |||
| <> | |||
| {(linkFlg && withWiki) && ( | |||
| <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 | |||
| to={`/wiki/${ encodeURIComponent (tag.name) }`} | |||
| @@ -55,11 +55,19 @@ export default (({ tag, | |||
| ? | |||
| </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) | |||
| ? ( | |||
| @@ -71,13 +79,23 @@ export default (({ tag, | |||
| ! | |||
| </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>)} | |||
| {nestLevel > 0 && ( | |||
| <span | |||
| @@ -1,4 +1,4 @@ | |||
| import type { Category } from 'types' | |||
| import type { Category, Platform } from 'types' | |||
| export const LIGHT_COLOUR_SHADE = 800 | |||
| export const DARK_COLOUR_SHADE = 300 | |||
| @@ -31,6 +31,11 @@ export const FETCH_POSTS_ORDER_FIELDS = [ | |||
| 'updated_at', | |||
| ] 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 = { | |||
| deerjikist: 'rose', | |||
| meme: 'purple', | |||
| @@ -9,11 +9,12 @@ export const postsKeys = { | |||
| ['posts', 'changes', p] as const } | |||
| 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 = { | |||
| root: ['wiki'] as const, | |||
| @@ -1,6 +1,6 @@ | |||
| import { apiGet } from '@/lib/api' | |||
| import type { FetchTagsParams, Tag, TagVersion } from '@/types' | |||
| import type { Deerjikist, FetchTagsParams, Tag, TagVersion } from '@/types' | |||
| export const fetchTags = async ( | |||
| @@ -56,3 +56,9 @@ export const fetchTagChanges = async ( | |||
| versions: TagVersion[] | |||
| count: number }> => | |||
| 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`) | |||
| @@ -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=""> </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 | |||
| @@ -1,5 +1,6 @@ | |||
| import { CATEGORIES, | |||
| FETCH_POSTS_ORDER_FIELDS, | |||
| PLATFORMS, | |||
| USER_ROLES, | |||
| ViewFlagBehavior } from '@/consts' | |||
| @@ -7,6 +8,8 @@ import type { ReactNode } from 'react' | |||
| export type Category = typeof CATEGORIES[number] | |||
| export type Deerjikist = { platform: Platform; code: string } | |||
| export type FetchPostsOrder = `${ FetchPostsOrderField }:${ 'asc' | 'desc' }` | |||
| export type FetchPostsOrderField = typeof FETCH_POSTS_ORDER_FIELDS[number] | |||
| @@ -114,6 +117,8 @@ export type NiconicoViewerHandle = { | |||
| showComments: () => void | |||
| hideComments: () => void } | |||
| export type Platform = typeof PLATFORMS[number] | |||
| export type Post = { | |||
| id: number | |||
| url: string | |||
| @@ -178,7 +183,8 @@ export type Tag = { | |||
| createdAt: string | |||
| updatedAt: string | |||
| hasWiki: boolean | |||
| materialId: number | |||
| materialId: number | null | |||
| hasDeerjikists: boolean | |||
| children?: Tag[] | |||
| matchedAlias?: string | null } | |||