diff --git a/frontend/src/components/Alumni/Alumni.module.css b/frontend/src/components/Alumni/Alumni.module.css index 843db8c12..e090f0d8c 100644 --- a/frontend/src/components/Alumni/Alumni.module.css +++ b/frontend/src/components/Alumni/Alumni.module.css @@ -92,6 +92,10 @@ align-items: flex-start; } +.filterToggleWrapper { + display: none; +} + .filtersSidebar { width: 225px; display: flex; @@ -234,18 +238,12 @@ } .infoGrid { - display: flex; - gap: 44px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; flex-wrap: wrap; } -.infoColumn { - display: flex; - flex-direction: column; - gap: 10px; - min-width: 83px; -} - .infoItem { display: flex; flex-direction: column; @@ -262,6 +260,8 @@ font-size: 12px; font-weight: 500; color: #000; + word-break: break-word; + overflow-wrap: break-word; } .socialIcons { @@ -284,3 +284,112 @@ .socialLink:hover { color: #4183c4; } + +.filterOverlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 2000; + display: flex; + align-items: flex-end; +} + +.filterModal { + background: var(--bg-default); + width: 100%; + border-radius: 12px 12px 0 0; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +.filterModalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid var(--border-default); +} + +.filterModalBody { + padding: 16px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 20px; +} + +.filterModalFooter { + padding: 16px; + border-top: 1px solid var(--border-default); + display: flex; +} + +.filterModalFooter > * { + flex: 1; +} + +@media (max-width: 768px) { + .filterToggleWrapper { + display: block; + } + + .filtersSidebar { + display: none; + } + + .topBarSpacer { + display: none; + } + + .searchContainer { + max-width: none; + } + + .searchInput { + flex: 1; + min-width: 0; + width: auto; + } + + .searchInput input { + min-width: 0 !important; + width: 100% !important; + } + .alumniCard { + flex-direction: column; + align-items: left; + } + + .profileImage { + width: 80px !important; + height: 80px !important; + } + + .cardContent { + width: 100%; + } + + .infoGrid { + display: grid; + grid-template-columns: 1 1; + gap: 10px; + } + + .socialIcons { + align-self: flex-end; + } +} + +@media (max-width: 440px) { + .infoGrid { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + } + .infoItem { + display: flex; + flex-direction: row; + gap: 2px; + } +} diff --git a/frontend/src/components/Alumni/Alumni.tsx b/frontend/src/components/Alumni/Alumni.tsx index 23ba3fca4..aa876768f 100644 --- a/frontend/src/components/Alumni/Alumni.tsx +++ b/frontend/src/components/Alumni/Alumni.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; +import { createPortal } from 'react-dom'; import dynamic from 'next/dynamic'; import { Container, Header, Input, Dropdown, Icon, Loader } from 'semantic-ui-react'; import { Emitters } from '../../utils'; @@ -6,6 +7,7 @@ import AlumniAPI from '../../API/AlumniAPI'; import CityCoordinatesAPI from '../../API/CityCoordinatesAPI'; import AlumniCard from './AlumniCard'; import styles from './Alumni.module.css'; +import Button from '../Common/Button/Button'; const AlumniMap = dynamic(() => import('./AlumniMap'), { ssr: false }); @@ -30,6 +32,38 @@ const Alumni: React.FC = () => { DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR ]); + const [filtersOpen, setFiltersOpen] = useState(false); + const savedScrollY = useRef(0); + const modalBodyRef = useRef(null); + + useEffect(() => { + if (!filtersOpen) return; + + savedScrollY.current = window.scrollY; + document.body.classList.add('modal-open'); + document.body.style.top = `-${savedScrollY.current}px`; + + const isInsideModal = (target: EventTarget | null) => + modalBodyRef.current?.contains(target as Node) ?? false; + + const lockTouch = (e: TouchEvent) => { + if (!isInsideModal(e.target)) e.preventDefault(); + }; + const lockWheel = (e: WheelEvent) => { + if (!isInsideModal(e.target)) e.preventDefault(); + }; + + document.addEventListener('touchmove', lockTouch, { passive: false, capture: true }); + document.addEventListener('wheel', lockWheel, { passive: false, capture: true }); + + return () => { + document.body.classList.remove('modal-open'); + document.body.style.top = ''; + window.scrollTo(0, savedScrollY.current); + document.removeEventListener('touchmove', lockTouch, { capture: true }); + document.removeEventListener('wheel', lockWheel, { capture: true }); + }; + }, [filtersOpen]); useEffect(() => { setIsLoading(true); @@ -154,6 +188,84 @@ const Alumni: React.FC = () => { })); }, [alumni]); + const filterContent = ( + <> +
+
+ Applied Filters +
+
+ +
+ setSelectedJobCategories(data.value as string[])} + /> +
+ +
+ setSelectedCompanies(data.value as string[])} + /> +
+ +
+ setSelectedDtiRoles(data.value as string[])} + /> +
+ +
+
+ Graduated in +
+
+ From + + setGradYearRange([parseInt(e.target.value, 10) || DEFAULT_MIN_YEAR, gradYearRange[1]]) + } + className={styles.yearInput} + /> + To + + setGradYearRange([gradYearRange[0], parseInt(e.target.value, 10) || DEFAULT_MAX_YEAR]) + } + className={styles.yearInput} + /> +
+
+ + ); + return (
@@ -186,91 +298,37 @@ const Alumni: React.FC = () => { +
+
-
-
-
- Applied Filters -
-
- -
- setSelectedJobCategories(data.value as string[])} - /> -
- -
- setSelectedCompanies(data.value as string[])} - /> -
+ {/* Desktop sidebar */} +
{filterContent}
-
- setSelectedDtiRoles(data.value as string[])} - /> -
- -
-
- Graduated in -
-
- From - - setGradYearRange([ - parseInt(e.target.value, 10) || DEFAULT_MIN_YEAR, - gradYearRange[1] - ]) - } - className={styles.yearInput} - /> - To - - setGradYearRange([ - gradYearRange[0], - parseInt(e.target.value, 10) || DEFAULT_MAX_YEAR - ]) - } - className={styles.yearInput} - /> -
-
-
+ {/* Mobile filter modal */} + {filtersOpen && + createPortal( +
setFiltersOpen(false)}> +
e.stopPropagation()}> +
+
+
+ {filterContent} +
+
+
+
+
, + document.body + )}
{isLoading && } diff --git a/frontend/src/components/Alumni/AlumniCard.tsx b/frontend/src/components/Alumni/AlumniCard.tsx index 23e26e07f..3b3fd1044 100644 --- a/frontend/src/components/Alumni/AlumniCard.tsx +++ b/frontend/src/components/Alumni/AlumniCard.tsx @@ -44,41 +44,35 @@ const AlumniCard: React.FC = ({ alum }) => {
-
-
- Company - {alum.company || 'N/A'} -
-
- Company Role - {alum.jobRole} -
+
+ Company: + {alum.company || 'N/A'} +
+
+ Company Role: + {alum.jobRole}
-
-
- Based in - {alum.location || 'N/A'} -
-
- Graduated in - {alum.gradYear || 'N/A'} -
+
+ Based in: + {alum.location || 'N/A'} +
+
+ Graduated in: + {alum.gradYear || 'N/A'}
-
-
- Role on DTI - - {alum.dtiRoles && alum.dtiRoles.length > 0 ? alum.dtiRoles.join(', ') : 'N/A'} - -
-
- Subteam on DTI - - {alum.subteams && alum.subteams.length > 0 ? alum.subteams.join(', ') : 'N/A'} - -
+
+ Role on DTI: + + {alum.dtiRoles && alum.dtiRoles.length > 0 ? alum.dtiRoles.join(', ') : 'N/A'} + +
+
+ Subteam on DTI: + + {alum.subteams && alum.subteams.length > 0 ? alum.subteams.join(', ') : 'N/A'} +
diff --git a/frontend/src/pages/index.css b/frontend/src/pages/index.css index 28eaedec9..0647497fc 100644 --- a/frontend/src/pages/index.css +++ b/frontend/src/pages/index.css @@ -19,6 +19,14 @@ body { -moz-osx-font-smoothing: grayscale; } +@media (max-width: 768px) { + body.modal-open { + overflow: hidden; + position: fixed; + width: 100%; + } +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/new-dti-website-redesign/src/components/Navbar.tsx b/new-dti-website-redesign/src/components/Navbar.tsx index eb4610f0c..235bd8aef 100644 --- a/new-dti-website-redesign/src/components/Navbar.tsx +++ b/new-dti-website-redesign/src/components/Navbar.tsx @@ -164,6 +164,62 @@ export default function Navbar({ demo }: NavbarProps) { // MOBILE ANIMATION LOGIC: // ####################### + const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); + const logoRef = useRef(null); + + const handleLogoContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + const menuWidth = 180; + const menuHeight = 80; + setContextMenu({ + x: Math.min(e.clientX, window.innerWidth - menuWidth), + y: Math.min(e.clientY, window.innerHeight - menuHeight) + }); + }; + + const handleDownload = async (format: 'svg' | 'png') => { + setContextMenu(null); + + if (format === 'svg') { + const link = document.createElement('a'); + link.href = '/wordmark.svg'; + link.download = 'dti-logo.svg'; + link.click(); + } else { + const img = new window.Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.naturalWidth * 2; + canvas.height = img.naturalHeight * 2; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + const link = document.createElement('a'); + link.href = canvas.toDataURL('image/png'); + link.download = 'dti-logo.png'; + link.click(); + }; + img.src = '/wordmark.svg'; + } + }; + + useEffect(() => { + if (!contextMenu) return () => {}; + const close = () => setContextMenu(null); + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setContextMenu(null); + }; + window.addEventListener('click', close); + window.addEventListener('scroll', close); + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('click', close); + window.removeEventListener('scroll', close); + window.removeEventListener('keydown', handleKeyDown); + }; + }, [contextMenu]); + const mobileMenuRef = useRef(null); // mobile menu height state @@ -211,6 +267,7 @@ export default function Navbar({ demo }: NavbarProps) { width={269} height={48} className="md:min-w-[269px] h-10 md:h-12 w-auto" + onContextMenu={handleLogoContextMenu} /> @@ -395,6 +452,26 @@ export default function Navbar({ demo }: NavbarProps) {
+ {contextMenu && ( +
e.stopPropagation()} + > + + +
+ )} ); }