From 27b6268a67003280d54e1282692539082beb0c2e Mon Sep 17 00:00:00 2001 From: mnsinri Date: Sun, 3 Sep 2023 03:42:57 +0900 Subject: [PATCH] feat: setting menu --- public/index.html | 11 -- src/App.tsx | 9 +- src/colors/blue.ts | 14 ++ src/colors/common.ts | 7 + src/colors/grey.ts | 19 +++ src/colors/icon.ts | 7 + src/colors/index.ts | 5 + src/colors/pink.ts | 14 ++ src/components/Header.tsx | 22 +-- src/components/MainContainer.tsx | 7 +- src/components/buttons/ThemeButton.tsx | 4 +- src/components/card/ServiceIcon.tsx | 9 +- src/components/card/StreamingCard.tsx | 14 +- src/components/card/ThumbnailBlock.tsx | 11 +- src/components/marquee/Marquee.tsx | 9 +- src/components/providers/ConfigProvider.tsx | 57 ++++++++ src/components/providers/ThemeProvider.tsx | 6 +- src/components/providers/index.ts | 1 + src/components/settingMenu/MenuItem.tsx | 52 +++++++ src/components/settingMenu/SettingMenu.tsx | 136 ++++++++++++++++++ src/components/settingMenu/ToggleButton.tsx | 131 +++++++++++++++++ .../settingMenu/ToggleButtonItem.tsx | 56 ++++++++ src/components/settingMenu/index.ts | 4 + src/configs/baseColors.ts | 23 --- src/configs/index.ts | 1 - src/hooks/index.ts | 2 + src/hooks/useBoolStateCache.ts | 13 ++ src/hooks/useConfig.ts | 12 ++ src/hooks/useShakeAnimation.ts | 23 +++ src/theme/dark.ts | 20 +-- src/theme/light.ts | 20 +-- src/types/theme.ts | 23 +-- 32 files changed, 619 insertions(+), 123 deletions(-) create mode 100644 src/colors/blue.ts create mode 100644 src/colors/common.ts create mode 100644 src/colors/grey.ts create mode 100644 src/colors/icon.ts create mode 100644 src/colors/index.ts create mode 100644 src/colors/pink.ts create mode 100644 src/components/providers/ConfigProvider.tsx create mode 100644 src/components/settingMenu/MenuItem.tsx create mode 100644 src/components/settingMenu/SettingMenu.tsx create mode 100644 src/components/settingMenu/ToggleButton.tsx create mode 100644 src/components/settingMenu/ToggleButtonItem.tsx create mode 100644 src/components/settingMenu/index.ts delete mode 100644 src/configs/baseColors.ts create mode 100644 src/hooks/useBoolStateCache.ts create mode 100644 src/hooks/useConfig.ts create mode 100644 src/hooks/useShakeAnimation.ts diff --git a/public/index.html b/public/index.html index fe660d2..b8a77bf 100644 --- a/public/index.html +++ b/public/index.html @@ -25,17 +25,6 @@ Vspo stream schedule -
- diff --git a/src/App.tsx b/src/App.tsx index 1546d19..ab54022 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { WindowSizeProvider, ThemeProvider, MainContainer, + ConfigProvider, Background, } from "./components"; @@ -12,9 +13,11 @@ export const App: React.FC = () => { - - - + + + + + diff --git a/src/colors/blue.ts b/src/colors/blue.ts new file mode 100644 index 0000000..6d5b191 --- /dev/null +++ b/src/colors/blue.ts @@ -0,0 +1,14 @@ +const blue = { + 50: "#ebeaf9", + 100: "#ccc9ef", + 200: "#aaa6e4", + 300: "#8983d8", + 400: "#7266cf", //vspo blue + 500: "#5e49c5", + 600: "#5840ba", + 700: "#5035ad", + 800: "#482aa2", + 900: "#3d128b", +}; + +export default blue; diff --git a/src/colors/common.ts b/src/colors/common.ts new file mode 100644 index 0000000..a59e16d --- /dev/null +++ b/src/colors/common.ts @@ -0,0 +1,7 @@ +// https://github.com/mui/material-ui/blob/master/packages/mui-material/src/colors/common.js +const common = { + black: "#000", + white: "#fff", +}; + +export default common; diff --git a/src/colors/grey.ts b/src/colors/grey.ts new file mode 100644 index 0000000..9ae57db --- /dev/null +++ b/src/colors/grey.ts @@ -0,0 +1,19 @@ +// https://github.com/mui/material-ui/blob/master/packages/mui-material/src/colors/grey.js +const grey = { + 50: "#fafafa", + 100: "#f5f5f5", + 200: "#eeeeee", + 300: "#e0e0e0", + 400: "#bdbdbd", + 500: "#9e9e9e", + 600: "#757575", + 700: "#616161", + 800: "#424242", + 900: "#212121", + A100: "#f5f5f5", + A200: "#eeeeee", + A400: "#bdbdbd", + A700: "#616161", +}; + +export default grey; diff --git a/src/colors/icon.ts b/src/colors/icon.ts new file mode 100644 index 0000000..30ec18c --- /dev/null +++ b/src/colors/icon.ts @@ -0,0 +1,7 @@ +const icon = { + youtube: "#ff0000", + twitch: "#9146FF", + twitCasting: "#0092fa", +}; + +export default icon; diff --git a/src/colors/index.ts b/src/colors/index.ts new file mode 100644 index 0000000..3416007 --- /dev/null +++ b/src/colors/index.ts @@ -0,0 +1,5 @@ +export { default as common } from "./common"; +export { default as grey } from "./grey"; +export { default as pink } from "./pink"; +export { default as blue } from "./blue"; +export { default as icon } from "./icon"; diff --git a/src/colors/pink.ts b/src/colors/pink.ts new file mode 100644 index 0000000..a3639d2 --- /dev/null +++ b/src/colors/pink.ts @@ -0,0 +1,14 @@ +const pink = { + 50: "#fee6ef", + 100: "#fec1d8", + 200: "#fe98be", + 300: "#ff6fa4", //vspo pink + 400: "#fe508e", + 500: "#ff3679", + 600: "#ed3375", + 700: "#d52f6f", + 800: "#c02b6a", + 900: "#992462", +}; + +export default pink; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 87851f8..90a65f2 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import { ThemeButton, GithubLinkButton } from "./buttons"; +import { SettingMenu } from "./settingMenu"; import { breakpoints } from "../configs"; import logo from "../logo.png"; import { useWindowSize } from "../hooks"; @@ -19,11 +19,11 @@ const Container = styled.div` `} `; -const Title = styled.div` - flex-grow: 1; +const Title = styled.div<{ isPhone: boolean }>` width: 100%; display: flex; justify-content: center; + margin-left: ${(p) => (p.isPhone ? "40px" : "0px")}; ${breakpoints.mediaQueries.md` justify-content: start; @@ -49,29 +49,21 @@ const TitleText = styled.div` `; const Wrapper = styled.div` - width: 35px; + width: 40px; display: flex; - justify-content: center; - - ${breakpoints.mediaQueries.md` - width: 120px; - justify-content: flex-end; - `} + justify-content: flex-end; `; export const Header: React.FC = () => { const { isPhoneSize } = useWindowSize(); return ( - + <Title isPhone={isPhoneSize}> <Icon src={logo} alt="logo" /> {!isPhoneSize && <TitleText>Vspo stream schedule</TitleText>} - - - - + ); diff --git a/src/components/MainContainer.tsx b/src/components/MainContainer.tsx index 1798725..afce430 100644 --- a/src/components/MainContainer.tsx +++ b/src/components/MainContainer.tsx @@ -3,13 +3,13 @@ import styled from "styled-components"; import { breakpoints } from "../configs"; import { StreamingTable } from "./StreamingTable"; import { DateBorder } from "./DateBorder"; -import { useVspoStreams } from "../hooks"; +import { useConfig, useVspoStreams } from "../hooks"; import { StreamInfo, StreamList } from "../types"; import { Header } from "./Header"; const Container = styled.div` margin: 0 auto; - background: rgba(240, 240, 240, 0.08); + background: rgba(240, 240, 240, 0.03); box-shadow: 0px 0px 4px 4px rgba(0, 0, 0, 0.2); color: ${(p) => p.theme.text.primary}; transition: color 0.3s ease; @@ -73,6 +73,7 @@ const TableContainer = styled.div` export const MainContainer = React.memo(() => { const streams = useVspoStreams(); + const { config } = useConfig(); const parseToStreamList = (streams: StreamInfo[]): StreamList[] => { const dateSet = new Set(streams.map((s) => s.scheduledDate)); @@ -85,7 +86,7 @@ export const MainContainer = React.memo(() => { }; return ( - +
{parseToStreamList(streams).map((s) => ( diff --git a/src/components/buttons/ThemeButton.tsx b/src/components/buttons/ThemeButton.tsx index 94566c8..a0039ed 100644 --- a/src/components/buttons/ThemeButton.tsx +++ b/src/components/buttons/ThemeButton.tsx @@ -13,7 +13,7 @@ const Wrapper = styled(animated.div)` export const ThemeButton: React.FC> = ({ ...props }) => { - const { themeType, toggleTheme } = useTheme(); + const { themeType, setThemeDark } = useTheme(); const transitions = useTransition(themeType, { from: { opacity: 0 }, @@ -23,7 +23,7 @@ export const ThemeButton: React.FC> = ({ }); return ( - + setThemeDark(true)} {...props}> {transitions((style, themeType) => ( {themeType === "dark" ? : } diff --git a/src/components/card/ServiceIcon.tsx b/src/components/card/ServiceIcon.tsx index 701e127..84d7111 100644 --- a/src/components/card/ServiceIcon.tsx +++ b/src/components/card/ServiceIcon.tsx @@ -6,7 +6,8 @@ import { IconContext } from "react-icons"; import { FaYoutube, FaTwitch } from "react-icons/fa"; import { TbBroadcast } from "react-icons/tb"; import { useTheme, useWindowSize } from "../../hooks"; -import { breakpoints, baseColors } from "../../configs"; +import { breakpoints } from "../../configs"; +import { icon } from "../../colors"; import { parseToJST } from "../../utils"; const Panel = styled(animated.div)` @@ -71,11 +72,11 @@ export const ServiceIcon: React.FC = ({ const serviceColor = useMemo(() => { switch (service) { case "youtube": - return baseColors.logo.youtube; + return icon.youtube; case "twitch": - return baseColors.logo.twitch; + return icon.twitch; case "twitCasting": - return baseColors.logo.twitCasting; + return icon.twitCasting; } }, [service]); const startDate = useMemo(() => new Date(startAt), [startAt]); diff --git a/src/components/card/StreamingCard.tsx b/src/components/card/StreamingCard.tsx index a2668ee..68df02f 100644 --- a/src/components/card/StreamingCard.tsx +++ b/src/components/card/StreamingCard.tsx @@ -1,9 +1,9 @@ -import React, { useMemo } from "react"; +import React from "react"; import { StreamingCardProps } from "../../types"; import { ServiceIcon } from "./ServiceIcon"; import { ThumbnailBlock } from "./ThumbnailBlock"; import styled from "styled-components"; -import { useHover, useWindowSize } from "../../hooks"; +import { useConfig, useHover, useWindowSize } from "../../hooks"; import { animated } from "@react-spring/web"; import { breakpoints } from "../../configs"; @@ -27,11 +27,9 @@ export const StreamingCard = React.memo( ({ title, thumbnail, name, icon, service, url, startAt }) => { const { hovered, hoverSpread } = useHover(); const { isMobile, isDesktopSize } = useWindowSize(); + const { config } = useConfig(); - const isHoverable = useMemo( - () => !isMobile && isDesktopSize, - [isMobile, isDesktopSize] - ); + const isHoverable = !isMobile && isDesktopSize; return ( @@ -43,7 +41,7 @@ export const StreamingCard = React.memo( ( thumbnail={thumbnail} name={name} icon={icon} - isExpand={!isHoverable || hovered} + isExpand={config.isExpandAlways || !isHoverable || hovered} /> diff --git a/src/components/card/ThumbnailBlock.tsx b/src/components/card/ThumbnailBlock.tsx index c10b0ee..65e19d8 100644 --- a/src/components/card/ThumbnailBlock.tsx +++ b/src/components/card/ThumbnailBlock.tsx @@ -1,10 +1,10 @@ import React from "react"; import styled from "styled-components"; -import { ThumbnailBlockProps } from "../../types"; import { animated, easings, useSpring } from "@react-spring/web"; import { breakpoints } from "../../configs"; -import { useWindowSize } from "../../hooks"; +import { useConfig, useWindowSize } from "../../hooks"; import { Marquee } from "../marquee"; +import { ThumbnailBlockProps } from "../../types"; const Panel = styled(animated.div)` width: 160px; @@ -112,6 +112,8 @@ export const ThumbnailBlock: React.FC = ({ ...props }) => { const { isPhoneSize } = useWindowSize(); + const { config } = useConfig(); + const baseSpringConfig = { height: isExpand ? "240px" : "180px", borderRadius: isExpand ? "10px 10px 0px 0px" : "10px 10px 10px 10px", @@ -156,7 +158,10 @@ export const ThumbnailBlock: React.FC = ({
- + {title} {name} diff --git a/src/components/marquee/Marquee.tsx b/src/components/marquee/Marquee.tsx index dacb8f1..814d60f 100644 --- a/src/components/marquee/Marquee.tsx +++ b/src/components/marquee/Marquee.tsx @@ -1,11 +1,4 @@ -import React, { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import styled from "styled-components"; import { MarqueeProps } from "../../types"; import { animated, useSpring, useSpringRef } from "@react-spring/web"; diff --git a/src/components/providers/ConfigProvider.tsx b/src/components/providers/ConfigProvider.tsx new file mode 100644 index 0000000..1b61010 --- /dev/null +++ b/src/components/providers/ConfigProvider.tsx @@ -0,0 +1,57 @@ +import React, { createContext, createRef } from "react"; +import { ChildrenNode } from "../../types"; +import { useBoolStateCache } from "../../hooks"; + +type ConfigSetter = { + setExpandAlways: React.Dispatch>; + setMarquee: React.Dispatch>; +}; + +type Config = { + scrollContainerRef: React.RefObject; + isExpandAlways: boolean; + isMarquee: boolean; +}; + +type ContextType = { + config: Config; + configSetter: ConfigSetter; +}; + +const initState = { + config: { + scrollContainerRef: null!, + isExpandAlways: true, + isMarquee: true, + }, + configSetter: { + setExpandAlways: null!, + setMarquee: null!, + }, +}; + +export const ConfigContext = createContext(initState); +const scrollContainerRef = createRef(); + +export const ConfigProvider: React.FC = ({ children }) => { + const [isExpandAlways, setExpandAlways] = useBoolStateCache( + "isExpandAlways", + initState.config.isExpandAlways + ); + + const [isMarquee, setMarquee] = useBoolStateCache( + "isMarquee", + initState.config.isMarquee + ); + + return ( + + {children} + + ); +}; diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx index 99aa8a0..4e5ea84 100644 --- a/src/components/providers/ThemeProvider.tsx +++ b/src/components/providers/ThemeProvider.tsx @@ -13,7 +13,7 @@ const cacheKey = "themeType"; export const ThemeContext = createContext({ themeType: "light", theme: {} as ColorTheme, - toggleTheme: () => {}, + setThemeDark: (isOn) => {}, }); export const ThemeProvider: React.FC = ({ children }) => { @@ -29,8 +29,8 @@ export const ThemeProvider: React.FC = ({ children }) => { () => ({ themeType, theme: themes[themeType], - toggleTheme: () => { - setTheme((p) => ("light" === p ? "dark" : "light")); + setThemeDark: (isOn) => { + setTheme(isOn ? "dark" : "light"); }, }), [themeType] diff --git a/src/components/providers/index.ts b/src/components/providers/index.ts index 03264d8..6954a10 100644 --- a/src/components/providers/index.ts +++ b/src/components/providers/index.ts @@ -1,3 +1,4 @@ export { ThemeProvider } from "./ThemeProvider"; export { VspoStreamingProvider } from "./VspoStreamingProvider"; export { WindowSizeProvider } from "./WindowSizeProvider"; +export { ConfigProvider } from "./ConfigProvider"; diff --git a/src/components/settingMenu/MenuItem.tsx b/src/components/settingMenu/MenuItem.tsx new file mode 100644 index 0000000..e6304fe --- /dev/null +++ b/src/components/settingMenu/MenuItem.tsx @@ -0,0 +1,52 @@ +import React, { CSSProperties, ReactNode } from "react"; +import styled, { css } from "styled-components"; + +const Item = styled.li<{ hoverable: boolean }>` + cursor: pointer; + list-style: none; + display: flex; + transition: background-color 0.3s ease; + padding: 7px 14px; + border-radius: 7px; + + ${(p) => + p.hoverable && + css` + &:hover { + background-color: ${(p) => p.theme.hoverd.secondary}; + } + `} +`; + +const IconContainer = styled.div` + width: 20px; + display: flex; + align-items: center; + margin-right: 5px; +`; + +const ItemText = styled.span``; + +type Props = { + children?: ReactNode; + icon?: ReactNode; + text?: string; + onClick?: () => void; + style?: CSSProperties; +}; + +export const MenuItem: React.FC = ({ + children, + icon, + text, + onClick, + style, +}) => { + return ( + + {icon && {icon}} + {text && {text}} + {children} + + ); +}; diff --git a/src/components/settingMenu/SettingMenu.tsx b/src/components/settingMenu/SettingMenu.tsx new file mode 100644 index 0000000..4716d9a --- /dev/null +++ b/src/components/settingMenu/SettingMenu.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { MenuItem } from "./MenuItem"; +import { ToggleButtonItem } from "./ToggleButtonItem"; +import { animated, useTransition } from "@react-spring/web"; +import styled from "styled-components"; +import { useConfig, useTheme, useWindowSize } from "../../hooks"; + +import { BiMenu, BiExpandAlt } from "react-icons/bi"; +import { FaGithub } from "react-icons/fa"; +import { TbMoonFilled } from "react-icons/tb"; +import { TbMarquee2 } from "react-icons/tb"; + +const MenuButton = styled(animated.button)` + border: 0; + border-radius: 5px; + background-color: transparent; + transition: 0.3s ease; + color: ${(p) => p.theme.text.primary}; + + &:hover { + background-color: ${(p) => p.theme.hoverd.primary}; + } + + &:active { + background-color: ${(p) => p.theme.hoverd.secondary}; + } +`; + +const Container = styled(animated.ol)<{ right: number }>` + position: absolute; + top: 60px; + right: ${(p) => p.right}px; + z-index: 100; + width: 250px; + border: 5px solid ${(p) => p.theme.bg.secondary}; + border-radius: 7px; + box-shadow: 0px 3px 6px 2px rgba(0, 0, 0, 0.2); + background-color: ${(p) => p.theme.bg.secondary}; + padding: 2px; +`; + +const Border = styled.hr` + border-top: 1px solid ${(p) => p.theme.border.primary}; + margin: 7px 0px; + padding: 0 5px; +`; + +export const SettingMenu: React.FC = () => { + const [isOpen, setOpen] = useState(false); + const refOl = useRef(null!); + const refBtn = useRef(null!); + const { width, isMobile, isDesktopSize } = useWindowSize(); + + const { themeType, setThemeDark } = useTheme(); + const { config, configSetter } = useConfig(); + + const right = useMemo( + () => width - refBtn.current?.getBoundingClientRect().right, + [width] + ); + + useEffect(() => { + const checkIfClickedOutside = (e: MouseEvent) => { + if (!(e.target instanceof Node)) return; + + const isOutSideMenu = refOl.current && !refOl.current.contains(e.target); + const isOutSideBtn = refBtn.current && !refBtn.current.contains(e.target); + + if (isOutSideMenu && isOutSideBtn) setOpen(false); + }; + + if (config.scrollContainerRef.current != undefined) { + config.scrollContainerRef.current.style.overflow = isOpen + ? "hidden" + : "scroll"; + } + + if (isOpen) document.addEventListener("click", checkIfClickedOutside); + return () => document.removeEventListener("click", checkIfClickedOutside); + }, [isOpen]); + + const transitions = useTransition(isOpen, { + from: { opacity: 0, y: -10 }, + enter: { opacity: 1, y: 0 }, + leave: { opacity: 0, y: -10 }, + }); + + return ( + <> + setOpen((o) => !o)}> + + + {transitions( + (style, item) => + item && ( + + } + text="Github" + onClick={() => + window.open("https://github.com/mnsinri/vspo-stream-schedule") + } + /> + + + , + text: "Dark theme", + }} + onChange={(isOn) => setThemeDark(isOn)} + /> + , + text: "Expand always", + }} + onChange={(isOn) => configSetter.setExpandAlways(isOn)} + /> + , + text: "Marquee title", + }} + onChange={(isOn) => configSetter.setMarquee(isOn)} + /> + + ) + )} + + ); +}; diff --git a/src/components/settingMenu/ToggleButton.tsx b/src/components/settingMenu/ToggleButton.tsx new file mode 100644 index 0000000..d48b795 --- /dev/null +++ b/src/components/settingMenu/ToggleButton.tsx @@ -0,0 +1,131 @@ +import { animated, useSpring } from "@react-spring/web"; +import React, { useMemo, useState } from "react"; +import { useShakeAnimation } from "../../hooks/useShakeAnimation"; +import styled from "styled-components"; +import { useTheme } from "../../hooks"; + +const Container = styled(animated.div)<{ width: number; height: number }>` + position: relative; + border-radius: ${(p) => p.height / 2 - 1}px; + height: ${(p) => p.height}px; + width: ${(p) => p.width}px; +`; + +const Feild = styled(animated.input)<{ cursor: string }>` + appearance: none; + outline: none; + border: none; + border-radius: inherit; + margin: 0; + padding: 0; + width: 100%; + height: 100%; + transition: 0.3s ease; + cursor: ${(p) => p.cursor}; +`; + +const Knob = styled(animated.label)` + position: absolute; + pointer-events: none; + background-color: #fafafa; + border-radius: inherit; + z-index: 1; +`; + +type Props = { + onChange: (on: boolean) => void; + size?: number; + initState?: boolean; + disabled?: boolean; +}; + +export const ToggleButton: React.FC = ({ + onChange, + size = 30, + initState = false, + disabled = false, +}) => { + const { theme } = useTheme(); + const [isOn, setOn] = useState(initState); + const rect = useMemo( + () => ({ + width: size * 1.8, + height: size, + }), + [size] + ); + + const [shakeStyle, shakeAnim] = useShakeAnimation(5); + + const onClick = (e: React.MouseEvent) => { + if (!disabled) return; + + e.preventDefault(); + shakeAnim(); + }; + + const onChangeStatus = (e: React.ChangeEvent) => { + if (disabled) { + e.preventDefault(); + return; + } + + setOn(e.target.checked); + onChange(e.target.checked); + }; + + const { x, backgroundColor, opacity } = useSpring({ + from: { + x: initState ? 1 : 0, + backgroundColor: initState ? theme.vspo.primary : "#c7cbdf", + opacity: disabled ? 0.5 : 1, + }, + x: isOn ? 1 : 0, + backgroundColor: isOn ? theme.vspo.primary : "#c7cbdf", + opacity: disabled ? 0.5 : 1, + config: { + duration: 150, + }, + }); + + const toggleStyle = useMemo(() => { + const knobSize = size * 0.7; + const margin = (size - knobSize) / 2; + const range = [0, 0.35, 0.65, 1]; + const step = (rect.width - size) / 3; + const d = 2; + + return { + top: x.to({ + range, + output: [1, 2, 2, 1].map((n) => n * margin), + }), + left: x.to({ + range, + output: [0, step + d, 2 * step - d, 3 * step].map((n) => n + margin), + }), + width: x.to({ + range, + output: [1, 1.4, 1.4, 1].map((n) => n * knobSize), + }), + height: x.to({ + range, + output: [1, 0.55, 0.55, 1].map((n) => n * knobSize), + }), + }; + }, [size]); + + return ( + + + + + ); +}; diff --git a/src/components/settingMenu/ToggleButtonItem.tsx b/src/components/settingMenu/ToggleButtonItem.tsx new file mode 100644 index 0000000..1a6069e --- /dev/null +++ b/src/components/settingMenu/ToggleButtonItem.tsx @@ -0,0 +1,56 @@ +import React, { ReactNode, useEffect, useMemo, useState } from "react"; +import styled from "styled-components"; +import { ToggleButton } from "./ToggleButton"; +import { MenuItem } from "./MenuItem"; + +const FlexEnd = styled.div` + margin-left: auto; + display: flex; + align-items: center; +`; + +type Contents = { + children?: ReactNode; + icon?: ReactNode; + text?: string; +}; + +type FuncContents = (isOn: boolean) => Contents; + +type Props = { + contents: Contents | FuncContents; + onChange: (isOn: boolean) => void; + initState?: boolean; + disabled?: boolean; +}; + +export const ToggleButtonItem: React.FC = ({ + contents, + onChange, + initState = false, + disabled, +}) => { + const [isOn, setOn] = useState(initState); + const { children, icon, text } = useMemo( + () => (typeof contents === "function" ? contents(isOn) : contents), + [contents, isOn] + ); + + useEffect(() => { + onChange(isOn); + }, [isOn]); + + return ( + + {children} + + setOn(isOn)} + initState={initState} + disabled={disabled} + /> + + + ); +}; diff --git a/src/components/settingMenu/index.ts b/src/components/settingMenu/index.ts new file mode 100644 index 0000000..501d497 --- /dev/null +++ b/src/components/settingMenu/index.ts @@ -0,0 +1,4 @@ +export * from "./SettingMenu"; +export * from "./MenuItem"; +export * from "./ToggleButton"; +export * from "./ToggleButtonItem"; diff --git a/src/configs/baseColors.ts b/src/configs/baseColors.ts deleted file mode 100644 index b4a62c5..0000000 --- a/src/configs/baseColors.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BaseColors } from "../types"; - -export const baseColors: BaseColors = { - white: { - 50: "#fefefe", - 100: "#fafafa", - 200: "#f5f5f5", - }, - black: { - 50: "#212121", - 100: "#424242", - 200: "#616161", - }, - logo: { - vspo: { - pink: "#FF6FA3", - blue: "#7266CF", - }, - youtube: "#ff0000", - twitch: "#9146FF", - twitCasting: "#0092fa", - }, -}; diff --git a/src/configs/index.ts b/src/configs/index.ts index b6b0e98..93e0723 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -1,6 +1,5 @@ import { easings, SpringConfig } from "@react-spring/web"; -export { baseColors } from "./baseColors"; export { breakpoints } from "./breakpoints"; export const springConfig: SpringConfig = { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 0b6c9fe..41f7017 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -4,3 +4,5 @@ export { useTheme } from "./useTheme"; export { useDB } from "./useDB"; export { useStreamInfo } from "./useStreamInfo"; export { useWindowSize } from "./useWindowSize"; +export { useConfig } from "./useConfig"; +export { useBoolStateCache } from "./useBoolStateCache"; diff --git a/src/hooks/useBoolStateCache.ts b/src/hooks/useBoolStateCache.ts new file mode 100644 index 0000000..cdddfe8 --- /dev/null +++ b/src/hooks/useBoolStateCache.ts @@ -0,0 +1,13 @@ +import { useEffect, useState } from "react"; + +export const useBoolStateCache = (key: string, initState: boolean) => { + const [value, setter] = useState( + localStorage.getItem(key)?.toLocaleLowerCase() === "true" ?? initState + ); + + useEffect(() => { + localStorage.setItem(key, value.toString()); + }, [value]); + + return [value, setter] as const; +}; diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts new file mode 100644 index 0000000..e3c8a7e --- /dev/null +++ b/src/hooks/useConfig.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import { ConfigContext } from "../components/providers/ConfigProvider"; + +export const useConfig = () => { + const context = useContext(ConfigContext); + + if (typeof context === "undefined") { + throw new Error("useConfig must be within a VspoStreamingProvider"); + } + + return context; +}; diff --git a/src/hooks/useShakeAnimation.ts b/src/hooks/useShakeAnimation.ts new file mode 100644 index 0000000..4edac04 --- /dev/null +++ b/src/hooks/useShakeAnimation.ts @@ -0,0 +1,23 @@ +import { useSpring, useSpringRef } from "@react-spring/web"; + +export const useShakeAnimation = (x: number) => { + const api = useSpringRef(); + const style = useSpring({ + ref: api, + x: 0, + }); + + const shake = () => { + api.start({ + x: 0, + from: { x }, + config: { + mass: 1, + tension: 500, + friction: 15, + }, + }); + }; + + return [style, shake] as const; +}; diff --git a/src/theme/dark.ts b/src/theme/dark.ts index 7482bb0..7059857 100644 --- a/src/theme/dark.ts +++ b/src/theme/dark.ts @@ -1,21 +1,23 @@ import { ColorTheme } from "../types"; -import { baseColors } from "../configs"; +import { common, grey, pink, blue } from "../colors"; export const dark: ColorTheme = { text: { - primary: baseColors.white[50], - secondary: baseColors.white[100], + primary: common.white, }, bg: { - primary: baseColors.black[50], - secondary: baseColors.black[100], + primary: grey[900], + secondary: grey[800], + }, + hoverd: { + primary: grey[700], + secondary: grey[600], }, border: { - primary: baseColors.white[50], - secondary: baseColors.white[100], + primary: grey[100], }, vspo: { - primary: baseColors.logo.vspo.pink, - secondary: baseColors.logo.vspo.blue, + primary: pink[300], + secondary: blue[400], }, }; diff --git a/src/theme/light.ts b/src/theme/light.ts index d8fd687..b438a3c 100644 --- a/src/theme/light.ts +++ b/src/theme/light.ts @@ -1,21 +1,23 @@ import { ColorTheme } from "../types"; -import { baseColors } from "../configs"; +import { common, grey, pink, blue } from "../colors"; export const light: ColorTheme = { text: { - primary: baseColors.black[50], - secondary: baseColors.black[100], + primary: common.black, }, bg: { - primary: baseColors.white[50], - secondary: baseColors.white[100], + primary: grey[50], + secondary: grey[100], + }, + hoverd: { + primary: grey[200], + secondary: grey[300], }, border: { - primary: baseColors.black[50], - secondary: baseColors.black[100], + primary: grey[900], }, vspo: { - primary: baseColors.logo.vspo.blue, - secondary: baseColors.logo.vspo.pink, + primary: blue[400], + secondary: pink[300], }, }; diff --git a/src/types/theme.ts b/src/types/theme.ts index f694c81..b676d77 100644 --- a/src/types/theme.ts +++ b/src/types/theme.ts @@ -1,23 +1,3 @@ -export type Colorlevel = { - 50: string; - 100: string; - 200: string; -}; - -export type BaseColors = { - black: Colorlevel; - white: Colorlevel; - logo: { - vspo: { - pink: string; - blue: string; - }; - youtube: string; - twitch: string; - twitCasting: string; - }; -}; - export type ColorLevel = { primary: string; secondary?: string; @@ -26,6 +6,7 @@ export type ColorLevel = { export type ColorTheme = { text: ColorLevel; bg: ColorLevel; + hoverd: ColorLevel; border: ColorLevel; vspo: ColorLevel; }; @@ -40,5 +21,5 @@ export type ThemeTypes = keyof Theme; export type ThemeContextType = { themeType: ThemeTypes; theme: ColorTheme; - toggleTheme: () => void; + setThemeDark: (isOn: boolean) => void; };