diff --git a/functions/.eslintrc.js b/functions/.eslintrc.js index 6df1fb5..75d646f 100644 --- a/functions/.eslintrc.js +++ b/functions/.eslintrc.js @@ -24,5 +24,11 @@ module.exports = { quotes: ["error", "double"], "import/no-unresolved": 0, indent: ["error", 2], + "prettier/prettier": [ + "error", + { + endOfLine: "auto", + }, + ], }, }; diff --git a/functions/src/index.ts b/functions/src/index.ts index 97e2115..49c5593 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -93,6 +93,8 @@ export const getStreams = onSchedule( region: "asia-northeast1", }, async () => { + const endTime = new Date().toISOString(); + // init const master = await getStreamerMaster(); const tokenDoc = db @@ -116,7 +118,6 @@ export const getStreams = onSchedule( const snap = await streamRef.get(); const { endedStreams, newStreams } = sortStreams(streams, snap.docs); - const endTime = new Date().toISOString(); for await (const { id, data } of endedStreams) { let stream = data; diff --git a/functions/src/refreshToken.ts b/functions/src/refreshToken.ts deleted file mode 100644 index e69de29..0000000 diff --git a/functions/src/utils/sortStreams.ts b/functions/src/utils/sortStreams.ts index faa22b9..f1fc381 100644 --- a/functions/src/utils/sortStreams.ts +++ b/functions/src/utils/sortStreams.ts @@ -20,7 +20,7 @@ export const sortStreams = ( ({ data }) => !streams.find( ({ id, platform }) => data.id === id && data.platform === platform, - ), + ) && !data.endTime, ); const newStreams = streams.filter( diff --git a/src/components/dropdownMenu/dropdownItem/index.tsx b/src/components/dropdownMenu/dropdownItem/index.tsx index e49109d..5bbe989 100644 --- a/src/components/dropdownMenu/dropdownItem/index.tsx +++ b/src/components/dropdownMenu/dropdownItem/index.tsx @@ -9,6 +9,7 @@ type Props = { text?: string; }; style?: CSSObject; + hoverable?: boolean; onClick?: () => void; }; @@ -16,12 +17,13 @@ export const DropdownItem: React.FC = ({ children, contents = {}, style, + hoverable = false, onClick, }) => { const { icon, text } = contents; return ( - + {icon && {icon}} {text && {text}} {children} diff --git a/src/components/dropdownMenu/index.tsx b/src/components/dropdownMenu/index.tsx index d2d435d..57f1ccb 100644 --- a/src/components/dropdownMenu/index.tsx +++ b/src/components/dropdownMenu/index.tsx @@ -28,6 +28,8 @@ type Props = { position?: Partial; entry?: Partial; children?: ReactNode; + onOpen?: () => void; + onClose?: () => void; } & DropdownContainerProps; const calcPosition = ({ @@ -49,7 +51,15 @@ const calcPosition = ({ }; export const Dropdown: FC = memo( - ({ trigger, width, position = {}, entry = {}, children }) => { + ({ + trigger, + width, + position = {}, + entry = {}, + children, + onOpen, + onClose, + }) => { const [isOpen, setOpen] = useState(false); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const refDropdown = useRef(null!); @@ -73,7 +83,13 @@ export const Dropdown: FC = memo( ); useEffect(() => { - if (isOpen) document.addEventListener("mousedown", checkClicksOutside); + if (isOpen) { + onOpen?.(); + document.addEventListener("mousedown", checkClicksOutside); + } else { + onClose?.(); + } + return () => document.removeEventListener("mousedown", checkClicksOutside); }, [isOpen]); diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index d398fc8..5e2883c 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -1,17 +1,27 @@ -import React from "react"; +import React, { FC } from "react"; import logo from "../../logo.png"; import { SettingMenu } from "../settingMenu"; import { Container, Icon, Title, TitleText, DropdownWrapper } from "./styles"; -export const Header: React.FC = () => { +type Props = { + isScrolled: boolean; + onOpenMenu?: () => void; + onCloseMenu?: () => void; +}; + +export const Header: FC = ({ isScrolled, onOpenMenu, onCloseMenu }) => { return ( - + <Icon src={logo} alt="logo" /> <TitleText>Vspo stream schedule</TitleText> - + ); diff --git a/src/components/header/styles.tsx b/src/components/header/styles.tsx index e919885..979a80c 100644 --- a/src/components/header/styles.tsx +++ b/src/components/header/styles.tsx @@ -1,16 +1,30 @@ import { breakpointMediaQueries } from "src/configs"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; -export const Container = styled.div` - width: 100%; - margin: 25px 0; - /* position: sticky; +export const Container = styled.div<{ isScrolled: boolean }>` + padding: 5px 10px; + margin-bottom: 5px; + position: sticky; top: 0; - left: 0; */ + left: 0; display: flex; align-items: center; - border-radius: 10px; + border-radius: 0 0 10px 10px; z-index: 10; + background-color: ${({ theme }) => theme.bg}; + transition: + background-color 0.3s ease-in-out, + box-shadow 0.2s ease-in-out; + + ${({ isScrolled }) => + isScrolled && + css` + box-shadow: 0px 10px 10px -3px rgba(0, 0, 0, 0.2); + `} + + ${breakpointMediaQueries.tablet` + padding: 10px 20px; + `} `; export const Title = styled.div` diff --git a/src/components/mainContainer/index.tsx b/src/components/mainContainer/index.tsx index c78b22e..41aca8c 100644 --- a/src/components/mainContainer/index.tsx +++ b/src/components/mainContainer/index.tsx @@ -13,6 +13,7 @@ import { StreamGrid } from "../streamGrid"; import { StreamGridHeader } from "../streamGridHeader"; import { useDisplaySize, useSetting, useVspoStream } from "src/providers"; import { toYYYYMMDD } from "src/utils"; +import { responsiveProperties } from "src/configs"; type DailyStream = { date: string; @@ -48,6 +49,7 @@ export const MainContainer: FC = () => { column: number; gap: number; }>({ column: 0, gap: 0 }); + const [isScrolled, setIsScrolled] = useState(false); const displaySize = useDisplaySize(); const { isDisplayHistory } = useSetting(); @@ -57,33 +59,67 @@ export const MainContainer: FC = () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = useRef(null!); useEffect(() => { - const [cardWidth, gapRange] = displaySize.mobile - ? [160, [10, 40]] - : [320, [20, 80]]; - - const resize = () => { - const style = window.getComputedStyle(containerRef.current); - const width = getPixel(style, "width"); - setGridProperties(calcGridProperties(width, cardWidth, { gapRange })); + const ref = containerRef.current; + + const { + card: { + width: cardWidth, + gap: { x }, + }, + } = responsiveProperties[displaySize]; + + const onResize = () => { + const resize = () => { + const style = window.getComputedStyle(ref); + const width = getPixel(style, "width") - 40; + console.log("onResize", width); + setGridProperties( + calcGridProperties(width, cardWidth, { gapRange: [x, x * 4] }), + ); + }; + // windowを最大化する際に即時にgetComputedStyleを実行するとその結果に不整合が生じるため、タイミングをずらす + setTimeout(resize, 200); + }; + onResize(); + window.addEventListener("resize", onResize); + + const onScroll = () => { + setIsScrolled(ref.scrollTop > 0); }; - resize(); + ref.addEventListener("scroll", onScroll); - window.addEventListener("resize", resize); - return () => window.removeEventListener("resize", resize); - }, [displaySize.mobile]); + return () => { + window.removeEventListener("resize", onResize); + ref.removeEventListener("scroll", onScroll); + window.removeEventListener("onload", onResize); + }; + }, [displaySize]); const calcStreamGridMinHeight = useCallback( (streamNum: number) => { - const [cardHeight, expandSize, gap] = displaySize.mobile - ? [90, 30, 20] - : [180, 60, 40]; + const { + card: { + height, + expandedHeight, + gap: { y }, + }, + } = responsiveProperties[displaySize]; + const row = Math.ceil(streamNum / gridProperties.column); - return row * (cardHeight + gap) - gap + expandSize; + return (row - 1) * (height + y) + expandedHeight; }, - [gridProperties.column, displaySize.mobile], + [gridProperties.column, displaySize], ); + const disableScroll = useCallback(() => { + containerRef.current.style.overflow = "hidden"; + }, []); + + const enableScroll = useCallback(() => { + containerRef.current.style.overflow = "scroll"; + }, []); + const dailyStreams: DailyStream[] = useMemo(() => { const now = Date.now(); const isEndedStream = (s: Stream) => s.endAt && s.endAt.getTime() <= now; @@ -110,7 +146,11 @@ export const MainContainer: FC = () => { return ( -
+
{dailyStreams.map(({ date, streams }) => ( diff --git a/src/components/mainContainer/styles.tsx b/src/components/mainContainer/styles.tsx index 557833e..c916254 100644 --- a/src/components/mainContainer/styles.tsx +++ b/src/components/mainContainer/styles.tsx @@ -9,10 +9,9 @@ export const Background = styled.div` `; export const Container = styled.div` - width: 90%; + width: 100%; height: 100%; margin: 0 auto; - padding: 0 5%; background: rgba(240, 240, 240, 0.03); box-shadow: 0px 0px 4px 4px rgba(0, 0, 0, 0.2); overflow: scroll; @@ -22,11 +21,14 @@ export const Container = styled.div` transition: width 0.3s ease-in-out; ${breakpointMediaQueries.desktop` - width: 85%; + width: 90%; + padding: 0 3%; `} `; export const DailyStreamContainer = styled.div` + padding: 0 20px; + &:last-child { padding-bottom: 30px; } diff --git a/src/components/settingMenu/index.tsx b/src/components/settingMenu/index.tsx index de79125..c4ed2c6 100644 --- a/src/components/settingMenu/index.tsx +++ b/src/components/settingMenu/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo } from "react"; +import React, { ComponentProps, FC, memo } from "react"; import { Border, Dropdown, @@ -13,10 +13,15 @@ import { FaGithub } from "react-icons/fa"; import { TbMoonFilled, TbMarquee2, TbHistory } from "react-icons/tb"; import { IoIosArrowBack } from "react-icons/io"; -export const SettingMenu: FC = memo(() => { +type Props = Pick< + ComponentProps, + "position" | "onOpen" | "onClose" +>; + +export const SettingMenu: FC = memo(({ position, onOpen, onClose }) => { const setting = useSetting(); const configDispatch = useSettingDispatch(); - const { mobile } = useDisplaySize(); + const displaySize = useDisplaySize(); const MenuButton = memo(() => ( @@ -95,7 +95,7 @@ export const StreamerFilter: FC = ({ ); - if (mobile) return Contents(mobile); + if (displaySize === "mobile") return Contents(displaySize); return ( = ({ /> } > - {Contents(mobile)} + {Contents(displaySize)} ); }; diff --git a/src/configs/index.ts b/src/configs/index.ts index 608e1e0..e467a9d 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -1,3 +1,4 @@ export * from "./colors"; export * from "./theme"; export * from "./breakpoints"; +export * from "./responsiveProperties"; diff --git a/src/configs/responsiveProperties.ts b/src/configs/responsiveProperties.ts new file mode 100644 index 0000000..475e494 --- /dev/null +++ b/src/configs/responsiveProperties.ts @@ -0,0 +1,31 @@ +import { ResponsiveProperty } from "types"; + +const mobile: ResponsiveProperty = { + card: { + width: 130, + height: 73.125, + expandedHeight: 97.5, + gap: { + x: 10, + y: 20, + }, + }, +}; + +const notMobile: ResponsiveProperty = { + card: { + width: 320, + height: 180, + expandedHeight: 240, + gap: { + x: 20, + y: 40, + }, + }, +}; + +export const responsiveProperties = { + mobile, + tablet: notMobile, + desktop: notMobile, +}; diff --git a/src/providers/deviceTypeProvider/context.ts b/src/providers/deviceTypeProvider/context.ts index 735747e..9a1c1fc 100644 --- a/src/providers/deviceTypeProvider/context.ts +++ b/src/providers/deviceTypeProvider/context.ts @@ -1,6 +1,4 @@ import { createContext } from "react"; -import { DisplaySizeInfo } from "types"; +import { BreakpointKey } from "types"; -export const displaySizeContext = createContext( - {} as DisplaySizeInfo, -); +export const displaySizeContext = createContext("desktop"); diff --git a/src/providers/deviceTypeProvider/provider.tsx b/src/providers/deviceTypeProvider/provider.tsx index ea0d670..f42bfbf 100644 --- a/src/providers/deviceTypeProvider/provider.tsx +++ b/src/providers/deviceTypeProvider/provider.tsx @@ -1,37 +1,18 @@ -import React, { ReactNode, useCallback, useEffect, useReducer } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { displaySizeContext } from "./context"; -import { DisplaySizeInfo } from "types"; +import { BreakpointKey } from "types"; import { calcBreakPoint } from "src/configs"; type Props = { children: ReactNode; }; -const baseState: DisplaySizeInfo = { - mobile: false, - tablet: false, - desktop: false, -}; - export const DisplaySizeProvider = ({ children }: Props) => { - const displaySizeReducer = useCallback( - (state: DisplaySizeInfo, width: number): DisplaySizeInfo => { - const type = calcBreakPoint(width); - if (state[type]) return state; - - return { ...baseState, [type]: true }; - }, - [], - ); - - const [displaySizeInfo, dispatch] = useReducer( - displaySizeReducer, - displaySizeReducer(baseState, window.innerWidth), - ); + const [type, setType] = useState("desktop"); useEffect(() => { const resize = () => { - dispatch(window.innerWidth); + setType(calcBreakPoint(window.innerWidth)); }; resize(); @@ -40,7 +21,7 @@ export const DisplaySizeProvider = ({ children }: Props) => { }, []); return ( - + {children} ); diff --git a/src/providers/settingProvider/provider.tsx b/src/providers/settingProvider/provider.tsx index f76f8a2..e36e135 100644 --- a/src/providers/settingProvider/provider.tsx +++ b/src/providers/settingProvider/provider.tsx @@ -80,10 +80,11 @@ export const SettingProvider = ({ children }: Props) => { const onUnmount = () => { setLocalSetting(setting); }; + onUnmount(); window.addEventListener("beforeunload", onUnmount); return () => { - setLocalSetting(setting); + onUnmount(); window.removeEventListener("beforeunload", onUnmount); }; }, [setting]); diff --git a/src/providers/vspoStreamProvider/provider.tsx b/src/providers/vspoStreamProvider/provider.tsx index 058b4b5..c1cec7f 100644 --- a/src/providers/vspoStreamProvider/provider.tsx +++ b/src/providers/vspoStreamProvider/provider.tsx @@ -20,11 +20,7 @@ type Props = { children: ReactNode; }; -const parseToStream = ( - streamRes: StreamResponse, - streamerId: string, - channel: Channel, -): Stream => { +const parseToStream = (streamRes: StreamResponse, channel: Channel): Stream => { const endAt = streamRes.endTime ? new Date(streamRes.endTime) : undefined; return { @@ -32,7 +28,7 @@ const parseToStream = ( title: streamRes.title, thumbnail: streamRes.thumbnail, url: streamRes.url, - streamerId, + streamerId: streamRes.streamerId, streamerName: channel.name, icon: channel.icon, platform: streamRes.platform, @@ -67,10 +63,10 @@ export const VspoStreamProvider = ({ children }: Props) => { const unSubStream = onSnapshot( collection(firestore, streamCollectionName), (snapshot) => { + const newStreams = snapshot.docs.map( + (doc) => doc.data() as StreamResponse, + ); setStreamsResponse((prev) => { - const newStreams = snapshot.docs.map( - (doc) => doc.data() as StreamResponse, - ); return [ ...newStreams, ...prev.filter((s) => !newStreams.some(({ id }) => id === s.id)), @@ -99,15 +95,14 @@ export const VspoStreamProvider = ({ children }: Props) => { const streams = useMemo(() => { return streamResponses.reduce((results: Stream[], streamRes) => { - const streamerId = streamRes.streamerId; - const channel = streamerMap[streamerId][streamRes.platform]; + const channel = streamerMap[streamRes.streamerId][streamRes.platform]; if (!channel) { - console.error(`streamerId is not found: ${streamerId}`); + console.error(`streamerId is not found: ${streamRes.streamerId}`); return results; } - return results.concat(parseToStream(streamRes, streamerId, channel)); + return results.concat(parseToStream(streamRes, channel)); }, []); }, [streamResponses, streamerMap]); diff --git a/types/config.ts b/types/config.ts index f0f6c18..b40fbc0 100644 --- a/types/config.ts +++ b/types/config.ts @@ -19,6 +19,15 @@ export type Breakpoints = { [key in BreakpointKey]: number; }; -export type DisplaySizeInfo = { - [key in BreakpointKey]: boolean; +export type ResponsiveProperty = { + card: { + width: number; + height: number; + expandedHeight: number; + gap: { x: number; y: number }; + }; +}; + +export type ResponsiveProperties = { + [key in BreakpointKey]: ResponsiveProperty; };