Skip to content

Commit 5825cd7

Browse files
authored
Merge pull request #101 from cniajp/feature/dk-monitor-page
Feature/dk monitor page
2 parents 59485a4 + b18f0b0 commit 5825cd7

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import {
2+
Track,
3+
Talk,
4+
useGetApiV1TracksQuery,
5+
useGetApiV1TalksQuery,
6+
} from '@/generated/dreamkast-api.generated'
7+
import config from '@/config'
8+
import { nowAccurate, startTimeSync } from '@/utils/time'
9+
import { useEffect, useMemo, useRef, useState } from 'react'
10+
11+
// onAirTalkはtalk_idまたはidのみを含む可能性がある
12+
type OnAirTalk = {
13+
id?: number
14+
talk_id?: number
15+
[key: string]: unknown
16+
}
17+
18+
export default function MonitorPage() {
19+
// 5秒間隔でポーリングして最新のOnAir情報を取得
20+
const {
21+
data: tracks,
22+
isLoading: tracksLoading,
23+
isFetching: tracksFetching,
24+
isSuccess: tracksSuccess,
25+
} = useGetApiV1TracksQuery(
26+
{ eventAbbr: config.dkEventAbbr },
27+
{ pollingInterval: 5000 }
28+
)
29+
30+
// トーク情報を取得(conferenceDayIdsを指定せずにすべてのトークを取得)
31+
const {
32+
data: talks,
33+
isLoading: talksLoading,
34+
isFetching: talksFetching,
35+
isSuccess: talksSuccess,
36+
} = useGetApiV1TalksQuery({ eventAbbr: config.dkEventAbbr })
37+
38+
const isLoading = tracksLoading || talksLoading
39+
const isFetching = tracksFetching || talksFetching
40+
const isSuccess = tracksSuccess && talksSuccess
41+
42+
const [currentTime, setCurrentTime] = useState(new Date())
43+
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null)
44+
const prevIsFetchingRef = useRef<boolean>(false)
45+
const isInitialLoadRef = useRef<boolean>(true)
46+
47+
// talk_idまたはidからTalk情報を取得するヘルパー関数
48+
const getTalkById = useMemo(() => {
49+
if (!talks) return null
50+
return (talkId: number | undefined): Talk | undefined => {
51+
if (talkId === undefined) return undefined
52+
return talks.find((talk) => talk.id === talkId)
53+
}
54+
}, [talks])
55+
56+
// 現在時刻をサーバー時刻基準で1秒ごとに更新
57+
useEffect(() => {
58+
startTimeSync()
59+
60+
const updateTime = () => {
61+
setCurrentTime(nowAccurate().toDate())
62+
}
63+
64+
updateTime()
65+
const timer = setInterval(updateTime, 1000)
66+
67+
return () => clearInterval(timer)
68+
}, [])
69+
70+
// フェッチが完了したときに最終更新時刻を記録
71+
useEffect(() => {
72+
// フェッチが完了したとき(isFetchingがtrueからfalseになったとき)に更新時刻を記録
73+
if (!isFetching && isSuccess && tracks) {
74+
// 前回がフェッチ中だった場合、または初回ロード完了時
75+
if (prevIsFetchingRef.current || isInitialLoadRef.current) {
76+
setLastUpdateTime(nowAccurate().toDate())
77+
isInitialLoadRef.current = false
78+
}
79+
}
80+
prevIsFetchingRef.current = isFetching
81+
}, [isFetching, isSuccess, tracks])
82+
83+
const formatTime = (date: Date) => {
84+
return date.toLocaleTimeString('ja-JP', {
85+
hour: '2-digit',
86+
minute: '2-digit',
87+
second: '2-digit',
88+
})
89+
}
90+
91+
const formatDateTime = (dateString?: string) => {
92+
if (!dateString) return '-'
93+
const date = new Date(dateString)
94+
return date.toLocaleTimeString('ja-JP', {
95+
hour: '2-digit',
96+
minute: '2-digit',
97+
})
98+
}
99+
100+
const getOnAirStatus = (track: Track) => {
101+
const onAirTalk = track.onAirTalk as OnAirTalk | null | undefined
102+
if (!onAirTalk) {
103+
return { isOnAir: false, talk: null, talkInfo: null }
104+
}
105+
const talkId = onAirTalk.talk_id ?? onAirTalk.id
106+
const talkInfo = getTalkById ? getTalkById(talkId) : undefined
107+
return { isOnAir: true, talk: onAirTalk, talkInfo }
108+
}
109+
110+
if (isLoading) {
111+
return (
112+
<div className="w-[1920px] h-[1080px] bg-transparent text-white flex items-center justify-center">
113+
<div className="text-center text-xl">読み込み中...</div>
114+
</div>
115+
)
116+
}
117+
118+
if (!tracks || tracks.length === 0) {
119+
return (
120+
<div className="w-[1920px] h-[1080px] bg-transparent text-white flex items-center justify-center">
121+
<div className="text-center text-xl">
122+
トラック情報が取得できませんでした
123+
</div>
124+
</div>
125+
)
126+
}
127+
128+
// トラックを最大4つまで取得(4分割表示用)
129+
const displayTracks = tracks.slice(0, 4)
130+
131+
// 各セクションの位置を定義(左上、右上、左下、右下)
132+
const sectionPositions = [
133+
{ top: 0, left: 0 }, // 左上
134+
{ top: 0, left: 960 }, // 右上
135+
{ top: 540, left: 0 }, // 左下
136+
{ top: 540, left: 960 }, // 右下
137+
]
138+
139+
return (
140+
<div className="w-[1920px] h-[1080px] bg-transparent text-white relative overflow-hidden">
141+
{displayTracks.map((track, index) => {
142+
const { isOnAir, talk, talkInfo } = getOnAirStatus(track)
143+
const position = sectionPositions[index]
144+
145+
return (
146+
<div
147+
key={track.id}
148+
className="absolute"
149+
style={{
150+
top: `${position.top}px`,
151+
left: `${position.left}px`,
152+
width: '960px',
153+
height: '540px',
154+
}}
155+
>
156+
<div
157+
className={`p-6 h-full flex flex-col ${
158+
index >= 2 ? 'justify-end' : ''
159+
} bg-black/40 backdrop-blur-sm`}
160+
>
161+
<div className="flex items-center justify-between mb-4">
162+
<h2 className="text-2xl font-bold">Track {track.name}</h2>
163+
<div
164+
className={`px-3 py-1 rounded-full text-sm font-semibold ${
165+
isOnAir
166+
? 'bg-red-500 text-white'
167+
: 'bg-gray-600 text-gray-300'
168+
}`}
169+
>
170+
{isOnAir ? 'ON AIR' : 'OFF AIR'}
171+
</div>
172+
</div>
173+
174+
{isOnAir && talk ? (
175+
<div className="space-y-2">
176+
<div>
177+
<div className="text-sm text-gray-400">トークID</div>
178+
<div className="text-lg font-semibold">
179+
{talk.talk_id ?? talk.id ?? '-'}
180+
</div>
181+
</div>
182+
{talkInfo ? (
183+
<>
184+
<div>
185+
<div className="text-sm text-gray-400">タイトル</div>
186+
<div className="text-lg font-semibold">
187+
{talkInfo.title || '-'}
188+
</div>
189+
</div>
190+
{talkInfo.speakers && talkInfo.speakers.length > 0 && (
191+
<div>
192+
<div className="text-sm text-gray-400">
193+
スピーカー
194+
</div>
195+
<div className="text-lg">
196+
{talkInfo.speakers
197+
.map((s) => s.name || `ID: ${s.id}`)
198+
.join(', ')}
199+
</div>
200+
</div>
201+
)}
202+
<div className="grid grid-cols-2 gap-4 mt-4">
203+
<div>
204+
<div className="text-base text-gray-400">
205+
開始時刻
206+
</div>
207+
<div className="text-2xl font-semibold">
208+
{formatDateTime(talkInfo.startTime)}
209+
</div>
210+
</div>
211+
<div>
212+
<div className="text-base text-gray-400">
213+
終了時刻
214+
</div>
215+
<div className="text-2xl font-semibold">
216+
{formatDateTime(talkInfo.endTime)}
217+
</div>
218+
</div>
219+
</div>
220+
</>
221+
) : (
222+
<div className="text-yellow-400 text-sm">
223+
トーク情報を取得中...
224+
</div>
225+
)}
226+
</div>
227+
) : (
228+
<div className="text-gray-400 text-center py-4">
229+
現在、OnAir中のトークはありません
230+
</div>
231+
)}
232+
233+
{track.videoPlatform && (
234+
<div className="mt-4 pt-4 border-t border-gray-600">
235+
<div className="text-sm text-gray-400">
236+
動画プラットフォーム
237+
</div>
238+
<div className="text-sm">{track.videoPlatform}</div>
239+
</div>
240+
)}
241+
</div>
242+
</div>
243+
)
244+
})}
245+
246+
{/* 現在時刻と更新時間を画面中央に表示 */}
247+
<div
248+
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center bg-black/40 rounded-2xl px-8 py-6 backdrop-blur-sm"
249+
style={{ zIndex: 10 }}
250+
>
251+
<div className="text-2xl font-bold mb-2">
252+
現在時刻: {formatTime(currentTime)}
253+
</div>
254+
<div className="text-lg text-gray-400">
255+
最終更新: {lastUpdateTime ? formatTime(lastUpdateTime) : '取得中...'}{' '}
256+
(自動更新: 5秒間隔)
257+
</div>
258+
</div>
259+
</div>
260+
)
261+
}

0 commit comments

Comments
 (0)