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 = {