This commit is contained in:
2026-05-17 21:09:43 +09:00
parent dc54f9cbb5
commit 09763982b5
14 changed files with 231 additions and 45 deletions
+2 -2
View File
@@ -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
+3 -7
View File
@@ -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: <>&thinsp;1&thinsp;</>, to: '/theatres/1' },
{ name: 'CyTube', to: '//cytube.mm428.net/r/deernijika' },
{ name: <>&thinsp;1&thinsp;</>,
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
+117 -18
View File
@@ -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)
}}>
&times;
</button>)}
{commentBox (comment)}
</div>))}
</div>
</form>
@@ -345,4 +443,5 @@ const TheatreDetailPage: FC = () => {
</div>)
}
export default TheatreDetailPage
+18 -5
View File
@@ -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 = {