diff --git a/web/src/components/ChatboxHeader/index.jsx b/web/src/components/ChatboxHeader/index.jsx
index 7947b64c..f70ba88c 100644
--- a/web/src/components/ChatboxHeader/index.jsx
+++ b/web/src/components/ChatboxHeader/index.jsx
@@ -1,112 +1,15 @@
import styles from "./index.module.css";
-import { useContext } from "react";
-import PropTypes from "prop-types";
-
-import Avatar from "@mui/material/Avatar";
-
-import BrightnessMediumIcon from "@mui/icons-material/BrightnessMedium";
-import LightModeIcon from "@mui/icons-material/LightMode";
-import DarkModeIcon from "@mui/icons-material/DarkMode";
-
-import { Dropdown, DropdownButton, DropdownMenu } from "@/components/DropdownMenu";
-import { ThemeContext } from "@/contexts/theme";
-import { UserContext } from "@/contexts/user";
-
-
-const ThemeIcon = ({ theme }) => {
- switch (theme) {
- case "light":
- return ;
- case "dark":
- return ;
- default:
- return ;
- }
-}
-ThemeIcon.propTypes = {
- theme: PropTypes.string,
-};
+import ThemeSelector from "@/components/ThemeSelector";
+import UserMenu from "@/components/UserMenu";
const ChatboxHeader = () => {
- const { theme, setTheme } = useContext(ThemeContext);
- const { username, avatar } = useContext(UserContext);
-
- const onThemeClick = (theme) => {
- setTheme(theme);
- }
-
- const handleLogout = async (e) => {
- e.preventDefault();
- // See
- // This is a bit hard-coded?
- window.location.href = "/oauth2/sign_out";
- };
-
return (
-
-
-
- Theme
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {username}
-
-
-
-
-
-
+
+
);
diff --git a/web/src/components/ChatboxHeader/index.module.css b/web/src/components/ChatboxHeader/index.module.css
index da337cd5..aa76c8d7 100644
--- a/web/src/components/ChatboxHeader/index.module.css
+++ b/web/src/components/ChatboxHeader/index.module.css
@@ -17,70 +17,3 @@
margin-left: auto;
margin-right: 2vw;
}
-
-.themeMenuTitle {
- background-color: transparent;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- justify-content: center;
- border: none;
-}
-
-.themeMenuTitle:hover {
- background-color: var(--bg-secondary);
- border-radius: 5px;
-}
-
-.themeMenuText {
- margin: 0.3em;
- text-align: start;
- font-size: 0.8rem;
-}
-
-.themeMenuList {
- background-color: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 3px;
- padding: 0.5rem;
-}
-
-.themeMenuItem {
- width: 100%;
- margin: 2px;
- background-color: transparent;
- /* must add transparent border or the button will shake on hover */
- border: 1px solid transparent;
- color: var(--text-primary);
- display: flex;
- align-items: center;
-}
-
-.themeMenuItem:hover {
- border: 1px solid var(--border-color);
- border-radius: 3px;
-}
-
-.themeMenuItem.selected {
- border: 1px solid var(--border-color);
- border-radius: 3px;
-}
-
-.userInfoMenu {
- background-color: transparent;
- border: none;
-}
-
-.userInfoMenuList {
- background-color: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 3px;
- position: absolute;
- right: 2vw;
- z-index: 1;
- padding: 0.5rem;
-}
-
-.userInfoMenuUsernameHr {
- border: 1px solid var(--border-color);
-}
diff --git a/web/src/components/ThemeSelector/index.jsx b/web/src/components/ThemeSelector/index.jsx
new file mode 100644
index 00000000..85a05d12
--- /dev/null
+++ b/web/src/components/ThemeSelector/index.jsx
@@ -0,0 +1,74 @@
+import styles from "./index.module.css";
+
+import { useContext } from "react";
+import PropTypes from "prop-types";
+
+import BrightnessMediumIcon from "@mui/icons-material/BrightnessMedium";
+import LightModeIcon from "@mui/icons-material/LightMode";
+import DarkModeIcon from "@mui/icons-material/DarkMode";
+
+import { Dropdown, DropdownButton, DropdownMenu } from "@/components/DropdownMenu";
+import { ThemeContext } from "@/contexts/theme";
+
+
+const ThemeIcon = ({ theme }) => {
+ switch (theme) {
+ case "light":
+ return ;
+ case "dark":
+ return ;
+ default:
+ return ;
+ }
+};
+ThemeIcon.propTypes = {
+ theme: PropTypes.string,
+};
+
+
+const ThemeSelector = () => {
+ const { theme, setTheme } = useContext(ThemeContext);
+
+ return (
+
+
+
+ Theme
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ThemeSelector;
diff --git a/web/src/components/ThemeSelector/index.module.css b/web/src/components/ThemeSelector/index.module.css
new file mode 100644
index 00000000..3440cad7
--- /dev/null
+++ b/web/src/components/ThemeSelector/index.module.css
@@ -0,0 +1,47 @@
+.themeMenuTitle {
+ background-color: transparent;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+}
+
+.themeMenuTitle:hover {
+ background-color: var(--bg-secondary);
+ border-radius: 5px;
+}
+
+.themeMenuText {
+ margin: 0.3em;
+ text-align: start;
+ font-size: 0.8rem;
+}
+
+.themeMenuList {
+ background-color: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 3px;
+ padding: 0.5rem;
+}
+
+.themeMenuItem {
+ width: 100%;
+ margin: 2px;
+ background-color: transparent;
+ /* must add transparent border or the button will shake on hover */
+ border: 1px solid transparent;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+}
+
+.themeMenuItem:hover {
+ border: 1px solid var(--border-color);
+ border-radius: 3px;
+}
+
+.themeMenuItem.selected {
+ border: 1px solid var(--border-color);
+ border-radius: 3px;
+}
diff --git a/web/src/components/ChatboxHeader/index.test.jsx b/web/src/components/ThemeSelector/index.test.jsx
similarity index 51%
rename from web/src/components/ChatboxHeader/index.test.jsx
rename to web/src/components/ThemeSelector/index.test.jsx
index 6e8d0403..5b035f26 100644
--- a/web/src/components/ChatboxHeader/index.test.jsx
+++ b/web/src/components/ThemeSelector/index.test.jsx
@@ -2,15 +2,12 @@ import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { ThemeContext } from "@/contexts/theme";
-import { UserContext } from "@/contexts/user";
-import ChatboxHeader from "./index";
+import ThemeSelector from "./index";
const setup = () => render(
-
-
-
+
);
@@ -20,18 +17,8 @@ const mockThemeContextValue = {
setTheme: mockSetTheme,
};
-const mockUserContextValue = {
- username: "testuser",
- avatar: "testavatar.png",
-};
describe("ChatboxHeader", () => {
- it("renders the username and avatar", () => {
- setup();
- expect(screen.getByAltText("testuser's avatar")).toBeDefined();
- expect(screen.getByText("testuser")).toBeDefined();
- });
-
it("renders the theme menu and allows theme change", () => {
setup();
fireEvent.click(screen.getByText("Theme"));
@@ -39,12 +26,4 @@ describe("ChatboxHeader", () => {
fireEvent.click(screen.getByLabelText("Set theme to dark mode"));
expect(mockSetTheme).toHaveBeenCalledWith("dark");
});
-
- it("handles logout", () => {
- setup();
- delete window.location;
- window.location = { href: "" };
- fireEvent.click(screen.getByText("Logout"));
- expect(window.location.href).toBe("/oauth2/sign_out");
- });
});
diff --git a/web/src/components/UserMenu/index.jsx b/web/src/components/UserMenu/index.jsx
new file mode 100644
index 00000000..bc0e0d7f
--- /dev/null
+++ b/web/src/components/UserMenu/index.jsx
@@ -0,0 +1,51 @@
+import styles from "./index.module.css";
+
+import { useContext } from "react";
+
+import Avatar from "@mui/material/Avatar";
+
+import { Dropdown, DropdownButton, DropdownMenu } from "@/components/DropdownMenu";
+import { UserContext } from "@/contexts/user";
+
+
+const UserMenu = () => {
+ const { username, avatar } = useContext(UserContext);
+
+ const handleLogout = async (e) => {
+ e.preventDefault();
+ // See
+ // This is a bit hard-coded?
+ window.location.href = "/oauth2/sign_out";
+ };
+
+ return (
+
+
+
+
+
+ {username}
+
+
+
+
+
+
+ );
+};
+
+export default UserMenu;
diff --git a/web/src/components/UserMenu/index.module.css b/web/src/components/UserMenu/index.module.css
new file mode 100644
index 00000000..0fd8a49f
--- /dev/null
+++ b/web/src/components/UserMenu/index.module.css
@@ -0,0 +1,19 @@
+
+.userInfoMenu {
+ background-color: transparent;
+ border: none;
+}
+
+.userInfoMenuList {
+ background-color: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 3px;
+ position: absolute;
+ right: 2vw;
+ z-index: 1;
+ padding: 0.5rem;
+}
+
+.userInfoMenuUsernameHr {
+ border: 1px solid var(--border-color);
+}
diff --git a/web/src/components/UserMenu/index.test.jsx b/web/src/components/UserMenu/index.test.jsx
new file mode 100644
index 00000000..0f3344e0
--- /dev/null
+++ b/web/src/components/UserMenu/index.test.jsx
@@ -0,0 +1,33 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+
+import { UserContext } from "@/contexts/user";
+
+import UserMenu from "./index";
+
+const setup = () => render(
+
+
+
+);
+
+const mockUserContextValue = {
+ username: "testuser",
+ avatar: "testavatar.png",
+};
+
+describe("ChatboxHeader", () => {
+ it("renders the username and avatar", () => {
+ setup();
+ expect(screen.getByAltText("testuser's avatar")).toBeDefined();
+ expect(screen.getByText("testuser")).toBeDefined();
+ });
+
+ it("handles logout", () => {
+ setup();
+ delete window.location;
+ window.location = { href: "" };
+ fireEvent.click(screen.getByText("Logout"));
+ expect(window.location.href).toBe("/oauth2/sign_out");
+ });
+});