Skip to content

Commit da1986d

Browse files
Pikalotevanugarteaspies06
authored
Recreated PR2: Added search UI (replaced PR #1716) (#1748)
* chore: move routes array to its own file * Removed search UI * Re-commit for PR 1 * Added search UI * update: Only 5 search suggestions are shown at once. Scroll to see more. * Debug: remove dessert pages * Fixed bug: user should land homepage after loggin in * Update: Prevent unnecessary API calls — user data is now fetched only when the search input actually changes. * Resolved conflict when merging with latest dev * Resolved incoming conflict when merging with dev * Removed search UI codes and improved permission check logic by implementing Lookup table * Fixed irrelevant permission check * Fixed lookup table to prevent excessive fetching * Improved routes caching in SearchModal using useMemo * 1. Removed scrollable dropdown codes. Just shows top 5 results. 2. Suggestions instantly display hardcoded pages recommendations. 3. Suggestions show user search results after a short delay for authorized users only and if there is any user matched * removed e.key === 'K', only implement api fetch for users list when officer is Officer and Manager * Fix: hide search suggestions when input is empty * Refactor: removed duplicate route filtering and improved keyboard selection logic * Update src/Components/ShortcutKeyModal/SearchModal.js Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * Update src/Components/ShortcutKeyModal/SearchModal.js Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * Committed changes as suggestions * Undo the changes in Home.js * Removed a space in line 1 of Home.js * Spitted PR and moved searching debounce logic to the new PR * Updated PERMISSION_LOOKUP_TABLE in Private Route -> no more functions mapping, cleaner code * Removed the commented out section - old SpeakerPage component * Removed unnecessary pageName prop * Resolved conflicts due to auto correction * Fixed typo: signedInRoutes * Added more arrays to RouteConfig.js * Changed RouteConfig.js to Routes.js and fixed linting errors * Merged with Aidan's routes.js code * Update src/index.js Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * Update src/Components/ShortcutKeyModal/SearchModal.js Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * Used scoped CSS * Moved suggestions rendering to a separate function. Switched to scoped CSS and renamed the class from modal to search-modal. Added a check for an empty suggestions list before handling search. * Rerversed merging with PR2. Waiting for approval * Recreated PR2 * Changed className: search-modal -> shortcut-search-modal * Update src/Components/ShortcutKeyModal/SearchModal.js Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * Changed key to r.path and slice top 5 suggestions before rendering * Changes: close search modal when users click outside of the modal content, clear out search box when modal is closed * Added dark mode, refined UI, and showed default signedOutRoutes suggestions when keyword is empty * Changed background color of suggestion list to be more neutral in Light mode * Rebased with main * Renamed getSuggestions to SuggestionsList and converted all functions to arrow functions * Removed 'Get Started row' and filtered out Edit User Info page * Fix: correct dependencies in useMemo for routes --------- Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> Co-authored-by: Aidan Spies <spiesaidan@gmail.com>
1 parent 3390863 commit da1986d

3 files changed

Lines changed: 268 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.shortcut-search-modal {
2+
position: fixed;
3+
width: 100%;
4+
height: 100%;
5+
background: rgba(0, 0, 0, 0.4);
6+
/* semi-transparent dark overlay */
7+
backdrop-filter: blur(4px);
8+
/* blurred background */
9+
display: flex;
10+
justify-content: center;
11+
align-items: flex-start;
12+
padding-top: 33vh;
13+
z-index: 9999;
14+
}
15+
16+
.shortcut-search-modal .input-wrapper {
17+
padding: 8px;
18+
background: white;
19+
border-radius: 8px;
20+
/* height: auto; */
21+
}
22+
23+
.shortcut-search-modal input {
24+
border: 1.5px solid darkgray;
25+
width: 35rem;
26+
border-radius: 0.25rem;
27+
padding: 0.75rem;
28+
height: 2.5rem;
29+
font-size: 1.2rem;
30+
}
31+
32+
.shortcut-search-modal .active {
33+
background: #3B82F6;
34+
color: white;
35+
border-radius: 0.25rem;
36+
padding: 0.25rem;
37+
}
38+
39+
.shortcut-search-modal .suggestion-list {
40+
background: #f1f5f9;
41+
font-size: 1.2rem;
42+
}
43+
44+
.shortcut-search-modal .suggestion-item {
45+
height: 3.5rem;
46+
display: flex;
47+
align-items: center;
48+
}
49+
50+
.shortcut-search-modal .hidden-tab {
51+
font-size: medium;
52+
color: white;
53+
}
54+
55+
/* Dark mode */
56+
@media (prefers-color-scheme: dark) {
57+
.shortcut-search-modal .active {
58+
background: lightslategray;
59+
}
60+
61+
.shortcut-search-modal .input-wrapper,
62+
.shortcut-search-modal .suggestion-list {
63+
background-color: #1e293b;
64+
}
65+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
2+
import './SearchModal.css';
3+
import { officerOrAdminRoutes, signedOutRoutes, memberRoutes, notAuthenticatedRoutes } from '../../Routes';
4+
import { membershipState } from '../../Enums';
5+
import { useUser } from '../context/UserContext';
6+
7+
export default function SearchModal({ appProps }) {
8+
const [open, setOpen] = useState(false);
9+
const inputRef = useRef(null);
10+
const modalRef = useRef(null);
11+
const [keyword, setKeyword] = useState('');
12+
const [suggestions, setSuggestions] = useState([...signedOutRoutes]);
13+
const [selectItem, setSelectItem] = useState(0);
14+
const { user } = useUser();
15+
const [errorMsg, setErrorMsg] = useState('');
16+
17+
/**
18+
* Returns the appropriate routes array based on the user's access level.
19+
* @dependencies user.accessLevel, appProps.authenticated
20+
*/
21+
const routes = useMemo(() => {
22+
if (user.accessLevel === membershipState.MEMBER)
23+
return [
24+
...memberRoutes.filter(r => r.pageName !== 'Edit User Info'),
25+
...signedOutRoutes
26+
];
27+
if (user.accessLevel >= membershipState.OFFICER)
28+
return [
29+
...officerOrAdminRoutes.filter(r => r.pageName !== 'Edit User Info'),
30+
...signedOutRoutes
31+
];
32+
if (!appProps.authenticated)
33+
return [
34+
...notAuthenticatedRoutes,
35+
...signedOutRoutes
36+
];
37+
return [...signedOutRoutes];
38+
}, [user.accessLevel, appProps.authenticated]);
39+
40+
/**
41+
* Helper function updates the keyword when the user types
42+
* @param e - The input change event
43+
*/
44+
const handleChanges = (e) => {
45+
setKeyword(e.target.value);
46+
setSelectItem(0);
47+
};
48+
49+
/** This helper function clears search box and all suggestions */
50+
const clearSearchModal = () => {
51+
setSuggestions([...signedOutRoutes]);
52+
setKeyword('');
53+
};
54+
55+
const SuggestionsList = () => {
56+
if (suggestions.length === 0) return <></>;
57+
58+
const topFiveItems = suggestions.slice(0, 5);
59+
return (
60+
<ul className='suggestion-list'>
61+
{topFiveItems.map((r, index) => ( // Still keep index to keep track of the selected item
62+
<li
63+
key={r.path} // Use r.path as key
64+
className={`suggestion-item ${index === selectItem ? 'active' : ''}`}
65+
onMouseEnter={() => setSelectItem(index)}
66+
onClick={() => {
67+
window.location.href = r.path;
68+
setOpen(false);
69+
}}
70+
>
71+
<span style={{ marginRight: '0.5rem' }}>
72+
{r.type === 'user' ? '👤' : '📄'}
73+
</span>
74+
<div className='text-wrapper'>
75+
{r.pageName}
76+
<div className='hidden-tab'>
77+
{selectItem === index && `${window.location.origin}${r.path}`}
78+
</div>
79+
</div>
80+
</li>
81+
))}
82+
</ul>
83+
);
84+
};
85+
86+
/**
87+
* An effect that instantly shows all hardcoded routes.
88+
* @dependencies keyword, routes, open
89+
*/
90+
useEffect(() => {
91+
if (!open) return;
92+
93+
// Return if keyword is blank
94+
if (!keyword) {
95+
setSuggestions([...signedOutRoutes]);
96+
return;
97+
}
98+
99+
// Instantly display for the hardcoded page recommendations
100+
const routeMatches = routes.filter((r) =>
101+
r.pageName?.toLowerCase().includes(keyword.toLowerCase())
102+
);
103+
setSuggestions(routeMatches);
104+
}, [open, keyword, routes]);
105+
106+
/**
107+
* Executes a search when Enter is pressed
108+
* @dependencies selectItem, suggestions
109+
*/
110+
const handleSearch = useCallback(() => {
111+
if (suggestions.length === 0) return; // Check if suggestions is empty
112+
113+
const target = suggestions[selectItem];
114+
if (target && target.path) {
115+
window.location.href = target.path;
116+
setOpen(false);
117+
clearSearchModal();
118+
}
119+
}, [suggestions, selectItem]);
120+
121+
/**
122+
* Listens for keyboard input and executes shortcut actions.
123+
* @dependencies open, suggestions, selectItem
124+
*/
125+
useEffect(() => {
126+
const listener = (e) => {
127+
if ((e.ctrlKey || e.metaKey) && (e.key.toLowerCase() === 'k')) {
128+
e.preventDefault();
129+
setOpen(prev => !prev);
130+
if (!open) {
131+
clearSearchModal();
132+
}
133+
} else if (e.key === 'Escape') {
134+
setOpen(false);
135+
clearSearchModal();
136+
} else if (e.key === 'Enter' && open) {
137+
e.preventDefault();
138+
handleSearch();
139+
} else if (e.key === 'ArrowDown') {
140+
e.preventDefault();
141+
if (suggestions.length > 0) {
142+
const minLength = Math.min(suggestions.length - 1, 4);
143+
setSelectItem(prev => Math.min(prev + 1, minLength));
144+
}
145+
} else if (e.key === 'ArrowUp') {
146+
e.preventDefault();
147+
setSelectItem(prev => Math.max(prev - 1, 0));
148+
}
149+
};
150+
151+
window.addEventListener('keydown', listener);
152+
return () => window.removeEventListener('keydown', listener);
153+
}, [open, suggestions, selectItem]);
154+
155+
useEffect(() => {
156+
if (open && inputRef.current) {
157+
inputRef.current.focus();
158+
}
159+
}, [open]);
160+
161+
/**
162+
* Listens for mouse input and closes the search modal when the user clicks outside the modal content.
163+
* @dependencies open
164+
*/
165+
useEffect(() => {
166+
const clickOut = (e) => {
167+
if (modalRef.current && !modalRef.current?.contains(e.target)) {
168+
setOpen(false);
169+
clearSearchModal();
170+
}
171+
};
172+
173+
if (open) {
174+
window.addEventListener('mousedown', clickOut);
175+
}
176+
177+
return () => {
178+
window.removeEventListener('mousedown', clickOut);
179+
};
180+
}, [open]);
181+
182+
if (!open) return null;
183+
184+
return (
185+
<div className='shortcut-search-modal'>
186+
<div ref={modalRef}>
187+
<div className='input-wrapper'>
188+
<input
189+
ref={inputRef}
190+
placeholder="Search here... (Ctrl + k)"
191+
value={keyword}
192+
onChange={handleChanges} />
193+
<SuggestionsList />
194+
</div>
195+
<div>
196+
{errorMsg && <p>{errorMsg}</p>}
197+
</div>
198+
</div>
199+
</div>
200+
);
201+
}

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import './index.css';
66
import Routing from './Routing';
77
import { checkIfUserIsSignedIn } from './APIFunctions/Auth';
88
import { UserContext } from './Components/context/UserContext';
9+
import SearchModal from './Components/ShortcutKeyModal/SearchModal';
910

1011
function App(props) {
1112
const [authenticated, setAuthenticated] = useState(false);
@@ -29,6 +30,7 @@ function App(props) {
2930
!isAuthenticating && (
3031
<UserContext.Provider value={{ user, setUser }}>
3132
<BrowserRouter>
33+
<SearchModal appProps={{ authenticated }} />
3234
<Routing appProps={{ authenticated, setAuthenticated, user }} />
3335
</BrowserRouter>
3436
</UserContext.Provider>

0 commit comments

Comments
 (0)