コミットを比較
2 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 3f2cddcd07 | |||
| 88b0195e03 |
@@ -488,25 +488,23 @@ const MaterialSidebar: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:block">
|
<SidebarComponent sidebarKey="materials" className="hidden md:block">
|
||||||
<SidebarComponent sidebarKey="materials">
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<MaterialFilterButtons
|
||||||
<MaterialFilterButtons
|
materialFilter={materialFilter}
|
||||||
materialFilter={materialFilter}
|
onChange={handleFilterChange}/>
|
||||||
onChange={handleFilterChange}/>
|
{isLoading && (
|
||||||
{isLoading && (
|
<p className="text-sm text-neutral-500 dark:text-stone-400">読込中……</p>)}
|
||||||
<p className="text-sm text-neutral-500 dark:text-stone-400">読込中……</p>)}
|
{isError && (
|
||||||
{isError && (
|
<p className="text-sm text-red-600 dark:text-red-300">
|
||||||
<p className="text-sm text-red-600 dark:text-red-300">
|
タグ一覧の取得に失敗しました.
|
||||||
タグ一覧の取得に失敗しました.
|
</p>)}
|
||||||
</p>)}
|
{(!isLoading && !isError) && (
|
||||||
{(!isLoading && !isError) && (
|
<ul>
|
||||||
<ul>
|
{renderDesktopTree (visibleRootTags)}
|
||||||
{renderDesktopTree (visibleRootTags)}
|
</ul>)}
|
||||||
</ul>)}
|
</div>
|
||||||
</div>
|
</SidebarComponent>
|
||||||
</SidebarComponent>
|
|
||||||
</div>
|
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,10 +146,13 @@ const DropSlot = ({ cat }: { cat: Category }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Props = { post: Post; sp?: boolean }
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
post: Post
|
||||||
|
sp?: boolean }
|
||||||
|
|
||||||
|
|
||||||
const TagDetailSidebar: FC<Props> = ({ post, sp }) => {
|
const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
|
||||||
sp = Boolean (sp)
|
sp = Boolean (sp)
|
||||||
|
|
||||||
const qc = useQueryClient ()
|
const qc = useQueryClient ()
|
||||||
@@ -278,7 +281,7 @@ const TagDetailSidebar: FC<Props> = ({ post, sp }) => {
|
|||||||
}, [baseTags])
|
}, [baseTags])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarComponent sidebarKey="post-detail-tags">
|
<SidebarComponent sidebarKey="post-detail-tags" className={className}>
|
||||||
<TagSearch/>
|
<TagSearch/>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { HelmetProvider } from 'react-helmet-async'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||||
|
|
||||||
|
|
||||||
|
const sidebarWidth = (element: Element | null): string =>
|
||||||
|
(element as HTMLElement).style.getPropertyValue ('--sidebar-width')
|
||||||
|
|
||||||
|
|
||||||
|
const sidebarRoot = (container: HTMLElement): Element =>
|
||||||
|
container.querySelector ('[style*="--sidebar-width"]')!
|
||||||
|
|
||||||
|
|
||||||
|
const sidebarTree = (props: Partial<Parameters<typeof SidebarComponent>[0]>) => (
|
||||||
|
<HelmetProvider>
|
||||||
|
<div data-sidebar-container>
|
||||||
|
<SidebarComponent sidebarKey="test-sidebar" {...props}>
|
||||||
|
Sidebar body
|
||||||
|
</SidebarComponent>
|
||||||
|
</div>
|
||||||
|
</HelmetProvider>)
|
||||||
|
|
||||||
|
|
||||||
|
const renderSidebar = (
|
||||||
|
props: Partial<Parameters<typeof SidebarComponent>[0]> = {},
|
||||||
|
containerWidth = 800,
|
||||||
|
) => {
|
||||||
|
Object.defineProperty (HTMLElement.prototype, 'clientWidth', {
|
||||||
|
configurable: true,
|
||||||
|
get () {
|
||||||
|
return this.hasAttribute ('data-sidebar-container') ? containerWidth : 0
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rendered = render (sidebarTree (props))
|
||||||
|
|
||||||
|
return rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe ('SidebarComponent', () => {
|
||||||
|
beforeEach (() => {
|
||||||
|
Object.defineProperty (window, 'innerWidth', {
|
||||||
|
configurable: true,
|
||||||
|
value: 1_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty (HTMLElement.prototype, 'setPointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn (),
|
||||||
|
})
|
||||||
|
Object.defineProperty (HTMLElement.prototype, 'releasePointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn (),
|
||||||
|
})
|
||||||
|
Object.defineProperty (HTMLElement.prototype, 'hasPointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn (() => true),
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.spyOn (HTMLElement.prototype, 'setPointerCapture').mockImplementation (() => {
|
||||||
|
;
|
||||||
|
})
|
||||||
|
vi.spyOn (HTMLElement.prototype, 'releasePointerCapture').mockImplementation (() => {
|
||||||
|
;
|
||||||
|
})
|
||||||
|
vi.spyOn (HTMLElement.prototype, 'hasPointerCapture').mockReturnValue (true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('uses maxWidth without applying the container main-width reserve twice', async () => {
|
||||||
|
localStorage.setItem ('sidebar:test-sidebar:left:width', '700')
|
||||||
|
const onWidthChange = vi.fn ()
|
||||||
|
|
||||||
|
const { container } = renderSidebar ({ maxWidth: 700, onWidthChange }, 800)
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('700px')
|
||||||
|
})
|
||||||
|
expect (onWidthChange).toHaveBeenLastCalledWith (700)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('resets width and removes only its storage key on double click', async () => {
|
||||||
|
localStorage.setItem ('sidebar:test-sidebar:right:width', '420')
|
||||||
|
localStorage.setItem ('sidebar:other-sidebar:right:width', '480')
|
||||||
|
|
||||||
|
const { container } = renderSidebar ({ side: 'right' }, 900)
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('420px')
|
||||||
|
})
|
||||||
|
|
||||||
|
fireEvent.doubleClick (screen.getByRole ('separator', { name: 'サイドバー幅' }))
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('256px')
|
||||||
|
})
|
||||||
|
expect (localStorage.getItem ('sidebar:test-sidebar:right:width')).toBeNull ()
|
||||||
|
expect (localStorage.getItem ('sidebar:other-sidebar:right:width')).toBe ('480')
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('clamps current width on maxWidth changes without re-reading storage', async () => {
|
||||||
|
localStorage.setItem ('sidebar:test-sidebar:left:width', '420')
|
||||||
|
|
||||||
|
const { container, rerender } = renderSidebar ({ maxWidth: 420 }, 900)
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('420px')
|
||||||
|
})
|
||||||
|
|
||||||
|
rerender (sidebarTree ({ maxWidth: 300 }))
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('300px')
|
||||||
|
})
|
||||||
|
|
||||||
|
rerender (sidebarTree ({ maxWidth: 500 }))
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('300px')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -47,23 +47,16 @@ const getSidebarMaxWidth = (containerWidth: number): number => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const clampSidebarWidth = (width: number, containerWidth: number): number =>
|
|
||||||
Math.min (getSidebarMaxWidth (containerWidth), Math.max (MIN_SIDEBAR_WIDTH, width))
|
|
||||||
|
|
||||||
|
|
||||||
const readStoredSidebarWidth = (
|
const readStoredSidebarWidth = (
|
||||||
sidebarKey: string,
|
sidebarKey: string,
|
||||||
side: SidebarSide,
|
side: SidebarSide): number => {
|
||||||
containerWidth: number): number => {
|
|
||||||
if (typeof window === 'undefined')
|
if (typeof window === 'undefined')
|
||||||
return DEFAULT_SIDEBAR_WIDTH
|
return DEFAULT_SIDEBAR_WIDTH
|
||||||
|
|
||||||
const value = window.localStorage.getItem (storageKeyForSidebar (sidebarKey, side))
|
const value = window.localStorage.getItem (storageKeyForSidebar (sidebarKey, side))
|
||||||
const width = value == null ? Number.NaN : Number (value)
|
const width = value == null ? Number.NaN : Number (value)
|
||||||
|
|
||||||
return Number.isFinite (width)
|
return Number.isFinite (width) ? width : DEFAULT_SIDEBAR_WIDTH
|
||||||
? clampSidebarWidth (width, containerWidth)
|
|
||||||
: clampSidebarWidth (DEFAULT_SIDEBAR_WIDTH, containerWidth)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +69,7 @@ const SidebarComponent: FC<Props> = ({
|
|||||||
side = 'left',
|
side = 'left',
|
||||||
}) => {
|
}) => {
|
||||||
const rootRef = useRef<HTMLDivElement | null> (null)
|
const rootRef = useRef<HTMLDivElement | null> (null)
|
||||||
|
const maxWidthRef = useRef (maxWidth)
|
||||||
const getContainerWidth = useCallback (() => {
|
const getContainerWidth = useCallback (() => {
|
||||||
const containerWidth = rootRef.current
|
const containerWidth = rootRef.current
|
||||||
?.closest ('[data-sidebar-container]')
|
?.closest ('[data-sidebar-container]')
|
||||||
@@ -90,18 +84,20 @@ const SidebarComponent: FC<Props> = ({
|
|||||||
typeof window === 'undefined' ? containerWidth : window.innerWidth
|
typeof window === 'undefined' ? containerWidth : window.innerWidth
|
||||||
const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
|
const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
|
||||||
const maxAllowedWidth =
|
const maxAllowedWidth =
|
||||||
maxWidth == null
|
maxWidthRef.current == null
|
||||||
? getSidebarMaxWidth (containerWidth)
|
? getSidebarMaxWidth (containerWidth)
|
||||||
: Math.max (MIN_SIDEBAR_WIDTH, Math.min (viewportMaxWidth, maxWidth))
|
: Math.max (
|
||||||
|
MIN_SIDEBAR_WIDTH,
|
||||||
|
Math.min (viewportMaxWidth, maxWidthRef.current))
|
||||||
|
|
||||||
return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextWidth))
|
return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextWidth))
|
||||||
}, [maxWidth])
|
}, [])
|
||||||
|
|
||||||
const [width, setWidth] = useState (() => (
|
const [width, setWidth] = useState (() => (
|
||||||
typeof window === 'undefined'
|
typeof window === 'undefined'
|
||||||
? DEFAULT_SIDEBAR_WIDTH
|
? DEFAULT_SIDEBAR_WIDTH
|
||||||
: getClampedWidth (
|
: getClampedWidth (
|
||||||
readStoredSidebarWidth (sidebarKey, side, window.innerWidth),
|
readStoredSidebarWidth (sidebarKey, side),
|
||||||
window.innerWidth)))
|
window.innerWidth)))
|
||||||
const [resizing, setResizing] = useState (false)
|
const [resizing, setResizing] = useState (false)
|
||||||
const widthRef = useRef (width)
|
const widthRef = useRef (width)
|
||||||
@@ -160,6 +156,21 @@ const SidebarComponent: FC<Props> = ({
|
|||||||
restoreBodyStyle ()
|
restoreBodyStyle ()
|
||||||
}, [clearLongPressTimer, restoreBodyStyle, sidebarKey])
|
}, [clearLongPressTimer, restoreBodyStyle, sidebarKey])
|
||||||
|
|
||||||
|
const applyWidth = useCallback ((nextWidth: number) => {
|
||||||
|
if (nextWidth === widthRef.current)
|
||||||
|
return
|
||||||
|
|
||||||
|
widthRef.current = nextWidth
|
||||||
|
setWidth (nextWidth)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
maxWidthRef.current = maxWidth
|
||||||
|
const containerWidth = getContainerWidth ()
|
||||||
|
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
|
||||||
|
applyWidth (nextWidth)
|
||||||
|
}, [applyWidth, getClampedWidth, getContainerWidth, maxWidth])
|
||||||
|
|
||||||
const updateWidth = useCallback ((clientX: number) => {
|
const updateWidth = useCallback ((clientX: number) => {
|
||||||
const state = resizeRef.current
|
const state = resizeRef.current
|
||||||
if (state == null)
|
if (state == null)
|
||||||
@@ -170,9 +181,8 @@ const SidebarComponent: FC<Props> = ({
|
|||||||
? clientX - state.startX
|
? clientX - state.startX
|
||||||
: state.startX - clientX
|
: state.startX - clientX
|
||||||
const nextWidth = getClampedWidth (state.startWidth + delta, containerWidth)
|
const nextWidth = getClampedWidth (state.startWidth + delta, containerWidth)
|
||||||
widthRef.current = nextWidth
|
applyWidth (nextWidth)
|
||||||
setWidth (nextWidth)
|
}, [applyWidth, getClampedWidth, getContainerWidth])
|
||||||
}, [getClampedWidth, getContainerWidth])
|
|
||||||
|
|
||||||
const handlePointerDown = (ev: PointerEvent<HTMLDivElement>) => {
|
const handlePointerDown = (ev: PointerEvent<HTMLDivElement>) => {
|
||||||
if (ev.pointerType !== 'mouse' && ev.pointerType !== 'touch')
|
if (ev.pointerType !== 'mouse' && ev.pointerType !== 'touch')
|
||||||
@@ -242,10 +252,9 @@ const SidebarComponent: FC<Props> = ({
|
|||||||
const resetWidth = useCallback (() => {
|
const resetWidth = useCallback (() => {
|
||||||
const containerWidth = getContainerWidth ()
|
const containerWidth = getContainerWidth ()
|
||||||
const nextWidth = getClampedWidth (DEFAULT_SIDEBAR_WIDTH, containerWidth)
|
const nextWidth = getClampedWidth (DEFAULT_SIDEBAR_WIDTH, containerWidth)
|
||||||
widthRef.current = nextWidth
|
applyWidth (nextWidth)
|
||||||
setWidth (nextWidth)
|
|
||||||
window.localStorage.removeItem (storageKeyForSidebar (sidebarKey, side))
|
window.localStorage.removeItem (storageKeyForSidebar (sidebarKey, side))
|
||||||
}, [getClampedWidth, getContainerWidth, sidebarKey, side])
|
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
|
||||||
|
|
||||||
const handleDoubleClick = (ev: MouseEvent<HTMLDivElement>) => {
|
const handleDoubleClick = (ev: MouseEvent<HTMLDivElement>) => {
|
||||||
ev.preventDefault ()
|
ev.preventDefault ()
|
||||||
@@ -256,11 +265,16 @@ const SidebarComponent: FC<Props> = ({
|
|||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const containerWidth = getContainerWidth ()
|
const containerWidth = getContainerWidth ()
|
||||||
const nextWidth = getClampedWidth (
|
const nextWidth = getClampedWidth (
|
||||||
readStoredSidebarWidth (sidebarKey, side, containerWidth),
|
readStoredSidebarWidth (sidebarKey, side),
|
||||||
containerWidth)
|
containerWidth)
|
||||||
widthRef.current = nextWidth
|
applyWidth (nextWidth)
|
||||||
setWidth (nextWidth)
|
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
|
||||||
}, [getClampedWidth, getContainerWidth, sidebarKey, side])
|
|
||||||
|
useEffect (() => {
|
||||||
|
const containerWidth = getContainerWidth ()
|
||||||
|
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
|
||||||
|
applyWidth (nextWidth)
|
||||||
|
}, [applyWidth, getClampedWidth, getContainerWidth])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (typeof window === 'undefined')
|
if (typeof window === 'undefined')
|
||||||
@@ -269,16 +283,12 @@ const SidebarComponent: FC<Props> = ({
|
|||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const containerWidth = getContainerWidth ()
|
const containerWidth = getContainerWidth ()
|
||||||
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
|
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
|
||||||
if (nextWidth === widthRef.current)
|
applyWidth (nextWidth)
|
||||||
return
|
|
||||||
|
|
||||||
widthRef.current = nextWidth
|
|
||||||
setWidth (nextWidth)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener ('resize', handleResize)
|
window.addEventListener ('resize', handleResize)
|
||||||
return () => window.removeEventListener ('resize', handleResize)
|
return () => window.removeEventListener ('resize', handleResize)
|
||||||
}, [getClampedWidth, getContainerWidth])
|
}, [applyWidth, getClampedWidth, getContainerWidth])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
onWidthChange?.(width)
|
onWidthChange?.(width)
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import type { FC } from 'react'
|
|||||||
|
|
||||||
|
|
||||||
const MaterialBasePage: FC = () => (
|
const MaterialBasePage: FC = () => (
|
||||||
<div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
<div
|
||||||
|
data-sidebar-container
|
||||||
|
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
||||||
<MaterialSidebar/>
|
<MaterialSidebar/>
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
</div>)
|
</div>)
|
||||||
|
|
||||||
export default MaterialBasePage
|
export default MaterialBasePage
|
||||||
|
|||||||
@@ -92,16 +92,16 @@ const PostDetailPage: FC<Props> = ({ user }) => {
|
|||||||
: 'bg-gray-500 hover:bg-gray-600')
|
: 'bg-gray-500 hover:bg-gray-600')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
<div
|
||||||
|
data-sidebar-container
|
||||||
|
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
{(post?.thumbnail || post?.thumbnailBase) && (
|
{(post?.thumbnail || post?.thumbnailBase) && (
|
||||||
<meta name="thumbnail" content={post.thumbnail! || post.thumbnailBase!}/>)}
|
<meta name="thumbnail" content={post.thumbnail! || post.thumbnailBase!}/>)}
|
||||||
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
|
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<div className="hidden md:block">
|
{post && <TagDetailSidebar post={post} className="hidden md:block"/>}
|
||||||
{post && <TagDetailSidebar post={post}/>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MainArea className="relative">
|
<MainArea className="relative">
|
||||||
{post
|
{post
|
||||||
@@ -179,9 +179,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
|
|||||||
: 'Loading...'}
|
: 'Loading...'}
|
||||||
</MainArea>
|
</MainArea>
|
||||||
|
|
||||||
<div className="md:hidden">
|
{post && <TagDetailSidebar post={post} sp className="md:hidden"/>}
|
||||||
{post && <TagDetailSidebar post={post} sp/>}
|
|
||||||
</div>
|
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const PostListPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-sidebar-container
|
||||||
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden"
|
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden"
|
||||||
ref={containerRef}>
|
ref={containerRef}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -105,6 +105,20 @@ const tagSection = (): HTMLElement =>
|
|||||||
screen.getAllByRole ('heading', { name: 'タグ' })[0].closest ('section')!
|
screen.getAllByRole ('heading', { name: 'タグ' })[0].closest ('section')!
|
||||||
|
|
||||||
const mockDefaultApi = () => {
|
const mockDefaultApi = () => {
|
||||||
|
let commentsFetched = false
|
||||||
|
let currentTheatreInfo = buildTheatreInfo ({
|
||||||
|
postId: currentPost.id,
|
||||||
|
postStartedAt: '2026-01-02T03:04:05.000Z',
|
||||||
|
postElapsedMs: 1_000,
|
||||||
|
watchingUsers,
|
||||||
|
skipVote: {
|
||||||
|
votesCount: 0,
|
||||||
|
requiredCount: 2,
|
||||||
|
watchingUsersCount: watchingUsers.length,
|
||||||
|
voted: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
api.apiGet.mockImplementation ((path: string) => {
|
api.apiGet.mockImplementation ((path: string) => {
|
||||||
switch (path)
|
switch (path)
|
||||||
{
|
{
|
||||||
@@ -112,13 +126,17 @@ const mockDefaultApi = () => {
|
|||||||
return Promise.resolve (theatre)
|
return Promise.resolve (theatre)
|
||||||
|
|
||||||
case '/theatres/7/comments':
|
case '/theatres/7/comments':
|
||||||
|
if (commentsFetched)
|
||||||
|
return Promise.resolve ([])
|
||||||
|
|
||||||
|
commentsFetched = true
|
||||||
return Promise.resolve ([
|
return Promise.resolve ([
|
||||||
buildTheatreComment ({
|
buildTheatreComment ({
|
||||||
theatreId: 7,
|
theatreId: 7,
|
||||||
no: 2,
|
no: 2,
|
||||||
user: { id: 1, name: 'tester' },
|
user: { id: 1, name: 'tester' },
|
||||||
content: '視聴コメント',
|
content: '視聴コメント',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
case '/theatres/7/programmes':
|
case '/theatres/7/programmes':
|
||||||
@@ -136,32 +154,22 @@ const mockDefaultApi = () => {
|
|||||||
switch (path)
|
switch (path)
|
||||||
{
|
{
|
||||||
case '/theatres/7/watching':
|
case '/theatres/7/watching':
|
||||||
return Promise.resolve (buildTheatreInfo ({
|
return Promise.resolve (currentTheatreInfo)
|
||||||
postId: currentPost.id,
|
|
||||||
postStartedAt: '2026-01-02T03:04:05.000Z',
|
|
||||||
postElapsedMs: 1_000,
|
|
||||||
watchingUsers,
|
|
||||||
skipVote: {
|
|
||||||
votesCount: 0,
|
|
||||||
requiredCount: 2,
|
|
||||||
watchingUsersCount: watchingUsers.length,
|
|
||||||
voted: false,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
case '/theatres/7/skip_vote':
|
case '/theatres/7/skip_vote':
|
||||||
return Promise.resolve (buildTheatreInfo ({
|
currentTheatreInfo = buildTheatreInfo ({
|
||||||
postId: currentPost.id,
|
postId: currentPost.id,
|
||||||
postStartedAt: '2026-01-02T03:04:05.000Z',
|
postStartedAt: '2026-01-02T03:04:05.000Z',
|
||||||
postElapsedMs: 2_000,
|
postElapsedMs: 2_000,
|
||||||
watchingUsers,
|
watchingUsers,
|
||||||
skipVote: {
|
skipVote: {
|
||||||
votesCount: 1,
|
votesCount: 1,
|
||||||
requiredCount: 2,
|
requiredCount: 2,
|
||||||
watchingUsersCount: watchingUsers.length,
|
watchingUsersCount: watchingUsers.length,
|
||||||
voted: true,
|
voted: true,
|
||||||
},
|
},
|
||||||
}))
|
})
|
||||||
|
return Promise.resolve (currentTheatreInfo)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Promise.reject (new Error (`Unexpected PUT ${ path }`))
|
return Promise.reject (new Error (`Unexpected PUT ${ path }`))
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
|
|
||||||
const applyTheatreInfo = useCallback ((nextInfo: TheatreInfo) => {
|
const applyTheatreInfo = useCallback ((nextInfo: TheatreInfo) => {
|
||||||
theatreInfoReceivedAtRef.current = performance.now ()
|
theatreInfoReceivedAtRef.current = performance.now ()
|
||||||
|
theatreInfoRef.current = nextInfo
|
||||||
setTheatreInfo (nextInfo)
|
setTheatreInfo (nextInfo)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -406,6 +407,9 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
setComments (prev => [...newComments, ...prev])
|
setComments (prev => [...newComments, ...prev])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loadingRef.current)
|
||||||
|
return
|
||||||
|
|
||||||
const currentInfo = theatreInfoRef.current
|
const currentInfo = theatreInfoRef.current
|
||||||
const ended =
|
const ended =
|
||||||
currentInfo.hostFlg
|
currentInfo.hostFlg
|
||||||
@@ -427,8 +431,9 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const watchingRequestedAt = performance.now ()
|
||||||
const nextInfo = await apiPut<TheatreInfo> (`/theatres/${ id }/watching`)
|
const nextInfo = await apiPut<TheatreInfo> (`/theatres/${ id }/watching`)
|
||||||
if (!(cancelled))
|
if (!(cancelled) && watchingRequestedAt >= theatreInfoReceivedAtRef.current)
|
||||||
applyTheatreInfo (nextInfo)
|
applyTheatreInfo (nextInfo)
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
@@ -557,6 +562,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
if (!(id) || !(post))
|
if (!(id) || !(post))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
loadingRef.current = true
|
||||||
setLoading (true)
|
setLoading (true)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -585,6 +591,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
loadingRef.current = false
|
||||||
setLoading (false)
|
setLoading (false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする