Skip to content

Commit 5d166e2

Browse files
Ayan / Dark and light mode for MCC #358 (#602)
# Purpose Adds Light mode and Dark mode # New Changes Fixes NAVbar issues (navbar being too big) Fixes hardcoded values for colors so future readers can read it easily
1 parent 6ef5931 commit 5d166e2

File tree

10 files changed

+216
-59
lines changed

10 files changed

+216
-59
lines changed

gs/frontend/mcc/package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gs/frontend/mcc/src/App.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Dashboard from "./pages/Dashboard";
88
import AROAdmin from "./pages/AROAdmin";
99
import LiveSession from "./pages/LiveSession";
1010
import Login from "./pages/Login";
11+
import { ThemeProvider } from "./contexts/ThemeContext";
1112
import PageNotFound from "./components/PageNotFound";
1213

1314
/**
@@ -16,19 +17,21 @@ import PageNotFound from "./components/PageNotFound";
1617
*/
1718
function App() {
1819
return (
19-
<>
20+
<ThemeProvider>
2021
<Nav />
2122
<Background />
22-
<Routes>
23-
<Route path="/" element={<Dashboard />} />
24-
<Route path="/commands" element={<Commands />} />
25-
<Route path="/telemetry-data" element={<AROAdmin />} />
26-
<Route path="/aro-requests" element={<LiveSession />} />
27-
<Route path="/login" element={<Login />} />
28-
<Route path="*" element={<PageNotFound />} />
29-
</Routes>
23+
<div className="pt-16">
24+
<Routes>
25+
<Route path="/" element={<Dashboard />} />
26+
<Route path="/commands" element={<Commands />} />
27+
<Route path="/telemetry-data" element={<AROAdmin />} />
28+
<Route path="/aro-requests" element={<LiveSession />} />
29+
<Route path="/login" element={<Login />} />
30+
<Route path="*" element={<PageNotFound />} />
31+
</Routes>
32+
</div>
3033
<ToastContainer />
31-
</>
34+
</ThemeProvider>
3235
);
3336
}
3437

gs/frontend/mcc/src/components/Background.test.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,26 @@ import { describe, it, expect } from "vitest";
22
import { render, screen } from "@testing-library/react";
33
import "@testing-library/jest-dom";
44
import Background from "./Background";
5+
import { ThemeProvider } from "../contexts/ThemeContext";
56

67
describe("Background", () => {
78
it("renders background image", () => {
8-
render(<Background />);
9+
render(
10+
<ThemeProvider>
11+
<Background />
12+
</ThemeProvider>
13+
);
914
expect(screen.getByAltText("background-image")).toBeInTheDocument();
1015
});
1116

1217
it("has correct CSS classes", () => {
13-
render(<Background />);
18+
render(
19+
<ThemeProvider>
20+
<Background />
21+
</ThemeProvider>
22+
);
1423
const image = screen.getByAltText("background-image");
15-
expect(image).toHaveClass("fixed bottom-0 left-0 w-full object-cover -z-10 opacity-40");
24+
// In light mode (default), opacity should be slightly higher for visibility
25+
expect(image).toHaveClass("opacity-30");
1626
});
1727
});

gs/frontend/mcc/src/components/Background.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import background from "../assets/earth_compressed.webp";
2+
import { useTheme } from "../contexts/ThemeContext";
23

34
/**
45
* @brief Background component displaying the Earth image
56
* @return tsx element of Background component
67
*/
78
function Background() {
9+
const { theme } = useTheme();
10+
811
return (
9-
<img
10-
src={background}
11-
alt="background-image"
12-
className="fixed bottom-0 left-0 w-full object-cover -z-10 opacity-40"
13-
/>
12+
<>
13+
<img
14+
src={background}
15+
alt="background-image"
16+
className={`fixed inset-0 h-full w-full object-cover -z-20 transition-opacity duration-300 ${
17+
theme === "dark" ? "opacity-40" : "opacity-30"
18+
}`}
19+
/>
20+
<div
21+
className={`fixed inset-0 -z-10 transition-colors duration-300 ${
22+
theme === "dark"
23+
? "bg-gradient-to-b from-gray-950/90 via-gray-950/70 to-transparent"
24+
: "bg-gradient-to-b from-slate-100/80 via-slate-100/50 to-transparent"
25+
}`}
26+
/>
27+
</>
1428
);
1529
}
1630

gs/frontend/mcc/src/components/Nav.test.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@ import { render, screen } from "@testing-library/react";
33
import { BrowserRouter } from "react-router-dom";
44
import "@testing-library/jest-dom";
55
import Nav from "./Nav";
6+
import { ThemeProvider } from "../contexts/ThemeContext";
67

78
describe("Nav", () => {
89
it("renders logo", () => {
910
render(
10-
<BrowserRouter>
11-
<Nav />
12-
</BrowserRouter>
11+
<ThemeProvider>
12+
<BrowserRouter>
13+
<Nav />
14+
</BrowserRouter>
15+
</ThemeProvider>
1316
);
1417
expect(screen.getByAltText("orbital logo")).toBeInTheDocument();
1518
});
1619

1720
it("renders navigation links", () => {
1821
render(
19-
<BrowserRouter>
20-
<Nav />
21-
</BrowserRouter>
22+
<ThemeProvider>
23+
<BrowserRouter>
24+
<Nav />
25+
</BrowserRouter>
26+
</ThemeProvider>
2227
);
2328
expect(screen.getByText("Dashboard")).toBeInTheDocument();
2429
expect(screen.getByText("Commands")).toBeInTheDocument();

gs/frontend/mcc/src/components/Nav.tsx

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Link } from "react-router-dom";
22
import orbital_logo from "../assets/orbital_logo.png";
33
import { NAVIGATION_LINKS } from "../utils/nav-links";
4+
import { useTheme } from "../contexts/ThemeContext";
45

56
/**
67
* @brief Nav component displaying the navigation bar
@@ -9,39 +10,95 @@ import { NAVIGATION_LINKS } from "../utils/nav-links";
910
function Nav() {
1011
// TODO: create user auth that checks if the user is logged in
1112
const isLoggedIn = false;
13+
const { theme, toggleTheme } = useTheme();
1214

1315
return (
14-
<nav className="m-7">
16+
<nav className="fixed top-0 left-0 right-0 h-16 px-8 flex items-center justify-between bg-background/80 backdrop-blur-sm border-b border-border z-50">
1517
{/* Logo */}
16-
<div className="fixed left-8">
17-
<Link to="/" className="hover:opacity-80 transition-opacity">
18-
<img src={orbital_logo} alt="orbital logo" className="h-12 w-auto" />
19-
</Link>
20-
</div>
18+
<Link to="/" className="hover:opacity-80 transition-opacity">
19+
<img src={orbital_logo} alt="orbital logo" className="h-10 w-auto" />
20+
</Link>
2121

2222
{/* Navigation Links */}
23-
<div className="fixed left-1/2 transform -translate-x-1/2 mt-3 flex space-x-7">
23+
<div className="flex space-x-7">
2424
{NAVIGATION_LINKS.map((link) => (
25-
<Link key={link.url} to={link.url} className="hover:underline">
25+
<Link
26+
key={link.url}
27+
to={link.url}
28+
className="hover:underline transition-colors"
29+
>
2630
{link.text}
2731
</Link>
2832
))}
2933
</div>
3034

31-
{/* Profile or Login page, depending on authentication state */}
32-
{isLoggedIn ? (
33-
<div className="fixed right-8 mt-2 border-1 border-white rounded-xl p-1 px-2 hover:bg-white hover:text-black">
34-
<Link to="/profile" className="">
35+
36+
<div className="flex items-center gap-4">
37+
38+
<button
39+
onClick={toggleTheme}
40+
className="w-10 h-10 flex items-center justify-center rounded-full border border-foreground/20 hover:bg-accent transition-colors"
41+
aria-label="Toggle theme"
42+
title={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
43+
>
44+
{theme === "light" ? (
45+
46+
<svg
47+
xmlns="http://www.w3.org/2000/svg"
48+
width="18"
49+
height="18"
50+
viewBox="0 0 24 24"
51+
fill="none"
52+
stroke="currentColor"
53+
strokeWidth="2"
54+
strokeLinecap="round"
55+
strokeLinejoin="round"
56+
>
57+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
58+
</svg>
59+
) : (
60+
61+
<svg
62+
xmlns="http://www.w3.org/2000/svg"
63+
width="18"
64+
height="18"
65+
viewBox="0 0 24 24"
66+
fill="none"
67+
stroke="currentColor"
68+
strokeWidth="2"
69+
strokeLinecap="round"
70+
strokeLinejoin="round"
71+
>
72+
<circle cx="12" cy="12" r="4" />
73+
<path d="M12 2v2" />
74+
<path d="M12 20v2" />
75+
<path d="m4.93 4.93 1.41 1.41" />
76+
<path d="m17.66 17.66 1.41 1.41" />
77+
<path d="M2 12h2" />
78+
<path d="M20 12h2" />
79+
<path d="m6.34 17.66-1.41 1.41" />
80+
<path d="m19.07 4.93-1.41 1.41" />
81+
</svg>
82+
)}
83+
</button>
84+
85+
86+
{isLoggedIn ? (
87+
<Link
88+
to="/profile"
89+
className="border border-foreground/20 rounded-xl px-4 py-2 hover:bg-accent transition-colors"
90+
>
3591
Profile
3692
</Link>
37-
</div>
38-
) : (
39-
<div className="fixed right-8 mt-2 border-1 border-white rounded-xl p-1 px-2 hover:bg-white hover:text-black">
40-
<Link to="/login" className="">
93+
) : (
94+
<Link
95+
to="/login"
96+
className="border border-foreground/20 rounded-xl px-4 py-2 hover:bg-accent transition-colors"
97+
>
4198
Login
4299
</Link>
43-
</div>
44-
)}
100+
)}
101+
</div>
45102
</nav>
46103
);
47104
}

gs/frontend/mcc/src/components/Table.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,21 @@ function Table<T>({
5656
placeholder="Search..."
5757
value={globalFilter ?? ""}
5858
onChange={(e) => setGlobalFilter(e.target.value)}
59-
className="flex-1 bg-gray-800/50 text-white px-4 py-2 rounded-lg border border-gray-600/50 focus:outline-none focus:border-gray-500"
59+
className="flex-1 bg-input text-foreground px-4 py-2 rounded-lg border border-border focus:outline-none focus:border-ring"
6060
/>
6161
</div>
6262
)}
6363

6464
{/* Table */}
65-
<div className="bg-gray-900/30 backdrop-blur-sm rounded-lg overflow-hidden border border-gray-700/50">
66-
<table className="w-full text-white">
65+
<div className="bg-card backdrop-blur-sm rounded-lg overflow-hidden border border-border">
66+
<table className="w-full text-foreground">
6767
<thead>
6868
{table.getHeaderGroups().map((headerGroup) => (
69-
<tr key={headerGroup.id} className="border-b border-gray-700/50">
69+
<tr key={headerGroup.id} className="border-b border-border">
7070
{headerGroup.headers.map((header) => (
7171
<th
7272
key={header.id}
73-
className="text-left px-6 py-4 font-normal text-gray-300"
73+
className="text-left px-6 py-4 font-normal text-muted-foreground"
7474
>
7575
{header.isPlaceholder
7676
? null
@@ -87,13 +87,13 @@ function Table<T>({
8787
{table.getRowModel().rows.map((row) => (
8888
<tr
8989
key={row.id}
90-
className={`border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors ${
90+
className={`border-b border-border hover:bg-accent transition-colors ${
9191
onRowClick ? "cursor-pointer" : ""
9292
}`}
9393
onClick={() => onRowClick?.(row.original)}
9494
>
9595
{row.getVisibleCells().map((cell) => (
96-
<td key={cell.id} className="px-6 py-4 text-gray-200">
96+
<td key={cell.id} className="px-6 py-4 text-foreground">
9797
{flexRender(cell.column.columnDef.cell, cell.getContext())}
9898
</td>
9999
))}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { createContext, useContext, useEffect, useState } from "react";
2+
3+
type Theme = "light" | "dark";
4+
5+
interface ThemeContextType {
6+
theme: Theme;
7+
toggleTheme: () => void;
8+
}
9+
10+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
11+
12+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
13+
14+
const [theme, setTheme] = useState<Theme>(() => {
15+
const stored = localStorage.getItem("theme") as Theme | null;
16+
if (stored) return stored;
17+
18+
19+
if (window.matchMedia("(prefers-color-scheme: dark)").matches) { // Find system Preference
20+
return "dark";
21+
}
22+
return "light";
23+
});
24+
25+
26+
useEffect(() => {
27+
const root = document.documentElement;
28+
if (theme === "dark") {
29+
root.classList.add("dark");
30+
} else {
31+
root.classList.remove("dark");
32+
}
33+
localStorage.setItem("theme", theme);
34+
}, [theme]);
35+
36+
const toggleTheme = () => {
37+
setTheme((prev) => (prev === "light" ? "dark" : "light"));
38+
};
39+
40+
return (
41+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
42+
{children}
43+
</ThemeContext.Provider>
44+
);
45+
}
46+
47+
export function useTheme() {
48+
const context = useContext(ThemeContext);
49+
if (!context) {
50+
throw new Error("useTheme must be used within ThemeProvider");
51+
}
52+
return context;
53+
}

0 commit comments

Comments
 (0)