From 2a9c69ff2539a65f4fff73092624595be9787770 Mon Sep 17 00:00:00 2001 From: Niyathi Kukkapalli Date: Wed, 25 Mar 2026 16:26:57 -0400 Subject: [PATCH 1/2] Added info button, decluttered popover with just two links --- .env.production.example | 3 +- frontend/src/components/Requirements.jsx | 116 +++++++++++++++-------- frontend/src/styles/Requirements.css | 7 ++ frontend/src/utils/manualHoverPopover.js | 5 + 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/.env.production.example b/.env.production.example index d5c80027..7ec83430 100644 --- a/.env.production.example +++ b/.env.production.example @@ -11,7 +11,8 @@ CSRF_TRUSTED_ORIGINS=https://path.tigerapps.org DATABASE_URL= # Local Redis (Docker service in docker-compose.prod.yml) -REDIS_URL=redis://redis:6379/1 +REDIS_URL=redis://localhost:6379/1 +# REDIS_URL=redis://redis:6379/1 REDIS_MAXMEMORY=128mb REDIS_MAXMEMORY_POLICY=allkeys-lru REDIS_MEM_LIMIT=192m diff --git a/frontend/src/components/Requirements.jsx b/frontend/src/components/Requirements.jsx index 0600b8bc..129b505c 100644 --- a/frontend/src/components/Requirements.jsx +++ b/frontend/src/components/Requirements.jsx @@ -5,8 +5,13 @@ import 'react-treeview/react-treeview.css'; import TreeView from 'react-treeview/lib/react-treeview.js'; const REQUIREMENTS_POPOVER_CLEANUP_KEY = '__tigerpathReqPopoverCleanup'; +const HEADER_POPOVER_CLEANUP_KEY = '__tigerpathHeaderPopoverCleanup'; const TREE_ITEM_CLICK_HANDLER_KEY = '__tigerpathTreeItemClickHandler'; +function escapeHref(url) { + return String(url).replace(/&/g, '&').replace(/"/g, '"'); +} + export default function Requirements({ onChange, requirements, schedule }) { const [loading, setLoading] = useState(false); const containerRef = useRef(null); @@ -52,7 +57,9 @@ export default function Requirements({ onChange, requirements, schedule }) { const Popover = window.bootstrap?.Popover; if (!Popover) return; - const reqLabels = containerRef.current.querySelectorAll('.reqLabel'); + const reqLabels = containerRef.current.querySelectorAll( + '.reqLabel:not(.reqLabel-main)' + ); reqLabels.forEach((reqLabel) => { const existingCleanup = reqLabel[REQUIREMENTS_POPOVER_CLEANUP_KEY]; if (typeof existingCleanup === 'function') { @@ -100,6 +107,48 @@ export default function Requirements({ onChange, requirements, schedule }) { }); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Main-req info icon: manual trigger + shared hover bridge so the popover stays + // open while moving the pointer onto links (Bootstrap hover trigger closes in the gap). + const addHeaderPopovers = useCallback(() => { + if (!containerRef.current) return; + const Popover = window.bootstrap?.Popover; + if (!Popover) return; + + const icons = containerRef.current.querySelectorAll('.info-icon'); + + icons.forEach((icon) => { + const existingCleanup = icon[HEADER_POPOVER_CLEANUP_KEY]; + if (typeof existingCleanup === 'function') { + existingCleanup(); + } + + const existing = Popover.getInstance(icon); + if (existing) existing.dispose(); + + const popoverInstance = new Popover(icon, { + trigger: 'manual', + html: true, + animation: true, + placement: 'left', + fallbackPlacements: ['left', 'top', 'bottom'], + boundary: 'viewport', + template: + '', + sanitize: false, + }); + + const cleanupHover = bindManualHoverPopover(icon, popoverInstance, { + hideDelayMs: 400, + }); + + icon[HEADER_POPOVER_CLEANUP_KEY] = () => { + cleanupHover(); + popoverInstance.dispose(); + delete icon[HEADER_POPOVER_CLEANUP_KEY]; + }; + }); + }, []); + const getReqCourses = (req_path) => { const searchQueryLabel = 'Satisfying: ' + req_path.split('//').pop(); onChange('searchQuery', searchQueryLabel); @@ -118,9 +167,10 @@ export default function Requirements({ onChange, requirements, schedule }) { requestAnimationFrame(() => { makeNodesClickable(); addReqPopovers(); + addHeaderPopovers(); }); } - }, [requirements, makeNodesClickable, addReqPopovers]); + }, [requirements, makeNodesClickable, addReqPopovers, addHeaderPopovers]); const toggleSettle = (course, pathTo, settle) => { let pathToType = pathTo.split('//', 3).join('//'); @@ -257,39 +307,27 @@ export default function Requirements({ onChange, requirements, schedule }) { ) { finished = 'req-done'; } - popoverContent = '
'; - if (mainReq.explanation) { - popoverContent += - '

' + mainReq.explanation.split('\n').join('
') + '

'; - } else if (mainReq.description) { - popoverContent += - '

' + mainReq.description.split('\n').join('
') + '

'; - } - if (mainReq.contacts) { - popoverContent += '
Contacts:
'; - mainReq.contacts.forEach((contact) => { + const urls = Array.isArray(mainReq.urls) + ? mainReq.urls.filter(Boolean) + : []; + popoverContent = '
'; + if (urls.length > 0) { + const show = urls.slice(0, 2); + const linkLabels = + show.length === 1 + ? ['Department page'] + : ['Department page', 'More information']; + show.forEach((url, i) => { popoverContent += - '

' + - contact.type + - ':
' + - contact.name + - '
' + - contact.email + - '

'; - }); - } - if (mainReq.urls) { - popoverContent += '
Reference Links:
'; - mainReq.urls.forEach((url) => { - popoverContent += - '

' + - url + + linkLabels[i] + '

'; }); + } else { + popoverContent += + '

No official program link is listed for this requirement.

'; } popoverContent += '
'; } else { @@ -317,14 +355,18 @@ export default function Requirements({ onChange, requirements, schedule }) { } let mainReqLabel = ( -
' + name + ''} - data-bs-content={popoverContent} - > - {name} +
+ {name} +
); + return ( { clearHideTimeout(); triggerElement.removeEventListener('mouseenter', onTriggerMouseEnter); triggerElement.removeEventListener('mouseleave', onTriggerMouseLeave); + triggerElement.removeEventListener('shown.bs.popover', onPopoverShown); triggerElement.removeEventListener('hidden.bs.popover', onPopoverHidden); detachPopoverListeners(); }; From 79cf9147e8ab92117e3bc02cd63d49c2c62be086 Mon Sep 17 00:00:00 2001 From: Niyathi Kukkapalli Date: Wed, 25 Mar 2026 16:44:34 -0400 Subject: [PATCH 2/2] Update .env.production.example --- .env.production.example | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.production.example b/.env.production.example index 7ec83430..d5c80027 100644 --- a/.env.production.example +++ b/.env.production.example @@ -11,8 +11,7 @@ CSRF_TRUSTED_ORIGINS=https://path.tigerapps.org DATABASE_URL= # Local Redis (Docker service in docker-compose.prod.yml) -REDIS_URL=redis://localhost:6379/1 -# REDIS_URL=redis://redis:6379/1 +REDIS_URL=redis://redis:6379/1 REDIS_MAXMEMORY=128mb REDIS_MAXMEMORY_POLICY=allkeys-lru REDIS_MEM_LIMIT=192m