Skip to content
Closed
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
10 changes: 10 additions & 0 deletions frontend/public/gpu-mode-logo/white-cropped.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 20 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "./App.css";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import AppLayout from "./components/app-layout/AppLayout";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { appTheme } from "./components/common/styles/theme";
import { createAppTheme } from "./components/common/styles/theme";
import Leaderboard from "./pages/leaderboard/Leaderboard";
import Home from "./pages/home/Home";
import News from "./pages/news/News";
Expand All @@ -13,7 +13,8 @@ import Lectures from "./pages/lectures/Lectures";
import ErrorPage from "./pages/Error";
import Login from "./pages/login/login";
import { useAuthStore } from "./lib/store/authStore";
import { useEffect } from "react";
import { useThemeStore } from "./lib/store/themeStore";
import { useEffect, useMemo } from "react";

const errorRoutes = [
{
Expand All @@ -32,7 +33,7 @@ const errorRoutes = [
path: "*",
code: 404,
title: "Page Not Found",
description: "The page youre looking for doesnt exist.",
description: "The page you're looking for doesn't exist.",
},
];

Expand All @@ -43,8 +44,23 @@ function App() {
fetchMe();
}, [fetchMe]);

const resolvedMode = useThemeStore((s) => s.resolvedMode);
const mode = useThemeStore((s) => s.mode);
const setMode = useThemeStore((s) => s.setMode);

const theme = useMemo(() => createAppTheme(resolvedMode), [resolvedMode]);

// Listen for OS-level dark mode changes when mode is "system"
useEffect(() => {
if (mode !== "system") return;
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => setMode("system");
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, [mode, setMode]);

return (
<ThemeProvider theme={appTheme}>
<ThemeProvider theme={theme}>
<CssBaseline />
<BrowserRouter basename="">
<AppLayout>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/app-layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const FooterLinkContainer = styled(Container)(({ theme }) => ({
}));

export const FooterBox = styled(Box)(({ theme }) => ({
borderTop: "1px solid #ddd",
borderTop: `1px solid ${theme.palette.divider}`,
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
textAlign: "center",
Expand Down
48 changes: 45 additions & 3 deletions frontend/src/components/app-layout/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// components/NavBar.tsx
import { AppBar, Toolbar, Link, Box } from "@mui/material";
import { AppBar, Toolbar, Link, Box, IconButton, Tooltip } from "@mui/material";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
import SettingsBrightnessIcon from "@mui/icons-material/SettingsBrightness";
import { useTheme } from "@mui/material/styles";
import {
flexRowCenter,
flexRowCenterMediumGap,
Expand All @@ -9,6 +13,7 @@ import {
import { appBarStyle, brandStyle } from "./styles";
import { ConstrainedContainer } from "./ConstrainedContainer";
import NavUserProfile from "./NavUserProfile";
import { useThemeStore } from "../../lib/store/themeStore";

export interface NavLink {
label: string;
Expand All @@ -17,6 +22,37 @@ export interface NavLink {
}

export default function NavBar() {
const mode = useThemeStore((s) => s.mode);
const setMode = useThemeStore((s) => s.setMode);
const theme = useTheme();
const isDark = theme.palette.mode === "dark";

const logoSrc = isDark
? "/gpu-mode-logo/white-cropped.svg"
: "/gpu-mode-logo/black-cropped.svg";

const cycleMode = () => {
const next =
mode === "light" ? "dark" : mode === "dark" ? "system" : "light";
setMode(next);
};

const modeIcon =
mode === "light" ? (
<LightModeIcon />
) : mode === "dark" ? (
<DarkModeIcon />
) : (
<SettingsBrightnessIcon />
);

const modeLabel =
mode === "light"
? "Switch to dark mode"
: mode === "dark"
? "Switch to system preference"
: "Switch to light mode";

const links: NavLink[] = [
{ label: "News", href: "/news" },
{ label: "Lectures", href: "/lectures" },
Expand All @@ -34,7 +70,7 @@ export default function NavBar() {
<Box sx={{ ...flexRowCenter }}>
<Box
component="img"
src="/gpu-mode-logo/black-cropped.svg"
src={logoSrc}
alt="GPU MODE"
sx={{
height: { xs: 24, sm: 32 },
Expand Down Expand Up @@ -74,7 +110,13 @@ export default function NavBar() {
</Link>
))}
</Box>
<Box sx={{ ml: "auto", flexShrink: 0 }}>

<Box sx={{ ml: "auto", flexShrink: 0, display: "flex", alignItems: "center", gap: 1 }}>
<Tooltip title={modeLabel}>
<IconButton onClick={cycleMode} color="inherit" size="small">
{modeIcon}
</IconButton>
</Tooltip>
<NavUserProfile />
</Box>
</Toolbar>
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/app-layout/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import type { SxProps, Theme } from "@mui/material";
import { flexRowCenter, mediumText } from "../common/styles/shared_style";

export const appBarStyle: SxProps<Theme> = {
backgroundColor: "white",
color: "black",
backgroundColor: "background.paper",
color: "text.primary",
boxShadow: "none",
borderBottom: "1px solid #ddd",
borderBottom: 1,
borderColor: "divider",
width: "100%",
maxWidth: "100vw",
};
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/codeblock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Box, IconButton, Tooltip, useTheme } from "@mui/material";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";

interface CodeBlockProps {
code: string;
Expand All @@ -29,6 +30,7 @@ const styles = {
export default function CodeBlock({ code }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const theme = useTheme();
const syntaxTheme = theme.palette.mode === "dark" ? oneDark : oneLight;

const handleCopy = () => {
navigator.clipboard.writeText(code).then(() => {
Expand Down Expand Up @@ -67,11 +69,14 @@ export default function CodeBlock({ code }: CodeBlockProps) {
fontFamily: "monospace !important",
background: "transparent !important",
},
"& code": {
background: "transparent !important",
},
}}
>
<SyntaxHighlighter
language="python"
style={oneLight}
style={syntaxTheme}
customStyle={{
margin: 0,
padding: 12,
Expand Down
105 changes: 59 additions & 46 deletions frontend/src/components/common/styles/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createTheme, type Components } from "@mui/material/styles";
import { createTheme, type Components, type PaletteMode } from "@mui/material/styles";

const colorPalette = {
primary: "#5865F2", // Blurple, Discord's brand color
Expand All @@ -25,78 +25,91 @@ declare module "@mui/material/styles" {

// the global styles apply to app-wide

const MuiCardGlobalStyle: Components["MuiCard"] = {
const getMuiCardGlobalStyle = (mode: PaletteMode): Components["MuiCard"] => ({
styleOverrides: {
root: {
border: `1px solid ${colorPalette.neutral}`,
border: `1px solid ${mode === "dark" ? "#333" : colorPalette.neutral}`,
boxShadow: "none",
borderRadius: "12px",
padding: "16px",
},
},
};
});

const MuiButtonGlobalStyle: Components["MuiButton"] = {
const getMuiButtonGlobalStyle = (
mode: PaletteMode,
): Components["MuiButton"] => ({
defaultProps: {
variant: "contained",
disableElevation: true,
},
styleOverrides: {
root: {
backgroundColor: "#e6f0ff",
color: "#333",
backgroundColor: mode === "dark" ? "#23272f" : "#e6f0ff",
color: mode === "dark" ? "#e0e0e0" : "#333",
fontWeight: 500,
fontSize: "0.9rem",
textTransform: "none",
borderRadius: "13px",
padding: "1px 5px",
boxShadow: "none",
border: "1px solid #ddd",
border: `1px solid ${mode === "dark" ? "#444" : "#ddd"}`,
"&:hover": {
backgroundColor: "#d0e4ff",
backgroundColor: mode === "dark" ? "#2d3340" : "#d0e4ff",
},
"&:active": {
backgroundColor: "#b3d4ff",
backgroundColor: mode === "dark" ? "#3a4050" : "#b3d4ff",
},
},
},
};
});

export const appTheme = createTheme({
palette: {
primary: {
main: colorPalette.primary,
},
secondary: {
main: colorPalette.secondary,
},
custom: colorPalette,
},
typography: {
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
fontSize: 14,
h1: {
fontSize: "1.875rem",
lineHeight: "2.25rem",
fontWeight: 700,
marginTop: "1rem",
export function createAppTheme(mode: PaletteMode) {
return createTheme({
palette: {
mode,
primary: {
main: colorPalette.primary,
},
secondary: {
main: colorPalette.secondary,
},
custom: colorPalette,
...(mode === "dark" && {
background: {
default: "#0f1117",
paper: "#181a20",
},
}),
},
h2: {
fontSize: "1.5rem",
lineHeight: "2rem",
fontWeight: 700,
marginTop: "1rem",
typography: {
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
fontSize: 14,
h1: {
fontSize: "1.875rem",
lineHeight: "2.25rem",
fontWeight: 700,
marginTop: "1rem",
},
h2: {
fontSize: "1.5rem",
lineHeight: "2rem",
fontWeight: 700,
marginTop: "1rem",
},
h3: {
fontSize: "1.25rem",
lineHeight: "1.75rem",
fontWeight: 700,
marginTop: "1rem",
},
},
h3: {
fontSize: "1.25rem",
lineHeight: "1.75rem",
fontWeight: 700,
marginTop: "1rem",
components: {
MuiCard: getMuiCardGlobalStyle(mode),
MuiButton: getMuiButtonGlobalStyle(mode),
},
},
components: {
MuiCard: MuiCardGlobalStyle,
MuiButton: MuiButtonGlobalStyle,
},
});
});
}

export const appTheme = createAppTheme("light");
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import { useTheme } from "@mui/material/styles";

type MarkdownRendererProps = {
content: string;
Expand Down Expand Up @@ -44,11 +45,18 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
}) => {
const mergedImageProps = { ...defaultImageProps, ...imageProps };
const { align, ...styleProps } = mergedImageProps;
const theme = useTheme();
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
a: ({ node, ...props }) => (
<a
style={{ color: theme.palette.primary.main, textDecoration: "none" }}
{...props}
/>
),
figure: ({ node, ...props }) => (
<figure style={{ textAlign: align, margin: "1.5rem 0" }} {...props} />
),
Expand Down
Loading
Loading