This commit is contained in:
@@ -63,7 +63,7 @@ const RouteTransitionWrapper = ({ user, setUser }: {
|
||||
<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/>}/>
|
||||
<Route path="/theatres/:id" element={<TheatreDetailPage user={user}/>}/>
|
||||
<Route path="/materials" element={<MaterialBasePage/>}>
|
||||
<Route index element={<MaterialListPage/>}/>
|
||||
<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/changes', visible: false },
|
||||
{ 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: '検索', to: '/wiki' },
|
||||
{ name: '新規', to: '/wiki/new' },
|
||||
@@ -71,6 +65,8 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
|
||||
visible: wikiPageFlg },
|
||||
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, 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', 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 },
|
||||
animate: { x: 0, y: 0, opacity: 1 },
|
||||
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>
|
||||
</motion.div>
|
||||
{item.subMenu
|
||||
|
||||
@@ -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
|
||||
|
||||
+18
-5
@@ -210,11 +210,24 @@ export type Theatre = {
|
||||
createdAt: 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 }
|
||||
|
||||
export type User = {
|
||||
|
||||
Reference in New Issue
Block a user