diff --git a/service/vspo-schedule/web/src/components/Elements/Card/ClipCard.tsx b/service/vspo-schedule/web/src/components/Elements/Card/ClipCard.tsx index b95b6618..62b07038 100644 --- a/service/vspo-schedule/web/src/components/Elements/Card/ClipCard.tsx +++ b/service/vspo-schedule/web/src/components/Elements/Card/ClipCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "next-i18next"; import PlayArrow from "@mui/icons-material/PlayArrow"; import { Avatar, Box, CardContent, Chip, Typography } from "@mui/material"; -import { styled } from "@mui/material/styles"; +import { styled, useTheme } from "@mui/material/styles"; import { getVideoIconUrl, isTrending } from "@/lib/utils"; import { Clip } from "@/types/streaming"; import { VideoCard } from "./VideoCard"; @@ -48,10 +48,15 @@ const roundViewCount = (viewCount: number) => { export const ClipCard: React.FC = ({ clip }) => { const { t } = useTranslation("clips"); + const theme = useTheme(); const iconUrl = getVideoIconUrl(clip); const cardHighlight = isTrending(clip) - ? { label: t("clipLabels.trending"), color: "red", bold: false } + ? { + label: t("clipLabels.trending"), + color: theme.vars.palette.customColors.videoHighlight.trending, + bold: false, + } : undefined; return ( diff --git a/service/vspo-schedule/web/src/components/Elements/Card/LivestreamCard.tsx b/service/vspo-schedule/web/src/components/Elements/Card/LivestreamCard.tsx index 54381ab3..ca29f3ba 100644 --- a/service/vspo-schedule/web/src/components/Elements/Card/LivestreamCard.tsx +++ b/service/vspo-schedule/web/src/components/Elements/Card/LivestreamCard.tsx @@ -1,19 +1,14 @@ import React, { useMemo } from "react"; import { CardContent, Typography, Avatar } from "@mui/material"; -import { styled } from "@mui/material/styles"; +import { styled, useTheme } from "@mui/material/styles"; import { Box } from "@mui/system"; -import { LiveStatus, Livestream } from "@/types/streaming"; +import { Livestream } from "@/types/streaming"; import { getLiveStatus, formatDate } from "@/lib/utils"; import { PlatformIcon } from "../Icon"; import { useTranslation } from "next-i18next"; import { useRouter } from "next/router"; import { VideoCard } from "./VideoCard"; -const highlightColors = { - live: "red", - upcoming: "rgb(45, 75, 112)", -} satisfies Partial>; - const ResponsiveTypography = styled(Typography)(({ theme }) => ({ paddingRight: "1em", display: "flex", @@ -67,6 +62,7 @@ export const LivestreamCard: React.FC = ({ livestream, }) => { const { t } = useTranslation("common"); + const theme = useTheme(); const router = useRouter(); const { locale } = router; const { title, channelTitle, scheduledStartTime, iconUrl, platform } = @@ -79,7 +75,8 @@ export const LivestreamCard: React.FC = ({ livestreamStatus === "live" || livestreamStatus === "upcoming" ? { label: t(`liveStatus.${livestreamStatus}`), - color: highlightColors[livestreamStatus], + color: + theme.vars.palette.customColors.videoHighlight[livestreamStatus], bold: true, } : undefined; diff --git a/service/vspo-schedule/web/src/components/Elements/Card/VideoCard.tsx b/service/vspo-schedule/web/src/components/Elements/Card/VideoCard.tsx index 3cb3275b..02ba5270 100644 --- a/service/vspo-schedule/web/src/components/Elements/Card/VideoCard.tsx +++ b/service/vspo-schedule/web/src/components/Elements/Card/VideoCard.tsx @@ -3,6 +3,7 @@ import { Box, Card, CardActionArea } from "@mui/material"; import { styled } from "@mui/material/styles"; import { useVideoModalContext } from "@/hooks"; import { Video } from "@/types/streaming"; +import { HighlightedVideoChip } from "../Chip"; type Props = { video: Video; @@ -14,34 +15,23 @@ type Props = { }; }; -const HighlightLabel = styled("div")<{ - highlightColor: string; - bold: boolean; -}>(({ theme, highlightColor, bold }) => ({ - minWidth: "78px", - padding: "0 12px", - color: "white", - fontSize: "15px", - fontWeight: bold ? "700" : "400", - fontFamily: "Roboto, sans-serif", - textAlign: "center", - lineHeight: "24px", - background: highlightColor, - borderRadius: "12px", - position: "absolute", - top: "-12px", - right: "6px", - zIndex: "3", - transformOrigin: "center right", - [theme.breakpoints.down("md")]: { - transform: "scale(0.875)", - right: "5px", - }, - [theme.breakpoints.down("sm")]: { - transform: "scale(0.75)", - right: "4px", - }, -})); +const StyledHighlightedVideoChip = styled(HighlightedVideoChip)( + ({ theme }) => ({ + position: "absolute", + top: "-12px", + right: "6px", + zIndex: "3", + transformOrigin: "center right", + [theme.breakpoints.down("md")]: { + transform: "scale(0.875)", + right: "5px", + }, + [theme.breakpoints.down("sm")]: { + transform: "scale(0.75)", + right: "4px", + }, + }), +); const StyledCard = styled(Card, { shouldForwardProp: (prop) => prop !== "highlightColor", @@ -53,7 +43,7 @@ const StyledCard = styled(Card, { border: highlightColor ? `3px solid ${highlightColor}` : "none", backgroundColor: "white", [theme.getColorSchemeSelector("dark")]: { - backgroundColor: "#353535", + backgroundColor: theme.vars.palette.customColors.gray, }, })); @@ -68,9 +58,12 @@ export const VideoCard: React.FC = ({ video, highlight, children }) => { return ( {highlight && ( - + {highlight.label} - + )} pushVideo(video)}> diff --git a/service/vspo-schedule/web/src/components/Elements/Chip/HighlightedVideoChip.tsx b/service/vspo-schedule/web/src/components/Elements/Chip/HighlightedVideoChip.tsx new file mode 100644 index 00000000..4c61b569 --- /dev/null +++ b/service/vspo-schedule/web/src/components/Elements/Chip/HighlightedVideoChip.tsx @@ -0,0 +1,19 @@ +import { styled } from "@mui/material/styles"; + +export const HighlightedVideoChip = styled("div", { + shouldForwardProp: (prop) => prop !== "highlightColor" && prop !== "bold", +})<{ + highlightColor: string; + bold: boolean; +}>(({ highlightColor, bold }) => ({ + minWidth: "78px", + padding: "0 12px", + color: "white", + fontSize: "15px", + fontWeight: bold ? "700" : "400", + fontFamily: "Roboto, sans-serif", + textAlign: "center", + lineHeight: "24px", + background: highlightColor, + borderRadius: "12px", +})); diff --git a/service/vspo-schedule/web/src/components/Elements/Chip/index.ts b/service/vspo-schedule/web/src/components/Elements/Chip/index.ts new file mode 100644 index 00000000..8efca258 --- /dev/null +++ b/service/vspo-schedule/web/src/components/Elements/Chip/index.ts @@ -0,0 +1 @@ +export * from "./HighlightedVideoChip"; diff --git a/service/vspo-schedule/web/src/components/Elements/Dialog/SearchDialog.tsx b/service/vspo-schedule/web/src/components/Elements/Dialog/SearchDialog.tsx index ca4b3114..a5c19298 100644 --- a/service/vspo-schedule/web/src/components/Elements/Dialog/SearchDialog.tsx +++ b/service/vspo-schedule/web/src/components/Elements/Dialog/SearchDialog.tsx @@ -32,10 +32,10 @@ const StyledFab = styled(Fab)(({ theme }) => ({ position: "fixed", bottom: "4rem", right: "2rem", - backgroundColor: "#7266cf", + backgroundColor: theme.vars.palette.customColors.vspoPurple, [theme.getColorSchemeSelector("dark")]: { - backgroundColor: "#353535", + backgroundColor: theme.vars.palette.customColors.gray, }, })); diff --git a/service/vspo-schedule/web/src/components/Elements/Drawer/Drawer.tsx b/service/vspo-schedule/web/src/components/Elements/Drawer/Drawer.tsx index c00a33d6..b3a198d9 100644 --- a/service/vspo-schedule/web/src/components/Elements/Drawer/Drawer.tsx +++ b/service/vspo-schedule/web/src/components/Elements/Drawer/Drawer.tsx @@ -68,8 +68,8 @@ const StyledListItemIcon = styled(ListItemIcon)(() => ({ const StyledChip = styled(Chip)(({ theme }) => ({ backgroundColor: "transparent", border: "1px solid", - borderColor: "rgb(45, 75, 112)", - color: "rgb(45, 75, 112)", + borderColor: theme.vars.palette.customColors.darkBlue, + color: theme.vars.palette.customColors.darkBlue, [theme.getColorSchemeSelector("dark")]: { borderColor: "white", @@ -77,16 +77,16 @@ const StyledChip = styled(Chip)(({ theme }) => ({ }, })); -const StyledBadge = styled(Badge)({ +const StyledBadge = styled(Badge)(({ theme }) => ({ "& .MuiBadge-badge": { - backgroundColor: "rgb(45, 75, 112)", + backgroundColor: theme.vars.palette.customColors.darkBlue, color: "white", transform: "scale(0.8)", fontSize: "0.65em", right: "-28px", top: "1px", }, -}); +})); const StyledButton = styled(Button)({ display: "flex", diff --git a/service/vspo-schedule/web/src/components/Elements/Modal/VideoModal.tsx b/service/vspo-schedule/web/src/components/Elements/Modal/VideoModal.tsx index 7680a9cb..23bac4b2 100644 --- a/service/vspo-schedule/web/src/components/Elements/Modal/VideoModal.tsx +++ b/service/vspo-schedule/web/src/components/Elements/Modal/VideoModal.tsx @@ -13,7 +13,7 @@ import { Tabs, BottomNavigation, } from "@mui/material"; -import { styled } from "@mui/material/styles"; +import { styled, useTheme } from "@mui/material/styles"; import { Video } from "@/types/streaming"; import { formatDate, @@ -34,35 +34,19 @@ import { useRouter } from "next/router"; import { ChatEmbed } from "../ChatEmbed"; import { useVideoModalContext } from "@/hooks"; import { useTranslation } from "next-i18next"; +import { HighlightedVideoChip } from "../Chip"; const StyledDialogTitle = styled(DialogTitle)(({ theme }) => ({ - backgroundColor: "#7266cf", + backgroundColor: theme.vars.palette.customColors.vspoPurple, borderBottom: `1px solid ${theme.vars.palette.divider}`, padding: theme.spacing(1), color: "white", [theme.getColorSchemeSelector("dark")]: { - backgroundColor: "#212121", + backgroundColor: theme.vars.palette.customColors.darkGray, }, })); -const LiveLabel = styled("div")<{ - isUpcoming?: boolean; -}>(({ isUpcoming }) => ({ - width: "78px", - minWidth: "fit-content", - padding: "0 12px", - color: "rgb(255, 255, 255)", - fontSize: "15px", - fontWeight: "700", - fontFamily: "Roboto, sans-serif", - textAlign: "center", - lineHeight: "24px", - background: isUpcoming ? "rgb(45, 75, 112)" : "rgb(255, 0, 0)", - borderRadius: "12px", - marginRight: "2.0rem", -})); - const StyledDialogContent = styled(DialogContent)({ overflow: "hidden", padding: "0", @@ -157,6 +141,7 @@ const a11yProps = (index: number) => { const InfoTabs: React.FC<{ video: Video }> = ({ video }) => { const [value, setValue] = React.useState(0); const { t } = useTranslation("common"); + const theme = useTheme(); const router = useRouter(); const { locale } = router; @@ -168,6 +153,7 @@ const InfoTabs: React.FC<{ video: Video }> = ({ video }) => { "MM/dd HH:mm~", { localeCode: locale }, ); + const liveStatus = isLivestream(video) ? getLiveStatus(video) : undefined; const showChatTab = isLivestream(video) && isOnPlatformWithChat(video) && @@ -209,15 +195,15 @@ const InfoTabs: React.FC<{ video: Video }> = ({ video }) => { {formattedStartTime} - {isLivestream(video) && ( - <> - {getLiveStatus(video) === "live" && ( - {t("liveStatus.live")} - )} - {getLiveStatus(video) === "upcoming" && ( - {t("liveStatus.upcoming")} - )} - + {(liveStatus === "live" || liveStatus === "upcoming") && ( + + {t(`liveStatus.${liveStatus}`)} + )} diff --git a/service/vspo-schedule/web/src/components/Elements/index.ts b/service/vspo-schedule/web/src/components/Elements/index.ts index 778dfe83..47b04db7 100644 --- a/service/vspo-schedule/web/src/components/Elements/index.ts +++ b/service/vspo-schedule/web/src/components/Elements/index.ts @@ -1,6 +1,7 @@ export * from "./Button"; export * from "./Card"; export * from "./ChatEmbed"; +export * from "./Chip"; export * from "./Dialog"; export * from "./Drawer"; export * from "./Google"; diff --git a/service/vspo-schedule/web/src/components/Layout/Header.tsx b/service/vspo-schedule/web/src/components/Layout/Header.tsx index 49e4cd23..fe9ce2d3 100644 --- a/service/vspo-schedule/web/src/components/Layout/Header.tsx +++ b/service/vspo-schedule/web/src/components/Layout/Header.tsx @@ -11,11 +11,11 @@ import { CustomDrawer } from "../Elements"; import { useTranslation } from "next-i18next"; const StyledAppBar = styled(AppBar)(({ theme }) => ({ - backgroundColor: "#7266cf", + backgroundColor: theme.vars.palette.customColors.vspoPurple, zIndex: 1300, [theme.getColorSchemeSelector("dark")]: { - backgroundColor: "#212121", + backgroundColor: theme.vars.palette.customColors.darkGray, }, })); const StyledTypography = styled(Typography)({ diff --git a/service/vspo-schedule/web/src/components/Templates/Livestreams.tsx b/service/vspo-schedule/web/src/components/Templates/Livestreams.tsx index 7aaec1cc..278f1f97 100644 --- a/service/vspo-schedule/web/src/components/Templates/Livestreams.tsx +++ b/service/vspo-schedule/web/src/components/Templates/Livestreams.tsx @@ -27,20 +27,20 @@ type Props = { const StyledAccordion = styled(Accordion)(({ theme }) => ({ width: "100%", - backgroundColor: "rgb(45, 75, 112)", + backgroundColor: theme.vars.palette.customColors.darkBlue, color: "white", fontWeight: "bold", borderRadius: "4px", [theme.getColorSchemeSelector("dark")]: { - backgroundColor: "#353535", + backgroundColor: theme.vars.palette.customColors.gray, }, })); const DateTypography = styled(Typography)(({ theme }) => ({ width: "100%", textAlign: "center", - backgroundColor: "rgb(45, 75, 112)", + backgroundColor: theme.vars.palette.customColors.darkBlue, color: "white", fontWeight: "bold", padding: "0.75rem", @@ -48,17 +48,17 @@ const DateTypography = styled(Typography)(({ theme }) => ({ whiteSpace: "pre-line", [theme.getColorSchemeSelector("dark")]: { - backgroundColor: "#353535", + backgroundColor: theme.vars.palette.customColors.gray, }, })); const TimeRangeLabel = styled(Typography)(({ theme }) => ({ width: "12rem", - color: "rgb(255, 255, 255)", + color: "white", fontSize: "1.5rem", fontWeight: 600, textAlign: "center", - backgroundColor: "rgb(45, 75, 112)", + backgroundColor: theme.vars.palette.customColors.darkBlue, borderRadius: "1.35rem", marginBottom: theme.spacing(2), display: "flex", @@ -66,7 +66,7 @@ const TimeRangeLabel = styled(Typography)(({ theme }) => ({ alignItems: "center", [theme.getColorSchemeSelector("dark")]: { - backgroundColor: "#353535", + backgroundColor: theme.vars.palette.customColors.gray, }, [theme.breakpoints.down("md")]: { width: "10rem", diff --git a/service/vspo-schedule/web/src/context/Theme.tsx b/service/vspo-schedule/web/src/context/Theme.tsx index 3a8973bf..95d02cee 100644 --- a/service/vspo-schedule/web/src/context/Theme.tsx +++ b/service/vspo-schedule/web/src/context/Theme.tsx @@ -1,4 +1,5 @@ import { + ColorSystemOptions, Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendTheme, } from "@mui/material/styles"; @@ -11,7 +12,27 @@ type ThemeProviderProps = { children: React.ReactNode; }; +const sharedColorSystemOptions: ColorSystemOptions = { + palette: { + customColors: { + vspoPurple: "#7266cf", + darkBlue: "rgb(45, 75, 112)", + gray: "#353535", + darkGray: "#212121", + videoHighlight: { + live: "red", + upcoming: "rgb(45, 75, 112)", + trending: "red", + }, + }, + }, +}; + const theme = extendTheme({ + colorSchemes: { + light: sharedColorSystemOptions, + dark: sharedColorSystemOptions, + }, mixins: { scrollbar: { scrollbarWidth: "none", diff --git a/service/vspo-schedule/web/src/types/deep-partial.ts b/service/vspo-schedule/web/src/types/deep-partial.ts new file mode 100644 index 00000000..93a645ed --- /dev/null +++ b/service/vspo-schedule/web/src/types/deep-partial.ts @@ -0,0 +1,5 @@ +export type DeepPartial = T extends object + ? { + [K in keyof T]?: DeepPartial; + } + : T; diff --git a/service/vspo-schedule/web/src/types/mui-styles.d.ts b/service/vspo-schedule/web/src/types/mui-styles.d.ts index 384953b7..ae452d29 100644 --- a/service/vspo-schedule/web/src/types/mui-styles.d.ts +++ b/service/vspo-schedule/web/src/types/mui-styles.d.ts @@ -1,8 +1,26 @@ // eslint-disable-next-line no-restricted-imports import { CSSProperties } from "@mui/material/styles/createMixins"; +import { DeepPartial } from "./deep-partial"; + +interface CustomPalette { + customColors: { + vspoPurple: string; + darkBlue: string; + gray: string; + darkGray: string; + videoHighlight: { + live: string; + upcoming: string; + trending: string; + }; + }; +} declare module "@mui/material/styles" { interface Mixins { scrollbar: CSSProperties; } + + interface Palette extends CustomPalette {} + interface PaletteOptions extends DeepPartial {} }