From fbfbbb815dc091444c4459e6349fc96bb76fe46d Mon Sep 17 00:00:00 2001 From: mnsinri Date: Sat, 19 Aug 2023 23:57:55 +0900 Subject: [PATCH 1/3] fix: use theme provider of styled-components --- src/components/Background.tsx | 13 +-- src/components/DateBorder.tsx | 41 ++++--- src/components/Header.tsx | 22 ++-- src/components/MainContainer.tsx | 35 +++--- src/components/StreamingTable.tsx | 4 +- src/components/buttons/BaseButton.tsx | 6 +- src/components/buttons/ThemeButton.tsx | 13 +-- src/components/card/ServiceIcon.tsx | 44 ++++---- src/components/card/StreamingCard.tsx | 6 +- src/components/card/ThumbnailBlock.tsx | 46 ++++---- src/components/providers/ThemeProvider.tsx | 101 +++++------------- .../providers/WindowSizeProvider.tsx | 9 +- .../colors.ts => configs/baseColors.ts} | 5 +- src/{theme => configs}/breakpoints.ts | 0 src/configs/index.ts | 9 ++ src/theme/dark.ts | 21 ++++ src/theme/index.ts | 11 +- src/theme/light.ts | 21 ++++ src/types/configs.ts | 33 ++++++ src/types/index.ts | 1 + src/types/theme.ts | 69 +++--------- 21 files changed, 241 insertions(+), 269 deletions(-) rename src/{theme/colors.ts => configs/baseColors.ts} (86%) rename src/{theme => configs}/breakpoints.ts (100%) create mode 100644 src/configs/index.ts create mode 100644 src/theme/dark.ts create mode 100644 src/theme/light.ts create mode 100644 src/types/configs.ts diff --git a/src/components/Background.tsx b/src/components/Background.tsx index 2121195..61af0b0 100644 --- a/src/components/Background.tsx +++ b/src/components/Background.tsx @@ -1,19 +1,14 @@ import React from "react"; import styled from "styled-components"; -import { animated } from "@react-spring/web"; import { ChildrenNode } from "../types"; -import { useTheme } from "../hooks"; -const Container = styled(animated.div)` +const Container = styled.div` height: 100svh; width: 100svw; + background-color: ${(p) => p.theme.bg.primary}; + transition: background-color 0.3s ease; `; export const Background = React.memo(({ children, ...props }) => { - const { springColors } = useTheme(); - return ( - - {children} - - ); + return {children}; }); diff --git a/src/components/DateBorder.tsx b/src/components/DateBorder.tsx index 696dfe1..60dbfd2 100644 --- a/src/components/DateBorder.tsx +++ b/src/components/DateBorder.tsx @@ -1,9 +1,9 @@ -import React from "react"; +import React, { useEffect } from "react"; import styled from "styled-components"; -import { animated, useSpring } from "@react-spring/web"; -import { DateBorderProps } from "../types"; -import { useTheme } from "../hooks"; +import { animated, useSpring, useSpringRef } from "@react-spring/web"; +import { DateBorderProps, ColorLevel } from "../types"; import { getFormattedDate, parseToJST } from "../utils"; +import { springConfig } from "../configs"; const Container = styled.div` display: flex; @@ -18,12 +18,14 @@ const Icon = styled.div` aspect-ratio: 1; `; -const Bar = styled(animated.div)` +const Bar = styled(animated.div)<{ type: keyof ColorLevel }>` margin-top: auto; width: 5px; + background-color: ${(p) => p.theme.vspo[p.type]}; + transition: background-color 0.3s ease; `; -const DateLabel = styled(animated.div)` +const DateLabel = styled.div` font-size: 48px; font-family: "Itim", cursive; letter-spacing: -0.03em; @@ -32,8 +34,6 @@ const DateLabel = styled(animated.div)` export const DateBorder = React.memo( ({ dateString, ...props }) => { - const { springColors, colors } = useTheme(); - const parseToViewDate = (dateString: string) => { const today = parseToJST(Date.now()); if (getFormattedDate(today) === dateString) { @@ -63,7 +63,9 @@ export const DateBorder = React.memo( const calcHeight = (max: number) => max - 7 * Math.random(); + const animApi = useSpringRef(); const { lh, mh, rh } = useSpring({ + ref: animApi, from: { lh: 0, mh: 0, @@ -74,25 +76,22 @@ export const DateBorder = React.memo( mh: calcHeight(20), rh: calcHeight(16), }, - config: colors.config, + delay: 200, + config: springConfig, }); + useEffect(() => { + animApi.start(); + }, []); + return ( - - - + + + - - {parseToViewDate(dateString)} - + {parseToViewDate(dateString)} ); } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4bff58e..87851f8 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,10 +1,9 @@ import React from "react"; import styled from "styled-components"; import { ThemeButton, GithubLinkButton } from "./buttons"; -import { theme } from "../theme"; +import { breakpoints } from "../configs"; import logo from "../logo.png"; -import { useTheme, useWindowSize } from "../hooks"; -import { animated } from "@react-spring/web"; +import { useWindowSize } from "../hooks"; const Container = styled.div` width: 100%; @@ -15,7 +14,7 @@ const Container = styled.div` display: flex; align-items: center; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` height: 100px; `} `; @@ -26,7 +25,7 @@ const Title = styled.div` display: flex; justify-content: center; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` justify-content: start; `} `; @@ -35,13 +34,13 @@ const Icon = styled.img` width: 60px; height: 60px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` width: 50px; height: 50px; `} `; -const TitleText = styled(animated.div)` +const TitleText = styled.div` margin-left: 10px; margin-top: 8px; font-size: 28px; @@ -54,7 +53,7 @@ const Wrapper = styled.div` display: flex; justify-content: center; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` width: 120px; justify-content: flex-end; `} @@ -62,16 +61,11 @@ const Wrapper = styled.div` export const Header: React.FC = () => { const { isPhoneSize } = useWindowSize(); - const { springColors } = useTheme(); return ( <Icon src={logo} alt="logo" /> - {!isPhoneSize ? ( - <TitleText style={{ color: springColors.text.primary }}> - Vspo stream schedule - </TitleText> - ) : null} + {!isPhoneSize && <TitleText>Vspo stream schedule</TitleText>} diff --git a/src/components/MainContainer.tsx b/src/components/MainContainer.tsx index 06785a4..1798725 100644 --- a/src/components/MainContainer.tsx +++ b/src/components/MainContainer.tsx @@ -1,17 +1,18 @@ import React from "react"; import styled from "styled-components"; -import { animated } from "@react-spring/web"; -import { theme } from "../theme"; +import { breakpoints } from "../configs"; import { StreamingTable } from "./StreamingTable"; import { DateBorder } from "./DateBorder"; import { useVspoStreams } from "../hooks"; import { StreamInfo, StreamList } from "../types"; import { Header } from "./Header"; -const Container = styled(animated.div)` +const Container = styled.div` margin: 0 auto; background: rgba(240, 240, 240, 0.08); box-shadow: 0px 0px 4px 4px rgba(0, 0, 0, 0.2); + color: ${(p) => p.theme.text.primary}; + transition: color 0.3s ease; height: 100%; overflow: scroll; @@ -22,40 +23,40 @@ const Container = styled(animated.div)` display: none; } - ${theme.breakpoints.mediaQueries.sm` - width: ${theme.breakpoints.values.sm}px; + ${breakpoints.mediaQueries.sm` + width: ${breakpoints.values.sm}px; `} - ${theme.breakpoints.mediaQueries.md` - width: ${theme.breakpoints.values.md}px; + ${breakpoints.mediaQueries.md` + width: ${breakpoints.values.md}px; `} - ${theme.breakpoints.mediaQueries.lg` - width: ${theme.breakpoints.values.lg}px; + ${breakpoints.mediaQueries.lg` + width: ${breakpoints.values.lg}px; `} - ${theme.breakpoints.mediaQueries.xl` - width: ${theme.breakpoints.values.xl}px; + ${breakpoints.mediaQueries.xl` + width: ${breakpoints.values.xl}px; `} - ${theme.breakpoints.mediaQueries.xxl` - width: ${theme.breakpoints.values.xxl}px; + ${breakpoints.mediaQueries.xxl` + width: ${breakpoints.values.xxl}px; `} `; -const InnerContainer = styled(animated.div)` +const InnerContainer = styled.div` margin: 0 auto; width: 90%; - ${theme.breakpoints.mediaQueries.sm` + ${breakpoints.mediaQueries.sm` width: 88%; `} - ${theme.breakpoints.mediaQueries.lg` + ${breakpoints.mediaQueries.lg` width: 73%; `} - ${theme.breakpoints.mediaQueries.xl` + ${breakpoints.mediaQueries.xl` width: 88%; `} `; diff --git a/src/components/StreamingTable.tsx b/src/components/StreamingTable.tsx index 6ada4d9..223b749 100644 --- a/src/components/StreamingTable.tsx +++ b/src/components/StreamingTable.tsx @@ -3,7 +3,7 @@ import styled from "styled-components"; import { StreamingTableProps } from "../types"; import { StreamingCard } from "./card"; import { useWindowSize } from "../hooks"; -import { theme } from "../theme"; +import { breakpoints } from "../configs"; const Container = styled.div<{ height: number }>` min-height: ${(p) => p.height}px; @@ -17,7 +17,7 @@ const FlexBox = styled.div` flex-direction: column; gap: 20px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` gap: 40px; `} `; diff --git a/src/components/buttons/BaseButton.tsx b/src/components/buttons/BaseButton.tsx index a0dbda5..c2ad8b9 100644 --- a/src/components/buttons/BaseButton.tsx +++ b/src/components/buttons/BaseButton.tsx @@ -1,9 +1,9 @@ import React from "react"; +import styled from "styled-components"; import { animated, useSpring } from "@react-spring/web"; import { IconContext } from "react-icons"; -import { useHover, useTheme, useWindowSize } from "../../hooks"; +import { useHover, useWindowSize } from "../../hooks"; import { BaseButtonProps } from "../../types"; -import styled from "styled-components"; const Container = styled.div` width: 27px; @@ -15,7 +15,6 @@ export const BaseButton: React.FC = ({ children, ...props }) => { - const { springColors } = useTheme(); const { hovered, hoverSpread } = useHover(); const { isMobile } = useWindowSize(); @@ -32,7 +31,6 @@ export const BaseButton: React.FC = ({ diff --git a/src/components/buttons/ThemeButton.tsx b/src/components/buttons/ThemeButton.tsx index 02138d5..94566c8 100644 --- a/src/components/buttons/ThemeButton.tsx +++ b/src/components/buttons/ThemeButton.tsx @@ -4,6 +4,7 @@ import { animated, useTransition } from "@react-spring/web"; import { IoMdSunny, IoMdMoon } from "react-icons/io"; import { useTheme } from "../../hooks"; import { BaseButton } from "./BaseButton"; +import { springConfig } from "../../configs"; const Wrapper = styled(animated.div)` position: absolute; @@ -12,20 +13,20 @@ const Wrapper = styled(animated.div)` export const ThemeButton: React.FC> = ({ ...props }) => { - const { colors, springColors, toggleTheme, isDark } = useTheme(); + const { themeType, toggleTheme } = useTheme(); - const transitions = useTransition(isDark, { + const transitions = useTransition(themeType, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, - config: colors.config, + config: springConfig, }); return ( - {transitions((style, isDark) => ( - - {isDark ? : } + {transitions((style, themeType) => ( + + {themeType === "dark" ? : } ))} diff --git a/src/components/card/ServiceIcon.tsx b/src/components/card/ServiceIcon.tsx index 9451f3d..701e127 100644 --- a/src/components/card/ServiceIcon.tsx +++ b/src/components/card/ServiceIcon.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ServiceIconProps } from "../../types"; import styled from "styled-components"; import { animated, easings, useSpring } from "@react-spring/web"; @@ -6,16 +6,18 @@ import { IconContext } from "react-icons"; import { FaYoutube, FaTwitch } from "react-icons/fa"; import { TbBroadcast } from "react-icons/tb"; import { useTheme, useWindowSize } from "../../hooks"; -import { theme } from "../../theme"; +import { breakpoints, baseColors } from "../../configs"; import { parseToJST } from "../../utils"; const Panel = styled(animated.div)` display: flex; + background-color: ${(p) => p.theme.bg.secondary}; + transition: background-color 0.3s ease; height: 18px; border-radius: 8px; box-shadow: inset 0px 2px 2px rgba(0, 0, 0, 0.25); - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` height: 28px; border-radius: 15px; box-shadow: inset 0px 3px 3px rgba(0, 0, 0, 0.25); @@ -29,7 +31,7 @@ const InnerContainer = styled(animated.div)` margin: auto; gap: 2px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` height: 24px; gap: 5px; `} @@ -40,7 +42,7 @@ const Icon = styled(animated.div)` width: 12px; display: flex; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` width: 20px; `} `; @@ -49,7 +51,7 @@ const StateText = styled(animated.div)` font-weight: bold; font-size: 10px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` font-size: 16px; `} `; @@ -65,24 +67,30 @@ export const ServiceIcon: React.FC = ({ isExpand, ...props }) => { - const { colors, springColors } = useTheme(); - const startTime = useRef(new Date(startAt)); const { isPhoneSize } = useWindowSize(); - const serviceColor = useRef(); + const serviceColor = useMemo(() => { + switch (service) { + case "youtube": + return baseColors.logo.youtube; + case "twitch": + return baseColors.logo.twitch; + case "twitCasting": + return baseColors.logo.twitCasting; + } + }, [service]); + const startDate = useMemo(() => new Date(startAt), [startAt]); + const [isLive, setLive] = useState(false); + const { theme } = useTheme(); - const checkLive = () => startTime.current.getTime() < Date.now(); - const [isLive, setLive] = useState(false); + const checkLive = () => startDate.getTime() < Date.now(); const ServiceIcon = useCallback(() => { switch (service) { case "youtube": - serviceColor.current = theme.colors.logoColors.youtube; return ; case "twitch": - serviceColor.current = theme.colors.logoColors.twitch; return ; case "twitCasting": - serviceColor.current = theme.colors.logoColors.twitCasting; return ; } }, [service]); @@ -106,7 +114,7 @@ export const ServiceIcon: React.FC = ({ const baseSpringConfig = { display: isExpand ? "block" : "none", width: isExpand ? "85px" : "30px", - color: isLive ? serviceColor.current : colors.text.primary, + color: isLive ? serviceColor : theme.text.primary, config: { duration: 150, easing: easings.easeInOutSine, @@ -131,16 +139,14 @@ export const ServiceIcon: React.FC = ({ return (
- + - + {isLive ? "LIVE" : getStartTime(startAt)} diff --git a/src/components/card/StreamingCard.tsx b/src/components/card/StreamingCard.tsx index fb12d94..a2668ee 100644 --- a/src/components/card/StreamingCard.tsx +++ b/src/components/card/StreamingCard.tsx @@ -5,12 +5,12 @@ import { ThumbnailBlock } from "./ThumbnailBlock"; import styled from "styled-components"; import { useHover, useWindowSize } from "../../hooks"; import { animated } from "@react-spring/web"; -import { theme } from "../../theme"; +import { breakpoints } from "../../configs"; const Container = styled(animated.div)` width: 160px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` width: 320px; `} `; @@ -18,7 +18,7 @@ const Container = styled(animated.div)` const Card = styled(animated.div)` position: relative; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` min-height: 180px; `} `; diff --git a/src/components/card/ThumbnailBlock.tsx b/src/components/card/ThumbnailBlock.tsx index 974022a..71b7f1d 100644 --- a/src/components/card/ThumbnailBlock.tsx +++ b/src/components/card/ThumbnailBlock.tsx @@ -2,8 +2,8 @@ import React from "react"; import styled from "styled-components"; import { ThumbnailBlockProps } from "../../types"; import { animated, easings, useSpring } from "@react-spring/web"; -import { theme } from "../../theme"; -import { useTheme, useWindowSize } from "../../hooks"; +import { breakpoints } from "../../configs"; +import { useWindowSize } from "../../hooks"; const Panel = styled(animated.div)` width: 160px; @@ -12,8 +12,10 @@ const Panel = styled(animated.div)` border-radius: 5px; filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25)); position: relative; + background-color: ${(p) => p.theme.bg.secondary}; + transition: background-color 0.3s ease; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` width: 320px; height: 180px; border-radius: 10px; @@ -24,13 +26,13 @@ const Thumbnail = styled(animated.img)` width: 160px; height: 90px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` width: 320px; height: 180px; `} `; -const Header = styled(animated.div)` +const Header = styled.div` width: 100%; display: flex; position: absolute; @@ -38,7 +40,7 @@ const Header = styled(animated.div)` bottom: 0; height: 30px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` height: 60px; `} `; @@ -51,7 +53,7 @@ const Icon = styled(animated.img)` border-radius: 50%; object-fit: cover; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` height: 50px; margin-left: 6px; `} @@ -65,13 +67,13 @@ const Contents = styled(animated.div)` flex-direction: column; justify-content: center; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` margin-left: 6px; width: 250px; `} `; -const Title = styled(animated.div)` +const Title = styled.div` font-family: "Zen Kaku Gothic New", sans-serif; font-size: 10px; width: 100%; @@ -80,13 +82,13 @@ const Title = styled(animated.div)` text-overflow: ellipsis; margin-top: 2px; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` font-size: 20px; margin-top: 0; `} `; -const Name = styled(animated.div)` +const Name = styled.div` font-family: "Zen Kaku Gothic New", sans-serif; font-size: 10px; transform: scale(0.8); @@ -96,7 +98,7 @@ const Name = styled(animated.div)` overflow: hidden; text-overflow: ellipsis; - ${theme.breakpoints.mediaQueries.md` + ${breakpoints.mediaQueries.md` font-size: 15px; transform: scale(1); `} @@ -110,12 +112,10 @@ export const ThumbnailBlock: React.FC = ({ isExpand, ...props }) => { - const { colors } = useTheme(); const { isPhoneSize } = useWindowSize(); const baseSpringConfig = { height: isExpand ? "240px" : "180px", borderRadius: isExpand ? "10px 10px 0px 0px" : "10px 10px 10px 10px", - display: isExpand ? "block" : "none", shadow: isExpand ? "drop-shadow(0px 0px 0px rgba(0, 0, 0, 0.25))" : "drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25))", @@ -133,7 +133,7 @@ export const ThumbnailBlock: React.FC = ({ : "drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.25))", }; - const { height, borderRadius, display, shadow } = useSpring({ + const { height, borderRadius, shadow } = useSpring({ ...baseSpringConfig, ...(isPhoneSize ? mobileSpringConfig : {}), }); @@ -142,18 +142,12 @@ export const ThumbnailBlock: React.FC = ({ opacity: isExpand ? 1 : 0, config: { duration: 250, - easing: easings.easeInQuart, + easing: easings.easeOutExpo, }, }); - const { backgroundColor, color } = useSpring({ - backgroundColor: colors.base.secondary, - color: colors.text.primary, - config: colors.config, - }); - return ( - + = ({ />
- - {title} - {name} + + {title} + {name}
diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx index a42554f..99aa8a0 100644 --- a/src/components/providers/ThemeProvider.tsx +++ b/src/components/providers/ThemeProvider.tsx @@ -1,99 +1,46 @@ -import React, { - createContext, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import React, { createContext, useEffect, useMemo, useState } from "react"; import { ChildrenNode, - SpringColors, - ThemeColors, + ColorTheme, + ThemeTypes, ThemeContextType, } from "../../types"; -import { theme } from "../../theme"; -import { easings, useSpring } from "@react-spring/web"; -import { useWindowSize } from "../../hooks"; +import themes from "../../theme"; +import { ThemeProvider as StyledThemeProvider } from "styled-components"; -const cacheVersion = "vspo"; -const cacheKey = "theme"; +const cacheKey = "themeType"; export const ThemeContext = createContext({ - colors: {} as ThemeColors, - springColors: {} as SpringColors, + themeType: "light", + theme: {} as ColorTheme, toggleTheme: () => {}, - isDark: false, }); export const ThemeProvider: React.FC = ({ children }) => { - const [isDark, setDark] = useState(false); - const { isMobile } = useWindowSize(); - - const config = { - duration: isMobile ? 0 : 500, - easing: easings.easeInOutSine, - }; - - const main = { - primary: isDark - ? theme.colors.logoColors.vspo.pink - : theme.colors.logoColors.vspo.blue, - secondary: isDark - ? theme.colors.logoColors.vspo.blue - : theme.colors.logoColors.vspo.pink, - }; - const base = { - primary: isDark ? theme.colors.black[50] : theme.colors.white[50], - secondary: isDark ? theme.colors.black[100] : theme.colors.white[100], - }; - const text = { - primary: isDark ? theme.colors.white[50] : theme.colors.black[50], - secondary: isDark ? theme.colors.white[100] : theme.colors.black[100], - }; - - const colors = { - main, - base, - text, - config, - }; - - const springColors = { - main: useSpring({ ...main, config }), - base: useSpring({ ...base, config }), - text: useSpring({ ...text, config }), - }; - - const toggleTheme = useCallback(() => setDark((isDark) => !isDark), []); - - useEffect(() => { - (async () => { - const cachedTheme = await caches - .match(cacheKey) - .then((r) => r?.text()) - .then((r) => JSON.parse(r ?? "null")); - - setDark(cachedTheme); - })(); - }, []); + const [themeType, setTheme] = useState( + (localStorage.getItem(cacheKey) as ThemeTypes) ?? "light" + ); useEffect(() => { - caches.open(cacheVersion).then((cache) => { - cache.put(cacheKey, new Response(JSON.stringify(isDark))); - }); - }, [isDark]); + localStorage.setItem(cacheKey, themeType); + }, [themeType]); const context = useMemo( () => ({ - colors, - springColors, - toggleTheme, - isDark, + themeType, + theme: themes[themeType], + toggleTheme: () => { + setTheme((p) => ("light" === p ? "dark" : "light")); + }, }), - [isDark] + [themeType] ); return ( - {children} + + + {children} + + ); }; diff --git a/src/components/providers/WindowSizeProvider.tsx b/src/components/providers/WindowSizeProvider.tsx index 7628bd3..0693ec1 100644 --- a/src/components/providers/WindowSizeProvider.tsx +++ b/src/components/providers/WindowSizeProvider.tsx @@ -4,11 +4,10 @@ import React, { useCallback, useEffect, useMemo, - useRef, useState, } from "react"; import { ChildrenNode, WindowSize, ClientType } from "../../types"; -import { theme } from "../../theme"; +import { breakpoints } from "../../configs"; export const WindowSizeContext = createContext(null!); @@ -45,9 +44,9 @@ export const WindowSizeProvider: React.FC = ({ children }) => { const clientType = useMemo( () => ({ isMobile, - isPhoneSize: size.width < theme.breakpoints.values.md, - isTabletSize: theme.breakpoints.values.md <= size.width, - isDesktopSize: theme.breakpoints.values.lg <= size.width, + isPhoneSize: size.width < breakpoints.values.md, + isTabletSize: breakpoints.values.md <= size.width, + isDesktopSize: breakpoints.values.lg <= size.width, }), [size, isMobile] ); diff --git a/src/theme/colors.ts b/src/configs/baseColors.ts similarity index 86% rename from src/theme/colors.ts rename to src/configs/baseColors.ts index 53bba62..b4a62c5 100644 --- a/src/theme/colors.ts +++ b/src/configs/baseColors.ts @@ -1,6 +1,6 @@ import { BaseColors } from "../types"; -export const colors: BaseColors = { +export const baseColors: BaseColors = { white: { 50: "#fefefe", 100: "#fafafa", @@ -11,8 +11,7 @@ export const colors: BaseColors = { 100: "#424242", 200: "#616161", }, - - logoColors: { + logo: { vspo: { pink: "#FF6FA3", blue: "#7266CF", diff --git a/src/theme/breakpoints.ts b/src/configs/breakpoints.ts similarity index 100% rename from src/theme/breakpoints.ts rename to src/configs/breakpoints.ts diff --git a/src/configs/index.ts b/src/configs/index.ts new file mode 100644 index 0000000..b6b0e98 --- /dev/null +++ b/src/configs/index.ts @@ -0,0 +1,9 @@ +import { easings, SpringConfig } from "@react-spring/web"; + +export { baseColors } from "./baseColors"; +export { breakpoints } from "./breakpoints"; + +export const springConfig: SpringConfig = { + duration: 500, + easing: easings.easeInOutSine, +}; diff --git a/src/theme/dark.ts b/src/theme/dark.ts new file mode 100644 index 0000000..7482bb0 --- /dev/null +++ b/src/theme/dark.ts @@ -0,0 +1,21 @@ +import { ColorTheme } from "../types"; +import { baseColors } from "../configs"; + +export const dark: ColorTheme = { + text: { + primary: baseColors.white[50], + secondary: baseColors.white[100], + }, + bg: { + primary: baseColors.black[50], + secondary: baseColors.black[100], + }, + border: { + primary: baseColors.white[50], + secondary: baseColors.white[100], + }, + vspo: { + primary: baseColors.logo.vspo.pink, + secondary: baseColors.logo.vspo.blue, + }, +}; diff --git a/src/theme/index.ts b/src/theme/index.ts index 45e71d8..3d2f010 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -1,8 +1,7 @@ -import { colors } from "./colors"; -import { breakpoints } from "./breakpoints"; -import { Theme } from "../types"; +import { dark } from "./dark"; +import { light } from "./light"; -export const theme: Theme = { - breakpoints, - colors, +export default { + dark, + light, }; diff --git a/src/theme/light.ts b/src/theme/light.ts new file mode 100644 index 0000000..d8fd687 --- /dev/null +++ b/src/theme/light.ts @@ -0,0 +1,21 @@ +import { ColorTheme } from "../types"; +import { baseColors } from "../configs"; + +export const light: ColorTheme = { + text: { + primary: baseColors.black[50], + secondary: baseColors.black[100], + }, + bg: { + primary: baseColors.white[50], + secondary: baseColors.white[100], + }, + border: { + primary: baseColors.black[50], + secondary: baseColors.black[100], + }, + vspo: { + primary: baseColors.logo.vspo.blue, + secondary: baseColors.logo.vspo.pink, + }, +}; diff --git a/src/types/configs.ts b/src/types/configs.ts new file mode 100644 index 0000000..3dbadc2 --- /dev/null +++ b/src/types/configs.ts @@ -0,0 +1,33 @@ +import { + CSSObject, + FlattenSimpleInterpolation, + SimpleInterpolation, +} from "styled-components"; + +export type BreakpointValues = { + xs: number; + sm: number; + md: number; + lg: number; + xl: number; + xxl: number; +}; + +export type BreakpointMediaQuery = ( + base: CSSObject | TemplateStringsArray, + ...interpolations: SimpleInterpolation[] +) => FlattenSimpleInterpolation; + +export type BreakpointMediaQueries = { + xs: BreakpointMediaQuery; + sm: BreakpointMediaQuery; + md: BreakpointMediaQuery; + lg: BreakpointMediaQuery; + xl: BreakpointMediaQuery; + xxl: BreakpointMediaQuery; +}; + +export type Breakpoints = { + values: BreakpointValues; + mediaQueries: BreakpointMediaQueries; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 4cb60a7..051e7d1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ export * from "./frontLogic"; export * from "./frontUI"; export * from "./theme"; +export * from "./configs"; diff --git a/src/types/theme.ts b/src/types/theme.ts index a3e5dff..f694c81 100644 --- a/src/types/theme.ts +++ b/src/types/theme.ts @@ -1,10 +1,3 @@ -import { SpringConfig, SpringValue } from "@react-spring/web"; -import { - CSSObject, - FlattenSimpleInterpolation, - SimpleInterpolation, -} from "styled-components"; - export type Colorlevel = { 50: string; 100: string; @@ -14,7 +7,7 @@ export type Colorlevel = { export type BaseColors = { black: Colorlevel; white: Colorlevel; - logoColors: { + logo: { vspo: { pink: string; blue: string; @@ -25,65 +18,27 @@ export type BaseColors = { }; }; -export type SpringColorLevel = { - primary: SpringValue; - secondary: SpringValue; -}; - export type ColorLevel = { primary: string; - secondary: string; + secondary?: string; }; -export type SpringColors = { - main: SpringColorLevel; - base: SpringColorLevel; - text: SpringColorLevel; -}; - -export type ThemeColors = { - main: ColorLevel; - base: ColorLevel; +export type ColorTheme = { text: ColorLevel; - config: SpringConfig; -}; - -export type BreakpointValues = { - xs: number; - sm: number; - md: number; - lg: number; - xl: number; - xxl: number; -}; - -export type BreakpointMediaQuery = ( - base: CSSObject | TemplateStringsArray, - ...interpolations: SimpleInterpolation[] -) => FlattenSimpleInterpolation; - -export type BreakpointMediaQueries = { - xs: BreakpointMediaQuery; - sm: BreakpointMediaQuery; - md: BreakpointMediaQuery; - lg: BreakpointMediaQuery; - xl: BreakpointMediaQuery; - xxl: BreakpointMediaQuery; -}; - -export type Breakpoints = { - values: BreakpointValues; - mediaQueries: BreakpointMediaQueries; + bg: ColorLevel; + border: ColorLevel; + vspo: ColorLevel; }; export type Theme = { - breakpoints: Breakpoints; - colors: BaseColors; + dark: ColorTheme; + light: ColorTheme; }; +export type ThemeTypes = keyof Theme; + export type ThemeContextType = { - colors: ThemeColors; - springColors: SpringColors; + themeType: ThemeTypes; + theme: ColorTheme; toggleTheme: () => void; - isDark: Boolean; }; From d9cbd59a716f8e5be146151fb72674f49af235cb Mon Sep 17 00:00:00 2001 From: mnsinri Date: Sun, 20 Aug 2023 09:05:22 +0900 Subject: [PATCH 2/3] feat: add marquee title --- src/components/card/ThumbnailBlock.tsx | 11 +-- src/components/marquee/Marquee.tsx | 99 ++++++++++++++++++++++++++ src/components/marquee/index.ts | 1 + src/types/frontUI.ts | 11 ++- 4 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/components/marquee/Marquee.tsx create mode 100644 src/components/marquee/index.ts diff --git a/src/components/card/ThumbnailBlock.tsx b/src/components/card/ThumbnailBlock.tsx index 71b7f1d..c10b0ee 100644 --- a/src/components/card/ThumbnailBlock.tsx +++ b/src/components/card/ThumbnailBlock.tsx @@ -4,6 +4,7 @@ import { ThumbnailBlockProps } from "../../types"; import { animated, easings, useSpring } from "@react-spring/web"; import { breakpoints } from "../../configs"; import { useWindowSize } from "../../hooks"; +import { Marquee } from "../marquee"; const Panel = styled(animated.div)` width: 160px; @@ -73,13 +74,10 @@ const Contents = styled(animated.div)` `} `; -const Title = styled.div` +const MarqueeTitle = styled(Marquee)` font-family: "Zen Kaku Gothic New", sans-serif; font-size: 10px; width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; margin-top: 2px; ${breakpoints.mediaQueries.md` @@ -97,6 +95,7 @@ const Name = styled.div` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding: 0 3%; ${breakpoints.mediaQueries.md` font-size: 15px; @@ -157,7 +156,9 @@ export const ThumbnailBlock: React.FC = ({
- {title} + + {title} + {name}
diff --git a/src/components/marquee/Marquee.tsx b/src/components/marquee/Marquee.tsx new file mode 100644 index 0000000..dacb8f1 --- /dev/null +++ b/src/components/marquee/Marquee.tsx @@ -0,0 +1,99 @@ +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import styled from "styled-components"; +import { MarqueeProps } from "../../types"; +import { animated, useSpring, useSpringRef } from "@react-spring/web"; +import { useWindowSize } from "../../hooks"; + +const Container = styled.div` + width: 100%; + display: flex; + overflow: hidden; + mask-image: linear-gradient( + to right, + transparent, + #fff 5%, + #fff 95%, + transparent + ); +`; + +const Item = styled(animated.div)` + white-space: nowrap; + padding: 0 20% 0 3%; +`; + +export const Marquee: React.FC = ({ + children, + animate = true, + speed = 0.05, + ...props +}) => { + const { isPhoneSize } = useWindowSize(); + const refParent = useRef(null!); + const refChild = useRef(null!); + const rect = useRef<{ parent: DOMRect; child: DOMRect }>(null!); + const [canMarquee, setCanMarquee] = useState(false); + + const animation = useSpringRef(); + const transform = useSpring({ + ref: animation, + from: { + x: "0%", + }, + }); + + const reset = () => { + animation.start({ + from: { + x: "0%", + }, + immediate: true, + }); + }; + + const restart = (duration: number) => { + animation.start({ + from: { + x: "0%", + }, + to: { + x: "-100%", + }, + reset: true, + loop: true, + delay: 900, + immediate: false, + config: { + duration, + }, + }); + }; + + useLayoutEffect(() => { + reset(); + const parent = refParent.current.getBoundingClientRect(); + const child = refChild.current.getBoundingClientRect(); + rect.current = { parent, child }; + setCanMarquee(parent.width < child.width); + }, [children, isPhoneSize]); + + useEffect(() => { + canMarquee && animate ? restart(rect.current.child.width / speed) : reset(); + }, [canMarquee, animate, speed]); + + return ( + + + {children} + + {canMarquee && {children}} + + ); +}; diff --git a/src/components/marquee/index.ts b/src/components/marquee/index.ts new file mode 100644 index 0000000..8bee1a4 --- /dev/null +++ b/src/components/marquee/index.ts @@ -0,0 +1 @@ +export { Marquee } from "./Marquee"; diff --git a/src/types/frontUI.ts b/src/types/frontUI.ts index 6b694d6..850b00b 100644 --- a/src/types/frontUI.ts +++ b/src/types/frontUI.ts @@ -1,4 +1,4 @@ -import { StreamInfo, Service } from "./frontLogic"; +import { StreamInfo, Service, ChildrenNode } from "./frontLogic"; export type WindowSize = { width: number; @@ -56,3 +56,12 @@ export type StreamList = { date: string; streams: StreamInfo[]; }; + +export type MarqueeProps = ChildrenNode & { + animate?: boolean; + speed?: number; +}; + +export type MarqueeItemProps = MarqueeProps & { + speed?: number; +}; From 5ef66545f0ce50e667ca0cbdfe739c778041aa22 Mon Sep 17 00:00:00 2001 From: mnsinri Date: Sun, 20 Aug 2023 09:12:24 +0900 Subject: [PATCH 3/3] fix: package-lock.json --- package-lock.json | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index d28d66a..2779226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8228,9 +8228,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -8288,9 +8288,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -8362,9 +8362,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -10499,9 +10499,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -12884,9 +12884,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -15096,9 +15096,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -17979,9 +17979,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "engines": { "node": ">=0.10.0" }