| @@ -44,7 +44,8 @@ class PostsController < ApplicationController | |||||
| filtered_posts | filtered_posts | ||||
| .joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id") | .joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id") | ||||
| .reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all")) | .reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all")) | ||||
| .preload(tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) | |||||
| .preload(:parents, :children, | |||||
| tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) | |||||
| .with_attached_thumbnail | .with_attached_thumbnail | ||||
| q = q.where('posts.url LIKE ?', "%#{ url }%") if url | q = q.where('posts.url LIKE ?', "%#{ url }%") if url | ||||
| @@ -95,7 +96,8 @@ class PostsController < ApplicationController | |||||
| end | end | ||||
| def random | def random | ||||
| post = filtered_posts.preload(tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) | |||||
| post = filtered_posts.preload(:parents, :childern, | |||||
| tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) | |||||
| .order('RAND()') | .order('RAND()') | ||||
| .first | .first | ||||
| return head :not_found unless post | return head :not_found unless post | ||||
| @@ -104,7 +106,11 @@ class PostsController < ApplicationController | |||||
| end | end | ||||
| def show | def show | ||||
| post = Post.includes(tags: [:deerjikists, :materials, { tag_name: :wiki_page }]).find_by(id: params[:id]) | |||||
| post = Post | |||||
| .preload(:parents, :children) | |||||
| .includes(:parents, :children, | |||||
| tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) | |||||
| .find_by(id: params[:id]) | |||||
| return head :not_found unless post | return head :not_found unless post | ||||
| render json: PostRepr.base(post, current_user) | render json: PostRepr.base(post, current_user) | ||||
| @@ -1,14 +1,21 @@ | |||||
| class TheatreCommentsController < ApplicationController | class TheatreCommentsController < ApplicationController | ||||
| def index | def index | ||||
| limit = params[:limit].to_i | |||||
| limit = 20 if limit <= 0 | |||||
| no_gt = params[:no_gt].to_i | no_gt = params[:no_gt].to_i | ||||
| no_gt = 0 if no_gt.negative? | |||||
| no_gt = 0 if no_gt < 0 | |||||
| comments = TheatreComment | comments = TheatreComment | ||||
| .where(theatre_id: params[:theatre_id]) | .where(theatre_id: params[:theatre_id]) | ||||
| .where('no > ?', no_gt) | .where('no > ?', no_gt) | ||||
| .order(no: :desc) | .order(no: :desc) | ||||
| .limit(limit) | |||||
| render json: comments.as_json(include: { user: { only: [:id, :name] } }) | |||||
| render json: comments.map { | |||||
| _1.as_json(include: { user: { only: [:id, :name] } }) | |||||
| .merge(content: _1.discarded? ? nil : _1.content, deleted: _1.discarded?) | |||||
| } | |||||
| end | end | ||||
| def create | def create | ||||
| @@ -29,4 +36,18 @@ class TheatreCommentsController < ApplicationController | |||||
| render json: comment, status: :created | render json: comment, status: :created | ||||
| end | end | ||||
| def destroy | |||||
| return head :unauthorized unless current_user | |||||
| theatre_id = params[:theatre_id].to_i | |||||
| no = params[:id].to_i | |||||
| comment = TheatreComment.find_by(theatre_id:, no:) | |||||
| return head :not_found unless comment | |||||
| comment.discard! | |||||
| head :no_content | |||||
| end | |||||
| end | end | ||||
| @@ -0,0 +1,17 @@ | |||||
| class TheatreProgrammesController < ApplicationController | |||||
| def index | |||||
| limit = params[:limit].to_i | |||||
| limit = 100 if limit <= 0 | |||||
| position_gt = params[:position_gt].to_i | |||||
| position_gt = 0 if position_gt < 0 | |||||
| programmes = TheatreProgramme | |||||
| .where(theatre_id: params[:theatre_id]) | |||||
| .where('position > ?', position_gt) | |||||
| .order(position: :desc).limit(100) | |||||
| .limit(limit) | |||||
| render json: programmes.as_json(include: { post: PostRepr::BASE }) | |||||
| end | |||||
| end | |||||
| @@ -43,11 +43,14 @@ class TheatresController < ApplicationController | |||||
| return head :not_found unless theatre | return head :not_found unless theatre | ||||
| return head :forbidden if theatre.host_user != current_user | return head :forbidden if theatre.host_user != current_user | ||||
| post = Post.where("url LIKE '%nicovideo.jp%'") | |||||
| .or(Post.where("url LIKE '%youtube.com%'")) | |||||
| .order('RAND()') | |||||
| .first | |||||
| theatre.update!(current_post: post, current_post_started_at: Time.current) | |||||
| ApplicationRecord.transaction do | |||||
| post = Post.where("url LIKE '%nicovideo.jp%'") | |||||
| .order('RAND()') | |||||
| .first | |||||
| theatre.update!(current_post: post, current_post_started_at: Time.current) | |||||
| position = (theatre.programmes.maximum(:position) || 0) + 1 | |||||
| theatre.programmes.create!(position:, post:) | |||||
| end | |||||
| head :no_content | head :no_content | ||||
| end | end | ||||
| @@ -81,7 +81,7 @@ class Tag < ApplicationRecord | |||||
| def material_id = materials.first&.id | def material_id = materials.first&.id | ||||
| def has_deerjikists = deerjikists.present? | |||||
| def has_deerjikists = deerjikists.exists? | |||||
| 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) | ||||
| @@ -7,6 +7,8 @@ class Theatre < ApplicationRecord | |||||
| class_name: 'TheatreWatchingUser', inverse_of: :theatre | class_name: 'TheatreWatchingUser', inverse_of: :theatre | ||||
| has_many :watching_users, through: :active_theatre_watching_users, source: :user | has_many :watching_users, through: :active_theatre_watching_users, source: :user | ||||
| has_many :programmes, class_name: 'TheatreProgramme' | |||||
| belongs_to :host_user, class_name: 'User', optional: true | belongs_to :host_user, class_name: 'User', optional: true | ||||
| belongs_to :current_post, class_name: 'Post', optional: true | belongs_to :current_post, class_name: 'Post', optional: true | ||||
| belongs_to :created_by_user, class_name: 'User' | belongs_to :created_by_user, class_name: 'User' | ||||
| @@ -0,0 +1,6 @@ | |||||
| class TheatreProgramme < ApplicationRecord | |||||
| self.primary_key = :theatre_id, :position | |||||
| belongs_to :theatre | |||||
| belongs_to :post | |||||
| end | |||||
| @@ -87,7 +87,8 @@ Rails.application.routes.draw do | |||||
| patch :next_post | patch :next_post | ||||
| end | end | ||||
| resources :comments, controller: :theatre_comments, only: [:index, :create] | |||||
| resources :comments, controller: :theatre_comments, only: [:index, :create, :destroy] | |||||
| resources :programmes, controller: :theatre_programmes, only: [:index] | |||||
| end | end | ||||
| resources :materials, only: [:index, :show, :create, :update, :destroy] | resources :materials, only: [:index, :show, :create, :update, :destroy] | ||||
| @@ -0,0 +1,10 @@ | |||||
| class CreateTheatreProgrammes < ActiveRecord::Migration[8.0] | |||||
| def change | |||||
| create_table :theatre_programmes, primary_key: [:theatre_id, :position] do |t| | |||||
| t.references :theatre, null: false, foreign_key: true | |||||
| t.integer :position, null: false | |||||
| t.references :post, null: false, foreign_key: true | |||||
| t.datetime :created_at, null: false | |||||
| end | |||||
| end | |||||
| end | |||||
| @@ -10,7 +10,7 @@ | |||||
| # | # | ||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||
| ActiveRecord::Schema[8.0].define(version: 2026_05_07_213300) do | |||||
| ActiveRecord::Schema[8.0].define(version: 2026_05_14_221900) do | |||||
| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| t.string "name", null: false | t.string "name", null: false | ||||
| t.string "record_type", null: false | t.string "record_type", null: false | ||||
| @@ -283,6 +283,15 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_07_213300) do | |||||
| t.index ["user_id"], name: "index_theatre_comments_on_user_id" | t.index ["user_id"], name: "index_theatre_comments_on_user_id" | ||||
| end | end | ||||
| create_table "theatre_programmes", primary_key: ["theatre_id", "position"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | |||||
| t.bigint "theatre_id", null: false | |||||
| t.integer "position", null: false | |||||
| t.bigint "post_id", null: false | |||||
| t.datetime "created_at", null: false | |||||
| t.index ["post_id"], name: "index_theatre_programmes_on_post_id" | |||||
| t.index ["theatre_id"], name: "index_theatre_programmes_on_theatre_id" | |||||
| end | |||||
| create_table "theatre_watching_users", primary_key: ["theatre_id", "user_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "theatre_watching_users", primary_key: ["theatre_id", "user_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| t.bigint "theatre_id", null: false | t.bigint "theatre_id", null: false | ||||
| t.bigint "user_id", null: false | t.bigint "user_id", null: false | ||||
| @@ -290,6 +299,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_07_213300) do | |||||
| t.datetime "created_at", null: false | t.datetime "created_at", null: false | ||||
| t.datetime "updated_at", null: false | t.datetime "updated_at", null: false | ||||
| t.index ["expires_at"], name: "index_theatre_watching_users_on_expires_at" | t.index ["expires_at"], name: "index_theatre_watching_users_on_expires_at" | ||||
| t.index ["theatre_id", "expires_at"], name: "idx_on_theatre_id_skip_expires_at_4c8de1dd42" | |||||
| t.index ["theatre_id", "expires_at"], name: "index_theatre_watching_users_on_theatre_id_and_expires_at" | t.index ["theatre_id", "expires_at"], name: "index_theatre_watching_users_on_theatre_id_and_expires_at" | ||||
| t.index ["theatre_id"], name: "index_theatre_watching_users_on_theatre_id" | t.index ["theatre_id"], name: "index_theatre_watching_users_on_theatre_id" | ||||
| t.index ["user_id"], name: "index_theatre_watching_users_on_user_id" | t.index ["user_id"], name: "index_theatre_watching_users_on_user_id" | ||||
| @@ -464,6 +474,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_07_213300) do | |||||
| add_foreign_key "tags", "tag_names" | add_foreign_key "tags", "tag_names" | ||||
| add_foreign_key "theatre_comments", "theatres" | add_foreign_key "theatre_comments", "theatres" | ||||
| add_foreign_key "theatre_comments", "users" | add_foreign_key "theatre_comments", "users" | ||||
| add_foreign_key "theatre_programmes", "posts" | |||||
| add_foreign_key "theatre_programmes", "theatres" | |||||
| add_foreign_key "theatre_watching_users", "theatres" | add_foreign_key "theatre_watching_users", "theatres" | ||||
| add_foreign_key "theatre_watching_users", "users" | add_foreign_key "theatre_watching_users", "users" | ||||
| add_foreign_key "theatres", "posts", column: "current_post_id" | add_foreign_key "theatres", "posts", column: "current_post_id" | ||||
| @@ -63,7 +63,7 @@ const RouteTransitionWrapper = ({ user, setUser }: { | |||||
| <Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/> | <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 user={user}/>}/> | |||||
| <Route path="/materials" element={<MaterialBasePage/>}> | <Route path="/materials" element={<MaterialBasePage/>}> | ||||
| <Route index element={<MaterialListPage/>}/> | <Route index element={<MaterialListPage/>}/> | ||||
| <Route path="new" element={<MaterialNewPage/>}/> | <Route path="new" element={<MaterialNewPage/>}/> | ||||
| @@ -158,4 +158,4 @@ const App: FC = () => { | |||||
| </>) | </>) | ||||
| } | } | ||||
| export default App | |||||
| export default App | |||||
| @@ -55,12 +55,6 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: { | |||||
| { name: '追加', to: '/materials/new' }, | { name: '追加', to: '/materials/new' }, | ||||
| { name: '全体履歴', to: '/materials/changes', visible: false }, | { name: '全体履歴', to: '/materials/changes', visible: false }, | ||||
| { name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] }, | { name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] }, | ||||
| { name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [ | |||||
| { name: <>第 1 会場</>, to: '/theatres/1' }, | |||||
| { name: 'CyTube', to: '//cytube.mm428.net/r/deernijika' }, | |||||
| { name: <>ニジカ放送局第 1 チャンネル</>, | |||||
| to: '//www.youtube.com/watch?v=DCU3hL4Uu6A' }, | |||||
| { name: 'ヘルプ', to: '/wiki/ヘルプ:上映会' }] }, | |||||
| { name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [ | { name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [ | ||||
| { name: '検索', to: '/wiki' }, | { name: '検索', to: '/wiki' }, | ||||
| { name: '新規', to: '/wiki/new' }, | { name: '新規', to: '/wiki/new' }, | ||||
| @@ -71,6 +65,8 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: { | |||||
| visible: wikiPageFlg }, | visible: wikiPageFlg }, | ||||
| { name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg }, | { name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg }, | ||||
| { name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] }, | { name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] }, | ||||
| { name: 'おたのしみ', visible: false, subMenu: [ | |||||
| { name: '上映会 (β)', to: '/theatres/1' }] }, | |||||
| { name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [ | { name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [ | ||||
| { name: '一覧', to: '/users', visible: false }, | { name: '一覧', to: '/users', visible: false }, | ||||
| { name: 'お前', to: `/users/${ user?.id }`, visible: false }, | { name: 'お前', to: `/users/${ user?.id }`, visible: false }, | ||||
| @@ -267,7 +263,7 @@ const TopNav: FC<Props> = ({ user }) => { | |||||
| : { initial: { x: 40, y: -40, opacity: 0 }, | : { initial: { x: 40, y: -40, opacity: 0 }, | ||||
| animate: { x: 0, y: 0, opacity: 1 }, | animate: { x: 0, y: 0, opacity: 1 }, | ||||
| exit: { x: 40, y: -40, opacity: 0 } })} | exit: { x: 40, y: -40, opacity: 0 } })} | ||||
| className="z-10 h-full flex items-center px-3 font-bold w-24"> | |||||
| className="z-10 h-full flex items-center px-3 font-bold w-28"> | |||||
| <h2>{item.name}</h2> | <h2>{item.name}</h2> | ||||
| </motion.div> | </motion.div> | ||||
| {item.subMenu | {item.subMenu | ||||
| @@ -6,20 +6,24 @@ import ErrorScreen from '@/components/ErrorScreen' | |||||
| import PostEmbed from '@/components/PostEmbed' | import PostEmbed from '@/components/PostEmbed' | ||||
| import PrefetchLink from '@/components/PrefetchLink' | import PrefetchLink from '@/components/PrefetchLink' | ||||
| import TagDetailSidebar from '@/components/TagDetailSidebar' | import TagDetailSidebar from '@/components/TagDetailSidebar' | ||||
| import SectionTitle from '@/components/common/SectionTitle' | |||||
| import { useDialogue } from '@/components/dialogues/DialogueProvider' | |||||
| import { Button } from '@/components/ui/button' | |||||
| import MainArea from '@/components/layout/MainArea' | import MainArea from '@/components/layout/MainArea' | ||||
| import SidebarComponent from '@/components/layout/SidebarComponent' | import SidebarComponent from '@/components/layout/SidebarComponent' | ||||
| import { SITE_TITLE } from '@/config' | import { SITE_TITLE } from '@/config' | ||||
| import { apiGet, apiPatch, apiPost, apiPut, isApiError } from '@/lib/api' | |||||
| import { apiGet, apiDelete, apiPatch, apiPost, apiPut, isApiError } from '@/lib/api' | |||||
| import { fetchPost } from '@/lib/posts' | import { fetchPost } from '@/lib/posts' | ||||
| import { dateString } from '@/lib/utils' | import { dateString } from '@/lib/utils' | ||||
| import type { FC } from 'react' | |||||
| import type { FC, User } from 'react' | |||||
| import type { NiconicoMetadata, | import type { NiconicoMetadata, | ||||
| NiconicoViewerHandle, | NiconicoViewerHandle, | ||||
| Post, | Post, | ||||
| Theatre, | Theatre, | ||||
| TheatreComment } from '@/types' | |||||
| TheatreComment, | |||||
| TheatreProgramme } from '@/types' | |||||
| type TheatreInfo = { | type TheatreInfo = { | ||||
| hostFlg: boolean | hostFlg: boolean | ||||
| @@ -34,9 +38,36 @@ const INITIAL_THEATRE_INFO = | |||||
| watchingUsers: [] as { id: number; name: string }[] } as const | watchingUsers: [] as { id: number; name: string }[] } as const | ||||
| const TheatreDetailPage: FC = () => { | |||||
| const commentBox: ReactNode[] = (comment: TheatreComment) => | |||||
| [( | |||||
| <div key={`${ comment.no }-content`} className="w-full"> | |||||
| {comment.deleted | |||||
| ? ( | |||||
| <span className="text-sm font-bold"> | |||||
| 削除されました. | |||||
| </span>) | |||||
| : comment.content} | |||||
| </div>), | |||||
| ( | |||||
| <div key={`${ comment.no }-user`} className="w-full text-sm text-right"> | |||||
| by {comment.user | |||||
| ? (comment.user.name || `名もなきニジラー(#${ comment.user.id })`) | |||||
| : '運営'} | |||||
| </div>), | |||||
| ( | |||||
| <div key={`${ comment.no }-createdAt`} className="w-full text-sm text-right"> | |||||
| {dateString (comment.createdAt)} | |||||
| </div>)] | |||||
| type Props = { user: User } | |||||
| const TheatreDetailPage: FC<Props> = ({ user }: Props) => { | |||||
| const { id } = useParams () | const { id } = useParams () | ||||
| const dialogue = useDialogue () | |||||
| const commentsRef = useRef<HTMLDivElement> (null) | const commentsRef = useRef<HTMLDivElement> (null) | ||||
| const embedRef = useRef<NiconicoViewerHandle> (null) | const embedRef = useRef<NiconicoViewerHandle> (null) | ||||
| const loadingRef = useRef (false) | const loadingRef = useRef (false) | ||||
| @@ -52,6 +83,7 @@ const TheatreDetailPage: FC = () => { | |||||
| const [theatre, setTheatre] = useState<Theatre | null> (null) | const [theatre, setTheatre] = useState<Theatre | null> (null) | ||||
| const [theatreInfo, setTheatreInfo] = useState<TheatreInfo> (INITIAL_THEATRE_INFO) | const [theatreInfo, setTheatreInfo] = useState<TheatreInfo> (INITIAL_THEATRE_INFO) | ||||
| const [post, setPost] = useState<Post | null> (null) | const [post, setPost] = useState<Post | null> (null) | ||||
| const [programmes, setProgrammes] = useState<TheatreProgramme[]> ([]) | |||||
| const [videoLength, setVideoLength] = useState (0) | const [videoLength, setVideoLength] = useState (0) | ||||
| useEffect (() => { | useEffect (() => { | ||||
| @@ -118,7 +150,7 @@ const TheatreDetailPage: FC = () => { | |||||
| { | { | ||||
| const newComments = await apiGet<TheatreComment[]> ( | const newComments = await apiGet<TheatreComment[]> ( | ||||
| `/theatres/${ id }/comments`, | `/theatres/${ id }/comments`, | ||||
| { params: { no_gt: lastCommentNoRef.current } }) | |||||
| { params: { no_gt: lastCommentNoRef.current, limit: '20' } }) | |||||
| if (!(cancelled) && newComments.length > 0) | if (!(cancelled) && newComments.length > 0) | ||||
| { | { | ||||
| @@ -214,10 +246,24 @@ const TheatreDetailPage: FC = () => { | |||||
| } | } | ||||
| }) () | }) () | ||||
| void (async () => { | |||||
| try | |||||
| { | |||||
| const data = await apiGet<TheatreProgramme[]> ( | |||||
| `/theatres/${ id }/programmes`, { params: { limit: '100' } }) | |||||
| if (!(cancelled)) | |||||
| setProgrammes (data) | |||||
| } | |||||
| catch (e) | |||||
| { | |||||
| console.error (e) | |||||
| } | |||||
| }) () | |||||
| return () => { | return () => { | ||||
| cancelled = true | cancelled = true | ||||
| } | } | ||||
| }, [theatreInfo.postId]) | |||||
| }, [id, theatreInfo.postId]) | |||||
| const syncPlayback = (meta: NiconicoMetadata) => { | const syncPlayback = (meta: NiconicoMetadata) => { | ||||
| if (!(theatreInfo.postStartedAt)) | if (!(theatreInfo.postStartedAt)) | ||||
| @@ -233,12 +279,30 @@ const TheatreDetailPage: FC = () => { | |||||
| embedRef.current?.seek (targetTime) | embedRef.current?.seek (targetTime) | ||||
| } | } | ||||
| const handleDelete = async (commentNo: number) => { | |||||
| try | |||||
| { | |||||
| await apiDelete (`/theatres/${ id }/comments/${ commentNo }`) | |||||
| setComments (prev => { | |||||
| const rtn = [...prev] | |||||
| const idx = rtn.findIndex (x => x.no === commentNo) | |||||
| rtn[idx] = { ...rtn[idx], deleted: true } | |||||
| return rtn | |||||
| }) | |||||
| } | |||||
| catch | |||||
| { | |||||
| ; | |||||
| } | |||||
| } | |||||
| if (status >= 400) | if (status >= 400) | ||||
| return <ErrorScreen status={status}/> | return <ErrorScreen status={status}/> | ||||
| return ( | return ( | ||||
| <div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden"> | <div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden"> | ||||
| <Helmet> | <Helmet> | ||||
| <meta name="robots" content="noindex"/> | |||||
| {theatre && ( | {theatre && ( | ||||
| <title> | <title> | ||||
| {'上映会場' | {'上映会場' | ||||
| @@ -270,6 +334,22 @@ const TheatreDetailPage: FC = () => { | |||||
| </PrefetchLink> | </PrefetchLink> | ||||
| </div> | </div> | ||||
| </>) : 'Loading...'} | </>) : 'Loading...'} | ||||
| <div> | |||||
| <SectionTitle> | |||||
| 上映履歴 | |||||
| </SectionTitle> | |||||
| <div> | |||||
| {programmes.map ((programme, i) => ( | |||||
| <div key={i}> | |||||
| <PrefetchLink to={`/posts/${ programme.post.id }`}> | |||||
| {programme.post.title} | |||||
| </PrefetchLink> | |||||
| ({dateString (programme.createdAt)}) | |||||
| </div>))} | |||||
| </div> | |||||
| </div> | |||||
| </MainArea> | </MainArea> | ||||
| <SidebarComponent> | <SidebarComponent> | ||||
| @@ -306,18 +386,36 @@ const TheatreDetailPage: FC = () => { | |||||
| className="overflow-x-hidden overflow-y-scroll text-wrap w-full | className="overflow-x-hidden overflow-y-scroll text-wrap w-full | ||||
| h-[32vh] md:h-[64vh] border rounded"> | h-[32vh] md:h-[64vh] border rounded"> | ||||
| {comments.map (comment => ( | {comments.map (comment => ( | ||||
| <div key={comment.no} className="p-2"> | |||||
| <div className="w-full"> | |||||
| {comment.content} | |||||
| </div> | |||||
| <div className="w-full text-sm text-right"> | |||||
| by {comment.user | |||||
| ? (comment.user.name || `名もなきニジラー(#${ comment.user.id })`) | |||||
| : '運営'} | |||||
| </div> | |||||
| <div className="w-full text-sm text-right"> | |||||
| {dateString (comment.createdAt)} | |||||
| </div> | |||||
| <div | |||||
| key={comment.no} | |||||
| className="p-2 group relative rounded py-1 hover:bg-gray-100 | |||||
| dark:hover:bg-gray-800"> | |||||
| {(user && comment.user?.id === user.id && !(comment.deleted)) && ( | |||||
| <button | |||||
| type="button" | |||||
| className="absolute left-1 top-1 hidden rounded text-md text-red-600 | |||||
| hover:bg-red-100 group-hover:inline-block | |||||
| dark:text-red-300 dark:hover:bg-red-950" | |||||
| aria-label="コメントを削除" | |||||
| onClick={async e => { | |||||
| e.stopPropagation () | |||||
| if (!(await dialogue.confirm ({ | |||||
| title: 'このコメントを削除しますか?', | |||||
| description: ( | |||||
| <div className="border border-black dark:border-white rounded | |||||
| my-3 p-2 w-64"> | |||||
| {commentBox (comment)} | |||||
| </div>), | |||||
| confirmText: '削除', | |||||
| variant: 'danger' }))) | |||||
| return | |||||
| await handleDelete (comment.no) | |||||
| }}> | |||||
| × | |||||
| </button>)} | |||||
| {commentBox (comment)} | |||||
| </div>))} | </div>))} | ||||
| </div> | </div> | ||||
| </form> | </form> | ||||
| @@ -345,4 +443,5 @@ const TheatreDetailPage: FC = () => { | |||||
| </div>) | </div>) | ||||
| } | } | ||||
| export default TheatreDetailPage | export default TheatreDetailPage | ||||
| @@ -210,11 +210,24 @@ export type Theatre = { | |||||
| createdAt: string | createdAt: string | ||||
| updatedAt: string } | updatedAt: string } | ||||
| export type TheatreComment = { | |||||
| theatreId: number, | |||||
| no: number, | |||||
| user: { id: number, name: string } | null | |||||
| content: string | |||||
| export type TheatreComment = | |||||
| | { theatreId: number | |||||
| no: number | |||||
| deteled: false | |||||
| user: { id: number, name: string } | null | |||||
| content: string | |||||
| createdAt: string } | |||||
| | { theatreId: number | |||||
| no: number | |||||
| deleted: true | |||||
| user: { id: number, name: string } | null | |||||
| content null, | |||||
| createdAt: string } | |||||
| export type TheatreProgramme = { | |||||
| theatreId: number | |||||
| position: number | |||||
| post: Post | |||||
| createdAt: string } | createdAt: string } | ||||
| export type User = { | export type User = { | ||||