diff --git a/src/components/displayHistoryButton/index.tsx b/src/components/displayHistoryButton/index.tsx new file mode 100644 index 0000000..54a51a1 --- /dev/null +++ b/src/components/displayHistoryButton/index.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Button } from "./styles"; +import { useSettingInterface } from "src/hooks"; + +export const DisplayHistoryButton = () => { + const { isDisplayHistory } = useSettingInterface(); + + return ( + + ); +}; diff --git a/src/components/displayHistoryButton/styles.tsx b/src/components/displayHistoryButton/styles.tsx new file mode 100644 index 0000000..add2d27 --- /dev/null +++ b/src/components/displayHistoryButton/styles.tsx @@ -0,0 +1,24 @@ +import styled, { css } from "styled-components"; + +export const Button = styled.button<{ state: boolean }>` + width: 40px; + border: 0; + border-radius: 5px; + background-color: ${({ theme }) => theme.displayHistoryButton.bg.normal}; + transition: 0.3s ease; + color: ${({ theme, state }) => + state + ? theme.displayHistoryButton.iconActive + : theme.displayHistoryButton.icon}; + ${({ state }) => + state && + css` + box-shadow: + inset 3px 3px 5px #bbbbbb, + inset -3px -3px 5px #ffffff; + `} + + &:hover { + background-color: ${({ theme }) => theme.displayHistoryButton.bg.hover}; + } +`; diff --git a/src/components/dropdownMenu/toggleButtonItem/index.tsx b/src/components/dropdownMenu/toggleButtonItem/index.tsx index b5ce7a1..a107446 100644 --- a/src/components/dropdownMenu/toggleButtonItem/index.tsx +++ b/src/components/dropdownMenu/toggleButtonItem/index.tsx @@ -17,13 +17,16 @@ type Contents = { type Props = { contents: Contents | ((isOn: boolean) => Contents); children?: ReactNode | ((isOn: boolean) => ReactNode); -} & ComponentProps; +} & Pick, "style"> & + ComponentProps; export const ToggleButtonItem: React.FC = ({ contents: _contents, children: _children, onChange: _onChange, initState = false, + size = 22, + style, disabled, }) => { const [isOn, setOn] = useState(initState); @@ -44,11 +47,11 @@ export const ToggleButtonItem: React.FC = ({ ); return ( - + {children} = ({ isScrolled, onOpenMenu, onCloseMenu }) => { Vspo stream schedule + ` - padding: 5px 10px; + padding: 10px 10px 10px 20px; margin-bottom: 5px; position: sticky; top: 0; left: 0; display: flex; align-items: center; + justify-content: space-between; border-radius: 0 0 10px 10px; z-index: 10; background-color: ${({ theme }) => theme.bg}; @@ -28,15 +29,8 @@ export const Container = styled.div<{ isScrolled: boolean }>` `; export const Title = styled.div` - width: 100%; display: flex; - justify-content: center; - margin-left: 40px; - - ${breakpointMediaQueries.tablet` - justify-content: start; - margin-left: 0px; - `} + justify-content: start; `; export const Icon = styled.img` @@ -59,7 +53,7 @@ export const TitleText = styled.div` `; export const DropdownWrapper = styled.div` - width: 40px; display: flex; + gap: 5px; justify-content: flex-end; `; diff --git a/src/components/inViewContainer/index.tsx b/src/components/inViewContainer/index.tsx new file mode 100644 index 0000000..ae97ef2 --- /dev/null +++ b/src/components/inViewContainer/index.tsx @@ -0,0 +1,45 @@ +import React, { + ReactElement, + ReactNode, + useEffect, + useRef, + useState, +} from "react"; +import { ObserverElement } from "./styles"; + +type Props = { + data: T[]; + renderItem: (props: T) => ReactNode; + observerHeight?: number; +}; + +export const InViewContainer = ({ + data, + renderItem, + observerHeight, +}: Props): ReactElement => { + const [renderDataIdx, setRenderDataIdx] = useState(1); + const ref = useRef(null!); + + useEffect(() => { + const observer = new IntersectionObserver(([entry]) => { + if (!entry.isIntersecting) return; + + setRenderDataIdx((n) => ++n); + }); + + observer.observe(ref.current); + + return () => { + observer.unobserve(ref.current); + setRenderDataIdx(1); + }; + }, [data]); + + return ( + <> + {data.slice(0, renderDataIdx).map((props) => renderItem(props))} + + + ); +}; diff --git a/src/components/inViewContainer/styles.tsx b/src/components/inViewContainer/styles.tsx new file mode 100644 index 0000000..c138500 --- /dev/null +++ b/src/components/inViewContainer/styles.tsx @@ -0,0 +1,6 @@ +import styled from "styled-components"; + +export const ObserverElement = styled.div<{ height?: number }>` + min-height: ${({ height }) => height ?? 0}px; + width: 100%; +`; diff --git a/src/components/mainContainer/index.tsx b/src/components/mainContainer/index.tsx index 41aca8c..9b55f67 100644 --- a/src/components/mainContainer/index.tsx +++ b/src/components/mainContainer/index.tsx @@ -14,6 +14,7 @@ import { StreamGridHeader } from "../streamGridHeader"; import { useDisplaySize, useSetting, useVspoStream } from "src/providers"; import { toYYYYMMDD } from "src/utils"; import { responsiveProperties } from "src/configs"; +import { InViewContainer } from "../inViewContainer"; type DailyStream = { date: string; @@ -91,7 +92,6 @@ export const MainContainer: FC = () => { return () => { window.removeEventListener("resize", onResize); ref.removeEventListener("scroll", onScroll); - window.removeEventListener("onload", onResize); }; }, [displaySize]); @@ -151,16 +151,20 @@ export const MainContainer: FC = () => { onOpenMenu={disableScroll} onCloseMenu={enableScroll} /> - {dailyStreams.map(({ date, streams }) => ( - - - - - ))} + ( + + + + + )} + observerHeight={30} + /> ); diff --git a/src/components/mainContainer/styles.tsx b/src/components/mainContainer/styles.tsx index c916254..4989885 100644 --- a/src/components/mainContainer/styles.tsx +++ b/src/components/mainContainer/styles.tsx @@ -28,8 +28,4 @@ export const Container = styled.div` 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 c4ed2c6..fa1e5a7 100644 --- a/src/components/settingMenu/index.tsx +++ b/src/components/settingMenu/index.tsx @@ -7,11 +7,12 @@ import { } from "../dropdownMenu"; import { Button } from "./styles"; import { StreamerFilter } from "../streamerFilter"; -import { useDisplaySize, useSetting, useSettingDispatch } from "src/providers"; -import { BiExpandAlt, BiMenu } from "react-icons/bi"; +import { useDisplaySize } from "src/providers"; +import { BiMenu } from "react-icons/bi"; import { FaGithub } from "react-icons/fa"; -import { TbMoonFilled, TbMarquee2, TbHistory } from "react-icons/tb"; import { IoIosArrowBack } from "react-icons/io"; +import { useSettingInterface } from "src/hooks"; +import { SettingComponentProps } from "types"; type Props = Pick< ComponentProps, @@ -19,8 +20,7 @@ type Props = Pick< >; export const SettingMenu: FC = memo(({ position, onOpen, onClose }) => { - const setting = useSetting(); - const configDispatch = useSettingDispatch(); + const settings = useSettingInterface(); const displaySize = useDisplaySize(); const MenuButton = memo(() => ( @@ -45,59 +45,17 @@ export const SettingMenu: FC = memo(({ position, onOpen, onClose }) => { )); - const ThemeSetting = memo(() => ( + const SettingMenuItem = (props: SettingComponentProps) => ( , - text: "Dark theme", + icon: , + text: props.label, }} - onChange={(payload) => configDispatch({ target: "isDarkTheme", payload })} - disabled={setting.isDarkTheme.isReadOnly} + onChange={props.onChange} + disabled={props.isReadOnly} /> - )); - - const ExpandSetting = memo(() => ( - , - text: "Expand always", - }} - onChange={(payload) => - configDispatch({ target: "isExpandAlways", payload }) - } - disabled={setting.isExpandAlways.isReadOnly} - /> - )); - - const MarqueeSetting = memo(() => ( - , - text: "Marquee title", - }} - onChange={(payload) => - configDispatch({ target: "isMarqueeTitle", payload }) - } - disabled={setting.isMarqueeTitle.isReadOnly} - /> - )); - - const HistorySetting = memo(() => ( - , - text: "Stream history", - }} - onChange={(payload: boolean) => - configDispatch({ target: "isDisplayHistory", payload }) - } - disabled={setting.isDisplayHistory.isReadOnly} - /> - )); + ); return ( = memo(({ position, onOpen, onClose }) => { onClose={onClose} > - - - - + + + + {displaySize !== "mobile" && } theme.dropdown.input.bg.normal}; diff --git a/src/components/streamGrid/index.tsx b/src/components/streamGrid/index.tsx index 7ec9f6c..cd818b9 100644 --- a/src/components/streamGrid/index.tsx +++ b/src/components/streamGrid/index.tsx @@ -11,7 +11,6 @@ type Props = { }; export const StreamGrid: FC = ({ streams, column, gap, minHeight }) => { const streamsMatrix = useMemo(() => { - console.log("streamsMatrix", column); const sortedStreams = [...streams].sort( (a, b) => a.startAt.getTime() - b.startAt.getTime() || diff --git a/src/components/streamGridHeader/index.tsx b/src/components/streamGridHeader/index.tsx index 9e43352..ea8a07c 100644 --- a/src/components/streamGridHeader/index.tsx +++ b/src/components/streamGridHeader/index.tsx @@ -1,6 +1,13 @@ import React, { FC, useMemo } from "react"; import { useTheme } from "styled-components"; -import { Bar, Container, DateLabel, Icon } from "./styles"; +import { + Bar, + Container, + DateContainer, + DateLabel, + DateLabelForOutline, + Icon, +} from "./styles"; import { toYYYYMMDD } from "src/utils"; type Props = { @@ -52,7 +59,10 @@ export const StreamGridHeader: FC = ({ dateString }) => { ))} - {parseToViewDate(dateString)} + + {parseToViewDate(dateString)} + {parseToViewDate(dateString)} + ); }; diff --git a/src/components/streamGridHeader/styles.tsx b/src/components/streamGridHeader/styles.tsx index 716e200..ccce101 100644 --- a/src/components/streamGridHeader/styles.tsx +++ b/src/components/streamGridHeader/styles.tsx @@ -4,13 +4,15 @@ export const Container = styled.div` display: flex; height: 50px; margin: 25px 0; + position: sticky; + top: 70px; + z-index: 2; `; export const Icon = styled.div` display: flex; gap: 5px; width: 30px; - aspect-ratio: 1; `; export const Bar = styled.div<{ height: number; bgColor: string }>` @@ -19,17 +21,34 @@ export const Bar = styled.div<{ height: number; bgColor: string }>` margin-top: auto; border-radius: 0 5px 0 3px; background-color: ${({ bgColor }) => bgColor}; - transition: 0.5s ease-out; + outline: 3px solid ${({ theme }) => theme.bg}; + transition: + height 0.5s ease-out, + outline 0.3s 0.2s ease-out; @starting-style { height: 0px; + outline: 0px solid transparent; } `; +export const DateContainer = styled.div` + position: relative; + margin-top: 5px; +`; + export const DateLabel = styled.div` font-size: 48px; font-family: "Itim", cursive; letter-spacing: -0.02em; - margin-top: 5px; color: ${({ theme }) => theme.cardHeader.text}; + transition: color 0.3s ease; +`; + +export const DateLabelForOutline = styled(DateLabel)` + position: absolute; + top: 0; + z-index: -1; + -webkit-text-stroke: 5px ${({ theme }) => theme.bg}; + transition: -webkit-text-stroke 0.3s ease; `; diff --git a/src/components/streamerFilter/styles.tsx b/src/components/streamerFilter/styles.tsx index 9a9099f..27bd2a9 100644 --- a/src/components/streamerFilter/styles.tsx +++ b/src/components/streamerFilter/styles.tsx @@ -42,7 +42,7 @@ export const Button = styled.button` export const StreamerIcon = styled.img<{ isClicked: boolean }>` height: 40px; - aspect-ratio: 1; + width: 40px; border-radius: 50%; object-fit: cover; cursor: pointer; @@ -62,6 +62,7 @@ export const StreamerIcon = styled.img<{ isClicked: boolean }>` ${({ theme, isClicked }) => breakpointMediaQueries.tablet` height: 50px; + width: 50px; border: 3px outset ${isClicked ? theme.cardHeader.icon[0] : "transparent"}; box-shadow: ${ isClicked diff --git a/src/configs/theme/dark.ts b/src/configs/theme/dark.ts index 93ffa5c..6176c80 100644 --- a/src/configs/theme/dark.ts +++ b/src/configs/theme/dark.ts @@ -62,4 +62,12 @@ export const darkTheme: DefaultTheme = { }, }, }, + displayHistoryButton: { + icon: colors.grey[100], + iconActive: colors.pink[300], + bg: { + normal: "transparent", + hover: colors.grey[700], + }, + }, }; diff --git a/src/configs/theme/light.ts b/src/configs/theme/light.ts index db865ee..6e046ce 100644 --- a/src/configs/theme/light.ts +++ b/src/configs/theme/light.ts @@ -62,4 +62,12 @@ export const lightTheme: DefaultTheme = { }, }, }, + displayHistoryButton: { + icon: colors.grey[900], + iconActive: colors.blue[400], + bg: { + normal: "transparent", + hover: colors.grey[200], + }, + }, }; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index cd64044..0318207 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,4 @@ export { useInterval } from "./useInterval"; export { useHover } from "./useHover"; export { useStreamFilter } from "./useStreamFilter"; +export { useSettingInterface } from "./useSettingInterface"; diff --git a/src/hooks/useSettingInterface.ts b/src/hooks/useSettingInterface.ts new file mode 100644 index 0000000..0d8b0c8 --- /dev/null +++ b/src/hooks/useSettingInterface.ts @@ -0,0 +1,65 @@ +import { useMemo } from "react"; +import { useSetting, useSettingDispatch } from "src/providers"; +import { BiExpandAlt } from "react-icons/bi"; +import { TbMoonFilled, TbMarquee2, TbHistory } from "react-icons/tb"; +import { SettingInterface } from "types"; + +export const useSettingInterface = (): SettingInterface => { + const setting = useSetting(); + const configDispatch = useSettingDispatch(); + + const isDarkTheme = useMemo( + () => ({ + label: "Dark theme", + icon: TbMoonFilled, + onChange: (payload: boolean) => { + configDispatch({ target: "isDarkTheme", payload }); + }, + ...setting.isDarkTheme, + }), + [setting.isDarkTheme.state], + ); + + const isExpandAlways = useMemo( + () => ({ + label: "Expand always", + icon: BiExpandAlt, + onChange: (payload: boolean) => { + configDispatch({ target: "isExpandAlways", payload }); + }, + ...setting.isExpandAlways, + }), + [setting.isExpandAlways.state], + ); + + const isMarqueeTitle = useMemo( + () => ({ + label: "Marquee title", + icon: TbMarquee2, + onChange: (payload: boolean) => { + configDispatch({ target: "isMarqueeTitle", payload }); + }, + ...setting.isMarqueeTitle, + }), + [setting.isMarqueeTitle.state], + ); + + const isDisplayHistory = useMemo( + () => ({ + label: "Stream history", + icon: TbHistory, + onChange: (payload: boolean) => { + configDispatch({ target: "isDisplayHistory", payload }); + }, + ...setting.isDisplayHistory, + }), + [setting.isDisplayHistory.state], + ); + + return { + isDarkTheme, + isExpandAlways, + isMarqueeTitle, + isDisplayHistory, + }; +}; diff --git a/types/setting.ts b/types/setting.ts index eb8e598..29d8862 100644 --- a/types/setting.ts +++ b/types/setting.ts @@ -1,5 +1,12 @@ +import { IconType } from "react-icons"; import { Streamer } from "./stream"; +export type SettingKey = + | "isDarkTheme" + | "isExpandAlways" + | "isMarqueeTitle" + | "isDisplayHistory"; + export type SettingState = { state: boolean; isReadOnly: boolean; @@ -10,9 +17,17 @@ export type FilterInfo = { }; export type Setting = { - isDarkTheme: SettingState; - isExpandAlways: SettingState; - isMarqueeTitle: SettingState; - isDisplayHistory: SettingState; + [key in SettingKey]: SettingState; +} & { filter: FilterInfo; }; + +export type SettingComponentProps = { + label: string; + icon: IconType; + onChange: (state: boolean) => void; +} & SettingState; + +export type SettingInterface = { + [key in SettingKey]: SettingComponentProps; +}; diff --git a/types/theme.ts b/types/theme.ts index 720c983..d2f8acb 100644 --- a/types/theme.ts +++ b/types/theme.ts @@ -48,6 +48,10 @@ export type DropdownTheme = { }; }; +export type DisplayHistoryButtonTheme = ButtonTheme & { + iconActive: string; +}; + declare module "styled-components" { export interface DefaultTheme { bg: string; @@ -55,6 +59,7 @@ declare module "styled-components" { cardHeader: CardHeaderTheme; header: HeaderTheme; dropdown: DropdownTheme; + displayHistoryButton: DisplayHistoryButtonTheme; } export type Themes = {