Skip to content

Commit 8282abd

Browse files
committed
background-color
1 parent 868ce41 commit 8282abd

File tree

7 files changed

+154
-15
lines changed

7 files changed

+154
-15
lines changed

api/main_endpoints/models/User.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ const UserSchema = new Schema(
7373
apiKey: {
7474
type: String,
7575
default: ''
76+
},
77+
backgroundColor: {
78+
type: String,
7679
}
7780
},
7881
{ collection: 'User' }

src/APIFunctions/Profile.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,37 @@ export function formatFirstAndLastName(user) {
1616
user.lastName.slice(1, user.lastName.length)
1717
);
1818
}
19+
20+
/**
21+
* Gets the appropriate icon text color for the
22+
* inputed background color
23+
* @param {String} color - The 6 character hex color code starting with #
24+
* @returns {String} The hex color of the icon text color
25+
*/
26+
export function getIconTextColor(color) {
27+
if(typeof color !== 'string') {
28+
throw new TypeError('color must be a string');
29+
}
30+
if(color == '' || color == '#2a323c') {
31+
return '#FFFFFF';
32+
}
33+
// get rgb values 0-255
34+
const r = parseInt(color.substring(1, 3), 16);
35+
const g = parseInt(color.substring(3, 5), 16);
36+
const b = parseInt(color.substring(5, 7), 16);
37+
// linearize the colors
38+
const colors = [r / 255.0, g / 255.0, b / 255.0];
39+
const c = colors.map((color) => {
40+
if(color <= 0.04045) {
41+
return color / 12.92;
42+
}
43+
return Math.pow(((color + 0.055) / 1.055), 2.4);
44+
});
45+
// luminance value
46+
const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
47+
// threshold of 0.179
48+
if(L > 0.179) {
49+
return '#000000';
50+
}
51+
return '#FFFFFF';
52+
}

src/APIFunctions/User.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export async function getAllUsers({
7878
* was verified
7979
* @param {(string|undefined)} userToEdit.emailOptIn - Opt into SCE's blast
8080
* week emails
81+
* @param {(string|undefined)} userToEdit.backgroundColor - The user's background
82+
* color for profile icon
8183
* @param {string} token - The jwt token for authentication
8284
* @returns {UserApiResponse} containing if the search was successful
8385
*/
@@ -100,6 +102,7 @@ export async function editUser(userToEdit, token) {
100102
lastLogin,
101103
emailVerified,
102104
emailOptIn,
105+
backgroundColor
103106
} = userToEdit;
104107
const url = new URL('/api/User/edit', BASE_API_URL);
105108
try {
@@ -125,7 +128,8 @@ export async function editUser(userToEdit, token) {
125128
accessLevel,
126129
lastLogin,
127130
emailVerified,
128-
emailOptIn
131+
emailOptIn,
132+
backgroundColor
129133
})
130134
});
131135
if (res.ok) {

src/Components/Navbar/UserNavbar.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,43 @@
11
import React, { useState, useEffect, useRef } from 'react';
22
import { membershipState } from '../../Enums';
33
import { useSCE } from '../context/SceContext';
4+
import { getUserById } from '../../APIFunctions/User';
5+
import { useBackgroundColor } from '../context/BackgroundColorContext';
6+
import { getIconTextColor } from '../../APIFunctions/Profile';
47

58
export default function UserNavbar(props) {
69
const { user, authenticated } = useSCE();
710
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
811
const dropdownRef = useRef(null);
12+
const { backgroundColorVersion } = useBackgroundColor() || {};
13+
const [backgroundColor, setBackgroundColor] = useState('#2a323c');
14+
const [transition, setTransition] = useState(false);
15+
16+
useEffect(() => {
17+
let timeoutId;
18+
async function getBackgroundColor() {
19+
const response = await getUserById(user._id, user.token);
20+
if(response.responseData && response.responseData.backgroundColor) {
21+
setBackgroundColor(response.responseData.backgroundColor);
22+
if(transition == false) {
23+
timeoutId = setTimeout(() => {
24+
setTransition(true);
25+
}, 600);
26+
}
27+
} else {
28+
setTransition(true);
29+
}
30+
}
31+
if(authenticated && user && user.token && user._id) {
32+
getBackgroundColor();
33+
}
34+
return () => {
35+
if(timeoutId) {
36+
clearTimeout(timeoutId);
37+
}
38+
};
39+
}, [authenticated, user, backgroundColorVersion]);
40+
941
let initials = '';
1042
if (user && user.firstName && user.lastName) {
1143
initials = user.firstName[0] + user.lastName[0];
@@ -98,16 +130,15 @@ export default function UserNavbar(props) {
98130
{getRoutesForNavbar()}
99131
</ul>
100132
</div>
101-
102133
<div className='relative inline-block dropdown-menu-wrapper' ref={dropdownRef}>
103134
<summary
104135
tabIndex={0}
105136
role="button"
106137
className="btn btn-ghost btn-circle avatar placeholder"
107138
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
108139
>
109-
<div className="w-12 rounded-full bg-neutral text-neutral-content">
110-
<span>{initials}</span>
140+
<div className={`w-12 rounded-full bg-neutral text-neutral-content ${transition ? ' transition-colors ease-in duration-500' : ''}`} style={{backgroundColor: backgroundColor}}>
141+
<span className={`${transition ? ' transition-colors ease-in duration-500' : ''}`} style={{color: getIconTextColor(backgroundColor)}}>{initials}</span>
111142
</div>
112143
</summary>
113144
{isDropdownOpen && (
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { createContext, useContext, useState } from 'react';
2+
3+
export const BackgroundColorContext = createContext(null);
4+
5+
export function useBackgroundColor() {
6+
return useContext(BackgroundColorContext);
7+
}
8+
9+
export default function BackgroundColorContextProvider({children}) {
10+
const [backgroundColorVersion, setBackgroundColorVersion] = useState(0);
11+
return (
12+
<BackgroundColorContext.Provider value={{ backgroundColorVersion, setBackgroundColorVersion }}>
13+
{children}
14+
</BackgroundColorContext.Provider>
15+
);
16+
}
17+

src/Pages/Profile/MemberView/Profile.js

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,54 @@
11
import React, { useEffect, useState } from 'react';
2-
import { getUserById } from '../../../APIFunctions/User';
2+
import { getUserById, editUser } from '../../../APIFunctions/User';
33
import ChangePasswordModal from './ChangePassword';
44
import DeleteAccountModal from './DeleteAccountModal';
55
import GetApiKeyModal from './GetApiKeyModal';
66
import { membershipState, membershipStateToString } from '../../../Enums';
77
import { useSCE } from '../../../Components/context/SceContext';
8+
import { useBackgroundColor } from '../../../Components/context/BackgroundColorContext';
89

910
export default function Profile() {
11+
const defaultColor = '#2a323c';
1012
const { user } = useSCE();
13+
const { setBackgroundColorVersion } = useBackgroundColor() || {};
1114
const [response, setResponse] = useState({});
1215
const [bannerMessage, setBannerMessage] = useState('');
1316
const [bannerColor, setBannerColor] = useState('');
17+
const [backgroundColor, setBackgroundColor] = useState(defaultColor);
18+
const [savedBackgroundColor, setSavedBackgroundColor] = useState(defaultColor);
19+
const [saved, setSaved] = useState(false);
1420

15-
async function getUserFromApi() {
16-
const response = await getUserById(user._id, user.token);
17-
if (response.responseData) {
18-
setResponse(response.responseData);
21+
useEffect(() => {
22+
async function getUserFromApi() {
23+
const response = await getUserById(user._id, user.token);
24+
const responseData = response.responseData;
25+
if (responseData) {
26+
setResponse(responseData);
27+
const bgColor = responseData.backgroundColor;
28+
if(bgColor) {
29+
setBackgroundColor(bgColor);
30+
setSavedBackgroundColor(bgColor);
31+
}
32+
}
1933
}
20-
}
34+
getUserFromApi();
35+
}, []);
2136

22-
useEffect(getUserFromApi, []);
37+
async function updateBackgroundColor() {
38+
if(saved) {
39+
return;
40+
}
41+
if(backgroundColor == savedBackgroundColor) {
42+
setSaved(true);
43+
return;
44+
}
45+
const response = await editUser({_id: user._id, backgroundColor}, user.token);
46+
if(!response.error) {
47+
setSavedBackgroundColor(backgroundColor);
48+
setSaved(true);
49+
setBackgroundColorVersion(v => v + 1);
50+
}
51+
}
2352

2453
function renderExpirationDate() {
2554
if (response.accessLevel >= membershipState.OFFICER) {
@@ -79,6 +108,24 @@ export default function Profile() {
79108
</button>
80109
</div>
81110
</div>
111+
<div className="pb-6 px-4">
112+
<div className="font-semibold text-gray-900">Profile Icon Color</div>
113+
<div className="py-2 flex gap-2">
114+
<input className="cursor-pointer h-8 w-[60px] rounded-lg" type='color' value={backgroundColor} style={{backgroundColor: backgroundColor}} onChange={(e) => {
115+
setBackgroundColor(e.target.value);
116+
if(e.target.value != savedBackgroundColor) {
117+
setSaved(false);
118+
}
119+
}}/>
120+
<button className="btn btn-sm btn-primary w-[60px]" onClick={updateBackgroundColor}>{saved ? 'Saved!' : 'Save'}</button>
121+
</div>
122+
<button className="btn btn-sm btn-error w-32" onClick={() => {
123+
setBackgroundColor(defaultColor);
124+
if(savedBackgroundColor != defaultColor) {
125+
setSaved(false);
126+
}
127+
}}>Reset</button>
128+
</div>
82129
<div className="text-gray-700">
83130
<div className="grid text-sm">
84131
<div className="grid grid-cols-2">

src/index.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Routing from './Routing';
77
import { checkIfUserIsSignedIn } from './APIFunctions/Auth';
88
import { SceContext } from './Components/context/SceContext';
99
import SearchModal from './Components/ShortcutKeyModal/SearchModal';
10+
import BackgroundColorContextProvider from './Components/context/BackgroundColorContext';
1011

1112
function App() {
1213
const [authenticated, setAuthenticated] = useState(false);
@@ -31,10 +32,12 @@ function App() {
3132
return (
3233
!isAuthenticating && (
3334
<SceContext.Provider value={{user, setUser, authenticated, setAuthenticated}}>
34-
<BrowserRouter>
35-
<SearchModal/>
36-
<Routing/>
37-
</BrowserRouter>
35+
<BackgroundColorContextProvider>
36+
<BrowserRouter>
37+
<SearchModal/>
38+
<Routing/>
39+
</BrowserRouter>
40+
</BackgroundColorContextProvider>
3841
</SceContext.Provider>
3942
)
4043
);

0 commit comments

Comments
 (0)