Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
96f61c5
chore: move routes array to its own file
Pikalot Jun 19, 2025
939de66
Removed search UI
Pikalot Jun 19, 2025
daf689f
Re-commit for PR 1
Pikalot Jun 19, 2025
3340534
Added search UI
Pikalot Jun 19, 2025
4d24d1a
update: Only 5 search suggestions are shown at once. Scroll to see more.
Pikalot Jun 21, 2025
fc3a654
Debug: remove dessert pages
Pikalot Jun 18, 2025
973cb78
Fixed bug: user should land homepage after loggin in
Pikalot Jun 18, 2025
370fa22
Update: Prevent unnecessary API calls — user data is now fetched only…
Pikalot Jun 18, 2025
490fa2e
Resolved conflict when merging with latest dev
Pikalot Jun 21, 2025
7bea3ad
Resolved incoming conflict when merging with dev
Pikalot Jun 21, 2025
6f69efa
Removed search UI codes and improved permission check logic by implem…
Pikalot Jun 26, 2025
5e813d2
Fixed irrelevant permission check
Pikalot Jun 26, 2025
32602cc
Fixed lookup table to prevent excessive fetching
Pikalot Jun 26, 2025
648c732
Improved routes caching in SearchModal using useMemo
Pikalot Jun 27, 2025
ccf81fb
1. Removed scrollable dropdown codes. Just shows top 5 results.
Pikalot Jun 29, 2025
e98ed48
removed e.key === 'K', only implement api fetch for users list when o…
Pikalot Jun 30, 2025
bbe539b
Fix: hide search suggestions when input is empty
Pikalot Jul 1, 2025
ffc55f1
Refactor: removed duplicate route filtering and improved keyboard sel…
Pikalot Jul 1, 2025
61a9f97
Update src/Components/ShortcutKeyModal/SearchModal.js
Pikalot Jul 1, 2025
70b5ce9
Update src/Components/ShortcutKeyModal/SearchModal.js
Pikalot Jul 1, 2025
dff6507
Committed changes as suggestions
Pikalot Jul 1, 2025
12e329d
Undo the changes in Home.js
Pikalot Jul 1, 2025
c5bab98
Removed a space in line 1 of Home.js
Pikalot Jul 1, 2025
1375e77
Spitted PR and moved searching debounce logic to the new PR
Pikalot Jul 1, 2025
2e74de1
Updated PERMISSION_LOOKUP_TABLE in Private Route -> no more functions…
Pikalot Jul 2, 2025
19450fd
Removed the commented out section - old SpeakerPage component
Pikalot Jul 2, 2025
e41c591
Removed unnecessary pageName prop
Pikalot Jul 3, 2025
ae2e1cc
Resolved conflicts due to auto correction
Pikalot Jul 3, 2025
0c81b4c
Fixed typo: signedInRoutes
Pikalot Jul 3, 2025
f1b8424
Added more arrays to RouteConfig.js
aspies06 Jul 3, 2025
d72b079
Changed RouteConfig.js to Routes.js and fixed linting errors
aspies06 Jul 3, 2025
e4f59e0
Merged with Aidan's routes.js code
Pikalot Jul 3, 2025
09bcde2
Update src/index.js
Pikalot Jul 3, 2025
4489ed4
Update src/Components/ShortcutKeyModal/SearchModal.js
Pikalot Jul 3, 2025
98eaa7c
Used scoped CSS
Pikalot Jul 3, 2025
6ee5a1f
Moved suggestions rendering to a separate function. Switched to scope…
Pikalot Jul 3, 2025
4133c24
Rerversed merging with PR2. Waiting for approval
Pikalot Jul 3, 2025
f458718
Recreated PR2
Pikalot Jul 3, 2025
ad8143f
Changed className: search-modal -> shortcut-search-modal
Pikalot Jul 4, 2025
b6e7728
Update src/Components/ShortcutKeyModal/SearchModal.js
Pikalot Jul 4, 2025
63fb60a
Changed key to r.path and slice top 5 suggestions before rendering
Pikalot Jul 4, 2025
89ee13a
Changes: close search modal when users click outside of the modal con…
Pikalot Jul 4, 2025
547f395
Added dark mode, refined UI, and showed default signedOutRoutes sugge…
Pikalot Jul 4, 2025
48a00f4
Changed background color of suggestion list to be more neutral in Lig…
Pikalot Jul 4, 2025
55ce1a0
Rebased with main
Pikalot Jul 5, 2025
dce4224
Renamed getSuggestions to SuggestionsList and converted all functions…
Pikalot Jul 5, 2025
d34ebab
Removed 'Get Started row' and filtered out Edit User Info page
Pikalot Jul 6, 2025
5a0168c
Fix: correct dependencies in useMemo for routes
Pikalot Jul 6, 2025
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
65 changes: 65 additions & 0 deletions src/Components/ShortcutKeyModal/SearchModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.shortcut-search-modal {
position: fixed;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
/* semi-transparent dark overlay */
backdrop-filter: blur(4px);
/* blurred background */
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 33vh;
z-index: 9999;
}

.shortcut-search-modal .input-wrapper {
padding: 8px;
background: white;
border-radius: 8px;
/* height: auto; */
}

.shortcut-search-modal input {
border: 1.5px solid darkgray;
width: 35rem;
border-radius: 0.25rem;
padding: 0.75rem;
height: 2.5rem;
font-size: 1.2rem;
}

.shortcut-search-modal .active {
background: #3B82F6;
color: white;
border-radius: 0.25rem;
padding: 0.25rem;
}

.shortcut-search-modal .suggestion-list {
background: #f1f5f9;
font-size: 1.2rem;
}

.shortcut-search-modal .suggestion-item {
height: 3.5rem;
display: flex;
align-items: center;
}

.shortcut-search-modal .hidden-tab {
font-size: medium;
color: white;
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
.shortcut-search-modal .active {
background: lightslategray;
}

.shortcut-search-modal .input-wrapper,
.shortcut-search-modal .suggestion-list {
background-color: #1e293b;
}
}
201 changes: 201 additions & 0 deletions src/Components/ShortcutKeyModal/SearchModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
import './SearchModal.css';
import { officerOrAdminRoutes, signedOutRoutes, memberRoutes, notAuthenticatedRoutes } from '../../Routes';
import { membershipState } from '../../Enums';
import { useUser } from '../context/UserContext';

export default function SearchModal({ appProps }) {
const [open, setOpen] = useState(false);
const inputRef = useRef(null);
const modalRef = useRef(null);
const [keyword, setKeyword] = useState('');
const [suggestions, setSuggestions] = useState([...signedOutRoutes]);
const [selectItem, setSelectItem] = useState(0);
const { user } = useUser();
const [errorMsg, setErrorMsg] = useState('');

/**
* Returns the appropriate routes array based on the user's access level.
* @dependencies user.accessLevel, appProps.authenticated
*/
const routes = useMemo(() => {
if (user.accessLevel === membershipState.MEMBER)
return [
...memberRoutes.filter(r => r.pageName !== 'Edit User Info'),
...signedOutRoutes
];
if (user.accessLevel >= membershipState.OFFICER)
return [
...officerOrAdminRoutes.filter(r => r.pageName !== 'Edit User Info'),
...signedOutRoutes
];
if (!appProps.authenticated)
return [
...notAuthenticatedRoutes,
...signedOutRoutes
];
return [...signedOutRoutes];
}, [user.accessLevel, appProps.authenticated]);

/**
* Helper function updates the keyword when the user types
* @param e - The input change event
*/
const handleChanges = (e) => {
setKeyword(e.target.value);
setSelectItem(0);
};

/** This helper function clears search box and all suggestions */
const clearSearchModal = () => {
setSuggestions([...signedOutRoutes]);
setKeyword('');
};

const SuggestionsList = () => {
if (suggestions.length === 0) return <></>;

const topFiveItems = suggestions.slice(0, 5);
return (
<ul className='suggestion-list'>
{topFiveItems.map((r, index) => ( // Still keep index to keep track of the selected item
<li
key={r.path} // Use r.path as key
className={`suggestion-item ${index === selectItem ? 'active' : ''}`}
onMouseEnter={() => setSelectItem(index)}
onClick={() => {
window.location.href = r.path;
setOpen(false);
}}
>
<span style={{ marginRight: '0.5rem' }}>
{r.type === 'user' ? '👤' : '📄'}
</span>
<div className='text-wrapper'>
{r.pageName}
<div className='hidden-tab'>
{selectItem === index && `${window.location.origin}${r.path}`}
</div>
</div>
</li>
))}
</ul>
);
};

/**
* An effect that instantly shows all hardcoded routes.
* @dependencies keyword, routes, open
*/
useEffect(() => {
if (!open) return;

// Return if keyword is blank
if (!keyword) {
setSuggestions([...signedOutRoutes]);
return;
}

// Instantly display for the hardcoded page recommendations
const routeMatches = routes.filter((r) =>
r.pageName?.toLowerCase().includes(keyword.toLowerCase())
);
setSuggestions(routeMatches);
}, [open, keyword, routes]);

/**
* Executes a search when Enter is pressed
* @dependencies selectItem, suggestions
*/
const handleSearch = useCallback(() => {
if (suggestions.length === 0) return; // Check if suggestions is empty

const target = suggestions[selectItem];
if (target && target.path) {
window.location.href = target.path;
setOpen(false);
clearSearchModal();
}
}, [suggestions, selectItem]);

/**
* Listens for keyboard input and executes shortcut actions.
* @dependencies open, suggestions, selectItem
*/
useEffect(() => {
const listener = (e) => {
if ((e.ctrlKey || e.metaKey) && (e.key.toLowerCase() === 'k')) {
e.preventDefault();
setOpen(prev => !prev);
if (!open) {
clearSearchModal();
}
} else if (e.key === 'Escape') {
setOpen(false);
clearSearchModal();
} else if (e.key === 'Enter' && open) {
e.preventDefault();
handleSearch();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (suggestions.length > 0) {
const minLength = Math.min(suggestions.length - 1, 4);
setSelectItem(prev => Math.min(prev + 1, minLength));
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectItem(prev => Math.max(prev - 1, 0));
}
};

window.addEventListener('keydown', listener);
return () => window.removeEventListener('keydown', listener);
}, [open, suggestions, selectItem]);

useEffect(() => {
if (open && inputRef.current) {
inputRef.current.focus();
}
}, [open]);

/**
* Listens for mouse input and closes the search modal when the user clicks outside the modal content.
* @dependencies open
*/
useEffect(() => {
const clickOut = (e) => {
if (modalRef.current && !modalRef.current?.contains(e.target)) {
setOpen(false);
clearSearchModal();
}
};

if (open) {
window.addEventListener('mousedown', clickOut);
}

return () => {
window.removeEventListener('mousedown', clickOut);
};
}, [open]);

if (!open) return null;

return (
<div className='shortcut-search-modal'>
<div ref={modalRef}>
<div className='input-wrapper'>
<input
ref={inputRef}
placeholder="Search here... (Ctrl + k)"
value={keyword}
onChange={handleChanges} />
<SuggestionsList />
</div>
<div>
{errorMsg && <p>{errorMsg}</p>}
</div>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import './index.css';
import Routing from './Routing';
import { checkIfUserIsSignedIn } from './APIFunctions/Auth';
import { UserContext } from './Components/context/UserContext';
import SearchModal from './Components/ShortcutKeyModal/SearchModal';

function App(props) {
const [authenticated, setAuthenticated] = useState(false);
Expand All @@ -29,6 +30,7 @@ function App(props) {
!isAuthenticating && (
<UserContext.Provider value={{ user, setUser }}>
<BrowserRouter>
<SearchModal appProps={{ authenticated }} />
<Routing appProps={{ authenticated, setAuthenticated, user }} />
</BrowserRouter>
</UserContext.Provider>
Expand Down