このコミットが含まれているのは:
@@ -6,20 +6,24 @@ import ErrorScreen from '@/components/ErrorScreen'
|
||||
import PostEmbed from '@/components/PostEmbed'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
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 SidebarComponent from '@/components/layout/SidebarComponent'
|
||||
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 { dateString } from '@/lib/utils'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, User } from 'react'
|
||||
|
||||
import type { NiconicoMetadata,
|
||||
NiconicoViewerHandle,
|
||||
Post,
|
||||
Theatre,
|
||||
TheatreComment } from '@/types'
|
||||
TheatreComment,
|
||||
TheatreProgramme } from '@/types'
|
||||
|
||||
type TheatreInfo = {
|
||||
hostFlg: boolean
|
||||
@@ -34,9 +38,36 @@ const INITIAL_THEATRE_INFO =
|
||||
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 dialogue = useDialogue ()
|
||||
|
||||
const commentsRef = useRef<HTMLDivElement> (null)
|
||||
const embedRef = useRef<NiconicoViewerHandle> (null)
|
||||
const loadingRef = useRef (false)
|
||||
@@ -52,6 +83,7 @@ const TheatreDetailPage: FC = () => {
|
||||
const [theatre, setTheatre] = useState<Theatre | null> (null)
|
||||
const [theatreInfo, setTheatreInfo] = useState<TheatreInfo> (INITIAL_THEATRE_INFO)
|
||||
const [post, setPost] = useState<Post | null> (null)
|
||||
const [programmes, setProgrammes] = useState<TheatreProgramme[]> ([])
|
||||
const [videoLength, setVideoLength] = useState (0)
|
||||
|
||||
useEffect (() => {
|
||||
@@ -118,7 +150,7 @@ const TheatreDetailPage: FC = () => {
|
||||
{
|
||||
const newComments = await apiGet<TheatreComment[]> (
|
||||
`/theatres/${ id }/comments`,
|
||||
{ params: { no_gt: lastCommentNoRef.current } })
|
||||
{ params: { no_gt: lastCommentNoRef.current, limit: '20' } })
|
||||
|
||||
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 () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [theatreInfo.postId])
|
||||
}, [id, theatreInfo.postId])
|
||||
|
||||
const syncPlayback = (meta: NiconicoMetadata) => {
|
||||
if (!(theatreInfo.postStartedAt))
|
||||
@@ -233,12 +279,30 @@ const TheatreDetailPage: FC = () => {
|
||||
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)
|
||||
return <ErrorScreen status={status}/>
|
||||
|
||||
return (
|
||||
<div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
||||
<Helmet>
|
||||
<meta name="robots" content="noindex"/>
|
||||
{theatre && (
|
||||
<title>
|
||||
{'上映会場'
|
||||
@@ -270,6 +334,22 @@ const TheatreDetailPage: FC = () => {
|
||||
</PrefetchLink>
|
||||
</div>
|
||||
</>) : '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>
|
||||
|
||||
<SidebarComponent>
|
||||
@@ -306,18 +386,36 @@ const TheatreDetailPage: FC = () => {
|
||||
className="overflow-x-hidden overflow-y-scroll text-wrap w-full
|
||||
h-[32vh] md:h-[64vh] border rounded">
|
||||
{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>
|
||||
</form>
|
||||
@@ -345,4 +443,5 @@ const TheatreDetailPage: FC = () => {
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
export default TheatreDetailPage
|
||||
|
||||
新しい課題から参照
ユーザをブロックする