From b1ad8743da9f158737b3494273ffe31d0a90d755 Mon Sep 17 00:00:00 2001 From: Ernest Nnamdi Date: Tue, 18 Feb 2025 23:05:01 +0100 Subject: [PATCH] feat: implement dark theme support --- src/components/LatestVerifiedContracts.tsx | 15 +++- src/components/TopBar.styled.tsx | 32 +++++---- src/components/TopBar.tsx | 19 ++++- src/components/icon/ThemeToggle.tsx | 58 +++++++++++++++ src/contexts/ThemeProvider.tsx | 84 ++++++++++++++++++++++ src/main.tsx | 5 +- 6 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 src/components/icon/ThemeToggle.tsx create mode 100644 src/contexts/ThemeProvider.tsx diff --git a/src/components/LatestVerifiedContracts.tsx b/src/components/LatestVerifiedContracts.tsx index 1e909ce..fe9a8f0 100644 --- a/src/components/LatestVerifiedContracts.tsx +++ b/src/components/LatestVerifiedContracts.tsx @@ -27,11 +27,20 @@ const ContractsList = styled(Box)({ flexDirection: "row", flexWrap: "wrap", gap: 24, + justifyContent: "flex-start", margin: "0 auto", - justifyContent: "left", - overflow: "auto", marginTop: 24, - "-webkit-text-size-adjust": "100%", + overflowX: "auto", + padding: "0 16px", + boxSizing: "border-box", + + "@media (max-width: 768px)": { + flexDirection: "column", + gap: 16, + justifyContent: "center", + }, + + "-webkit-text-size-adjust": "none", }); const AddressText = styled(Box)({ diff --git a/src/components/TopBar.styled.tsx b/src/components/TopBar.styled.tsx index e8ae221..fd17712 100644 --- a/src/components/TopBar.styled.tsx +++ b/src/components/TopBar.styled.tsx @@ -14,31 +14,36 @@ interface TopBarWrapperProps { const TopBarWrapper = styled(Box)(({ theme }) => (props: TopBarWrapperProps) => ({ display: props.isMobile ? "flex" : "inherit", alignItems: props.isMobile ? "center" : "inherit", - fontWight: 700, - color: "#fff", + fontWeight: 700, minHeight: props.isMobile ? 80 : headerHeight, height: props.showExpanded && !props.isMobile ? expandedHeaderHeight : props.isMobile - ? 80 - : headerHeight, - background: "#fff", + ? 80 + : headerHeight, + background: theme.palette.mode === "dark" ? theme.palette.background.default : "#fff", borderBottomLeftRadius: theme.spacing(6), borderBottomRightRadius: theme.spacing(6), - border: "0.5px solid rgba(114, 138, 150, 0.24)", - boxShadow: "rgb(114 138 150 / 8%) 0px 2px 16px", + border: + theme.palette.mode === "dark" + ? `0.5px solid ${theme.palette.divider}` + : "0.5px solid rgba(114, 138, 150, 0.24)", + boxShadow: + theme.palette.mode === "dark" + ? `${theme.palette.background.paper} 0px 2px 16px` + : "rgb(114 138 150 / 8%) 0px 2px 16px", })); const ContentColumn = styled(CenteringBox)(() => ({ gap: 10, })); -const LinkWrapper = styled(Link)(() => ({ +const LinkWrapper = styled(Link)(({ theme }) => ({ display: "flex", alignItems: "center", gap: 10, - color: "#000", + color: theme.palette.mode === "dark" ? theme.palette.text.primary : "#000", textDecoration: "none", cursor: "pointer", })); @@ -53,21 +58,22 @@ const TopBarContent = styled(CenteringBox)(({ theme }) => ({ })); const AppLogo = styled("h4")(({ theme }) => ({ - color: "#000", + color: theme.palette.mode === "dark" ? theme.palette.text.primary : "#000", fontSize: 20, fontWeight: 800, [theme.breakpoints.down("sm")]: { fontSize: 16, }, })); -const GitLogo = styled("h5")(() => ({ - color: "#000", + +const GitLogo = styled("h5")(({ theme }) => ({ + color: theme.palette.mode === "dark" ? theme.palette.text.primary : "#000", fontWeight: 700, fontSize: 18, })); const TopBarHeading = styled("h3")(({ theme }) => ({ - color: "#000", + color: theme.palette.mode === "dark" ? theme.palette.text.primary : "#000", fontSize: 26, marginTop: 0, textAlign: "center", diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 766614d..1b3498a 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,6 +1,6 @@ import icon from "../assets/icon.svg"; import React, { useEffect, useState } from "react"; -import { useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import github from "../assets/github-dark.svg"; import { AddressInput } from "../components/AddressInput"; import { CenteringBox } from "./Common.styled"; @@ -20,6 +20,8 @@ import MenuRoundedIcon from "@mui/icons-material/MenuRounded"; import { MobileMenu } from "./MobileMenu"; import { useNavigatePreserveQuery } from "../lib/useNavigatePreserveQuery"; import { StyledTonConnectButton } from "../styles"; +import ThemeToggle from "./icon/ThemeToggle"; +import { usePage } from "../contexts/ThemeProvider"; export function TopBar() { const { pathname } = useLocation(); @@ -35,6 +37,14 @@ export function TopBar() { setShowExpanded(pathname.length === 1); }, [pathname]); + const { pageTheme, switchTheme } = usePage(); + const handleSwitchTheme = (event: { preventDefault: () => void }) => { + event.preventDefault(); + switchTheme(); + }; + + const isThemeDark = pageTheme === "dark"; + return ( GitHub + + + )} diff --git a/src/components/icon/ThemeToggle.tsx b/src/components/icon/ThemeToggle.tsx new file mode 100644 index 0000000..41eec41 --- /dev/null +++ b/src/components/icon/ThemeToggle.tsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from "react"; + +interface ThemeToggleProps { + className?: string; + isDark?: boolean; + onClick?: () => void; +} + +const ThemeToggle: React.FC = ({ className = "", isDark = false, onClick }) => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( +
+ {isDark ? ( + + + + ) : ( + + + + + )} +
+ ); +}; + +export default ThemeToggle; diff --git a/src/contexts/ThemeProvider.tsx b/src/contexts/ThemeProvider.tsx new file mode 100644 index 0000000..5e4d15a --- /dev/null +++ b/src/contexts/ThemeProvider.tsx @@ -0,0 +1,84 @@ +import { createTheme, ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; +import { createContext, useContext, useEffect, useState, ReactNode } from "react"; + +interface ThemeContextType { + pageTheme: string; + switchTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +export const usePage = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("usePage must be used within a ThemeProvider"); + } + return context; +}; + +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + const [pageTheme, setPageTheme] = useState(() => { + return localStorage.getItem("theme") || "light"; + }); + + const theme = createTheme({ + spacing: (factor: number) => `${8 * factor}px`, + palette: { + mode: pageTheme as "light" | "dark", + primary: { + main: pageTheme === "light" ? "#1976d2" : "#90caf9", + }, + background: { + default: pageTheme === "light" ? "#ffffff" : "#121212", + paper: pageTheme === "light" ? "#ffffff" : "#1e1e1e", + }, + text: { + primary: pageTheme === "light" ? "#000000" : "#ffffff", + secondary: pageTheme === "light" ? "#666666" : "#b3b3b3", + }, + }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 900, + lg: 1200, + xl: 1536, + }, + }, + components: { + MuiIconButton: { + styleOverrides: { + root: { + color: pageTheme === "light" ? "#000000" : "#ffffff", + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + color: pageTheme === "light" ? "#000000" : "#ffffff", + }, + }, + }, + }, + }); + + const switchTheme = () => { + setPageTheme((prevTheme) => (prevTheme === "dark" ? "light" : "dark")); + }; + + useEffect(() => { + document.documentElement.setAttribute("data-theme", pageTheme); + localStorage.setItem("theme", pageTheme); + + document.body.style.backgroundColor = pageTheme === "light" ? "#ffffff" : "#121212"; + document.body.style.color = pageTheme === "light" ? "#000000" : "#ffffff"; + }, [pageTheme]); + + return ( + + {children} + + ); +}; diff --git a/src/main.tsx b/src/main.tsx index f09c726..f714938 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,8 +6,7 @@ import App from "./App"; import ContractInteract from "./components/admin/ContractInteract"; import "./index.css"; import { SnackbarProvider } from "notistack"; -import { ThemeProvider } from "@mui/material"; -import { theme } from "./theme"; +import { ThemeProvider } from "./contexts/ThemeProvider"; import { Admin } from "./components/admin/Admin"; import { initGA } from "./lib/googleAnalytics"; import { TactDeployer } from "./components/tactDeployer/TactDeployer"; @@ -21,7 +20,7 @@ initGA(); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - +