Skip to content

Commit ece6ed4

Browse files
committed
番組表 整理
1 parent 6120932 commit ece6ed4

File tree

5 files changed

+477
-430
lines changed

5 files changed

+477
-430
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { TVerChannelId } from '@midra/nco-api/types/constants'
2+
3+
import { cn } from '@nextui-org/react'
4+
import { tverToJikkyoChId } from '@midra/nco-api/utils/tverToJikkyoChId'
5+
import { JIKKYO_CHANNELS } from '@midra/nco-api/constants'
6+
7+
import { COLUMN_WIDTH } from './TverEpg'
8+
9+
export type ChannelCellProps = {
10+
tverChId: TVerChannelId
11+
}
12+
13+
export const ChannelCell: React.FC<ChannelCellProps> = ({ tverChId }) => {
14+
const jkChId = tverToJikkyoChId(tverChId)
15+
const chName = JIKKYO_CHANNELS[jkChId!]
16+
17+
return (
18+
<div
19+
className={cn(
20+
'flex items-center justify-center',
21+
'shrink-0',
22+
'border-r-1 border-divider',
23+
'bg-content2 text-content2-foreground',
24+
'text-mini font-semibold',
25+
'line-clamp-1'
26+
)}
27+
style={{
28+
width: COLUMN_WIDTH,
29+
height: 20,
30+
}}
31+
>
32+
<span>{chName}</span>
33+
</div>
34+
)
35+
}
36+
37+
export type ChannelsProps = {
38+
tverChIds: TVerChannelId[]
39+
}
40+
41+
export const Channels: React.FC<ChannelsProps> = ({ tverChIds }) => {
42+
return (
43+
<div
44+
className={cn(
45+
'sticky top-0 z-30',
46+
'flex flex-row',
47+
'border-b-1 border-divider'
48+
)}
49+
>
50+
<div
51+
className={cn('shrink-0 bg-content2', 'border-r-1 border-divider')}
52+
style={{ width: 20 }}
53+
/>
54+
55+
{tverChIds.map((tverChId) => (
56+
<ChannelCell key={tverChId} tverChId={tverChId} />
57+
))}
58+
</div>
59+
)
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { memo } from 'react'
2+
import { cn } from '@nextui-org/react'
3+
4+
import { ROW_HEIGHT } from './TverEpg'
5+
6+
export type HourCellProps = {
7+
hour: number
8+
}
9+
10+
export const HourCell: React.FC<HourCellProps> = ({ hour }) => {
11+
return (
12+
<div
13+
className={cn(
14+
'flex flex-col items-center',
15+
'shrink-0',
16+
'border-b-1 border-divider',
17+
'bg-content2 text-content2-foreground',
18+
'text-mini font-semibold'
19+
)}
20+
style={{
21+
height: ROW_HEIGHT,
22+
}}
23+
>
24+
<span className={cn('sticky top-[21px]', 'py-1')}>{hour}</span>
25+
</div>
26+
)
27+
}
28+
29+
export const Hours: React.FC = memo(() => (
30+
<div
31+
className={cn(
32+
'sticky left-0 z-20',
33+
'flex flex-col',
34+
'shrink-0',
35+
'border-r-1 border-divider'
36+
)}
37+
style={{
38+
width: 20,
39+
}}
40+
>
41+
{Array(24)
42+
.fill(0)
43+
.map((_, i) => (i + 5) % 24)
44+
.map((hour) => (
45+
<HourCell key={hour} hour={hour} />
46+
))}
47+
</div>
48+
))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import type { StateSlotDetailJikkyo } from '@/ncoverlay/state'
2+
import type { EPGProgram, EPGContent, EPGData } from '.'
3+
4+
import { useMemo } from 'react'
5+
import {
6+
Divider,
7+
Popover,
8+
PopoverTrigger,
9+
PopoverContent,
10+
cn,
11+
} from '@nextui-org/react'
12+
import { darken, saturate, toHex } from 'color2k'
13+
import { normalize } from '@midra/nco-parser/normalize'
14+
import { tverToJikkyoChId } from '@midra/nco-api/utils/tverToJikkyoChId'
15+
16+
import { zeroPadding } from '@/utils/zeroPadding'
17+
import { readableColor } from '@/utils/color'
18+
import { useNcoState } from '@/hooks/useNco'
19+
20+
import { SlotItem } from '@/components/slot-item'
21+
22+
import { COLUMN_WIDTH, ROW_HEIGHT } from './TverEpg'
23+
24+
export type ProgramContentProps = {
25+
program: EPGProgram
26+
bgColor: [light: string, dark: string]
27+
}
28+
29+
export const ProgramContent: React.FC<ProgramContentProps> = ({
30+
program,
31+
bgColor,
32+
}) => {
33+
const fgColor = useMemo(() => {
34+
return bgColor.map((color) => readableColor(toHex(color))) as typeof bgColor
35+
}, [bgColor])
36+
37+
const startMinutes = useMemo(() => {
38+
return zeroPadding(new Date(program.startAt * 1000).getMinutes(), 2)
39+
}, [program.startAt])
40+
41+
return (
42+
<div
43+
className={cn(
44+
'size-full',
45+
'border-b-1 border-divider',
46+
'overflow-hidden',
47+
'cursor-pointer'
48+
)}
49+
>
50+
<div
51+
className={cn('flex flex-col gap-1', 'break-all text-mini')}
52+
title={program.description}
53+
>
54+
<span className="flex items-start gap-1">
55+
{/* 分 */}
56+
<span className="flex shrink-0 font-semibold">
57+
<span
58+
className={cn(
59+
'text-center',
60+
'w-6 py-1',
61+
'border-b-1 border-r-1 border-divider',
62+
'dark:hidden'
63+
)}
64+
style={{
65+
backgroundColor: bgColor[0],
66+
color: fgColor[0],
67+
}}
68+
>
69+
{startMinutes}
70+
</span>
71+
72+
<span
73+
className={cn(
74+
'text-center',
75+
'w-6 py-1',
76+
'border-b-1 border-r-1 border-divider',
77+
'hidden dark:inline'
78+
)}
79+
style={{
80+
backgroundColor: bgColor[1],
81+
color: fgColor[1],
82+
}}
83+
>
84+
{startMinutes}
85+
</span>
86+
</span>
87+
88+
{/* タイトル */}
89+
<span className="pr-1 pt-1 font-semibold">
90+
{`${program.prefix} ${program.title}`.trim()}
91+
</span>
92+
</span>
93+
94+
{/* 概要 */}
95+
<span className="px-1 text-foreground-500 dark:text-foreground-600">
96+
{program.description}
97+
</span>
98+
</div>
99+
</div>
100+
)
101+
}
102+
103+
export type ProgramPopoverProps = {
104+
tverChId: EPGContent['tverChId']
105+
program: EPGProgram
106+
}
107+
108+
export const ProgramPopover: React.FC<ProgramPopoverProps> = ({
109+
tverChId,
110+
program,
111+
}) => {
112+
const stateSlotDetails = useNcoState('slotDetails')
113+
114+
const ids = useMemo(() => {
115+
return stateSlotDetails?.map((v) => v.id)
116+
}, [stateSlotDetails])
117+
118+
const { title, description, prefix, startAt, endAt } = program
119+
120+
const id = `${tverToJikkyoChId(tverChId)}:${startAt}-${endAt}`
121+
122+
const slotDetail: StateSlotDetailJikkyo = {
123+
type: 'jikkyo',
124+
id,
125+
status: 'pending',
126+
info: {
127+
id: program.id ?? null,
128+
title: `${prefix} ${
129+
normalize(description).includes(normalize(title))
130+
? description
131+
: `${title}\n${description}`.trim()
132+
}`.trim(),
133+
duration: endAt - startAt,
134+
date: [startAt * 1000, endAt * 1000],
135+
count: {
136+
comment: 0,
137+
},
138+
},
139+
}
140+
141+
return (
142+
<div
143+
className={cn('flex flex-col items-start gap-1', 'w-80 p-2', 'text-tiny')}
144+
>
145+
{title && (
146+
<span className="font-semibold">{`${prefix} ${title}`.trim()}</span>
147+
)}
148+
{description && (
149+
<span className="text-foreground-500 dark:text-foreground-600">
150+
{description}
151+
</span>
152+
)}
153+
154+
<Divider className="my-1" />
155+
156+
<SlotItem
157+
classNames={{
158+
wrapper: 'w-full',
159+
}}
160+
detail={slotDetail}
161+
isSearch
162+
isDisabled={ids?.includes(id)}
163+
/>
164+
</div>
165+
)
166+
}
167+
168+
export type ProgramCellProps = {
169+
date: EPGData['date']
170+
genre: EPGData['genre']
171+
tverChId: EPGContent['tverChId']
172+
program: EPGProgram
173+
}
174+
175+
export const ProgramCell: React.FC<ProgramCellProps> = ({
176+
date,
177+
genre,
178+
tverChId,
179+
program,
180+
}) => {
181+
const { startAt, endAt } = program
182+
183+
const height = ((endAt - startAt) / 3600) * ROW_HEIGHT
184+
const top = ((startAt - date[0]) / 3600) * ROW_HEIGHT
185+
186+
const color = genre[program.genre]?.color ?? '#D3D3D3'
187+
188+
const bgColorLight = saturate(color, 0.2)
189+
const bgColorDark = saturate(darken(bgColorLight, 0.2), -0.4)
190+
191+
return (
192+
<Popover
193+
classNames={{
194+
backdrop: 'bg-transparent',
195+
content: 'border-1 border-foreground-100',
196+
}}
197+
backdrop="opaque"
198+
placement="right-start"
199+
>
200+
<PopoverTrigger
201+
className={cn(
202+
'bg-content1 hover:bg-content2/90 aria-expanded:bg-content2/90',
203+
'!duration-150 transition-background',
204+
'aria-expanded:scale-100',
205+
'aria-expanded:opacity-100',
206+
program.isDisabled && 'pointer-events-none opacity-50'
207+
)}
208+
>
209+
<div className="absolute w-full" style={{ top, height }}>
210+
<ProgramContent
211+
program={program}
212+
bgColor={[bgColorLight, bgColorDark]}
213+
/>
214+
</div>
215+
</PopoverTrigger>
216+
217+
<PopoverContent className="p-0">
218+
<ProgramPopover tverChId={tverChId} program={program} />
219+
</PopoverContent>
220+
</Popover>
221+
)
222+
}
223+
224+
export type ProgramsProps = {
225+
data: EPGData
226+
}
227+
228+
export const Programs: React.FC<ProgramsProps> = ({ data }) => {
229+
const { date, contents, genre } = data
230+
231+
return (
232+
<div
233+
className="flex shrink-0 flex-row overflow-hidden bg-content3"
234+
style={{ maxHeight: ROW_HEIGHT * 24 }}
235+
>
236+
{contents.map((content, idx) => (
237+
<div
238+
key={idx}
239+
className={cn(
240+
'relative',
241+
'flex flex-col',
242+
'shrink-0',
243+
'border-r-1 border-divider'
244+
)}
245+
style={{ width: COLUMN_WIDTH }}
246+
>
247+
{content.programs.map((program, idx) => (
248+
<ProgramCell
249+
key={idx}
250+
date={date}
251+
genre={genre}
252+
tverChId={content.tverChId}
253+
program={program}
254+
/>
255+
))}
256+
</div>
257+
))}
258+
</div>
259+
)
260+
}

0 commit comments

Comments
 (0)