Skip to content

Commit 53120d5

Browse files
committed
refactor: extract theme selector and user menu
1 parent c182de2 commit 53120d5

File tree

8 files changed

+230
-191
lines changed

8 files changed

+230
-191
lines changed

web/src/components/ChatboxHeader/index.jsx

Lines changed: 4 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,15 @@
11
import styles from "./index.module.css";
22

3-
import { useContext } from "react";
4-
import PropTypes from "prop-types";
5-
6-
import Avatar from "@mui/material/Avatar";
7-
8-
import BrightnessMediumIcon from "@mui/icons-material/BrightnessMedium";
9-
import LightModeIcon from "@mui/icons-material/LightMode";
10-
import DarkModeIcon from "@mui/icons-material/DarkMode";
11-
12-
import { Dropdown, DropdownButton, DropdownMenu } from "@/components/DropdownMenu";
13-
import { ThemeContext } from "@/contexts/theme";
14-
import { UserContext } from "@/contexts/user";
15-
16-
17-
const ThemeIcon = ({ theme }) => {
18-
switch (theme) {
19-
case "light":
20-
return <LightModeIcon />;
21-
case "dark":
22-
return <DarkModeIcon />;
23-
default:
24-
return <BrightnessMediumIcon />;
25-
}
26-
}
27-
ThemeIcon.propTypes = {
28-
theme: PropTypes.string,
29-
};
3+
import ThemeSelector from "@/components/ThemeSelector";
4+
import UserMenu from "@/components/UserMenu";
305

316

327
const ChatboxHeader = () => {
33-
const { theme, setTheme } = useContext(ThemeContext);
34-
const { username, avatar } = useContext(UserContext);
35-
36-
const onThemeClick = (theme) => {
37-
setTheme(theme);
38-
}
39-
40-
const handleLogout = async (e) => {
41-
e.preventDefault();
42-
// See <https://oauth2-proxy.github.io/oauth2-proxy/docs/features/endpoints/>
43-
// This is a bit hard-coded?
44-
window.location.href = "/oauth2/sign_out";
45-
};
46-
478
return (
489
<div className={styles.chatboxHeader}>
4910
<div className={styles.rightElems}>
50-
<Dropdown>
51-
<DropdownButton className={styles.themeMenuTitle}>
52-
<ThemeIcon theme={theme} />
53-
<span className={styles.themeMenuText}>Theme</span>
54-
</DropdownButton>
55-
<DropdownMenu className={styles.themeMenuList}>
56-
<li>
57-
<button
58-
className={`${styles.themeMenuItem} ${theme === "system" && styles.selected}`}
59-
onClick={() => onThemeClick("system")}
60-
aria-label="Set theme to system default"
61-
>
62-
<BrightnessMediumIcon />
63-
<span className={styles.themeMenuText}>OS Default</span>
64-
</button>
65-
</li>
66-
<li>
67-
<button
68-
className={`${styles.themeMenuItem} ${theme === "light" && styles.selected}`}
69-
onClick={() => onThemeClick("light")}
70-
aria-label="Set theme to light mode"
71-
>
72-
<LightModeIcon />
73-
<span className={styles.themeMenuText}>Light</span>
74-
</button>
75-
</li>
76-
<li>
77-
<button
78-
className={`${styles.themeMenuItem} ${theme === "dark" && styles.selected}`}
79-
onClick={() => onThemeClick("dark")}
80-
aria-label="Set theme to dark mode"
81-
>
82-
<DarkModeIcon />
83-
<span className={styles.themeMenuText}>Dark</span>
84-
</button>
85-
</li>
86-
</DropdownMenu>
87-
</Dropdown>
88-
<Dropdown>
89-
<DropdownButton className={styles.userInfoMenu}>
90-
<Avatar
91-
// NOTE: className not working on Avatar
92-
sx={{
93-
width: 24,
94-
height: 24,
95-
}}
96-
src={avatar}
97-
alt={`${username}'s avatar`}
98-
/>
99-
</DropdownButton>
100-
<DropdownMenu className={styles.userInfoMenuList}>
101-
<li><span>{username}</span></li>
102-
<hr className={styles.userInfoMenuUsernameHr} />
103-
<li>
104-
<button className={styles.themeMenuItem} onClick={handleLogout} aria-label="Logout">
105-
<span className={styles.themeMenuText}>Logout</span>
106-
</button>
107-
</li>
108-
</DropdownMenu>
109-
</Dropdown>
11+
<ThemeSelector />
12+
<UserMenu />
11013
</div>
11114
</div>
11215
);

web/src/components/ChatboxHeader/index.module.css

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -17,70 +17,3 @@
1717
margin-left: auto;
1818
margin-right: 2vw;
1919
}
20-
21-
.themeMenuTitle {
22-
background-color: transparent;
23-
color: var(--text-primary);
24-
display: flex;
25-
align-items: center;
26-
justify-content: center;
27-
border: none;
28-
}
29-
30-
.themeMenuTitle:hover {
31-
background-color: var(--bg-secondary);
32-
border-radius: 5px;
33-
}
34-
35-
.themeMenuText {
36-
margin: 0.3em;
37-
text-align: start;
38-
font-size: 0.8rem;
39-
}
40-
41-
.themeMenuList {
42-
background-color: var(--bg-primary);
43-
border: 1px solid var(--border-color);
44-
border-radius: 3px;
45-
padding: 0.5rem;
46-
}
47-
48-
.themeMenuItem {
49-
width: 100%;
50-
margin: 2px;
51-
background-color: transparent;
52-
/* must add transparent border or the button will shake on hover */
53-
border: 1px solid transparent;
54-
color: var(--text-primary);
55-
display: flex;
56-
align-items: center;
57-
}
58-
59-
.themeMenuItem:hover {
60-
border: 1px solid var(--border-color);
61-
border-radius: 3px;
62-
}
63-
64-
.themeMenuItem.selected {
65-
border: 1px solid var(--border-color);
66-
border-radius: 3px;
67-
}
68-
69-
.userInfoMenu {
70-
background-color: transparent;
71-
border: none;
72-
}
73-
74-
.userInfoMenuList {
75-
background-color: var(--bg-primary);
76-
border: 1px solid var(--border-color);
77-
border-radius: 3px;
78-
position: absolute;
79-
right: 2vw;
80-
z-index: 1;
81-
padding: 0.5rem;
82-
}
83-
84-
.userInfoMenuUsernameHr {
85-
border: 1px solid var(--border-color);
86-
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import styles from "./index.module.css";
2+
3+
import { useContext } from "react";
4+
import PropTypes from "prop-types";
5+
6+
import BrightnessMediumIcon from "@mui/icons-material/BrightnessMedium";
7+
import LightModeIcon from "@mui/icons-material/LightMode";
8+
import DarkModeIcon from "@mui/icons-material/DarkMode";
9+
10+
import { Dropdown, DropdownButton, DropdownMenu } from "@/components/DropdownMenu";
11+
import { ThemeContext } from "@/contexts/theme";
12+
13+
14+
const ThemeIcon = ({ theme }) => {
15+
switch (theme) {
16+
case "light":
17+
return <LightModeIcon />;
18+
case "dark":
19+
return <DarkModeIcon />;
20+
default:
21+
return <BrightnessMediumIcon />;
22+
}
23+
};
24+
ThemeIcon.propTypes = {
25+
theme: PropTypes.string,
26+
};
27+
28+
29+
const ThemeSelector = () => {
30+
const { theme, setTheme } = useContext(ThemeContext);
31+
32+
return (
33+
<Dropdown>
34+
<DropdownButton className={styles.themeMenuTitle}>
35+
<ThemeIcon theme={theme} />
36+
<span className={styles.themeMenuText}>Theme</span>
37+
</DropdownButton>
38+
<DropdownMenu className={styles.themeMenuList}>
39+
<li>
40+
<button
41+
className={`${styles.themeMenuItem} ${theme === "system" && styles.selected}`}
42+
onClick={() => setTheme("system")}
43+
aria-label="Set theme to system default"
44+
>
45+
<BrightnessMediumIcon />
46+
<span className={styles.themeMenuText}>OS Default</span>
47+
</button>
48+
</li>
49+
<li>
50+
<button
51+
className={`${styles.themeMenuItem} ${theme === "light" && styles.selected}`}
52+
onClick={() => setTheme("light")}
53+
aria-label="Set theme to light mode"
54+
>
55+
<LightModeIcon />
56+
<span className={styles.themeMenuText}>Light</span>
57+
</button>
58+
</li>
59+
<li>
60+
<button
61+
className={`${styles.themeMenuItem} ${theme === "dark" && styles.selected}`}
62+
onClick={() => setTheme("dark")}
63+
aria-label="Set theme to dark mode"
64+
>
65+
<DarkModeIcon />
66+
<span className={styles.themeMenuText}>Dark</span>
67+
</button>
68+
</li>
69+
</DropdownMenu>
70+
</Dropdown>
71+
);
72+
};
73+
74+
export default ThemeSelector;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.themeMenuTitle {
2+
background-color: transparent;
3+
color: var(--text-primary);
4+
display: flex;
5+
align-items: center;
6+
justify-content: center;
7+
border: none;
8+
}
9+
10+
.themeMenuTitle:hover {
11+
background-color: var(--bg-secondary);
12+
border-radius: 5px;
13+
}
14+
15+
.themeMenuText {
16+
margin: 0.3em;
17+
text-align: start;
18+
font-size: 0.8rem;
19+
}
20+
21+
.themeMenuList {
22+
background-color: var(--bg-primary);
23+
border: 1px solid var(--border-color);
24+
border-radius: 3px;
25+
padding: 0.5rem;
26+
}
27+
28+
.themeMenuItem {
29+
width: 100%;
30+
margin: 2px;
31+
background-color: transparent;
32+
/* must add transparent border or the button will shake on hover */
33+
border: 1px solid transparent;
34+
color: var(--text-primary);
35+
display: flex;
36+
align-items: center;
37+
}
38+
39+
.themeMenuItem:hover {
40+
border: 1px solid var(--border-color);
41+
border-radius: 3px;
42+
}
43+
44+
.themeMenuItem.selected {
45+
border: 1px solid var(--border-color);
46+
border-radius: 3px;
47+
}

web/src/components/ChatboxHeader/index.test.jsx renamed to web/src/components/ThemeSelector/index.test.jsx

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@ import { render, screen, fireEvent } from "@testing-library/react";
22
import { describe, it, expect, vi } from "vitest";
33

44
import { ThemeContext } from "@/contexts/theme";
5-
import { UserContext } from "@/contexts/user";
65

7-
import ChatboxHeader from "./index";
6+
import ThemeSelector from "./index";
87

98
const setup = () => render(
109
<ThemeContext.Provider value={mockThemeContextValue}>
11-
<UserContext.Provider value={mockUserContextValue}>
12-
<ChatboxHeader />
13-
</UserContext.Provider>
10+
<ThemeSelector />
1411
</ThemeContext.Provider>
1512
);
1613

@@ -20,31 +17,13 @@ const mockThemeContextValue = {
2017
setTheme: mockSetTheme,
2118
};
2219

23-
const mockUserContextValue = {
24-
username: "testuser",
25-
avatar: "testavatar.png",
26-
};
2720

2821
describe("ChatboxHeader", () => {
29-
it("renders the username and avatar", () => {
30-
setup();
31-
expect(screen.getByAltText("testuser's avatar")).toBeDefined();
32-
expect(screen.getByText("testuser")).toBeDefined();
33-
});
34-
3522
it("renders the theme menu and allows theme change", () => {
3623
setup();
3724
fireEvent.click(screen.getByText("Theme"));
3825
expect(screen.getByLabelText("Set theme to light mode")).toBeDefined();
3926
fireEvent.click(screen.getByLabelText("Set theme to dark mode"));
4027
expect(mockSetTheme).toHaveBeenCalledWith("dark");
4128
});
42-
43-
it("handles logout", () => {
44-
setup();
45-
delete window.location;
46-
window.location = { href: "" };
47-
fireEvent.click(screen.getByText("Logout"));
48-
expect(window.location.href).toBe("/oauth2/sign_out");
49-
});
5029
});

0 commit comments

Comments
 (0)