Skip to content

feat: implement dark theme support #373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/components/LatestVerifiedContracts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)({
Expand Down
32 changes: 19 additions & 13 deletions src/components/TopBar.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}));
Expand All @@ -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",
Expand Down
19 changes: 18 additions & 1 deletion src/components/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();
Expand All @@ -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 (
<TopBarWrapper
px={headerSpacings ? 2.4 : 0}
Expand All @@ -61,6 +71,13 @@ export function TopBar() {
<img src={github} alt="Github icon" width={20} height={20} />
<GitLogo>GitHub</GitLogo>
</LinkWrapper>
<Link
className="contrast"
onClick={handleSwitchTheme}
aria-label={isThemeDark ? "Turn off dark mode" : "Turn on dark mode"}
to={""}>
<ThemeToggle isDark={isThemeDark} className="w-6 h-6" />
</Link>
</ContentColumn>
</TopBarContent>
)}
Expand Down
58 changes: 58 additions & 0 deletions src/components/icon/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useState, useEffect } from "react";

interface ThemeToggleProps {
className?: string;
isDark?: boolean;
onClick?: () => void;
}

const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = "", isDark = false, onClick }) => {
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return null;
}

return (
<div
onClick={onClick}
className={`inline-flex items-center justify-center cursor-pointer ${className}`}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
role="button"
tabIndex={0}>
{isDark ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
className="text-yellow-200">
<path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
className="text-yellow-500">
<path d="M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10z" />
<path
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)}
</div>
);
};

export default ThemeToggle;
84 changes: 84 additions & 0 deletions src/contexts/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextType | undefined>(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<string>(() => {
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 (
<ThemeContext.Provider value={{ pageTheme, switchTheme }}>
<MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
</ThemeContext.Provider>
);
};
5 changes: 2 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -21,7 +20,7 @@ initGA();

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<SnackbarProvider maxSnack={3}>
<ThemeProvider theme={theme}>
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<TonConnectUIProvider manifestUrl="https://verifier.ton.org/tonconnect-manifest.json">
<BrowserRouter basename={import.meta.env.BASE_URL}>
Expand Down