diff --git a/src/components/Footer.js b/src/components/Footer.js index 4ee2802..c423443 100644 --- a/src/components/Footer.js +++ b/src/components/Footer.js @@ -11,6 +11,7 @@ import { faLinkedin, faGithub, } from '@fortawesome/free-brands-svg-icons'; +import { contactFormLink } from '../content/nav'; export default function Footer() { return ( @@ -93,7 +94,7 @@ export default function Footer() { Contact diff --git a/src/components/JoinUsHero.jsx b/src/components/JoinUsHero.jsx new file mode 100644 index 0000000..7f635b1 --- /dev/null +++ b/src/components/JoinUsHero.jsx @@ -0,0 +1,49 @@ +export default JoinUsHero = () => ( +
+
+
+ + + + People chatting + + +
+ + +

+ Interested in joining us? +

+ +
+ + +

+ We have applications for various positions that open at the + start of each semester. Upon completion, we will contact you + for an interview. If you are not sure which position is best + for you, apply to all positions! +

+ +
+ + + + + + + +
+ +
+
+
+
+
+); \ No newline at end of file diff --git a/src/components/JumbotronHeader.jsx b/src/components/JumbotronHeader.jsx new file mode 100644 index 0000000..b21a523 --- /dev/null +++ b/src/components/JumbotronHeader.jsx @@ -0,0 +1,46 @@ +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; + +export default function JumbotronHeader({ title, subtitle, actions }) { + return ( +
+
+ + + + +

+ {title} +

+ +
+ + +

+ {subtitle} +

+ +
+ + + {actions} + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/MemberCard.js b/src/components/MemberCard.js deleted file mode 100644 index 4b426c7..0000000 --- a/src/components/MemberCard.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import Row from 'react-bootstrap/Row'; -import Col from 'react-bootstrap/Col'; -import Image from 'react-bootstrap/Image'; -import Card from 'react-bootstrap/Card'; - -export default function MemberCard() { - return ( -
- - - - - - - - -
Name
- -
- - -

- I have a passion for data analytics, hoping to apply my expertise - to the field of sustainability -

- -
-
-
- ); -} diff --git a/src/components/Navbar.js b/src/components/Navbar.js deleted file mode 100644 index 024b552..0000000 --- a/src/components/Navbar.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import Head from 'next/head'; -import Link from 'next/link'; - -import Navbar from 'react-bootstrap/Navbar'; -import Nav from 'react-bootstrap/Nav'; -import Container from 'react-bootstrap/Container'; - -export default function Index() { - return ( -
- - BUILD UMass - - - - BUILD UMass - - - - - - -
- ); -} \ No newline at end of file diff --git a/src/components/nav/Navbar.js b/src/components/nav/Navbar.js new file mode 100644 index 0000000..4aef5f2 --- /dev/null +++ b/src/components/nav/Navbar.js @@ -0,0 +1,30 @@ +import React from 'react'; +import Container from 'react-bootstrap/Container'; +import { useBreakpoint } from '../../hooks/useBreakpoint.js'; + +import NavbarLg from './NavbarLg.js'; +import NavbarSm from './NavbarSm.js'; +import { content } from '../../content/nav.js'; + +export default function Navbar() { + + const { smAndDown } = useBreakpoint(); + const { pages } = content; + + return ( +
+ {smAndDown ? + : + + + + } +
+ ); +} \ No newline at end of file diff --git a/src/components/nav/NavbarLg.js b/src/components/nav/NavbarLg.js new file mode 100644 index 0000000..48a9a87 --- /dev/null +++ b/src/components/nav/NavbarLg.js @@ -0,0 +1,27 @@ +import Navbar from 'react-bootstrap/Navbar'; +import Nav from 'react-bootstrap/Nav'; +import NavbarLink from './NavbarLink'; + +export default function NavbarLg({ pages }) { + return ( + + + BUILD UMass + + + + + ); +} \ No newline at end of file diff --git a/src/components/nav/NavbarLink.js b/src/components/nav/NavbarLink.js new file mode 100644 index 0000000..4921bad --- /dev/null +++ b/src/components/nav/NavbarLink.js @@ -0,0 +1,22 @@ +import Link from 'next/link'; + +export default function NavbarLink({ page, linkDisplay }) { + const isExternal = page.link.startsWith('http'); + + return ( + isExternal ? ( + + {linkDisplay || page.name} + ) : ( + + + {linkDisplay || page.name} + + + ) + ); +} \ No newline at end of file diff --git a/src/components/nav/NavbarMenu.js b/src/components/nav/NavbarMenu.js new file mode 100644 index 0000000..0719099 --- /dev/null +++ b/src/components/nav/NavbarMenu.js @@ -0,0 +1,90 @@ +import { useOuterClick } from '../../hooks/useOuterClick'; +import NavbarLink from './NavbarLink'; +import { useEffect, useState } from 'react'; +import NavbarMenuItem from './NavbarMenuItem'; + +export default function NavbarMenu({ show, pages, close }) { + const innerRef = useOuterClick(() => !show || close()); + const [rendering, setRendering] = useState(true); + useEffect(() => setRendering(false)) + + const menuStyle = { + position: 'fixed', + top: 0, + right: 0, + zIndex: 100, + width: '400px', + maxWidth: '80%', + height: '100%', + transform: `translateX(${show ? '0' : '100vw'})`, + transition: 'transform 0.3s ease', + background: 'linear-gradient(to top, #900202, #d13c06)', + display: 'flex', + flexDirection: 'column', + gap: '8px', + boxShadow: '0 0 10px 0 rgba(0, 0, 0, 0.5)', + }; + + const circleDecorationStyle = { + position: 'absolute', + bottom: '-400px', + left: '100px', + width: 'calc(100% - 40px)', + borderRadius: '50%', + height: '700px', + width: '700px', + background: 'black', + opacity: 0.3, + border: '120px dashed #d13c06', + } + + const backgroundDarkenerStyle = { + position: 'fixed', + top: 0, + left: 0, + zIndex: 99, + width: '100%', + height: '100%', + background: show ? 'rgba(0, 0, 0, 0.5)' : 'transparent', + pointerEvents: show ? 'auto' : 'none', + transition: 'background 0.3s ease', + } + + const menuTitleStyle = { + color: 'white', + fontSize: '30px', + fontWeight: '700', + } + + return rendering || ( + <> +
+ +
+ +

+ Browse BUILD +

+ + {pages.map((page, i) => ( + } + > + ))} + +
+ +
+ + ); +} \ No newline at end of file diff --git a/src/components/nav/NavbarMenuItem.js b/src/components/nav/NavbarMenuItem.js new file mode 100644 index 0000000..0e82e48 --- /dev/null +++ b/src/components/nav/NavbarMenuItem.js @@ -0,0 +1,42 @@ + +export default function NavbarMenuItem({ page }) { + + const itemParentStyle = { + borderRadius: '5px', + background: 'rgba(0, 0, 0, 0.1)', + cursor: 'pointer', + display: 'flex', + flexDirection: 'row', + gap: '14px', + overflow: 'hidden', + }; + + const itemTitleStyle = { + color: 'white', + fontSize: '20px', + fontWeight: '600', + }; + + const itemDescriptionStyle = { + color: 'rgba(255, 255, 255, 0.7)', + fontSize: '14px', + fontWeight: '100', + lineHeight: '1.4', + }; + + return ( +
+
+ + {page.longName ?? page.name} + +

+ {page.description} +

+
+
+ ); +} \ No newline at end of file diff --git a/src/components/nav/NavbarSm.js b/src/components/nav/NavbarSm.js new file mode 100644 index 0000000..82a5714 --- /dev/null +++ b/src/components/nav/NavbarSm.js @@ -0,0 +1,41 @@ +import Navbar from 'react-bootstrap/Navbar'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBars } from '@fortawesome/free-solid-svg-icons'; +import { useState } from 'react'; +import NavbarMenu from './NavbarMenu'; + +export default function NavbarSm({ pages }) { + const [show, setShow] = useState(false); + + const close = () => setShow(false); + const open = () => setShow(true) + + return ( + <> + + + BUILD UMass + + + + + + + + ); +} \ No newline at end of file diff --git a/src/content/nav.js b/src/content/nav.js new file mode 100644 index 0000000..2fd5e86 --- /dev/null +++ b/src/content/nav.js @@ -0,0 +1,41 @@ +export const contactFormLink = 'https://forms.gle/uuA2s98v4oC4o8TU7'; + +export const content = { + pages: [ + { + name: 'Home', + link: '/', + description: 'Welcome to BUILD UMass!', + }, + { + name: 'About', + longName: 'About Us', + link: '/about', + description: 'Learn about our mission, story, and team.', + }, + { + name: 'Services', + longName: 'Our Services', + link: '/services', + description: 'The services we provide to our community.', + }, + { + name: 'Projects', + longName: 'Our Projects', + link: '/projects', + description: 'Take a look through our past and present projects.', + }, + { + name: 'Apply', + longName: 'Join Us', + link: '/apply', + description: 'Join our team and help us make a difference.', + }, + { + name: 'Contact', + longName: 'Contact Us', + link: contactFormLink, + description: 'Get in touch with us.', + }, + ] +} \ No newline at end of file diff --git a/src/hooks/useBreakpoint.js b/src/hooks/useBreakpoint.js index 8629b18..1d170c7 100644 --- a/src/hooks/useBreakpoint.js +++ b/src/hooks/useBreakpoint.js @@ -2,8 +2,20 @@ import { useState, useEffect } from 'react'; import { debounce } from 'lodash'; /** - * @typedef {"xs" | "sm" | "md" | "lg" | "xl" | "xxl"} Breakpoint - */ + * @usage + * import { useBreakpoint } from '@hooks/useBreakpoint'; + * const { xs, sm, md, lg, xl, xxl, smAndUp, lgAndDown } = useBreakpoint(); + * style={{ width: xs ? '100%' : '50%' }} + * xs is a reactively changing boolean that evaluates to true if the current breakpoint is xs +*/ + +/** + * @typedef {"smAndDown" | "smAndUp" | "mdAndDown" | "mdAndUp" | "lgAndDown" | "lgAndUp"} BreakpointHelper +*/ + +/** + * @typedef {"xs" | "sm" | "md" | "lg" | "xl" | "xxl" | BreakpointHelper} Breakpoint +*/ /** * @type {Record} @@ -25,14 +37,37 @@ export const BreakpointSize = { * @example Breakpoint.xs === 'xs' */ export const Breakpoint = { + + // base props xs: 'xs', sm: 'sm', md: 'md', lg: 'lg', xl: 'xl', xxl: 'xxl', + + // helper props + smAndDown: 'smAndDown', + smAndUp: 'smAndUp', + mdAndDown: 'mdAndDown', + mdAndUp: 'mdAndUp', + lgAndDown: 'lgAndDown', + lgAndUp: 'lgAndUp', }; +/** + * @type {Record boolean>} + * @description maps helper props to their respective predicates + */ +export const breakpointHelpers = { + smAndDown: (size) => size === Breakpoint.xs || size === Breakpoint.sm, + smAndUp: (size) => size !== Breakpoint.xs, + mdAndDown: (size) => size === Breakpoint.xs || size === Breakpoint.sm || size === Breakpoint.md, + mdAndUp: (size) => size !== Breakpoint.xs && size !== Breakpoint.sm, + lgAndDown: (size) => size === Breakpoint.xl || size === Breakpoint.xxl, + lgAndUp: (size) => size !== Breakpoint.xl && size !== Breakpoint.xxl, +} + /** * @param {number} width * @returns {Breakpoint} @@ -48,7 +83,7 @@ const resolveBreakpoint = (width) => { */ export const useBreakpoint = () => { - // for server side rendering + // for ssr if (typeof window === 'undefined') { return Breakpoint.xxl; } @@ -62,5 +97,10 @@ export const useBreakpoint = () => { return () => window.removeEventListener('resize', calcInnerWidth); }, []); - return size; + return new Proxy(Breakpoint, { + get(target, prop) { + if (!(prop in target)) throw new Error(`Invalid breakpoint: ${prop}`); + return (prop in breakpointHelpers) ? breakpointHelpers[prop](size) : size === prop; + }, + }); }; \ No newline at end of file diff --git a/src/hooks/useOuterClick.js b/src/hooks/useOuterClick.js new file mode 100644 index 0000000..ec325cf --- /dev/null +++ b/src/hooks/useOuterClick.js @@ -0,0 +1,22 @@ +import { useEffect, useRef } from "react"; + +export function useOuterClick(callback) { + const callbackRef = useRef(); + const innerRef = useRef(); + + useEffect(() => { callbackRef.current = callback; }); + + useEffect(() => { + document.addEventListener("click", handleClick); + return () => document.removeEventListener("click", handleClick); + function handleClick(e) { + if ( + innerRef.current && + callbackRef.current && + !innerRef.current.contains(e.target) + ) callbackRef.current(e); + } + }, []); + + return innerRef; +} diff --git a/src/pages/_app.js b/src/pages/_app.js index 4dfb4d2..450c472 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -1,8 +1,16 @@ /* eslint-disable */ import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; import '../styles/style.scss'; +import Footer from '../components/Footer'; +import Navbar from '../components/nav/Navbar'; // Globalize CSS export default function MyApp({ Component, pageProps }) { - return ; + return ( + <> + + +