Skip to content
Merged
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
105 changes: 4 additions & 101 deletions web/src/components/ChatboxHeader/index.jsx
Original file line number Diff line number Diff line change
@@ -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 <LightModeIcon />;
case "dark":
return <DarkModeIcon />;
default:
return <BrightnessMediumIcon />;
}
}
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 <https://oauth2-proxy.github.io/oauth2-proxy/docs/features/endpoints/>
// This is a bit hard-coded?
window.location.href = "/oauth2/sign_out";
};

return (
<div className={styles.chatboxHeader}>
<div className={styles.rightElems}>
<Dropdown>
<DropdownButton className={styles.themeMenuTitle}>
<ThemeIcon theme={theme} />
<span className={styles.themeMenuText}>Theme</span>
</DropdownButton>
<DropdownMenu className={styles.themeMenuList}>
<li>
<button
className={`${styles.themeMenuItem} ${theme === "system" && styles.selected}`}
onClick={() => onThemeClick("system")}
aria-label="Set theme to system default"
>
<BrightnessMediumIcon />
<span className={styles.themeMenuText}>OS Default</span>
</button>
</li>
<li>
<button
className={`${styles.themeMenuItem} ${theme === "light" && styles.selected}`}
onClick={() => onThemeClick("light")}
aria-label="Set theme to light mode"
>
<LightModeIcon />
<span className={styles.themeMenuText}>Light</span>
</button>
</li>
<li>
<button
className={`${styles.themeMenuItem} ${theme === "dark" && styles.selected}`}
onClick={() => onThemeClick("dark")}
aria-label="Set theme to dark mode"
>
<DarkModeIcon />
<span className={styles.themeMenuText}>Dark</span>
</button>
</li>
</DropdownMenu>
</Dropdown>
<Dropdown>
<DropdownButton className={styles.userInfoMenu}>
<Avatar
// NOTE: className not working on Avatar
sx={{
width: 24,
height: 24,
}}
src={avatar}
alt={`${username}'s avatar`}
/>
</DropdownButton>
<DropdownMenu className={styles.userInfoMenuList}>
<li><span>{username}</span></li>
<hr className={styles.userInfoMenuUsernameHr} />
<li>
<button className={styles.themeMenuItem} onClick={handleLogout} aria-label="Logout">
<span className={styles.themeMenuText}>Logout</span>
</button>
</li>
</DropdownMenu>
</Dropdown>
<ThemeSelector />
<UserMenu />
</div>
</div>
);
Expand Down
67 changes: 0 additions & 67 deletions web/src/components/ChatboxHeader/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
74 changes: 74 additions & 0 deletions web/src/components/ThemeSelector/index.jsx
Original file line number Diff line number Diff line change
@@ -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 <LightModeIcon />;
case "dark":
return <DarkModeIcon />;
default:
return <BrightnessMediumIcon />;
}
};
ThemeIcon.propTypes = {
theme: PropTypes.string,
};


const ThemeSelector = () => {
const { theme, setTheme } = useContext(ThemeContext);

return (
<Dropdown>
<DropdownButton className={styles.themeMenuTitle}>
<ThemeIcon theme={theme} />
<span className={styles.themeMenuText}>Theme</span>
</DropdownButton>
<DropdownMenu className={styles.themeMenuList}>
<li>
<button
className={`${styles.themeMenuItem} ${theme === "system" && styles.selected}`}
onClick={() => setTheme("system")}
aria-label="Set theme to system default"
>
<BrightnessMediumIcon />
<span className={styles.themeMenuText}>OS Default</span>
</button>
</li>
<li>
<button
className={`${styles.themeMenuItem} ${theme === "light" && styles.selected}`}
onClick={() => setTheme("light")}
aria-label="Set theme to light mode"
>
<LightModeIcon />
<span className={styles.themeMenuText}>Light</span>
</button>
</li>
<li>
<button
className={`${styles.themeMenuItem} ${theme === "dark" && styles.selected}`}
onClick={() => setTheme("dark")}
aria-label="Set theme to dark mode"
>
<DarkModeIcon />
<span className={styles.themeMenuText}>Dark</span>
</button>
</li>
</DropdownMenu>
</Dropdown>
);
};

export default ThemeSelector;
47 changes: 47 additions & 0 deletions web/src/components/ThemeSelector/index.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ThemeContext.Provider value={mockThemeContextValue}>
<UserContext.Provider value={mockUserContextValue}>
<ChatboxHeader />
</UserContext.Provider>
<ThemeSelector />
</ThemeContext.Provider>
);

Expand All @@ -20,31 +17,13 @@ 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"));
expect(screen.getByLabelText("Set theme to light mode")).toBeDefined();
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");
});
});
Loading
Loading